Skip to main content
中断允许您在特定点暂停图执行,并在继续之前等待外部输入。这使得人机回环模式成为可能,当您需要外部输入才能继续时,此模式非常有用。当触发中断时,LangGraph 会使用其持久化层保存图状态,并无限期等待,直到您恢复执行。 中断通过在图节点中的任何位置调用 interrupt() 函数来工作。该函数接受任何 JSON 可序列化的值,该值会呈现给调用者。当您准备好继续时,通过使用 Command 重新调用图来恢复执行,然后该值将成为节点内部 interrupt() 调用的返回值。 与静态断点(在特定节点之前或之后暂停)不同,中断是动态的:它们可以放置在代码中的任何位置,并且可以根据您的应用程序逻辑有条件地触发。
  • 检查点保存您的位置:检查点写入确切的图状态,以便您稍后可以恢复,即使处于错误状态。
  • thread_id 是您的指针:使用 { configurable: { thread_id: ... } } 作为 invoke 方法的选项,告诉检查点加载哪个状态。
  • 中断有效负载显示为 __interrupt__:您传递给 interrupt() 的值会返回到调用者的 __interrupt__ 字段中,以便您知道图正在等待什么。
您选择的 thread_id 实际上是您的持久化游标。重用它会恢复相同的检查点;使用新值会启动一个具有空状态的全新线程。

使用 interrupt 暂停

interrupt 函数暂停图执行并向调用者返回一个值。当您在节点内调用 interrupt 时,LangGraph 会保存当前图状态,并等待您使用输入恢复执行。 要使用 interrupt,您需要:
  1. 一个检查点器来持久化图状态(在生产中使用持久的检查点器)
  2. 配置中的线程 ID,以便运行时知道从哪个状态恢复
  3. 在您想要暂停的位置调用 interrupt()(有效负载必须是 JSON 可序列化的)
import { interrupt } from "@langchain/langgraph";

async function approvalNode(state: State) {
    // 暂停并请求批准
    const approved = interrupt("Do you approve this action?");

    // Command({ resume: ... }) 提供返回到此变量的值
    return { approved };
}
当您调用 interrupt 时,会发生以下情况:
  1. 图执行在调用 interrupt 的确切点被挂起
  2. 状态使用检查点器保存,以便稍后可以恢复执行。在生产中,这应该是一个持久的检查点器(例如,由数据库支持)
  3. 值在 __interrupt__ 下返回给调用者;它可以是任何 JSON 可序列化的值(字符串、对象、数组等)
  4. 图无限期等待,直到您使用响应恢复执行
  5. 当您恢复时,响应被传回节点,成为 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: 'Do you approve this action?', ... }]

// 使用人类的响应恢复
// Command({ resume }) 从节点中的 interrupt() 返回该值
await graph.invoke(new Command({ resume: true }), config);
关于恢复的关键点:
  • 恢复时必须使用与中断发生时相同的线程 ID
  • 传递给 new Command({ resume: ... }) 的值成为 interrupt 调用的返回值
  • 恢复时,节点从调用 interrupt 的节点开头重新启动,因此 interrupt 之前的任何代码都会再次运行
  • 您可以传递任何 JSON 可序列化的值作为恢复值
new Command({ resume: ... })唯一旨在作为 invoke()/stream() 输入的 Command 模式。其他 Command 参数(updategotograph)设计用于从节点函数返回。不要将 new Command({ update: ... }) 作为输入传递以继续多轮对话——而是传递一个普通的输入对象。

常见模式

中断解锁的关键功能是能够暂停执行并等待外部输入。这对于各种用例非常有用,包括:
  • 批准工作流:在执行关键操作(API 调用、数据库更改、金融交易)之前暂停
  • 处理多个中断:在单次调用中恢复多个中断时,将中断 ID 与恢复值配对
  • 审阅和编辑:让人类在继续之前审阅和修改 LLM 输出或工具调用
  • 中断工具调用:在执行工具调用之前暂停,以便在执行前审阅和编辑工具调用
  • 验证人类输入:在继续下一步之前暂停以验证人类输入

使用人机回环 (HITL) 中断进行流式传输

在构建具有人机回环工作流的交互式代理时,您可以同时流式传输消息块和节点更新,以在处理中断时提供实时反馈。 使用多个流模式("messages""updates")以及 subgraphs=True(如果存在子图),以便:
  • 实时流式传输 AI 响应
  • 检测图何时遇到中断
  • 无缝处理用户输入并恢复执行
  • Command(resume=...):使用用户提供的数据恢复图执行

