Install any skill in seconds. Free to start, no credit card required.
Get Started Free →人机交互与动态中断和断点:暂停执行以供人工审查和使用 Command 恢复
.claude/skills/majiayu000-langgraph-interrupts/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-08 | ✗→✓ | ▲ Improved | 1% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 26% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 62% | 0% |
| case-21 | ✓→✓ | = Same ✓ | 57% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 41% | 0% |
name: langgraph-interrupts description: 人机交互与动态中断和断点 - 暂停执行以供人工审查和使用 Command 恢复
中断通过暂停图执行以等待外部输入来实现人机交互模式。LangGraph 保存状态并无限期等待,直到您恢复执行。
关键类型:
interrupt() 函数interruptBefore/interruptAfter| 类型 | 设置时机 | 使用场景 | |------|----------|----------| | 动态 (interrupt()) | 在节点代码内 | 基于逻辑的条件暂停 | | 静态 (interruptBefore) | 在编译时 | 在特定节点之前调试/测试 | | 静态 (interruptAfter) | 在编译时 | 在特定节点之后审查输出 |
typescriptimport { interrupt, Command } from "@langchain/langgraph"; import { MemorySaver } from "@langchain/langgraph"; const reviewNode = async (state) => { // 有条件地暂停以供审查 if (state.needsReview) { // 暂停并向用户展示数据 const userResponse = interrupt({ action: "review", data: state.draft, question: "Approve this draft?", }); // userResponse 来自 Command({ resume: ... }) if (userResponse === "reject") { return { status: "rejected" }; } } return { status: "approved" }; }; const checkpointer = new MemorySaver(); const graph = new StateGraph(State) .addNode("review", reviewNode) .addEdge(START, "review") .addEdge("review", END) .compile({ checkpointer }); // 必需! // 初始调用 - 将暂停 const config = { configurable: { thread_id: "1" } }; const result = await graph.invoke( { needsReview: true, draft: "content" }, config ); // 检查中断 if ("__interrupt__" in result) { console.log(result.__interrupt__); // 查看中断负载 } // 使用用户决策恢复 const finalResult = await graph.invoke( new Command({ resume: "approve" }), // 用户的响应 config );
typescriptconst checkpointer = new MemorySaver(); const graph = new StateGraph(State) .addNode("step1", step1) .addNode("step2", step2) .addNode("step3", step3) .addEdge(START, "step1") .addEdge("step1", "step2") .addEdge("step2", "step3") .addEdge("step3", END) .compile({ checkpointer, interruptBefore: ["step2"], // 在 step2 之前暂停 interruptAfter: ["step3"], // 在 step3 之后暂停 }); const config = { configurable: { thread_id: "1" } }; // 运行到第一个断点 await graph.invoke({ data: "test" }, config); // 恢复(在下一个断点暂停) await graph.invoke(null, config); // null = 恢复 // 再次恢复 await graph.invoke(null, config);
typescriptimport { interrupt, Command } from "@langchain/langgraph"; const toolExecutor = async (state) => { const toolCalls = state.messages.at(-1)?.tool_calls || []; const results = []; for (const toolCall of toolCalls) { // 为每个工具调用暂停 const userDecision = interrupt({ tool: toolCall.name, args: toolCall.args, question: "Execute this tool?", }); let result; if (userDecision.type === "approve") { // 执行工具 result = await executeTool(toolCall); } else if (userDecision.type === "edit") { // 使用编辑后的参数 result = await executeTool(userDecision.args); } else { // reject result = "Tool execution rejected"; } // 存储结果 results.push(new ToolMessage({ content: result, tool_call_id: toolCall.id, })); } return { messages: results }; }; // 使用 const result = await graph.invoke({ messages: [...] }, config); // 审查并批准 await graph.invoke(new Command({ resume: { type: "approve" } }), config); // 或编辑参数 await graph.invoke( new Command({ resume: { type: "edit", args: { query: "modified" } } }), config ); // 或拒绝 await graph.invoke(new Command({ resume: { type: "reject" } }), config);
typescriptconst config = { configurable: { thread_id: "1" } }; // 运行到中断 await graph.invoke({ data: "test" }, config); // 在恢复之前修改状态 await graph.updateState(config, { data: "manually edited" }); // 使用编辑后的状态恢复 await graph.invoke(null, config);
typescriptconst config = { configurable: { thread_id: "1" }, streamMode: ["updates", "messages"] as const, }; for await (const [mode, chunk] of await graph.stream({ query: "test" }, config)) { if (mode === "updates") { if ("__interrupt__" in chunk) { // 处理中断 const interruptInfo = chunk.__interrupt__[0].value; const userInput = await getUserInput(interruptInfo); // 恢复 await graph.invoke(new Command({ resume: userInput }), config); break; } } }
✅ 在节点中的任何位置调用 interrupt() ✅ 设置编译时断点 ✅ 使用 Command({ resume: ... }) 恢复 ✅ 在中断期间编辑状态 ✅ 在处理中断时流式传输 ✅ 条件中断逻辑
❌ 在没有检查点器的情况下中断 ❌ 修改中断机制 ❌ 在没有 thread_id 的情况下恢复
typescript// ❌ 错误 - 没有检查点器 const graph = builder.compile(); // 没有持久化! await graph.invoke(...); // 中断不起作用 // ✅ 正确 const checkpointer = new MemorySaver(); const graph = builder.compile({ checkpointer });
typescript// ❌ 错误 - 没有 thread_id await graph.invoke({ data: "test" }); // 无法恢复! // ✅ 正确 const config = { configurable: { thread_id: "session-1" } }; await graph.invoke({ data: "test" }, config);
typescript// ❌ 错误 - 传递常规对象 await graph.invoke({ resumeData: "approve" }, config); // 重新开始! // ✅ 正确 - 使用 Command import { Command } from "@langchain/langgraph"; await graph.invoke(new Command({ resume: "approve" }), config);
typescript// ❌ 错误 const result = graph.invoke({}, config); console.log(result); // Promise! // ✅ 正确 const result = await graph.invoke({}, config); console.log(result);
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-04 | pass→pass | 10,257 | 10,652 | +4% | 1 | 1 | 0% | 1,862 | 3,025 | +62% | 0 | 0 | — |
case-21 | pass→pass | 11,261 | 7,409 | -34% | 1 | 1 | 0% | 2,178 | 3,429 | +57% | 0 | 0 | — |
case-01 | pass→pass | 15,881 | 9,606 | -40% | 1 | 1 | 0% | 2,017 | 2,843 | +41% | 0 | 0 | — |
case-02 | pass→pass | 15,009 | 9,366 | -38% | 1 | 1 | 0% | 1,853 | 2,758 | +49% | 0 | 0 | — |
case-03 | pass→pass | 11,668 | 6,790 | -42% | 1 | 1 | 0% | 1,911 | 3,307 | +73% | 0 | 0 | — |
case-05 | pass→pass | 15,178 | 10,571 | -30% | 1 | 1 | 0% | 1,847 | 2,934 | +59% | 0 | 0 | — |
case-06 | pass→pass | 20,023 | 19,555 | -2% | 1 | 1 | 0% | 3,727 | 4,945 | +33% | 0 | 0 | — |
case-07 | pass→pass | 4,928 | 8,115 | +65% | 1 | 1 | 0% | 901 | 2,493 | +177% | 0 | 0 | — |
case-08 | fail→pass | 18,902 | 8,505 | -55% | 1 | 1 | 0% | 3,411 | 3,461 | +1% | 0 | 0 | — |
case-09 | pass→pass | 11,588 | 3,812 | -67% | 1 | 1 | 0% | 1,206 | 2,533 | +110% | 0 | 0 | — |
case-10 | pass→pass | 11,623 | 8,390 | -28% | 1 | 1 | 0% | 1,205 | 2,538 | +111% | 0 | 0 | — |
case-11 | pass→pass | 19,940 | 12,428 | -38% | 1 | 1 | 0% | 2,506 | 4,149 | +66% | 0 | 0 | — |
case-12 | pass→pass | 16,037 | 13,969 | -13% | 1 | 1 | 0% | 2,760 | 3,510 | +27% | 0 | 0 | — |
case-13 | pass→pass | 6,306 | 3,849 | -39% | 1 | 1 | 0% | 1,102 | 2,641 | +140% | 0 | 0 | — |
case-14 | pass→pass | 18,917 | 8,279 | -56% | 1 | 1 | 0% | 2,336 | 3,281 | +40% | 0 | 0 | — |
case-15 | pass→pass | 11,758 | 3,871 | -67% | 1 | 1 | 0% | 1,002 | 2,525 | +152% | 0 | 0 | — |
case-16 | fail→pass | 13,521 | 6,443 | -52% | 1 | 1 | 0% | 2,476 | 3,114 | +26% | 0 | 0 | — |
case-17 | pass→pass | 15,268 | 7,333 | -52% | 1 | 1 | 0% | 2,698 | 3,260 | +21% | 0 | 0 | — |
case-18 | pass→pass | 5,960 | 9,621 | +61% | 1 | 1 | 0% | 1,006 | 2,709 | +169% | 0 | 0 | — |
case-19 | pass→pass | 7,391 | 9,881 | +34% | 1 | 1 | 0% | 1,488 | 2,956 | +99% | 0 | 0 | — |
case-20 | pass→pass | 16,277 | 9,318 | -43% | 1 | 1 | 0% | 2,214 | 3,857 | +74% | 0 | 0 | — |
case-22 | pass→pass | 8,569 | 8,988 | +5% | 1 | 1 | 0% | 442 | 2,670 | +504% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.