📖 本章学习目标
- ✅ 理解"人在回路"(Human-in-the-Loop)的核心价值
- ✅ 掌握
interrupt()函数暂停图执行并等待人类输入- ✅ 学会配置静态断点(breakpoints)在特定节点前后暂停
- ✅ 实现工具调用前的人工审批确认机制
- ✅ 构建完整的"AI 执行→人工审批→继续执行"工作流
- ✅ 避免常见的中断陷阱和安全问题
一、为什么需要人在回路
1、完全自动化的风险
随着 Agent 能力越来越强,它能执行的操作也越来越"危险"------删除文件、发送邮件、转移资金......如果 AI 理解有偏差直接执行,后果可能是灾难性的。
需要人工干预的典型场景:
- 🗄️ 执行数据库删除操作前需要确认
- 📧 发送重要邮件前需要人工审阅内容
- 💰 金融交易需要用户授权
- 🔒 涉及敏感权限的 API 调用
- 🤔 AI 对任务理解不确定时需要追问
- ⚖️ 法律/医疗建议需要专业人士审核
❌ 无审批流程
AI生成方案
直接执行
可能出错
无法挽回
✅ 有审批流程
批准
拒绝
AI生成方案
⏸️ 暂停等待审批
人工审核
安全执行
重新规划
安全可靠✅
2、Human-in-the-Loop 架构
✅ 批准
❌ 拒绝
✏️ 修改
Agent 开始执行
规划节点
⏸️ 暂停
等待人工审批
执行节点
修订节点
人工修改
完成
核心价值:
- ✅ 安全性:防止AI错误决策造成损失
- ✅ 可控性:人类保留最终决定权
- ✅ 可追溯:每次审批都有记录可查
- ✅ 灵活性:可随时介入调整方向
二、interrupt() 函数:动态中断
1、基本用法
interrupt() 函数可以在节点执行中途暂停,等待外部输入后再继续:
步骤1:导入依赖
typescript
import * as dotenv from 'dotenv';
dotenv.config();
import {
interrupt,
MemorySaver,
MessagesAnnotation,
StateGraph
} from '@langchain/langgraph';
import { ChatOpenAI } from '@langchain/openai';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
步骤2:定义审批节点
typescript
const model = new ChatOpenAI({ model: 'gpt-4o-mini' });
async function reviewNode(state: typeof MessagesAnnotation.State) {
const lastMsg = state.messages[state.messages.length - 1] as AIMessage;
// 暂停执行,向外部暴露需要审批的内容
const humanDecision = interrupt({
message: 'AI 生成了以下回复,请审批:',
content: lastMsg.content,
options: ['approve', 'reject', 'modify'],
});
// humanDecision 是人类的输入(resume 时传入)
if (humanDecision === 'reject') {
return { messages: [new HumanMessage('请重新生成,要求更简洁')] };
}
return {}; // 批准则不做任何修改
}
interrupt(value)暂停当前节点的执行,value 可以是任意可序列化的数据,传给等待审批的系统。函数的返回值是 resume() 时传入的人类决策。注意:interrupt() 之后的代码只在 resume 后才执行
工作原理:
- 节点执行到
interrupt()时暂停 - 将
value序列化保存到检查点 - 返回当前状态给调用方
- 等待外部调用
resume恢复执行 resume的值作为interrupt()的返回值
2、interrupt 的执行流程
第一次调用:触发中断
typescript
const checkpointer = new MemorySaver();
const graph = new StateGraph(MessagesAnnotation)
.addNode('generate', async (state) => {
const response = await model.invoke(state.messages);
return { messages: [response] };
})
.addNode('review', reviewNode)
.addEdge('__start__', 'generate')
.addEdge('generate', 'review')
.addEdge('review', '__end__')
.compile({ checkpointer });
const config = { configurable: { thread_id: 'approval-flow-001' } };
// 第一次调用:执行到 interrupt 处暂停
const firstResult = await graph.invoke(
{ messages: [new HumanMessage('写一首关于春天的短诗')] },
config
);
console.log('图已暂停,等待审批');
- 图执行到
reviewNode中的interrupt()后暂停 firstResult包含当前状态和中断信息- 此时图并未结束,只是暂停等待人工输入
- 必须使用
checkpointer才能使用interrupt
检查中断状态
typescript
// 检查暂停状态
const state = await graph.getState(config);
console.log('等待人工输入的内容:', state.tasks[0]?.interrupts);
console.log('下一步:', state.next); // ['review']
// 输出示例:
// 等待人工输入的内容: [{
// value: {
// message: 'AI 生成了以下回复,请审批:',
// content: '春天来了,万物复苏...',
// options: ['approve', 'reject', 'modify']
// }
// }]
getState():获取当前图的运行状态state.tasks[0].interrupts包含interrupt()传入的数据state.next:显示下一步要执行的节点- 这些信息可以展示给前端,让人工审批
恢复执行
typescript
// 人工审批后,用 resume 继续执行
const finalResult = await graph.invoke(
// null 表示不更新 messages,直接继续
new Command({ resume: 'approve' }),
config
);
console.log('审批通过,图继续执行完成');
Command({ resume: value })携带人类决策,恢复图的执行 resume 的值就是interrupt() 函数的返回值。可以使用相同的config(thread_id相同),图从暂停处继续执行,直到完成或再次中断。
三种常见resume值:
- 'approve':批准,继续执行
- 'reject':拒绝,可能需要重新规划
- 自定义对象:携带人工修改的数据
完整流程图:
人工审批者 LangGraph 应用程序 人工审批者 LangGraph 应用程序 第一阶段:触发中断 第二阶段:人工审批 第三阶段:恢复执行 invoke(初始输入) 执行generate节点 执行review节点 遇到interrupt(),暂停 返回中断状态 展示审批内容 选择"approve" invoke(Command({resume:'approve'})) interrupt()返回'approve' 继续执行后续逻辑 返回最终结果
三、静态断点(Breakpoints)
1、在特定节点前后暂停
静态断点不需要修改节点代码,直接在编译时配置:
typescript
import { Command } from '@langchain/langgraph';
const graphWithBreakpoint = new StateGraph(MessagesAnnotation)
.addNode('plan', planNode)
.addNode('execute', executeNode)
.addNode('validate', validateNode)
.addEdge('__start__', 'plan')
.addEdge('plan', 'execute')
.addEdge('execute', 'validate')
.addEdge('validate', '__end__')
.compile({
checkpointer,
interruptBefore: ['execute'], // 执行 execute 节点前暂停
interruptAfter: ['validate'], // validate 节点执行后暂停
});
interruptBefore:在指定节点执行之前暂停,适合审批即将执行的操作interruptAfter:在指定节点执行之后暂停,适合检查结果是否符合预期- 两者都不修改节点函数,适合在不改动业务逻辑的情况下加审批
- 支持数组,可同时为多个节点设置断点
断点位置示意图:
start
plan节点
⏸️ interruptBefore
execute
execute节点
validate节点
⏸️ interruptAfter
validate
end
2、断点的典型应用
typescript
async function main() {
const config = { configurable: { thread_id: 'breakpoint-demo' } };
// 第一次运行:执行 plan,然后在 execute 前暂停
await graphWithBreakpoint.invoke(
{ messages: [new HumanMessage('帮我删除所有过期日志文件')] },
config
);
const state = await graphWithBreakpoint.getState(config);
console.log('规划结果:', state.values.messages[state.values.messages.length - 1]);
console.log('即将执行的节点:', state.next); // ['execute']
// 人工审查规划结果后,决定是否继续
const approved = await humanReview(state); // 伪代码:人工审批函数
if (approved) {
// 批准:继续执行
await graphWithBreakpoint.invoke(null, config);
} else {
// 拒绝:手动更新状态,修改规划
await graphWithBreakpoint.updateState(
config,
{ messages: [new HumanMessage('请重新规划,只删除30天前的日志')] },
'plan' // 重新从 plan 节点开始
);
await graphWithBreakpoint.invoke(null, config);
}
}
实际应用场景:
- 数据库迁移:先展示迁移计划,人工确认后执行
- 批量操作:预览影响范围,确认后执行
- 代码部署:审查变更列表,批准后部署
四、工具调用审批
1、在工具执行前要求确认
这是生产中最常见的人机交互场景------让 AI 提出执行计划,人工确认后再执行:
步骤1:定义危险工具
模拟文件删除工具,实际项目中应谨慎使用。
typescript
import { Annotation, StateGraph, interrupt, MemorySaver } from '@langchain/langgraph';
import { ToolNode } from '@langchain/langgraph/prebuilt';
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
import { RemoveMessage } from '@langchain/core/messages';
// 定义一个"危险"工具
const deleteFileTool = tool(
async ({ filePath }) => {
console.log(`🗑️ 正在删除文件: ${filePath}`);
// 实际删除操作...
return `文件 ${filePath} 已删除`;
},
{
name: 'delete_file',
description: '删除指定路径的文件',
schema: z.object({ filePath: z.string().describe('要删除的文件完整路径') }),
}
);
步骤2:定义审批节点
typescript
// 审批节点:在工具调用前插入人工确认
async function approvalNode(state: typeof MessagesAnnotation.State) {
const lastMsg = state.messages[state.messages.length - 1] as AIMessage;
if (!lastMsg.tool_calls?.length) return {};
// 展示 AI 想要调用的工具,等待人工确认
const decision = interrupt({
type: 'tool_approval',
toolCalls: lastMsg.tool_calls.map(tc => ({
name: tc.name,
args: tc.args,
})),
message: '以上工具调用需要您的确认',
});
if (decision === 'reject') {
// 取消工具调用:移除包含 tool_calls 的 AI 消息
return {
messages: [new RemoveMessage({ id: lastMsg.id! })],
};
}
return {}; // 批准:继续执行工具
}
代码解读:
- 检查最后一条消息是否有tool_calls
- 如果有,展示工具名称和参数,等待人工确认
- 拒绝时:使用
RemoveMessage删除tool_calls消息 - 批准时:返回空对象,继续执行tools节点
步骤3:组装带审批的图
typescript
const agentNode = async (state: typeof MessagesAnnotation.State) => {
const modelWithTools = model.bindTools([deleteFileTool]);
const response = await modelWithTools.invoke(state.messages);
return { messages: [response] };
};
const approvalGraph = new StateGraph(MessagesAnnotation)
.addNode('agent', agentNode)
.addNode('approval', approvalNode) // 插入审批节点
.addNode('tools', new ToolNode([deleteFileTool]))
.addEdge('__start__', 'agent')
.addConditionalEdges('agent', (state) => {
const last = state.messages[state.messages.length - 1] as AIMessage;
return last.tool_calls?.length ? 'approval' : '__end__';
})
.addConditionalEdges('approval', (state) => {
const last = state.messages[state.messages.length - 1] as AIMessage;
return last.tool_calls?.length ? 'tools' : '__end__';
})
.addEdge('tools', 'agent')
.compile({ checkpointer: new MemorySaver() });
代码解读:
- 在 agent 和 tools 之间插入 approval 节点
- approval 节点使用
interrupt()暂停,等待人工决策 - 批准后继续到 tools 执行,拒绝则移除 tool_calls 消息
- 这个模式将 AI 的"想法"和"行动"分离,人工把控行动
工具审批流程图:
否
是
批准
拒绝
agent节点
LLM推理
有tool_calls?
end
approval节点
⏸️ 中断等待审批
人工决策
tools节点
执行工具
移除tool_calls
五、动态输入收集
1、Agent 主动向用户提问
有时 Agent 信息不足,需要主动向用户询问更多细节:
typescript
async function clarifyNode(state: typeof MessagesAnnotation.State) {
const lastMsg = state.messages[state.messages.length - 1] as AIMessage;
// AI 判断是否需要追问
if (lastMsg.content.includes('[需要澄清]')) {
const userAnswer = interrupt({
type: 'clarification',
question: lastMsg.content,
});
// 将用户的补充信息加入对话
return {
messages: [new HumanMessage(`用户补充:${userAnswer}`)]
};
}
return {};
}
- AI 在回复中用特殊标记(如 [需要澄清])表示需要追问
- interrupt() 暂停等待用户填写补充信息
- 收到用户回答后,将其作为新消息加入对话继续处理
- 这是构建表单填写 Agent 的核心模式
实际应用场景:
- 客服系统:信息不全时主动询问
- 订票助手:询问日期、人数等细节
- 报告生成:确认报告范围和格式
示例对话流程:
用户: 帮我订机票
AI: [需要澄清]请问您想从哪里飞到哪里?什么日期?
⏸️ 中断,等待用户输入
用户: 北京到上海,明天
✅ 恢复,AI继续处理
六、最佳实践和踩坑指南
💡 实践 1:interrupt 数据要包含足够上下文
❌ 不足的中断数据:
typescript
const decision = interrupt('需要审批'); // 审批者看不到内容
✅ 包含完整上下文:
typescript
const decision = interrupt({
type: 'approval',
summary: '删除 /logs 目录下所有 .log 文件',
details: { files: filesToDelete, totalSize: '2.3GB' },
riskLevel: 'high',
message: '此操作不可逆,请仔细确认',
});
原因:审批者需要足够的信息才能做出明智决策。
💡 实践 2:提供明确的批准/拒绝选项
typescript
// 不只是 true/false,提供有语义的选项
const decision = interrupt({
options: {
approve: '批准,立即执行',
modify: '修改参数后执行',
reject: '拒绝,取消此操作',
},
});
// resume 时传入对应的 key
// new Command({ resume: 'modify' })
原因:清晰的选项减少审批者的认知负担。
💡 实践 3:超时自动处理
typescript
// 带超时的审批流程
async function approveWithTimeout(config: any, timeoutMs: number = 10000) {
const timeoutPromise = new Promise((resolve) => {
setTimeout(() => resolve('timeout'), timeoutMs);
});
const approvalPromise = new Promise((resolve) => {
// 这里应该由前端或外部系统调用resume
// 简化示例,实际需要事件监听
});
const result = await Promise.race([approvalPromise, timeoutPromise]);
if (result === 'timeout') {
console.warn('审批超时,自动拒绝');
return await graph.invoke(new Command({ resume: 'reject' }), config);
}
return result;
}
原因:防止审批流程无限期等待,影响用户体验。
⚠️ 常见问题
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 没有启用 checkpointer | interrupt() requires a checkpointer |
必须在 compile 时传入 checkpointer |
| resume 时 thread_id 不一致 | 找不到暂停的检查点 | resume 必须使用与首次调用完全相同的 thread_id |
| interrupt 数据不可序列化 | 序列化错误 | 只传 JSON 安全的数据(无 Function、Symbol) |
| 忘记处理 reject 情况 | 拒绝后图意外结束 | 为每种审批决策都设计相应的处理逻辑 |
| 多次resume同一检查点 | 行为未定义 | 每个检查点只能resume一次 |
| 中断嵌套过深 | 难以追踪状态 | 避免在节点中多次调用interrupt |
📝 本章小结
核心知识点回顾
| 知识点 | 关键要点 | 应用场景 |
|---|---|---|
interrupt() |
动态暂停,等待外部输入 | 运行时需要人工决策的场景 |
interruptBefore/After |
静态断点,不改代码 | 对特定节点加审批 |
Command({ resume }) |
携带人类决策恢复执行 | 所有中断的恢复 |
| 工具调用审批 | 在 agent 和 tools 间插入审批节点 | 危险操作的安全控制 |
| 动态输入收集 | AI主动提问,等待用户回答 | 信息不全时的澄清流程 |
| 超时处理 | 防止无限期等待 | 提升用户体验 |
🎯 动手练习
练习 1:简单审批流程
- 目标:实现"AI 起草邮件→人工审批→发送"的流程
- 要求 :
- AI根据主题生成邮件草稿
- interrupt显示邮件内容,支持批准/修改/拒绝三种决策
- 批准后打印"已发送",修改后重新生成,拒绝后取消
- 验收标准 :
- 三种决策都有正确处理
- 修改后可以重新生成
- 拒绝后不会发送邮件
练习 2:多步骤审批
- 目标:实现"规划→审批规划→执行→审批结果→完成"的五步流程
- 要求 :
- 每个审批点都能修改前一步的输出
- 使用interruptBefore和interruptAfter组合
- 记录每次审批的决策和时间
- 验收标准 :
- 任意步骤的拒绝都能触发重新执行
- 审批历史完整记录
- 流程清晰可追踪
练习 3:超时自动处理
- 目标:审批超过10秒未响应时,自动选择默认决策(如"拒绝")
- 要求 :
- 结合 Promise.race 实现超时机制
- 超时时自动resume为'reject'
- 记录超时事件到日志
- 验收标准 :
- 10秒内无操作自动走拒绝流程
- 不死等,不影响其他请求
- 超时事件可追溯
练习 4:工具调用审计系统
- 目标:记录所有需要审批的工具调用
- 要求 :
- 创建auditLog State字段
- 每次审批记录:工具名、参数、决策、时间、审批人
- 提供查询审计日志的功能
- 验收标准 :
- 所有审批都有完整记录
- 可按时间、工具名查询
- 符合安全审计要求