Skip to content

容错处理(Fault tolerance) ​

在 LangGraph 中配置单节点超时、重试和错误处理程序。

当节点发生故障时(由于缓慢的外部 API、瞬时网络错误或未处理的异常),LangGraph 为你提供了三种可组合的机制来应对:

使用 setNodeDefaults 为所有节点一次性配置这些机制,而不必在每次 addNode 调用时重复它们。

这些机制按固定的顺序组合:当节点尝试引发任何异常(包括因超时引发的 NodeTimeoutError)时,重试策略决定是否重试。只有在重试耗尽后,错误处理程序才会运行。

关于在超步边界整洁地停止运行并在稍后恢复,请参阅优雅关闭。

INFO

单节点超时和节点级错误处理程序需要 @langchain/langgraph>=1.4.0。

重试(Retries) ​

重试策略根据异常类型和退避设置自动重试失败的节点尝试。

将 retryPolicy 传递给 addNode:

typescript
import { StateGraph } from "@langchain/langgraph";

const graph = new StateGraph(State)
  .addNode("callApi", callApi, { retryPolicy: { maxAttempts: 3 } })
  .compile();

默认行为 ​

重试是可选的。节点仅在配置了 retryPolicy 时重试(可以直接配置,也可以通过带有 setNodeDefaults 的图默认设置)。空策略({})即可生效。如果没有策略,首次失败将终止尝试,并且 LangGraph 不会调用 retryOn。

如果策略省略了 retryOn,LangGraph 会使用内置的处理程序重试抛出的错误,但以下错误除外:

  • 中止和取消错误:error.name === "AbortError",或者 error.message 以 "Cancel" 或 "AbortError" 开头
  • GraphValueError,按 error.name 匹配
  • 中止的连接:error.code === "ECONNABORTED"
  • 状态码为 400、401、402、403、404、405、406、407 或 409 的 HTTP 客户端错误,从 fetch、Axios 及类似客户端的 error.response?.status 或 error.status 中读取
  • OpenAI 风格的配额错误:error.error?.code === "insufficient_quota"

除非你重写 retryOn,否则其他 HTTP 状态(包括 408 和 5xx 响应)均可重试。NodeTimeoutError 不在此黑名单中,因此在配置了重试策略时它可以重试。

某些故障会绕过 retryOn。图控制流错误(如 GraphInterrupt 和 Command 路由)无需重试即可冒泡。中止运行信号也会停止重试循环,即使 retryOn 返回 true。

参数 ​

参数类型默认值说明
maxAttemptsnumber3最大尝试次数(包括首次尝试)。
initialIntervalnumber500首次重试前的毫秒数。
backoffFactornumber2.0每次重试后应用于间隔的乘数。
maxIntervalnumber128000重试之间的最大毫秒数。
jitterbooleantrue为间隔添加随机抖动。
retryOn(error: unknown) => boolean内置处理程序(设置策略时)返回 true 表示可重试异常的可调用函数。仅在配置了 retryPolicy 时使用。
logWarningbooleantrue尝试重试时是否记录警告日志。

自定义重试逻辑 ​

将可调用函数传递给 retryOn。与 Python 不同,JavaScript 没有导出的 defaultRetryOn 辅助函数——请实现自己的谓词:

typescript
import { StateGraph } from "@langchain/langgraph";

class MyCustomError extends Error {}

const graph = new StateGraph(State)
  .addNode("callApi", callApi, {
    retryPolicy: {
      maxAttempts: 3,
      retryOn: (error: unknown) => {
        if (error instanceof MyCustomError) return false;
        // 其他错误重试
        return true;
      },
    },
  })
  .compile();

检查重试状态 ​

在节点内使用执行信息来检查当前的尝试次数。当主调用持续失败时,这对于切换到回退方案很有用:

typescript
import { StateGraph, StateSchema, START, END, type Runtime } from "@langchain/langgraph";
import * as z from "zod";

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

const myNode = async (state: typeof State.State, runtime: Runtime<typeof State>) => {
  if ((runtime.executionInfo?.nodeAttempt ?? 1) > 1) {
    return { result: await callFallbackApi() };
  }
  return { result: await callPrimaryApi() };
};

const graph = new StateGraph(State)
  .addNode("myNode", myNode, { retryPolicy: { maxAttempts: 3 } })
  .addEdge(START, "myNode")
  .addEdge("myNode", END)
  .compile();

executionInfo 暴露以下字段:

