第 08 章

中断与人机交互

让人类介入 AI 的决策过程

interrupt() 暂停 → 人类审批 → Command 恢复

没有刹车

智能体在无人监督的情况下自动执行了所有操作——包括那些不该自动执行的操作。

发了不该发的邮件

智能体自动给客户发了一封通知邮件,措辞不当引发了投诉。如果发之前有人审核一下就好了。

执行了危险操作

智能体调用 DELETE FROM users 清空了测试数据库。删除操作应该有人确认。

缺少关键信息

智能体处理退款请求时,需要用户确认退款金额和收款账户。但图已经跑完了,没机会问。

interrupt() 就是为这个设计的:在任意节点暂停执行,等待人类输入后再继续。暂停期间状态完整保存在 checkpointer 中,可以无限期等待。

开始之前

前置知识

  • 持久化 — checkpointer 是中断的前提
  • 快速上手 — 完整的智能体代码
  • 理解节点和状态的基本概念

学完后你应该能

  • 使用 interrupt() 暂停图执行
  • 使用 Command({ resume }) 恢复执行
  • 实现审批、审查、输入验证等模式
  • 避免 interrupt 的常见陷阱

核心机制

interrupt() 暂停,Command({ resume }) 恢复

图:中断与恢复的完整流程
思考题:你的自动化脚本要删除服务器上的临时文件,但删除操作不可逆。能不能在执行危险操作前暂停,等你确认后再继续?
import { interrupt } from "@langchain/langgraph";

async function approvalNode(state) {
    // 暂停并请求审批
    const approved = interrupt("Do you approve this action?");
    // Command({ resume: true }) 传入的值成为 approved
    return { approved };
}

📄 完整代码:08_basic_interrupt.ts

三步走:① 调用 interrupt() 暂停执行,传入提示信息 → ② 状态保存到 checkpointer → ③ 用 Command({ resume: value }) 恢复,value 成为 interrupt() 的返回值。

暂停与恢复

完整的中断-恢复代码流程

思考题:interrupt() 暂停后,如何通过 Command({ resume }) 恢复执行?恢复时传入的值如何成为 interrupt() 的返回值?
import { Command } from "@langchain/langgraph";

const config = { configurable: { thread_id: "thread-1" } };

// 第一次运行 — 遇到中断暂停
const result = await graph.invoke({ input: "data" }, config);

// 查看中断内容
console.log(result.__interrupt__);
// [{ value: 'Do you approve this action?', ... }]

// 用人类的响应恢复
await graph.invoke(new Command({ resume: true }), config);

📄 完整代码:08_basic_interrupt.ts

三个关键点

  • 同一个 thread_id:恢复时必须用和暂停时相同的线程
  • resume 值 = 返回值:Command({ resume: true }) 中的 true 成为 interrupt() 的返回值
  • 节点从头执行:恢复后节点重新执行,不是从中断行继续

审批模式

interrupt 返回布尔值,决定走哪条路

思考题:你有一个文档审批流程,通过后直接发布,驳回则退回修改。怎么在 interrupt 恢复后,根据用户决策走不同的分支?
const approvalNode: typeof State.Node = (state) => {
  const isApproved = interrupt({
    question: "Do you want to proceed?",
    details: state.actionDetails,
  });

  if (isApproved) {
    return new Command({ goto: "proceed" });
  } else {
    return new Command({ goto: "cancel" });
  }
};

// 恢复时传入 true 审批或 false 拒绝
await graph.invoke(new Command({ resume: true }), config);   // 审批
await graph.invoke(new Command({ resume: false }), config);  // 拒绝

📄 完整代码:08_approval_pattern.ts

审查与编辑

人类修改智能体生成的内容后再继续

思考题:AI 生成的文章草稿总是差点意思,你想在发布前人工润色一下。但润色结果怎么传回图中继续后续流程?
const reviewNode: typeof State.Node = (state) => {
  const editedContent = interrupt({
    instruction: "Review and edit this content",
    content: state.generatedText,
  });
  return { generatedText: editedContent };
};

// 恢复时提供编辑后的内容
await graph.invoke(
  new Command({ resume: "The edited and improved text" }),
  config
);

📄 完整代码:08_review_edit.ts

模式:智能体生成内容 → interrupt 暂停并展示给人类 → 人类编辑 → resume 传回编辑后的内容 → 智能体用编辑后的内容继续。

工具中的中断

interrupt 不只能在节点里用——工具函数里也能用

思考题:你的 AI Agent 有一个"发送邮件"工具,用户说"帮我发邮件"就立刻发出去了。但万一理解错了收件人,邮件已经发出去了。怎么在工具真正执行前加一道人工确认?
import { tool } from "@langchain/core/tools";

const sendEmailTool = tool(
  async ({ to, subject, body }) => {
    const response = interrupt({
      action: "send_email",
      to, subject, body,
      message: "Approve sending this email?",
    });

    if (response?.action === "approve") {
      return `Email sent to ${response.to ?? to}`;
    }
    return "Email cancelled by user";
  },
  {
    name: "send_email",
    description: "Send an email",
    schema: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
  }
);

📄 完整代码:08_tool_interrupt.ts

