深入浅出 LangGraph —— 第8章:人机交互:中断与审批流程

📖 本章学习目标

  • ✅ 理解"人在回路"(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 后才执行

工作原理:

  1. 节点执行到interrupt()时暂停
  2. value序列化保存到检查点
  3. 返回当前状态给调用方
  4. 等待外部调用resume恢复执行
  5. 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值:

  1. 'approve':批准,继续执行
  2. 'reject':拒绝,可能需要重新规划
  3. 自定义对象:携带人工修改的数据

完整流程图:
人工审批者 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 的核心模式

实际应用场景:

  1. 客服系统:信息不全时主动询问
  2. 订票助手:询问日期、人数等细节
  3. 报告生成:确认报告范围和格式

示例对话流程:

复制代码
用户: 帮我订机票
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 起草邮件→人工审批→发送"的流程
  • 要求 :
    1. AI根据主题生成邮件草稿
    2. interrupt显示邮件内容,支持批准/修改/拒绝三种决策
    3. 批准后打印"已发送",修改后重新生成,拒绝后取消
  • 验收标准 :
    • 三种决策都有正确处理
    • 修改后可以重新生成
    • 拒绝后不会发送邮件

练习 2:多步骤审批

  • 目标:实现"规划→审批规划→执行→审批结果→完成"的五步流程
  • 要求 :
    1. 每个审批点都能修改前一步的输出
    2. 使用interruptBefore和interruptAfter组合
    3. 记录每次审批的决策和时间
  • 验收标准 :
    • 任意步骤的拒绝都能触发重新执行
    • 审批历史完整记录
    • 流程清晰可追踪

练习 3:超时自动处理

  • 目标:审批超过10秒未响应时,自动选择默认决策(如"拒绝")
  • 要求 :
    1. 结合 Promise.race 实现超时机制
    2. 超时时自动resume为'reject'
    3. 记录超时事件到日志
  • 验收标准 :
    • 10秒内无操作自动走拒绝流程
    • 不死等,不影响其他请求
    • 超时事件可追溯

练习 4:工具调用审计系统

  • 目标:记录所有需要审批的工具调用
  • 要求 :
    1. 创建auditLog State字段
    2. 每次审批记录:工具名、参数、决策、时间、审批人
    3. 提供查询审计日志的功能
  • 验收标准 :
    • 所有审批都有完整记录
    • 可按时间、工具名查询
    • 符合安全审计要求

📚 延伸阅读


下一章:第9章 ------ 流式输出:实时响应用户

相关推荐
Wanderer X1 小时前
【VLM】diffusion
人工智能
mahtengdbb11 小时前
三阶段压缩瓶颈改进YOLOv26特征提取效率与通道自适应能力提升
人工智能·yolo·目标跟踪
嵌入式小企鹅1 小时前
CPU需求变化、RISC-V安全方案、DeepSeek V4适配、太空算力动态
人工智能·驱动开发·华为·开源·算力·risc-v
HyperAI超神经1 小时前
利用堆叠集成学习,英国研究团队实现251颗盾牌座δ型星星震学指数高精度预测
人工智能·机器学习·集成学习
AI刀刀1 小时前
手机deepseek怎么导出pdf
人工智能·ai·pdf·豆包·deepseek·ds随心转
专注&突破1 小时前
用AI学习graphify
人工智能·学习
wayz111 小时前
Day 16 编程实战:PCA主成分分析与技术指标降维
人工智能·算法·机器学习
超梦dasgg1 小时前
SpringAi学习
人工智能·学习·ai编程
05大叔2 小时前
贝叶斯,支持向量机,深度学习
人工智能·分类·数据挖掘