惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
Forbes - Security
Forbes - Security
Help Net Security
Help Net Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Hacker News: Ask HN
Hacker News: Ask HN
TaoSecurity Blog
TaoSecurity Blog
IT之家
IT之家
Microsoft Azure Blog
Microsoft Azure Blog
云风的 BLOG
云风的 BLOG
博客园 - 司徒正美
B
Blog
阮一峰的网络日志
阮一峰的网络日志
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Google DeepMind News
Google DeepMind News
Microsoft Security Blog
Microsoft Security Blog
The Register - Security
The Register - Security
美团技术团队
C
CERT Recently Published Vulnerability Notes
I
Intezer
C
Cybersecurity and Infrastructure Security Agency CISA
Google Online Security Blog
Google Online Security Blog
B
Blog RSS Feed
PCI Perspectives
PCI Perspectives
C
Cyber Attacks, Cyber Crime and Cyber Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
MyScale Blog
MyScale Blog
S
Securelist
Recorded Future
Recorded Future
Know Your Adversary
Know Your Adversary
Security Archives - TechRepublic
Security Archives - TechRepublic
M
MIT News - Artificial intelligence
C
Check Point Blog
T
Threat Research - Cisco Blogs
博客园 - Franky
P
Proofpoint News Feed
人人都是产品经理
人人都是产品经理
U
Unit 42
F
Fortinet All Blogs
S
Security @ Cisco Blogs
The GitHub Blog
The GitHub Blog
Apple Machine Learning Research
Apple Machine Learning Research
MongoDB | Blog
MongoDB | Blog
The Hacker News
The Hacker News
酷 壳 – CoolShell
酷 壳 – CoolShell
F
Full Disclosure
aimingoo的专栏
aimingoo的专栏
大猫的无限游戏
大猫的无限游戏
D
Docker
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
L
LINUX DO - 热门话题

博客园 - Zhentiw

[GenAI] Migration Plan: Flat API → Layered Architecture [Skill] Frontend Design Skill Claude Code Hooks: Complete Guide [GenAI] Pre-retieval overview [GenAI] About Indexing [GenAI] Indexing overview [Vibe Coding] 降低大模型幻觉 - 重试机制 [Vibe coding] 降低大模型幻觉 - JSON 安全输出提示词 [Node.js] WebSocket基础知识 [LangGraph] 应用结构 [LangGrpah] Unit testing [LangGraph] Functional API [LangGraph] 中断注意事项 [LangGraph] 中断相关细节 [LangGrpah] 静态断点 [LangGraph] 阻塞式中断 [LangGraph] 语义搜索 [LangGraph] 长期记忆 [LangGraph] 管理短期记忆 [LangGraph] 自定义checkpointer [LangGraph] 短期记忆 [LangGraph] 时间旅行 [LangGraph] checkpoint常用API [LangGraph] 元数据标记 [LangGraph] 流 [Vitest] mockClear, mockReset, mockRestore [LangGraph] 将子图添加为节点
[LangGrpah] 非阻塞式中断
Zhentiw · 2026-03-16 · via 博客园 - Zhentiw

基本介绍

langgraph中的中断分为两种:

  1. 静态断点
  2. 动态中断

1. 静态断点

在编译图时配置。适用于“在某个节点执行必须暂停”的场景。

  • interrupt_before=["node_name"]: 在指定节点开始执行前暂停。
  • interrupt_after=["node_name"]: 在指定节点执行完毕后暂停。
  • 位置:定义在图的结构上(边与节点的连接处)。
  • 控制粒度:节点级。要么整个节点不执行,要么执行完暂停,不能在节点函数的某一行暂停。

2. 动态中断

这是一种比较推荐的方式。

该方式是在节点内部代码中使用 interrupt() 函数。适用于更灵活的逻辑,例如“只有当置信度低于 0.8 时才请求人工介入”。

  • 位置:定义在节点函数内部(Runtime)。
  • 控制粒度:语句级。可以在函数逻辑的任意位置暂停,并且可以根据逻辑判断决定是否暂停。

工作流程:

  • 使用 interrupt 函数。
  • 图执行到该行代码时会暂停,并抛出中断信号。
  • 恢复时,用户提供的数据会作为 interrupt() 函数的返回值赋给变量。

示意代码如下:

const humanInput = interrupt({
  value: "请审核邮件…"
});

interrupt做了3件关键事情:

  1. 立即停止图的执行器
  2. 将当前图状态保存在 checkpointer 中
  3. 返回控制权给 main,结束 graph.stream

也就是说:interrupt 是异步的、可恢复的、线程安全的暂停点。

之后可以通过 Command 做恢复:

new Command({ resume: "approve" })

这就是langgraph提供的中断机制。

对于前面场景案例中的企业审批系统而言,使用langgraph的中断机制能够做到:

  • 审批未完成可挂起 24 小时
  • 审批流状态可存数据库
  • 服务重启后可继续
  • 多审批节点(一级 → 二级 → 三级)
  • 流程可回放
  • 用户掉线后继续执行
  1. 上节课实现的阻塞式中断的版本,会导致:
    1. 程序卡死、等待输入
    2. 图不能继续
    3. 系统也不能干别的,直到人类输入为止
  2. 使用啦langgraph中的中断机制(有一点像浏览器的事件循环机制)
    1. 图暂停
    2. 系统不暂停
    3. 图不会继续执行后续节点,但系统外部是自由流动的

快速上手

好了,了解了langgraph所提供的中断的意义后,接下来我们来看一下具体该如何使用。