为什么在工具里中断:不是所有工具调用都需要人类确认。但发邮件、删除数据、支付等高风险操作,应该在执行前暂停等待人类批准。

输入验证

用 while 循环反复询问,直到获得合法输入

思考题:注册流程要求用户输入邮箱,但用户可能乱填。你需要反复提示直到输入合法。interrupt 每次暂停后恢复,怎么保持之前的验证状态?
const getAgeNode: typeof State.Node = (state) => {
  let prompt = "What is your age?";

  while (true) {
    const answer = interrupt(prompt);
    if (typeof answer === "number" && answer > 0) {
      return { age: answer };
    } else {
      prompt = `'${answer}' is not a valid age. Please enter a positive number.`;
    }
  }
};

📄 完整代码:08_input_validation.ts

图:输入验证的循环模式

多个中断

并行分支各有中断时,用 ID 映射恢复值

思考题:注册流程需要分步收集姓名、年龄、爱好三项信息。一个节点里有多个 interrupt(),怎么逐个恢复?
import { isInterrupted, INTERRUPT } from "@langchain/langgraph";

// 两个并行节点各有一个 interrupt
const interruptedResult = await graph.invoke({ vals: [] }, config);
// { __interrupt__: [{ id: '...', value: 'question_a' }, { id: '...', value: 'question_b' }] }

// 一次恢复所有中断
const resumeMap: Record<string, string> = {};
if (isInterrupted(interruptedResult)) {
  for (const i of interruptedResult[INTERRUPT]) {
    if (i.id != null) {
      resumeMap[i.id] = `answer for ${i.value}`;
    }
  }
}
const result = await graph.invoke(new Command({ resume: resumeMap }), config);

📄 完整代码:08_multiple_interrupts.ts

五条铁律

违反任何一条都会导致中断行为异常

规则 1:不要用 try/catch 包裹 interrupt
interrupt 通过抛出异常来暂停执行。try/catch 会捕获这个异常,导致图无法正确暂停。
规则 2:不要在节点内重排 interrupt 调用
interrupt 的匹配是基于索引的。如果节点内有多个 interrupt,恢复时按顺序匹配。调换顺序会导致值错位。
规则 3:只传可 JSON 序列化的值
不要传函数、Symbol、循环引用等复杂对象。interrupt 的值会被序列化保存。
规则 4:interrupt 前的副作用必须幂等
恢复时节点会从头重新执行。interrupt 之前的代码会重新运行。如果之前发了 HTTP 请求,恢复时会再发一次。
规则 5:必须启用 checkpointer 并固定 thread_id
interrupt 依赖持久化层保存状态、等待恢复。没有 checkpointer 就无法暂停;恢复时若不用和暂停时相同的 thread_id,将读不到中断现场。

排错指南

interrupt 没暂停

症状:调用了 interrupt() 但图继续执行了。

修复:检查是否被 try/catch 包裹了。另外确认编译时传了 checkpointer——没有 checkpointer 就没有持久化,中断无法工作。

恢复后拿到错误的值

症状:resume 传入 true,但 interrupt() 返回的不是 true。

修复:确认使用同一个 thread_id。不同线程的中断互不影响。

中断后无法恢复

症状:invoke(new Command({ resume })) 报错。

修复:必须用相同的 thread_id 和配置。中断信息绑定在特定线程上。

动手练习

构建一个带审批门的文档发布流程

任务:构建一个图,包含"生成文档 → 人工审批 → 发布/拒绝"三步。
  • 节点 1:生成文档内容
  • 节点 2:interrupt() 暂停,展示文档给人类审批
  • 如果审批通过 → 节点 3:发布文档
  • 如果审批拒绝 → 回到节点 1,附带拒绝理由重新生成

参考答案

const State = new StateSchema({
  topic: z.string(),
  content: z.string(),
  feedback: z.string().optional(),
});

// 节点 1:生成文档
const generateDoc: GraphNode<typeof State> = async (state) => {
  const prompt = state.feedback
    ? `Rewrite doc about ${state.topic}. Feedback: ${state.feedback}`
    : `Write a doc about ${state.topic}`;
  const msg = await model.invoke(prompt);
  return { content: msg.content };
};

// 节点 2:审批
const review: GraphNode<typeof State> = (state) => {
  const decision = interrupt({ content: state.content, question: "Approve?" });
  if (decision.approved) return new Command({ goto: "publish" });
  return { feedback: decision.feedback };
};
模式:interrupt 获取人类决策 → Command(goto) 路由到不同节点。拒绝时附带 feedback,生成节点根据 feedback 重新创作。

本章回顾

到这里,你已经能

  • 使用 interrupt() 在任意点暂停执行
  • 使用 Command({ resume }) 恢复执行
  • 实现审批、审查、输入验证模式
  • 处理并行分支中的多个中断
  • 避免 interrupt 的五条铁律

它和前后章节的关系

核心结论:interrupt() 让人类成为智能体决策环的一部分。高风险操作必须经过人类确认,缺失信息可以向人类询问。但记住五条铁律——违反任何一条都会导致意外行为。

延伸阅读

继续本课程

相关章节