Skip to main content
并非每个智能体操作都应该无监督运行。当智能体即将发送电子邮件、删除记录、执行金融交易或执行任何不可逆操作时,您需要先让人类进行审核和批准。Human-in-the-Loop (HITL) 模式允许您的智能体暂停执行,向用户展示待处理的操作,并在明确批准后才能继续。 由于 HITL 基于LangGraph中断和检查点构建,因此暂停是持久的。用户可以刷新页面,审核员可以从不同的组件回答,而智能体仍然可以从执行停止的确切点恢复,而不是重播整个运行。 :::python
本页面使用默认的批量中断模式。对于每次调用中断(interrupt_mode="per_call"),请参阅每次工具调用暂停。
::: :::js
本页面使用默认的批量中断模式。对于每次调用中断(interruptMode: "per_call"),请参阅每次工具调用暂停。
:::

中断的工作原理

LangGraph 智能体支持中断,这是智能体明确将控制权交还给客户端的暂停点。当智能体遇到中断时:
  1. 智能体停止执行并发出中断有效负载
  2. The @[useStream] 钩子通过 stream.interrupt 提交中断
  3. 您的 UI 渲染带有批准/拒绝/编辑选项的审核卡片
  4. 用户做出决定
  5. 您的代码调用 stream.submit() 并带有恢复命令
  6. 智能体从停止的地方继续
前端 SDK 将中断与线程状态的其他部分一起保留,因此您的 UI 可以在合适的位置渲染它:在转录中内联、在审核队列中、在管理控制台中,或在阻止下一个用户操作的模态中,直到做出决定。

设置 useStream

将 @[useStream] 连接到您的 Human-in-the-Loop 智能体。当图遇到中断时,钩子会在 stream.interrupt 上暴露待处理的负载。在设置该值时渲染一个审批卡片,然后在用户批准、拒绝或编辑操作后使用 stream.submit(null, { command: { resume: response } }) 恢复运行。
代码示例使用 useStream<typeof myAgent> 进行类型安全的流状态管理。请参考 Python 或 JavaScript 后端的类型推断说明。

中断有效负载

当智能体暂停时,stream.interrupt 包含一个 @[HITLRequest],其结构如下:

决策类型

HITL 模式支持四种决策类型:

批准

用户确认操作应按原样进行:

拒绝

用户拒绝操作并提供可选的原因。工具不会执行:
当操作被拒绝时,智能体会收到拒绝原因并决定如何继续。如果您省略 message,后端会使用默认消息告知模型工具未执行,并且除非用户请求,否则不要重试相同的工具调用。对于有副作用的工具,传递一个清晰的消息,告诉智能体是否放弃操作、询问后续问题或尝试更安全的替代方案。

编辑

用户在批准前修改操作的参数:

响应

用户为“询问用户”风格的工具提供直接回复。message 成为工具结果,而工具本身不会执行:
当工具有意作为人类输入的占位符时使用 respond,例如一个 ask_user 工具,提示智能体从用户那里收集信息。不要使用 respond 来拒绝建议的操作,因为它会作为成功的工具结果返回给模型。

构建审批卡片

这是审批卡片使用的决策接线。UI 可以将每个操作拆分为自己的卡片,但恢复负载是一个 HITLResponse,每个待处理操作都有一个决策:

恢复流程

在用户做出决定后,完整周期如下:
  1. 调用 stream.submit(null, { command: { resume: hitlResponse } })
  2. The @[useStream] 钩子将恢复命令发送到 LangGraph 后端
  3. 智能体收到 HITLResponse 并继续执行。decisions 中的每个条目可能是:
    • { type: "approve" }:智能体继续执行操作
    • { type: "reject", message }:工具不会执行,智能体在决定下一步之前会收到拒绝消息
    • { type: "edit", editedAction }:智能体使用编辑后的参数运行工具
    • { type: "respond", message }:人类的消息直接作为工具结果返回,而不会执行工具
  4. interrupt 属性在智能体恢复流式传输时重置为 null
您可以在单个智能体运行中链式多个 HITL 检查点。例如,智能体可能会请求批准搜索,然后在发送包含结果的电子邮件之前再次请求批准。每个中断都是独立处理的。

处理多个待处理操作

当中断包含多个 actionRequests 时,智能体希望一次性执行多个操作。为每个渲染卡片并收集所有决策后再恢复:

自定义中断表单

恢复流程 使用 humanInTheLoopMiddleware,它将工具包装在一个通用的批准/拒绝/编辑/响应卡片中。有时一组按钮是不够的:预订航班、批准退款和审核社交帖子都需要一个不同的表单,具有自己的字段、验证和文案。为此,从工具内部提升 interrupt(),并让负载描述 UI 应渲染的确切表单。每个工具可以显示完全不同的界面。

在中断有效负载中描述表单

interrupt() 接受任何 JSON 可序列化的值,这允许你提供一个前端知道如何渲染的“卡片”,例如,表单类型、标题、人类正在审查的上下文以及收集的字段。interrupt() 对其输入和返回类型(interrupt<I, R>(value: I): R)是泛型的,因此你可以为发送的卡片(InterruptCard)和用户解析的值(ReviewDecision)进行类型注解。导出这些类型,以便前端可以导入它们并保持同步:
为每个工具分配一个唯一的 formType(例如 "refund-approval"、"content-review"),以便前端可以基于它切换并渲染匹配的表单。

每个工具渲染不同的表单

在客户端,卡片作为 stream.interrupt.value 到达。从你的智能体模块中导入 InterruptCard 和 ReviewDecision 类型,以便表单和有效负载保持同步,基于 formType 选择正确的表单,并将 fields 输入到你的输入中:

使用 respond(decision, { update }) 在屏幕上保持卡片

当你解析一个普通的中断时,卡片会在中断清除的瞬间消失,并且只返回工具结果。这意味着一个丰富的审查卡片会在运行中途消失。为了在屏幕上保持它,解析中断 并且 提交一个携带卡片的消息到 相同 超步中的状态,使用 @[useStream] 的 respond:
respond(response, { update }) 乐观地应用 update:卡片立即渲染,一旦恢复的运行回显相同消息,就会通过 ID 进行协调。后端不会重新发出卡片,因此它在(可能较慢的)工具运行时保持渲染,没有任何闪烁。通过从消息中读取来渲染解析的卡片:
由于解析的卡片存在于消息历史中,它会在刷新时存活,并且每个读取线程和人类的组件都能看到它,人类的决定成为持久的记录,而不仅仅是临时的 UI 状态。

最佳实践

在实现 HITL 工作流时,请记住以下指南:
  • 显示清晰的上下文。始终显示智能体想要做什么以及 为什么。包括操作描述和完整参数。
  • 使批准是最简单的路径。如果操作看起来是正确的,批准应该是单击操作。为拒绝/编辑保留多步流程。
  • 验证编辑的参数。当用户编辑操作参数时,在发送之前验证 JSON 结构。对于格式不正确的输入,显示内联错误。
  • 持久化中断状态。如果用户刷新页面,中断仍然应该可见。@[useStream] 通过线程的检查点处理此问题。
  • 记录所有决定。对于审计跟踪,记录每个批准/拒绝/编辑决定,包括时间戳和做出决定的用户。
  • 谨慎设置超时。长时间运行的智能体不应无限期地阻塞人类审查。考虑显示智能体等待了多长时间。

官方原文:src/oss/langchain/frontend/human-in-the-loop.mdx · 非官方翻译,译文由 AI 生成,以英文原文为准。
Last modified on November 3, 2026