处理多个中断

当并行分支同时中断时(例如,分发到多个节点,每个节点都调用 interrupt()),您可能需要在单次调用中恢复多个中断。 当在单次调用中恢复多个中断时,将每个中断 ID 映射到其恢复值。 这确保每个响应在运行时与正确的中断配对。
import {
  Annotation,
  Command,
  END,
  INTERRUPT,
  MemorySaver,
  START,
  StateGraph,
  interrupt,
  isInterrupted,
} from "@langchain/langgraph";

const State = Annotation.Root({
  vals: Annotation<string[]>({
    reducer: (left, right) =>
      left.concat(Array.isArray(right) ? right : [right]),
    default: () => [],
  }),
});

function nodeA(_state: typeof State.State) {
  const answer = interrupt("question_a") as string;
  return { vals: [`a:${answer}`] };
}

function nodeB(_state: typeof State.State) {
  const answer = interrupt("question_b") as string;
  return { vals: [`b:${answer}`] };
}

const graph = new StateGraph(State)
  .addNode("a", nodeA)
  .addNode("b", nodeB)
  .addEdge(START, "a")
  .addEdge(START, "b")
  .addEdge("a", END)
  .addEdge("b", END)
  .compile({ checkpointer: new MemorySaver() });

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

async function main() {
  // 步骤 1: 调用 - 两个并行节点触达 interrupt() 并暂停
  const interruptedResult = await graph.invoke({ vals: [] }, config);
  console.log(interruptedResult);
  /*
  {
    vals: [],
    __interrupt__: [
      { id: '...', value: 'question_a' },
      { id: '...', value: 'question_b' }
    ]
  }
  */

  // 步骤 2: 一次性恢复所有挂起的中断
  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);

  console.log("Final state:", result);
  //> Final state: { vals: ['a:answer for question_a', 'b:answer for question_b'] }
}

main().catch(console.error);

批准或拒绝

中断最常见的用途之一是在关键操作之前暂停并请求批准。例如,您可能希望让人类批准 API 调用、数据库更改或任何其他重要决策。
import { interrupt, Command } from "@langchain/langgraph";

const approvalNode: typeof State.Node = (state) => {
  // 暂停执行;有效负载显示在 result.__interrupt__ 中
  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);
import {
  Command,
  MemorySaver,
  START,
  END,
  StateGraph,
  StateSchema,
  interrupt,
} from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  actionDetails: z.string(),
  status: z.enum(["pending", "approved", "rejected"]).nullable(),
});

const graphBuilder = new StateGraph(State)
  .addNode("approval", async (state) => {
    // 暴露详细信息,以便调用者可以在 UI 中呈现它们
    const decision = interrupt({
      question: "Approve this action?",
      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: "Transfer $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";

const reviewNode: typeof State.Node = (state) => {
  // 暂停并显示当前内容以供审阅(显示在 result.__interrupt__ 中)
  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" }), // 值成为 interrupt() 的返回值
  config
);
import {
  Command,
  MemorySaver,
  START,
  END,
  StateGraph,
  StateSchema,
  interrupt,
} from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  generatedText: z.string(),
});

const builder = new StateGraph(State)
  .addNode("review", async (state) => {
    // 要求审阅者编辑生成的内容
    const updated = interrupt({
      instruction: "Review and edit this content",
      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: "Initial draft" }, config);
console.log(initial.__interrupt__);
// [{ value: { instruction: ..., content: ... } }]

// 使用审阅者编辑后的文本恢复
const finalState = await graph.invoke(
  new Command({ resume: "Improved draft after review" }),
  config,
);
console.log(finalState.generatedText); // -> "Improved draft after review"

工具中的中断

您也可以直接在工具函数中放置中断。这使得工具本身在每次调用时暂停以请求批准,并允许在执行前对工具调用进行人类审阅和编辑。 首先,定义一个使用 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: "Approve sending this email?",
    });

    if (response?.action === "approve") {
      // 恢复值可以在执行前覆盖输入
      const finalTo = response.to ?? to;
      const finalSubject = response.subject ?? subject;
      const finalBody = response.body ?? body;
      return `Email sent to ${finalTo} with subject '${finalSubject}'`;
    }
    return "Email cancelled by user";
  },
  {
    name: "send_email",
    description: "Send an email to a recipient",
    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,
  StateSchema,
  MessagesValue,
  GraphNode,
  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: "Approve sending this email?",
    });

    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 `Email sent to ${finalTo}`;
    }
    return "Email cancelled by user";
  },
  {
    name: "send_email",
    description: "Send an email to a recipient",
    schema: z.object({
      to: z.string(),
      subject: z.string(),
      body: z.string(),
    }),
  },
);