使用中断时会和checkpointer以及 thread 配合着一起使用。

  1. checkpointer:中断的前提是能“记住”当前的状态。checkpointer负责在每一步执行后将图的状态持久化保存到数据库或内存中,没有checkpointer,图暂停后就会丢失状态,无法恢复。
  2. thread id:用于区分不同的对话或执行流。当需要恢复一个中断的图时,必须提供相同的 thread_id,以便langgraph找到之前的状态。

除此之外,还会用到Command.

当执行因为 interrupt 而暂停后,可以通过再次调用图并传入包含恢复值的 Command 来恢复执行。这个恢复值会作为 interrupt() 的返回值传回,使得节点能够继续执行,并使用外部输入作为上下文。

import {
  StateGraph,
  START,
  END,
  interrupt,
  MemorySaver,
  Command,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import readlineSync from "readline-sync";
import { z } from "zod/v4";
import "dotenv/config";

// 图的状态
const Schema = z.object({
  subject: z.string().default("").describe("邮件主题"),
  message: z.string().default("").describe("邮件内容"),
  feedback: z.string().default("").describe("反馈"),
});

// 根据Schema生成的ts类型
type TState = z.infer<typeof Schema>;

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
});

let hasShownCurrentEmail = false; // 是否要显示当前生成的email

// 节点:1. 写邮件的节点  2. 用户审查节点  3. 发送邮件
async function writeEmail(state: TState) {
  console.log("AI: 正在生成/修改邮件内容...");

  // 提示词
  const lines: string[] = [
    "你是一个专业的中文邮件撰写助手。",
    "请用自然、礼貌、简洁的中文撰写邮件正文。",
    "只输出邮件正文内容本身,不要输出额外的解释说明。",
  ];

  if (!state.message || !state.feedback) {
    // 说明是初次生成邮件内容,当前只有主题
    lines.push(`邮件主题:${state.subject}`);
    lines.push("请根据以上主题撰写第一版邮件正文。");
  } else if (state.feedback !== "approve") {
    // 说明有修改意见,需要根据上一版的正文 + 用户的反馈意见来进行修改
    lines.push("下面是上一版邮件正文:");
    lines.push(state.message);
    lines.push("下面是用户给出的修改意见:");
    lines.push(state.feedback);
    lines.push(
      "请严格根据修改意见,在保留合理内容的前提下,重写一封新的邮件正文,只输出修改后的完整邮件内容。"
    );
  }

  const pt = lines.join("\n");

  const result = await model.invoke(pt);

  const content =
    typeof result.content === "string"
      ? result.content
      : JSON.stringify(result.content);

  return {
    message: content,
  };
}

async function humanReview(state: TState) {
  // 先将模型生成的邮件内容显示出来
  if (!hasShownCurrentEmail) {
    console.log("\n===== 当前 AI 生成的邮件内容 =====\n");
    console.log(state.message);
    console.log("\n系统: 等待人类审核...\n");
    hasShownCurrentEmail = true;
  }

  // const input = readlineSync.question(
  //   "是否发送?请输入 'approve' 表示发送,或输入你的修改意见:"
  // );

  // value是在图中断的时候,返回给外界的信息
  const input = interrupt({});

  console.log(`\n\n用户的反馈为:${input}`);

  // 这里代表着一轮反馈已经处理完了
  hasShownCurrentEmail = false;

  return {
    feedback: input,
  };
}

function sendEmail(state: TState) {
  console.log("\n===== 模拟发送邮件 =====");
  const to = "demo@example.com";
  console.log(`收件人: ${to}`);
  console.log(`主题: ${state.subject}`);
  console.log("正文:\n");
  console.log(state.message);
  console.log("\n[模拟] 邮件已发送!");
  return {};
}

const checkpointer = new MemorySaver();
const config = {
  configurable: {
    thread_id: "send-email",
  },
};

// 构建图
const graph = new StateGraph(Schema)
  .addNode("writeEmail", writeEmail)
  .addNode("humanReview", humanReview)
  .addNode("sendEmail", sendEmail)
  .addEdge(START, "writeEmail")
  .addEdge("writeEmail", "humanReview")
  .addConditionalEdges("humanReview", (state: TState) => {
    if (state.feedback === "approve") return "sendEmail";
    return "writeEmail";
  })
  .addEdge("sendEmail", END)
  .compile({
    checkpointer,
  });

async function main() {
  const subject = readlineSync.question("请输入邮件的主题:");

  console.log(
    "\n===== 开始:大模型根据主题生成邮件,并支持多次人工修改 =====\n"
  );

  // 第一次执行图,这个图就会在 interrupt 的地方暂停
  const stream = await graph.stream({ subject }, config);

  for await (const _e of stream) {
  }

  // 接下来就会回到这里的主逻辑,也就意味着主逻辑继续往后面执行
  while (true) {
    // 不断的接受用户的反馈
    const input = readlineSync.question(
      "\n是否发送?输入 'approve' 表示发送;否则输入修改意见:"
    );

    console.log("\n拿到用户的输入,接下来就需要恢复图的执行\n");

    const stream = await graph.stream(
      // 恢复图的执行
      // 注意:在恢复图的执行的时候,是将中断的那个节点函数,一整个重新执行一次
      new Command({
        resume: input,
      }),
      config
    );

    for await (const _ of stream) {
    }

    if (input === "approve") {
      // 说明上面在恢复图的执行的时候,图的执行是会结束的
      console.log("\n===== 人类已批准,流程结束 =====\n");
      break;
    }

    // 如果没有进入上面的if,说明后面图又会重新执行一遍
    // 图的执行同样又会中断
    console.log(
      "\n===== 已根据修改意见生成新版邮件,将再次进入人工审核环节 ====="
    );
  }

  console.log("\n===== 流程结束 =====");
}

main();