并非所有智能体操作都应无人监督地运行。当智能体即将发送电子邮件、删除记录、执行金融交易或进行任何不可逆操作时,需要人工先审核并批准该操作。人机协作(HITL)模式允许您的智能体暂停执行,将待处理操作呈现给用户,并仅在获得明确批准后才恢复执行。 由于 HITL 基于 LangGraph 的中断和检查点构建,因此暂停是持久的。用户可以刷新页面,审核员可以从不同的组件回复,并且智能体仍会从执行停止的确切点恢复,而不是重新播放整个运行过程。

中断如何工作

LangGraph 智能体支持中断,即智能体将控制权交还给客户端的显式暂停点。当智能体触发中断时:
  1. 智能体停止执行并发出中断负载
  2. useStream 钩子通过 stream.interrupt 展示中断
  3. 您的 UI 渲染一个带有批准/拒绝/编辑选项的审核卡片
  4. 用户做出决定
  5. 您的代码调用 stream.submit() 并传递恢复命令
  6. 智能体从它离开的地方继续执行
前端 SDK 将中断与线程状态的其余部分一起保存,因此您的 UI 可以在任何有意义的位置渲染它:在对话记录中、在审核队列中、在管理仪表板中,或在阻止下一个用户操作直到做出决定的模态框中。

设置 useStream

将 useStream 连接到您的人机协作智能体。当图触发中断时,该钩子会在 stream.interrupt 上公开待处理的负载。当该值被设置时,渲染一个批准卡片,然后在用户批准、拒绝或编辑操作后,使用 stream.submit(null, { command: { resume: response } }) 恢复运行。
The code examples use useStream<typeof myAgent> for type-safe stream state. See Type inference for Python or JavaScript backends.

中断负载

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

决策类型

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

批准

用户确认操作应按原样继续:

拒绝

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

编辑

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

回复

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

构建 ApprovalCard

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

恢复流程

在用户做出决定后,完整的流程如下:
  1. 调用 stream.submit(null, { command: { resume: hitlResponse } })
  2. 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 通过线程的检查点处理此问题。
  • 记录所有决策。对于审计跟踪,记录每个批准/拒绝/编辑决策,包括时间戳和做出决策的用户。
  • 谨慎设置超时。长时间运行的智能体不应无限期地阻塞在人工审核上。考虑显示智能体已等待了多长时间。