属性类型说明
nodeAttemptnumber当前尝试次数(从 1 开始)。首次尝试为 1,首次重试为 2,依此类推。
nodeFirstAttemptTimenumber | undefined首次尝试开始时的 Unix 时间戳(毫秒)。在重试之间保持恒定。
threadIdstring | undefined当前执行的线程 ID。没有检查点时为 undefined。
runIdstring | undefined当前执行的运行 ID。配置中未提供时为 undefined。
checkpointIdstring当前执行的检查点 ID。
checkpointNsstring当前执行的检查点命名空间。
taskIdstring当前执行的任务 ID。

即使没有重试策略,executionInfo 也可用——nodeAttempt 默认为 1。

超时(Timeouts) ​

INFO

需要 @langchain/langgraph>=1.4.0。

addNode 上的 timeout 参数限制单个节点尝试可以运行的最长时间。传递数字(毫秒)或 TimeoutPolicy 以分别指定运行和空闲限制:

typescript
import { StateGraph, type TimeoutPolicy } from "@langchain/langgraph";

// 简单的挂钟上限(60 秒)
new StateGraph(State).addNode("callModel", callModel, { timeout: 60_000 });

// 分开的运行与空闲限制
new StateGraph(State).addNode("callModel", callModel, {
  timeout: { runTimeout: 120_000, idleTimeout: 30_000 },
});

运行超时(Run timeout) ​

runTimeout 是单次尝试的硬性挂钟上限。无论节点活动如何,它都不会被刷新:

typescript
const graph = new StateGraph(State)
  .addNode("callModel", callModel, {
    timeout: { runTimeout: 120_000 },
  })
  .compile();

当超出限制时,LangGraph 会引发 NodeTimeoutError,清除失败尝试中的所有写入,并让重试策略决定是否重试。

空闲超时(Idle timeout) ​

idleTimeout 是进度重置上限。它仅在节点在指定的持续时间内停止产生可观察的进度时才会触发——与 runTimeout 不同,每当节点产生进度信号时,时钟就会重置:

typescript
const graph = new StateGraph(State)
  .addNode("callModel", callModel, {
    timeout: { idleTimeout: 30_000 },
  })
  .compile();

你可以同时设置 runTimeout 和 idleTimeout。无论哪一个先触发,都会取消尝试。

进度信号 ​

在默认的 refreshOn: "auto" 下,空闲时钟在以下任何情况发生时都会重置:

  • 通过图写入路径进行的状态写入
  • 通过 runtime.writer 输出的自定义流
  • 子任务调度
  • 来自节点或其子级的任何 LangChain 回调事件(LLM Token、工具调用、链启动/结束等)

心跳模式 ​

将 refreshOn: "heartbeat" 设置为仅通过显式的 runtime.heartbeat() 调用缩小刷新源。当你想要一个严格的空闲定义且不被频繁的下属活动重置时,这很有用:

typescript
const graph = new StateGraph(State)
  .addNode("callModel", callModel, {
    timeout: { idleTimeout: 30_000, refreshOn: "heartbeat" },
  })
  .compile();

手动心跳 ​

对于不会自然发出进度信号的长时间运行工作,请调用 runtime.heartbeat() 手动重置空闲时钟:

typescript
import {
  StateGraph,
  StateSchema,
  START,
  END,
  type Runtime,
} from "@langchain/langgraph";
import * as z from "zod";

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

const longRunningNode = async (
  state: typeof State.State,
  runtime: Runtime<typeof State>
) => {
  for (const batch of fetchBatches()) {
    process(batch);
    runtime.heartbeat?.();
  }
  return { result: "done" };
};

const graph = new StateGraph(State)
  .addNode("longRunningNode", longRunningNode, {
    timeout: { idleTimeout: 30_000, refreshOn: "heartbeat" },
  })
  .addEdge(START, "longRunningNode")
  .addEdge("longRunningNode", END)
  .compile();

在空闲计时尝试之外,runtime.heartbeat() 为空操作(no-op),因此你可以无条件调用它。

NodeTimeoutError ​

当超时触发时,LangGraph 会引发 NodeTimeoutError,并带有关于达到哪个限制的结构化上下文:

属性类型说明
nodestring执行超时的节点名称。
elapsednumber超时触发前经过的毫秒数。
kind"idle" | "run"触发了哪种超时。
timeoutnumber触发的超时值(毫秒)。
idleTimeoutnumber | undefined配置的空闲超时(毫秒),如果有的话。
runTimeoutnumber | undefined配置的运行超时(毫秒),如果有的话。

