LangGraph HITL 实战:从 interrupt 到 useStream 的最小闭环

0 阅读2分钟

image.png Human-in-the-loop(HITL)是 Agent 落地时绕不开的一环:模型能自己跑,但关键动作得让人点个头。LangGraph 把这套机制做成了 interrupt + useStream 的组合,后端抛问题、前端答问题,流程自动恢复。下面用一个“搜索前审批”的例子,把用法讲清楚。

一、后端:用 interrupt 把问题抛出去

python

queries = [state["messages"][-1].tool_calls.get("query") for call in calls]
approved = interrupt({
    "question": "模型想搜索以下内容,是否允许?",
    "queries": queries,
})

三件事值得注意:

  1. interrupt 接收任意可序列化的 dict,它会原样传给前端。这里传了 question 和 queries 两个字段,前端就能拼出一句完整的提示。
  2. interrupt 的返回值就是前端 respond 回来的值。用户点“确定”,approved 就是 true;点“取消”,就是 false。后端拿到这个值,就能决定后续走哪条分支。
  3. 抛 interrupt 的位置很关键——要放在“模型已经决定要调工具、但工具还没执行”的窗口期。这样人审批的是“意图”,而不是“已发生的结果”,拦截才有意义。

二、前端:用 useStream 接住问题并回传

js

const stream = useStream({ assistantId, apiUrl: () => apiUrl.value })
const pendingInterrupt = computed(() => stream.interrupt.value?.value ?? null)

watch(pendingInterrupt, (val) => {
  let res = confirm(val.question + val.queries.join(","))
  stream.respond(res)
})

拆解一下:

  • stream.interrupt 是一个响应式引用,后端一抛 interrupt,它就会更新,里面带着后端传过来的 payload(即 question 和 queries)。
  • watch 监听它,一旦有新的中断就弹窗。这里用 confirm 是最简单的交互,val.question + val.queries.join(",") 拼出提示文案。
  • stream.respond(res) 把用户的答案回传给后端。这个值会作为 interrupt(...) 的返回值,让后端节点从暂停处继续执行。

三、这个闭环的完整时序

  1. 模型决定调用搜索工具,生成 tool_calls。
  2. 节点执行到 interrupt(...),LangGraph 暂停当前图,把 payload 推给前端。
  3. 前端 useStream 收到中断,watch 触发 confirm 弹窗。
  4. 用户点确定/取消,stream.respond(true/false) 发回后端。
  5. 后端 interrupt 返回该值,赋给 approved,节点继续往下跑。

四、用法要点小结

角色做什么关键 API
后端在需要人决策的位置抛问题interrupt(payload),返回值即用户答复
前端监听中断、展示、回传stream.interrupt、stream.respond(value)
连接payload 和后端返回值是同一个值前后端字段要约定好

这就是 HITL 的最小可用闭环:后端 interrupt 抛问题,前端 useStream 接问题,respond 回答案,图自动恢复。掌握这个模式,你就可以把 confirm 换成审批表单、把布尔值换成结构化对象,按业务需要扩展交互粒度。
后端interrupt断点所在的函数会在用户确认后重新执行,所以会执行两遍