const model = new ChatAnthropic({ model: "claude-sonnet-4-6" }).bindTools([sendEmailTool]);

const State = new StateSchema({
  messages: MessagesValue,
});

const agent: typeof State.Node = async (state) => {
  // LLM 可能决定调用工具;中断在发送前暂停
  const response = await model.invoke(state.messages);
  return { messages: [response] };
};

const graphBuilder = new StateGraph(State)
  .addNode("agent", agent)
  .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: "Send an email to alice@example.com about the meeting" },
    ],
  },
  config,
);
console.log(initial.__interrupt__); // -> [{ value: { action: 'send_email', ... } }]

// 使用批准和可选的编辑参数恢复
const resumed = await graph.invoke(
  new Command({
    resume: { action: "approve", subject: "Updated subject" },
  }),
  config,
);
console.log(resumed.messages.at(-1)); // -> send_email 返回的工具结果

验证人类输入

有时您需要验证来自人类的输入,并在无效时再次询问。您可以使用循环中的多个 interrupt 调用来实现这一点。
import { interrupt } from "@langchain/langgraph";

const getAgeNode: typeof State.Node = (state) => {
  let prompt = "What is your age?";

  while (true) {
    const answer = interrupt(prompt); // 有效负载显示在 result.__interrupt__ 中

    // 验证输入
    if (typeof answer === "number" && answer > 0) {
      // 有效输入 - 继续
      return { age: answer };
    } else {
      // 无效输入 - 使用更具体的提示再次询问
      prompt = `'${answer}' is not a valid age. Please enter a positive number.`;
    }
  }
}
每次您使用无效输入恢复图时,它都会使用更清晰的消息再次询问。一旦提供有效输入,节点完成,图继续执行。
import {
  Command,
  MemorySaver,
  START,
  END,
  StateGraph,
  StateSchema,
  interrupt,
} from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  age: z.number().nullable(),
});

const builder = new StateGraph(State)
  .addNode("collectAge", (state) => {
    let prompt = "What is your age?";

    while (true) {
      const answer = interrupt(prompt); // 有效负载显示在 result.__interrupt__ 中

      if (typeof answer === "number" && answer > 0) {
        return { age: answer };
      }

      prompt = `'${answer}' is not a valid age. Please enter a positive number.`;
    }
  })
  .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: "What is your age?", ... }]

// 提供无效数据;节点重新提示
const retry = await graph.invoke(new Command({ resume: "thirty" }), config);
console.log(retry.__interrupt__); // -> [{ value: "'thirty' is not a valid age...", ... }]

// 提供有效数据;循环退出并更新状态
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 调用与易出错的代码分开
  • ✅ 如果需要,有条件地捕获错误
const nodeA: GraphNode<typeof State> = async (state) => {
  // ✅ 好:先中断,然后单独处理错误条件
  const name = interrupt("What's your name?");
  try {
    await fetchData(); // 这可能会失败
  } catch (err) {
    console.error(error);
  }
  return state;
}
  • 🔴 不要在裸 try/catch 块中包裹 interrupt 调用
async function nodeA(state: State) {
    // ❌ 坏:在裸 try/catch 中包裹中断会捕获中断异常
    try {
        const name = interrupt("What's your name?");
    } catch (err) {
        console.error(error);
    }
    return state;
}

不要在节点内重新排序 interrupt 调用

在单个节点中使用多个中断很常见,但如果处理不当,可能会导致意外行为。 当节点包含多个中断调用时,LangGraph 会维护一个特定于执行节点的任务的恢复值列表。每当执行恢复时,它都会从节点开头开始。对于遇到的每个中断,LangGraph 会检查任务的恢复列表中是否存在匹配值。匹配是严格基于索引的,因此节点内中断调用的顺序很重要。
  • ✅ 保持 interrupt 调用在节点执行之间一致