在 TypeScript 中使用 isNodeTimeoutError(error) 来收窄捕获的错误。

NodeTimeoutError 默认是可重试的。将 timeout 与重试策略结合使用开箱即用——超时时钟在每次新尝试时重置,并且在下一次重试之前清除超时尝试的写入:

typescript
const graph = new StateGraph(State)
  .addNode("callModel", callModel, {
    timeout: { idleTimeout: 30_000 },
    retryPolicy: { maxAttempts: 3 },
  })
  .compile();

使用 Send 动态设置超时 ​

当使用 Send 动态分发节点时(例如在 Map-Reduce 模式中),你可以直接在 Send 上传递超时,以针对该特定推送覆盖目标节点的静态超时:

typescript
import { Send } from "@langchain/langgraph";

const fanOut = (state: typeof State.State) =>
  state.items.map(
    (item) =>
      new Send("processItem", { item }, { timeout: { idleTimeout: 15_000 } })
  );

如果在 Send 上省略了超时,则应用目标节点的超时(在 addNode 时设置)。这使你可以在节点上设置默认超时,并在单个调用中将其收紧。

错误处理(Error handling) ​

INFO

需要 @langchain/langgraph>=1.4.0。

错误处理程序在节点失败且所有重试耗尽后运行。它接收当前状态,并可以使用 Command 对其进行更新或路由到不同的节点。这对于补偿流(Saga 模式)很有用,在这种流中你希望优雅地恢复而不是中止整个图。

将 errorHandler 仅传递给 StateGraph 上的 addNode(而不是基础 Graph 类):

typescript
import {
  StateGraph,
  StateSchema,
  START,
  Command,
  NodeError,
} from "@langchain/langgraph";
import * as z from "zod";

class ConnectionError extends Error {}

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

const chargePayment = () => {
  throw new Error("payment gateway timeout");
};

const paymentErrorHandler = (
  state: typeof State.State,
  error: NodeError
) =>
  new Command({
    update: { status: `compensated: ${error.error.message}` },
    goto: "finalize",
  });

const finalize = (state: typeof State.State) => state;

const graph = new StateGraph(State)
  .addNode("chargePayment", chargePayment, {
    retryPolicy: {
      maxAttempts: 3,
      retryOn: (err) => err instanceof ConnectionError,
    },
    errorHandler: paymentErrorHandler,
  })
  .addNode("finalize", finalize)
  .addEdge(START, "chargePayment")
  .compile();

处理程序仅在重试策略耗尽后触发,或者在未配置重试策略时立即触发。重试策略和错误处理程序保持解耦:分别独立配置何时重试以及何时补偿。

NodeError ​

错误处理程序通过类型化 error: NodeError 参数接收故障上下文:

typescript
import { Command, NodeError } from "@langchain/langgraph";

const myHandler = (state: typeof State.State, error: NodeError) => {
  console.log(`Node ${error.node} failed with: ${error.error.message}`);
  return new Command({
    update: { status: "recovered" },
    goto: "nextStep",
  });
};

NodeError 是一个包含两个字段的类:

属性类型说明
nodestring执行失败的节点名称。
errorError失败节点引发的异常。

error: NodeError 参数是可选的。不需要故障上下文的处理程序可以省略第二个参数,仅接收 state。

使用 Command 进行路由 ​

错误处理程序可以返回 Command 来更新状态并路由到特定节点,从而启用 Saga / 补偿模式:

typescript
import {
  StateGraph,
  StateSchema,
  START,
  Command,
  NodeError,
} from "@langchain/langgraph";
import * as z from "zod";

class ConnectionError extends Error {}

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

const reserveInventory = () => ({ status: "reserved" });

const chargePayment = () => {
  throw new Error("payment timeout");
};

const paymentErrorHandler = (
  state: typeof State.State,
  error: NodeError
) =>
  new Command({
    update: {
      status: `compensated_after_${error.node}: ${error.error.message}`,
    },
    goto: "finalize",
  });

const finalize = (state: typeof State.State) => state;

const graph = new StateGraph(State)
  .addNode("reserveInventory", reserveInventory)
  .addNode("chargePayment", chargePayment, {
    retryPolicy: {
      maxAttempts: 3,
      retryOn: (err) => err instanceof ConnectionError,
    },
    errorHandler: paymentErrorHandler,
  })
  .addNode("finalize", finalize)
  .addEdge(START, "reserveInventory")
  .addEdge("reserveInventory", "chargePayment")
  .compile();

