Human-in-the-loop(HITL)是 Agent 落地时绕不开的一环:模型能自己跑,但关键动作得让人点个头。LangGraph 把这套机制做成了 interrupt + useStream 的组合,后端抛问题、前端答问题,流程自动恢复。下面用一个"搜索前审批"的例子,把用法讲清楚。
一、后端:用 interrupt 把问题抛出去
python
makefile
queries = [state["messages"][-1].tool_calls.get("query") for call in calls]
approved = interrupt({
"question": "模型想搜索以下内容,是否允许?",
"queries": queries,
})
三件事值得注意:
interrupt接收任意可序列化的 dict ,它会原样传给前端。这里传了question和queries两个字段,前端就能拼出一句完整的提示。interrupt的返回值就是前端 respond 回来的值 。用户点"确定",approved就是true;点"取消",就是false。后端拿到这个值,就能决定后续走哪条分支。- 抛 interrupt 的位置很关键------要放在"模型已经决定要调工具、但工具还没执行"的窗口期。这样人审批的是"意图",而不是"已发生的结果",拦截才有意义。
二、前端:用 useStream 接住问题并回传
js
javascript
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(...)的返回值,让后端节点从暂停处继续执行。
三、这个闭环的完整时序
- 模型决定调用搜索工具,生成
tool_calls。 - 节点执行到
interrupt(...),LangGraph 暂停当前图,把 payload 推给前端。 - 前端
useStream收到中断,watch触发confirm弹窗。 - 用户点确定/取消,
stream.respond(true/false)发回后端。 - 后端
interrupt返回该值,赋给approved,节点继续往下跑。
四、用法要点小结
| 角色 | 做什么 | 关键 API |
|---|---|---|
| 后端 | 在需要人决策的位置抛问题 | interrupt(payload),返回值即用户答复 |
| 前端 | 监听中断、展示、回传 | stream.interrupt、stream.respond(value) |
| 连接 | payload 和后端返回值是同一个值 | 前后端字段要约定好 |
这就是 HITL 的最小可用闭环:后端 interrupt 抛问题,前端 useStream 接问题,respond 回答案,图自动恢复 。掌握这个模式,你就可以把 confirm 换成审批表单、把布尔值换成结构化对象,按业务需要扩展交互粒度。
后端interrupt断点所在的函数会在用户确认后重新执行,所以会执行两遍