中断机制允许您在特定点暂停图执行,并在继续之前等待外部输入。这实现了人机协同模式,在需要外部输入才能继续时非常有用。当中断被触发时,LangGraph 会使用其持久化层保存图状态,并无限期等待直到您恢复执行。
中断通过在图的任何节点中调用 interrupt() 函数来工作。该函数接受任何可 JSON 序列化的值,这些值会暴露给调用者。当您准备好继续时,通过使用 Command 重新调用图来恢复执行,该值随后成为节点内部 interrupt() 调用的返回值。
与静态断点(在特定节点之前或之后暂停)不同,中断是动态的——它们可以放在代码中的任何位置,并且可以根据应用程序逻辑有条件地触发。
- 检查点保持您的位置: 检查点写入确切的图状态,因此您可以在以后恢复,即使在错误状态下也是如此。
thread_id 是您的指针: 使用 { configurable: { thread_id: ... } } 作为 invoke 方法的选项,告诉检查点器要加载哪个状态。
- 中断负载以
__interrupt__ 形式暴露: 您传递给 interrupt() 的值会在 __interrupt__ 字段中返回给调用者,以便您知道图在等待什么。
您选择的 thread_id 实际上是您的持久化游标。重复使用它会恢复相同的检查点;使用新值会启动一个具有空状态的全新线程。
使用 interrupt 暂停
interrupt 函数暂停图执行并向调用者返回一个值。当您在节点内调用 interrupt 时,LangGraph 会保存当前图状态并等待您提供输入来恢复执行。
要使用 interrupt,您需要:
- 一个检查点器来持久化图状态(在生产环境中使用持久化检查点器)
- 配置中的线程 ID,以便运行时知道从哪个状态恢复
- 在想要暂停的地方调用
interrupt()(负载必须是可 JSON 序列化的)
import { interrupt } from "@langchain/langgraph";
async function approvalNode(state: State) {
// 暂停并请求批准
const approved = interrupt("您是否批准此操作?");
// Command({ resume: ... }) 提供返回到此变量的值
return { approved };
}
当您调用 interrupt 时,会发生以下情况:
- 图执行在
interrupt 调用的确切点被挂起
- 状态被保存使用检查点器,以便以后可以恢复执行,在生产环境中,这应该是一个持久化检查点器(例如由数据库支持)
- 值返回给调用者在
__interrupt__ 下;它可以是任何可 JSON 序列化的值(字符串、对象、数组等)
- 图无限期等待直到您使用响应恢复执行
- 响应在恢复时传递回节点,成为
interrupt() 调用的返回值
恢复中断
中断暂停执行后,您可以通过再次调用图并使用包含恢复值的 Command 来恢复图。恢复值会传递回 interrupt 调用,允许节点继续执行并使用外部输入。
import { Command } from "@langchain/langgraph";
// 初始运行 - 遇到中断并暂停
// thread_id 是指回已保存检查点的持久化指针
const config = { configurable: { thread_id: "thread-1" } };
const result = await graph.invoke({ input: "data" }, config);
// 检查中断了什么
// __interrupt__ 镜像您传递给 interrupt() 的每个负载
console.log(result.__interrupt__);
// [{ value: '您是否批准此操作?', ... }]
// 使用人工响应恢复
// Command({ resume }) 从节点中的 interrupt() 返回该值
await graph.invoke(new Command({ resume: true }), config);
关于恢复的关键点:
- 恢复时必须使用与中断发生时相同的线程 ID
- 传递给
Command(resume=...) 的值成为 interrupt 调用的返回值
- 节点在恢复时从调用
interrupt 的节点开头重新启动,因此 interrupt 之前的任何代码都会再次运行
- 您可以将任何可 JSON 序列化的值作为恢复值传递
常见模式
中断机制的关键能力是能够暂停执行并等待外部输入。这对于各种用例非常有用,包括:
- 审批工作流:在执行关键操作(API 调用、数据库更改、金融交易)之前暂停
- 审查和编辑状态:让人类在继续之前审查和修改 LLM 输出或工具调用
- 中断工具调用:在执行工具调用之前暂停,以审查和编辑工具调用
- 验证人工输入:在继续下一步之前暂停以验证人工输入
批准或拒绝
中断最常见的用途之一是在关键操作之前暂停并请求批准。例如,您可能希望请求人类批准 API 调用、数据库更改或任何其他重要决策。
import { interrupt, Command } from "@langchain/langgraph";
function approvalNode(state: State): Command {
// 暂停执行;负载在 result.__interrupt__ 中暴露
const isApproved = interrupt({
question: "您要继续吗?",
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);
import {
Command,
MemorySaver,
START,
END,
StateGraph,
interrupt,
} from "@langchain/langgraph";
import * as z from "zod";
const State = z.object({
actionDetails: z.string(),
status: z.enum(["pending", "approved", "rejected"]).nullable(),
});
const graphBuilder = new StateGraph(State)
.addNode("approval", async (state) => {
// 暴露详细信息,以便调用者可以在 UI 中呈现它们
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);
// 在生产环境中使用更持久的检查点器
const checkpointer = new MemorySaver();
const graph = graphBuilder.compile({ checkpointer });
const config = { configurable: { thread_id: "approval-123" } };
const initial = await graph.invoke(
{ actionDetails: "转账 500 美元", status: "pending" },
config,
);
console.log(initial.__interrupt__);
// [{ value: { question: ..., details: ... } }]
// 使用决策恢复;true 路由到 proceed,false 路由到 cancel
const resumed = await graph.invoke(new Command({ resume: true }), config);
console.log(resumed.status); // -> "approved"
审查和编辑状态
有时您希望让人类在继续之前审查和编辑部分图状态。这对于纠正 LLM、添加缺失信息或进行调整非常有用。
import { interrupt } from "@langchain/langgraph";
function reviewNode(state: State) {
// 暂停并显示当前内容以供审查(在 result.__interrupt__ 中暴露)
const editedContent = interrupt({
instruction: "审查并编辑此内容",
content: state.generatedText
});
// 使用编辑后的版本更新状态
return { generatedText: editedContent };
}
恢复时,提供编辑后的内容:
await graph.invoke(
new Command({ resume: "编辑和改进后的文本" }), // 值成为 interrupt() 的返回
config
);
import {
Command,
MemorySaver,
START,
END,
StateGraph,
interrupt,
} from "@langchain/langgraph";
import * as z from "zod";
const State = z.object({
generatedText: z.string(),
});
const builder = new StateGraph(State)
.addNode("review", async (state) => {
// 请求审查者编辑生成的内容
const updated = interrupt({
instruction: "审查并编辑此内容",
content: state.generatedText,
});
return { generatedText: updated };
})
.addEdge(START, "review")
.addEdge("review", END);
const checkpointer = new MemorySaver();
const graph = builder.compile({ checkpointer });
const config = { configurable: { thread_id: "review-42" } };
const initial = await graph.invoke({ generatedText: "初始草稿" }, config);
console.log(initial.__interrupt__);
// [{ value: { instruction: ..., content: ... } }]
// 使用审查者编辑后的文本恢复
const finalState = await graph.invoke(
new Command({ resume: "审查后的改进草稿" }),
config,
);
console.log(finalState.generatedText); // -> "审查后的改进草稿"
工具中的中断
您也可以将中断直接放在工具函数内部。这使得工具本身在每次调用时都会暂停等待批准,并允许在工具执行之前进行人工审查和编辑工具调用。
首先,定义一个使用 interrupt 的工具:
import { tool } from "@langchain/core/tools";
import { interrupt } from "@langchain/langgraph";
import * as z from "zod";
const sendEmailTool = tool(
async ({ to, subject, body }) => {
// 在发送前暂停;负载在 result.__interrupt__ 中暴露
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;
return `邮件已发送至 ${finalTo},主题为 '${finalSubject}'`;
}
return "邮件已被用户取消";
},
{
name: "send_email",
description: "向收件人发送邮件",
schema: z.object({
to: z.string(),
subject: z.string(),
body: z.string(),
}),
},
);
当您希望批准逻辑与工具本身共存时,这种方法非常有用,使其可在图的不同部分重复使用。LLM 可以自然地调用工具,中断会在工具被调用时暂停执行,允许您批准、编辑或取消操作。
import { tool } from "@langchain/core/tools";
import { ChatAnthropic } from "@langchain/anthropic";
import {
Command,
MemorySaver,
START,
END,
StateGraph,
interrupt,
} from "@langchain/langgraph";
import * as z from "zod";
const sendEmailTool = tool(
async ({ to, subject, body }) => {
// 在发送前暂停;负载在 result.__interrupt__ 中暴露
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 model = new ChatAnthropic({ model: "claude-sonnet-4-5" }).bindTools([sendEmailTool]);
const Message = z.object({
role: z.enum(["user", "assistant", "tool"]),
content: z.string(),
});
const State = z.object({
messages: z.array(Message),
});
const graphBuilder = new StateGraph(State)
.addNode("agent", async (state) => {
// LLM 可能决定调用工具;中断在发送前暂停
const response = await model.invoke(state.messages);
return { messages: [...state.messages, response] };
})
.addEdge(START, "agent")
.addEdge("agent", END);
const checkpointer = new MemorySaver();
const graph = graphBuilder.compile({ checkpointer });
const config = { configurable: { thread_id: "email-workflow" } };
const initial = await graph.invoke(
{
messages: [
{ role: "user", content: "向 alice@example.com 发送关于会议的邮件" },
],
},
config,
);
console.log(initial.__interrupt__); // -> [{ value: { action: 'send_email', ... } }]
// 使用批准和可选编辑的参数恢复
const resumed = await graph.invoke(
new Command({
resume: { action: "approve", subject: "更新后的主题" },
}),
config,
);
console.log(resumed.messages.at(-1)); // -> send_email 返回的工具结果
验证人工输入
有时您需要验证来自人类的输入,并在输入无效时再次询问。您可以在循环中使用多个 interrupt 调用来实现这一点。
import { interrupt } from "@langchain/langgraph";
function getAgeNode(state: State) {
let prompt = "您的年龄是多少?";
while (true) {
const answer = interrupt(prompt); // 负载在 result.__interrupt__ 中暴露
// 验证输入
if (typeof answer === "number" && answer > 0) {
// 有效输入 - 继续
return { age: answer };
} else {
// 无效输入 - 使用更具体的提示再次询问
prompt = `'${answer}' 不是有效的年龄。请输入一个正数。`;
}
}
}
每次您使用无效输入恢复图时,它都会使用更清晰的消息再次询问。一旦提供有效输入,节点完成并且图继续。
import {
Command,
MemorySaver,
START,
END,
StateGraph,
interrupt,
} from "@langchain/langgraph";
import * as z from "zod";
const State = z.object({
age: z.number().nullable(),
});
const builder = new StateGraph(State)
.addNode("collectAge", (state) => {
let prompt = "您的年龄是多少?";
while (true) {
const answer = interrupt(prompt); // 负载在 result.__interrupt__ 中暴露
if (typeof answer === "number" && answer > 0) {
return { age: answer };
}
prompt = `'${answer}' 不是有效的年龄。请输入一个正数。`;
}
})
.addEdge(START, "collectAge")
.addEdge("collectAge", END);
const checkpointer = new MemorySaver();
const graph = builder.compile({ checkpointer });
const config = { configurable: { thread_id: "form-1" } };
const first = await graph.invoke({ age: null }, config);
console.log(first.__interrupt__); // -> [{ value: "您的年龄是多少?", ... }]
// 提供无效数据;节点重新提示
const retry = await graph.invoke(new Command({ resume: "三十" }), config);
console.log(retry.__interrupt__); // -> [{ value: "'三十' 不是有效的年龄...", ... }]
// 提供有效数据;循环退出且状态更新
const final = await graph.invoke(new Command({ resume: 30 }), config);
console.log(final.age); // -> 30
中断规则
当您在节点内调用 interrupt 时,LangGraph 通过引发异常来暂停执行,该异常通知运行时暂停。此异常通过调用堆栈向上传播,并被运行时捕获,该运行时通知图保存当前状态并等待外部输入。
当执行恢复时(在您提供请求的输入之后),运行时从节点开头重新启动整个节点——它不会从调用 interrupt 的确切行恢复。这意味着在 interrupt 之前运行的任何代码将再次执行。因此,在使用中断时需要遵循一些重要规则,以确保它们按预期行为。
不要将 interrupt 调用包装在 try/catch 中
interrupt 通过在调用点抛出特殊异常来暂停执行。如果将 interrupt 调用包装在 try/catch 块中,您将捕获此异常,中断将不会传递回图。
- ✅ 将
interrupt 调用与易出错代码分开
- ✅ 如果需要,有条件地捕获错误
async function nodeA(state: State) {
// ✅ 良好:先中断,然后单独处理错误条件
const name = interrupt("您叫什么名字?");
try {
await fetchData(); // 这可能失败
} catch (err) {
console.error(error);
}
return state;
}
- 🔴 不要将
interrupt 调用包装在裸 try/catch 块中
async function nodeA(state: State) {
// ❌ 不良:将 interrupt 包装在裸 try/catch 中将捕获中断异常
try {
const name = interrupt("您叫什么名字?");
} catch (err) {
console.error(error);
}
return state;
}
不要在节点内重新排序 interrupt 调用
在单个节点中使用多个中断很常见,但如果不小心处理,可能会导致意外行为。
当节点包含多个中断调用时,LangGraph 会为执行节点的任务维护一个特定的恢复值列表。每当执行恢复时,它从节点的开头开始。对于遇到的每个中断,LangGraph 检查任务恢复列表中是否存在匹配的值。匹配是严格基于索引的,因此节点内中断调用的顺序很重要。
async function nodeA(state: State) {
// ✅ 良好:中断调用每次以相同顺序发生
const name = interrupt("您叫什么名字?");
const age = interrupt("您的年龄是多少?");
const city = interrupt("您所在的城市是?");
return {
name,
age,
city
};
}
- 🔴 不要有条件地跳过节点内的
interrupt 调用
- 🔴 不要使用在跨执行中非确定性的逻辑来循环
interrupt 调用
async function nodeA(state: State) {
// ❌ 不良:有条件地跳过中断会改变顺序
const name = interrupt("您叫什么名字?");
// 第一次运行时,这可能会跳过中断
// 恢复时,它可能不会跳过 - 导致索引不匹配
if (state.needsAge) {
const age = interrupt("您的年龄是多少?");
}
const city = interrupt("您所在的城市是?");
return { name, city };
}
不要在 interrupt 调用中返回复杂值
根据使用的检查点器,复杂值可能无法序列化(例如,您无法序列化函数)。为了使您的图能够适应任何部署,最佳实践是仅使用可以合理序列化的值。
- ✅ 将简单的、可 JSON 序列化的类型传递给
interrupt
- ✅ 传递包含简单值的字典/对象
async function nodeA(state: State) {
// ✅ 良好:传递可序列化的简单类型
const name = interrupt("您叫什么名字?");
const count = interrupt(42);
const approved = interrupt(true);
return { name, count, approved };
}
- 🔴 不要将函数、类实例或其他复杂对象传递给
interrupt
function validateInput(value: string): boolean {
return value.length > 0;
}
async function nodeA(state: State) {
// ❌ 不良:将函数传递给 interrupt
// 函数无法序列化
const response = interrupt({
question: "您叫什么名字?",
validator: validateInput // 这将失败
});
return { name: response };
}
interrupt 之前调用的副作用必须是幂等的
因为中断通过重新调用它们被调用的节点来工作,所以在 interrupt 之前调用的副作用应该(理想情况下)是幂等的。上下文中的幂等性意味着相同的操作可以应用多次而不会改变初始执行之外的结果。
例如,您可能有一个在节点内部更新记录的 API 调用。如果在进行该调用之后调用 interrupt,则在节点恢复时将多次重新运行它,可能会覆盖初始更新或创建重复记录。
- ✅ 在
interrupt 之前使用幂等操作
- ✅ 将副作用放在
interrupt 调用之后
- ✅ 尽可能将副作用分离到单独的节点中
async function nodeA(state: State) {
// ✅ 良好:使用幂等的 upsert 操作
// 多次运行此操作将具有相同的结果
await db.upsertUser({
userId: state.userId,
status: "pending_approval"
});
const approved = interrupt("批准此更改?");
return { approved };
}
- 🔴 不要在
interrupt 之前执行非幂等操作
- 🔴 不要在不检查是否存在的情况下创建新记录
async function nodeA(state: State) {
// ❌ 不良:在中断之前创建新记录
// 这将在每次恢复时创建重复记录
const auditId = await db.createAuditLog({
userId: state.userId,
action: "pending_approval",
timestamp: new Date()
});
const approved = interrupt("批准此更改?");
return { approved, auditId };
}
与作为函数调用的子图一起使用
当在节点内调用子图时,父图将从调用子图并触发 interrupt 的节点开头恢复执行。类似地,子图也将从调用 interrupt 的节点开头恢复。
async function nodeInParentGraph(state: State) {
someCode(); // <-- 这将在恢复时重新执行
// 将子图作为函数调用。
// 子图包含一个 `interrupt` 调用。
const subgraphResult = await subgraph.invoke(someInput);
// ...
}
async function nodeInSubgraph(state: State) {
someOtherCode(); // <-- 这也将在恢复时重新执行
const result = interrupt("您叫什么名字?");
// ...
}
使用中断调试
要调试和测试图,您可以使用静态中断作为断点,逐步执行图执行,一次一个节点。静态中断在定义的点触发,要么在节点执行之前,要么在节点执行之后。您可以通过在编译图时指定 interruptBefore 和 interruptAfter 来设置这些断点。
静态中断不推荐用于人机协同工作流。请改用 interrupt 方法。
const graph = builder.compile({
interruptBefore: ["node_a"],
interruptAfter: ["node_b", "node_c"],
checkpointer,
});
// 将线程 ID 传递给图
const config = {
configurable: {
thread_id: "some_thread"
}
};
// 运行图直到断点
await graph.invoke(inputs, config);# [!code highlight]
await graph.invoke(null, config); # [!code highlight]
- 断点在
compile 期间设置。
interruptBefore 指定在执行节点之前应暂停执行的节点。
interruptAfter 指定在执行节点之后应暂停执行的节点。
- 需要检查点器来启用断点。
- 图运行直到遇到第一个断点。
- 通过传入
null 作为输入来恢复图。这将运行图直到遇到下一个断点。
// 运行图直到断点
graph.invoke(inputs, {
interruptBefore: ["node_a"],
interruptAfter: ["node_b", "node_c"],
configurable: {
thread_id: "some_thread"
}
});
// 恢复图
await graph.invoke(null, config);
- 使用
interruptBefore 和 interruptAfter 参数调用 graph.invoke。这是一个运行时配置,可以为每次调用更改。
interruptBefore 指定在执行节点之前应暂停执行的节点。
interruptAfter 指定在执行节点之后应暂停执行的节点。
- 图运行直到遇到第一个断点。
- 通过传入
null 作为输入来恢复图。这将运行图直到遇到下一个断点。
使用 LangGraph Studio
您可以使用 LangGraph Studio 在运行图之前在 UI 中设置静态中断。您还可以使用 UI 在任何执行点检查图状态。