async function nodeA(state: State) {
    // ✅ 好:中断调用每次都以相同的顺序发生
    const name = interrupt("What's your name?");
    const age = interrupt("What's your age?");
    const city = interrupt("What's your city?");

    return {
        name,
        age,
        city
    };
}
  • 🔴 不要在节点内有条件地跳过 interrupt 调用
  • 🔴 不要使用在执行之间非确定性的逻辑循环 interrupt 调用
const nodeA: GraphNode<typeof State> = async (state) => {
  // ❌ 坏:有条件地跳过中断会改变顺序
  const name = interrupt("What's your name?");

  // 在第一次运行时,这可能会跳过中断
  // 在恢复时,它可能不会跳过 - 导致索引不匹配
  if (state.needsAge) {
    const age = interrupt("What's your age?");
  }

  const city = interrupt("What's your city?");

  return { name, city };
}

不要在 interrupt 调用中返回复杂值

根据使用的检查点器,复杂值可能无法序列化(例如,您无法序列化函数)。为了使您的图适应任何部署,最佳实践是仅使用可以合理序列化的值。
  • ✅ 将简单的、JSON 可序列化的类型传递给 interrupt
  • ✅ 传递具有简单值的字典/对象
const nodeA: GraphNode<typeof State> = async (state) => {
  // ✅ 好:传递可序列化的简单类型
  const name = interrupt("What's your name?");
  const count = interrupt(42);
  const approved = interrupt(true);

  return { name, count, approved };
}
  • 🔴 不要将函数、类实例或其他复杂对象传递给 interrupt
function validateInput(value: string): boolean {
    return value.length > 0;
}

const nodeA: GraphNode<typeof State> = async (state) => {
  // ❌ 坏:将函数传递给中断
  // 函数无法序列化
  const response = interrupt({
    question: "What's your name?",
    validator: validateInput  // 这将失败
  });
  return { name: response };
}

interrupt 之前调用的副作用必须是幂等的

因为中断通过重新运行它们被调用的节点来工作,所以在 interrupt 之前调用的副作用应该是(理想情况下)幂等的。就上下文而言,幂等性意味着相同的操作可以应用多次,而不会改变初始执行之外的结果。 例如,您可能在节点内有一个更新记录的 API 调用。如果在调用该调用之后调用 interrupt,则在节点恢复时,它将被多次重新运行,可能会覆盖初始更新或创建重复记录。
  • ✅ 在 interrupt 之前使用幂等操作
  • ✅ 将副作用放在 interrupt 调用之后
  • ✅ 如果可能,将副作用分离到单独的节点中
const nodeA: GraphNode<typeof State> = async (state) => {
  // ✅ 好:使用幂等的 upsert 操作
  // 多次运行将产生相同的结果
  await db.upsertUser({
    userId: state.userId,
    status: "pending_approval"
  });

  const approved = interrupt("Approve this change?");

  return { approved };
}
  • 🔴 不要在 interrupt 之前执行非幂等操作
  • 🔴 不要在未检查是否存在的情况下创建新记录
const nodeA: GraphNode<typeof State> = async (state) => {
  // ❌ 坏:在中断之前创建新记录
  // 这将在每次恢复时创建重复记录
  const auditId = await db.createAuditLog({
    userId: state.userId,
    action: "pending_approval",
    timestamp: new Date()
  });

  const approved = interrupt("Approve this change?");

  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("What's your name?");
    // ...
}

使用中断进行调试

要调试和测试图,您可以使用静态断点作为断点,以逐步执行图执行,一次一个节点。静态断点在节点执行之前或之后的定义点触发。您可以通过在编译图时指定 interruptBeforeinterruptAfter 来设置这些。
静态断点推荐用于人机回环工作流。请改用 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]
  1. 断点在 compile 时设置。
  2. interruptBefore 指定在节点执行之前应暂停执行的节点。
  3. interruptAfter 指定在节点执行之后应暂停执行的节点。
  4. 需要检查点器才能启用断点。
  5. 图运行直到触达第一个断点。
  6. 通过为输入传递 null 来恢复图。这将运行图直到触达下一个断点。
要调试您的中断,请使用 LangSmith

使用 LangSmith Studio

您可以使用 LangSmith Studio 在 UI 中设置图中的静态断点,然后再运行图。您还可以使用 UI 在执行过程中的任何点检查图状态。 image