chargePayment 在 ConnectionError 上最多重试 3 次。如果重试耗尽(或者错误不是 ConnectionError),处理程序将通过更新状态并路由到 finalize 进行补偿,而不是中止图。

恢复安全故障(Resume-safe failures) ​

INFO

故障来源会被记录在检查点中。如果图被中断,或者在节点失败之后但在处理程序完成之前进程崩溃,当图从检查点恢复时,处理程序会看到相同的 NodeError 上下文。

与 interrupt() 的配合行为 ​

WARNING

在节点内部引发的 interrupt() 不会路由到错误处理程序。中断使用 GraphBubbleUp 机制为人工协同工作流暂停图执行,绕过重试策略和错误处理程序。图照常暂停。

子图故障 ​

如果某个节点包装了一个子图,且该子图引发了未处理的异常,则该异常会出现在父节点中。如果父节点有错误处理程序,则处理程序将触发,并在 error.error 中带有子图的异常。

图全局默认设置(Graph defaults) ​

INFO

需要 @langchain/langgraph>=1.4.0。

无需在每次 addNode 调用时重复相同的 retryPolicy、errorHandler、timeout 或 cachePolicy,可以使用 setNodeDefaults 在一个位置配置图范围的默认设置:

typescript
import { StateGraph, START, NodeError } from "@langchain/langgraph";

const defaultErrorHandler = (
  state: typeof State.State,
  error: NodeError
) => ({ status: `handled: ${error.error.message}` });

const graph = new StateGraph(State)
  .setNodeDefaults({
    retryPolicy: { maxAttempts: 3 },
    errorHandler: defaultErrorHandler,
    timeout: { runTimeout: 30_000 },
    cachePolicy: { ttl: 60 },
  })
  .addNode("stepA", stepA)
  .addNode("stepB", stepB)
  .addEdge(START, "stepA")
  .compile();

stepA 和 stepB 现在共享相同的重试策略、错误处理程序、超时和缓存策略,无需任何重复。

优先级 ​

直接传递给 addNode() 的单节点值始终会覆盖由 setNodeDefaults() 设置的默认值。默认值在 compile() 时解析,因此你可以在 addNode() 之前或之后以任何顺序调用 setNodeDefaults():

typescript
import { StateGraph, START } from "@langchain/langgraph";

const graph = new StateGraph(State)
  .setNodeDefaults({ errorHandler: defaultErrorHandler })
  .addNode("stepA", stepA) // 使用 defaultErrorHandler
  .addNode("stepB", stepB, { errorHandler: customErrorHandler }) // 覆盖默认设置
  .addEdge(START, "stepA")
  .compile();

默认错误处理程序 ​

当每次图运行都映射到外部流程(例如后台任务行)并且任何未处理的节点故障都应将该流程标记为失败,而不必在每个 addNode 上重复 errorHandler 时,errorHandler 默认设置特别有价值。当某一步骤需要自己的补偿逻辑时,单个节点处理程序仍然优先:

typescript
import { Command, NodeError, StateGraph, START } from "@langchain/langgraph";

const markProcessFailed = (
  state: typeof State.State,
  error: NodeError
) => {
  // 在以 processId 为键的外部流程行上持久化失败。
  return { status: `failed at ${error.node}: ${error.error.message}` };
};

const refundPayment = (state: typeof State.State, error: NodeError) =>
  new Command({
    update: { status: `compensated after ${error.node}` },
    goto: "finalize",
  });

const graph = new StateGraph(State)
  .setNodeDefaults({
    retryPolicy: { maxAttempts: 3 },
    errorHandler: markProcessFailed,
  })
  .addNode("fetchData", fetchData) // 使用 markProcessFailed
  .addNode("chargePayment", chargePayment, {
    errorHandler: refundPayment, // 覆盖图范围的默认设置
  })
  .addNode("finalize", finalize)
  .addEdge(START, "fetchData")
  .addEdge("fetchData", "chargePayment")
  .compile();

如果 fetchData 在重试后失败,将运行 markProcessFailed。如果 chargePayment 在重试后失败,将运行 refundPayment,因为单节点处理程序覆盖了默认设置。

适用性矩阵 ​

并非所有默认设置都适用于所有节点类型。错误处理程序节点(通过 addNode(..., { errorHandler }) 注册的节点)被排除在某些默认设置之外,以防止不安全行为:

setNodeDefaults 参数适用于常规节点适用于错误处理程序节点原因
retryPolicy✅✅发生瞬态故障时应重试处理程序
timeout✅✅卡住的处理程序应像卡住的常规节点一样被取消
errorHandler✅❌处理程序绝不能捕获自身
cachePolicy✅❌缓存处理程序结果是不安全的

