[LangGraph] 中断相关细节

  1. 外界获取中断的值
  2. 用户编辑图状态
  3. 工具内的中断
  4. 验证用户输入

外界获取中断的值

result.__interrupt__ 就是中断时,langgraph 暴露给外界的数据。外界(UI / CLI / 后端系统)就是靠它拿到 interrupt 里传出的内容的。

interrupt({
  // 配置要传递给外界的数据
})

const result = await graph.invoke();
result.__interrupt__
// 演示外界获取中断的值
import {
  Command,
  MemorySaver,
  START,
  END,
  StateGraph,
  interrupt,
} from "@langchain/langgraph";
import { z } from "zod/v4";

// 图的状态定义
const State = z.object({
  actionDetails: z.string().describe("待审批的操作描述"),
  status: z
    .enum(["pending", "approved", "rejected"])
    .nullable()
    .describe("当前审批状态"),
  __interrupt__: z.any().optional(),
});

const checkpointer = new MemorySaver();

const graph = new StateGraph(State)
  // 待审批
  .addNode(
    "approval",
    async (state) => {
      // 首先这里就有一个中断,这里question和details就会传递给外界
      const decision = interrupt({
        question: "是否批准该操作?",
        details: state.actionDetails,
      });

      return new Command({
        goto: decision ? "proceed" : "cancel",
      });
    },
    {
      ends: ["proceed", "cancel"],
    },
  ) // 审批通过
  .addNode("proceed", () => ({
    status: "approved",
  }))
  // 审批不通过
  .addNode("cancel", () => ({
    status: "rejected",
  }))
  .addEdge(START, "approval")
  .addEdge("proceed", END)
  .addEdge("cancel", END)
  .compile({
    checkpointer,
  });

const config = {
  configurable: {
    thread_id: "approval-123",
  },
};

// 第一次执行图,会在中断的地方返回到外界
const result = await graph.invoke(
  {
    actionDetails: "转账 500 元",
    status: "pending",
  },
  config,
);
console.log(result.__interrupt__);

const finalResult = await graph.invoke(
  new Command({
    resume: true,
  }),
  config,
);

console.log(finalResult);

用户编辑图状态

所谓用户编辑状态,就是:

  1. 先中断
  2. 外界接收用户的输入
  3. 恢复图的执行,拿到用户的输入
  4. 更新图的状态
// 演示用户编辑图状态
import {
  Command,
  MemorySaver,
  START,
  END,
  StateGraph,
  interrupt,
} from "@langchain/langgraph";
import { z } from "zod/v4";

// 图的状态定义
const State = z.object({
  generatedText: z.string().describe("当前生成的文本内容"),
  __interrupt__: z.any().optional(),
});

const checkpointer = new MemorySaver();

const graph = new StateGraph(State)
  .addNode("review", async (state) => {
    // 这里会产生一个中断,instruction和content就会传递给外界
    // 外界在恢复图的执行的时候,resume的值就会作为interrupt的返回值
    const updated = interrupt({
      instruction: "请审阅并修改以下内容",
      content: state.generatedText,
    });

    // 使用外界resume的数据来更新图的状态
    return {
      generatedText: updated,
    };
  })
  .addEdge(START, "review")
  .addEdge("review", END)
  .compile({
    checkpointer,
  });
const config = {
  configurable: {
    thread_id: "review-42",
  },
};

await graph.invoke(
  {
    generatedText: "初稿状态",
  },
  config,
);
// console.log(result.__interrupt__);

const result = await graph.invoke(
  new Command({
    resume: "由外界用户输入的数据",
  }),
  config,
);
console.log(result);

工具内中断

不仅图中的节点可以触发 interrupt,连工具在被调用时也可以主动暂停流程、等待人工审批。

也就是说:工具本身也可以有“人工确认步骤”。

为什么工具需要 interrupt?

因为真实企业场景里,有很多“高风险操作”,必须要人工确认。例如:

  1. 场景 1:财务扣款工具,LLM 调用了:
deductMoney({ userId: 1001, amount: 20000 });

在工具内部判断:

if (amount > 5000) {
       interrupt({ value: "是否确认扣款 20000 元?" });
   }

系统暂停 → 人类确认 → resume → 扣款继续执行。

  1. 场景 2:发版工具 / 发布配置,工具内部:
interrupt({
       value: "发布将会影响线上用户,是否继续?"
   });

避免 LLM 意外触发危险操作。

  1. 场景 3:修改数据库、删除文件等不可逆操作,工具内部:

    if (operation === "delete") {
        interrupt({ value: "确认删除文件吗?" });
    }
    
// 演示工具中断
import { tool } from "@langchain/core/tools";
import {
  Command,
  MemorySaver,
  START,
  END,
  StateGraph,
  interrupt,
} from "@langchain/langgraph";
import { z } from "zod/v4";
import "dotenv/config";

// 这是一个发送邮件的工具
const sendEmailTool = tool(
  async ({ to, subject, body }) => {
    // 在这个工具的工具逻辑内部,首先就产生了一个中断
    const response = interrupt({
      action: "send_email",
      to,
      subject,
      body,
      message: "是否确认发送这封邮件?",
    });

    if (response?.action === "approve") {
      const finalTo = response.to ?? to;
      const finalSubject = response.subject ?? subject;
      const finalBody = response.body ?? body;

      console.log("[sendEmailTool]", finalTo, finalSubject, finalBody);

      return `邮件已发送给 ${finalTo}`;
    }

    return "邮件已被用户取消";
  },
  {
    name: "send_email",
    description: "向指定收件人发送一封邮件",
    schema: z.object({
      to: z.string(),
      subject: z.string(),
      body: z.string(),
    }),
  },
);

const Message = z.object({
  role: z.enum(["user", "system"]),
  content: z.string(),
});

// 图的状态
const State = z.object({
  messages: z.array(Message),
  __interrupt__: z.any().optional(),
});

const checkpointer = new MemorySaver();
const graph = new StateGraph(State)
  .addNode("sendEmail", async (state) => {
    // 这里直接调用工具
    const result = await sendEmailTool.invoke({
      to: "alice@example.com",
      subject: "会议通知",
      body: "请确认今天下午的会议安排。",
    });

    return {
      messages: [...state.messages, { role: "system", content: result }],
    };
  })
  .addEdge(START, "sendEmail")
  .addEdge("sendEmail", END)
  .compile({
    checkpointer,
  });

const config = {
  configurable: {
    thread_id: "email-workflow",
  },
};

// 第一次执行图,会在执行工具的时候,产生中断
const result = await graph.invoke(
  {
    messages: [
      {
        role: "user",
        content: "发一封会议邮件",
      },
    ],
  },
  config,
);
console.log(result.__interrupt__);

// 恢复图的执行
const finalResult = await graph.invoke(
  new Command({
    resume: {
      action: "approve",
      subject: "【已确认】会议通知",
    },
  }),
  config,
);
console.log(finalResult);

验证用户输入

用户输入信息后,可以验证用户的输入是否符合要求,如果不符合,就反复中断回到外界,让用户重新输入。

// 演示验证用户输入
import {
  Command,
  MemorySaver,
  START,
  END,
  StateGraph,
  interrupt,
} from "@langchain/langgraph";
import { z } from "zod/v4";

// 图的状态定义
const State = z.object({
  age: z.number().nullable().describe("用户年龄"),
});

const checkpointer = new MemorySaver();

const graph = new StateGraph(State)
  .addNode("collectAge", () => {
    let prompt = "请输入你的年龄";

    while (true) {
      // 先产生中断
      const answer = interrupt(prompt);

      if (typeof answer === "number" && answer > 0) {
        // 如果符合要求,才结束这个节点
        return { age: answer };
      }

      prompt = `“${answer}”不是有效的年龄,请输入一个大于 0 的数字`;
    }
  })
  .addEdge(START, "collectAge")
  .addEdge("collectAge", END)
  .compile({
    checkpointer,
  });

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

const first = await graph.invoke({}, config);
console.log("first>>>", first);

const second = await graph.invoke(
  new Command({
    resume: "二十",
  }),
  config,
);
console.log("second>>>", second);

// 再次恢复图的执行
const third = await graph.invoke(
  new Command({
    resume: 20,
  }),
  config,
);
console.log("third>>>", third);

posted @ 2026-03-19 14:26  Zhentiw  阅读(4)  评论(0)    收藏  举报