作用范围 ​

在父图上设置的默认值不会被子图继承。每个图独立维护自己的默认值。

Functional API ​

timeout 选项在 task 和 entrypoint 上均可用;task 还接受 retry 选项(不是 retryPolicy):

typescript
import { entrypoint, task } from "@langchain/langgraph";

const callApi = task(
  {
    name: "callApi",
    timeout: { idleTimeout: 30_000 },
    retry: { maxAttempts: 3 },
  },
  async (url: string) => {
    const response = await fetch(url);
    return response.text();
  }
);

const myWorkflow = entrypoint(
  { name: "myWorkflow", timeout: 60_000 },
  async (inputs: { url: string }) => {
    return await callApi(inputs.url);
  }
);

行为与 addNode 一致:超时时引发 NodeTimeoutError,缓冲的写入被清除,重试策略决定是否重试。JavaScript/TypeScript SDK 中的 task / entrypoint 上不提供错误处理程序——请改用 StateGraph.addNode(..., { errorHandler })。

优雅关闭(Graceful shutdown) ​

协同关闭允许你在当前超步完成后停止在飞(in-flight)的图运行,并保存可恢复的检查点。这对于处理 SIGTERM 信号或任何需要回收资源而不丢失工作的外部主管系统非常有用。

INFO

需要 @langchain/langgraph>=1.4.0。

创建 RunControl 并将其作为 control 传递给 invoke 或 stream。从任何上下文调用 requestDrain() 以发出应停止运行的信号:

typescript
import { RunControl, GraphDrained } from "@langchain/langgraph";

const control = new RunControl();

// 在信号处理程序或主管系统中:
// control.requestDrain("sigterm");

try {
  const result = await graph.invoke(inputs, { ...config, control });
} catch (e) {
  if (e instanceof GraphDrained) {
    // 图提前停止并保存了检查点。
    // 稍后使用相同的配置恢复。
    console.log(`Drained: ${e.reason}`);
  } else {
    throw e;
  }
}

语义 ​

排空(Drain)是协同的,并在超步之间运行,绝不会抢占已经在运行的工作:

场景行为
节点正在执行中运行至完成。排空在下一个超步生效。
具有重试策略的节点当前正在重试重试循环运行至耗尽或成功。排空在此之后生效。
图在排空的同一时钟刻度(tick)自然完成正常返回。检查 control.drainRequested 以与正常运行区分。
仍有更多超步引发 GraphDrained(reason)。检查点已保存且可恢复。
子图请求排空GraphDrained 冒泡穿过父图并在其自身的下一个超步边界处将其停止。

排空后恢复 ​

使用相同的 thread_id 通过 invoke(null, config) 恢复已排空的运行:

typescript
const result = await graph.invoke(null, config);

在节点内部读取排空状态 ​

通过 runtime 参数访问排空状态,以在到达超步边界之前调整节点行为:

typescript
import { type Runtime } from "@langchain/langgraph";

const myNode = async (state: typeof State.State, runtime: Runtime<typeof State>) => {
  if (runtime.control?.drainRequested) {
    // 跳过高开销工作并返回最小结果
    return { status: "skipped", reason: runtime.control.drainReason };
  }
  return { status: await doWork() };
};

SIGTERM 钩子模式 ​

处理进程关闭的推荐模式:

typescript
import process from "node:process";
import { RunControl, GraphDrained } from "@langchain/langgraph";

const control = new RunControl();
process.on("SIGTERM", () => control.requestDrain("sigterm"));

try {
  const result = await graph.invoke(inputs, { ...config, control });
} catch (e) {
  if (e instanceof GraphDrained) {
    console.log(`graph drained: ${e.reason}`);
    // 下次启动时使用相同的配置恢复
  } else {
    throw e;
  }
}

INFO

requestDrain() 不会取消在飞的异步工作。对于硬上限,请将排空与优雅超时及 AbortSignal 搭配使用。

局限性 ​

  • setNodeDefaults 不会被子图继承:每个图独立管理自己的默认设置。
  • 错误处理程序仅适用于 StateGraph:将 errorHandler 传递给 StateGraph.addNode,而不是基础 Graph 类。错误处理程序在 task / entrypoint 上不可用。
  • 每个节点一个处理程序:每个节点最多可以有一个 errorHandler。
  • 处理程序故障向外冒泡:如果错误处理程序本身抛出异常,该异常将像节点没有处理程序一样传播。