容错处理(Fault tolerance)
在 LangGraph 中配置单节点超时、重试和错误处理程序。
当节点发生故障时(由于缓慢的外部 API、瞬时网络错误或未处理的异常),LangGraph 为你提供了三种可组合的机制来应对:
- 重试(Retries):根据异常类型和退避设置自动重试失败的尝试
- 超时(Timeouts):限制单次尝试的最长运行时间
- 错误处理(Error handling):在所有重试耗尽后运行恢复函数
使用 setNodeDefaults 为所有节点一次性配置这些机制,而不必在每次 addNode 调用时重复它们。
这些机制按固定的顺序组合:当节点尝试引发任何异常(包括因超时引发的 NodeTimeoutError)时,重试策略决定是否重试。只有在重试耗尽后,错误处理程序才会运行。
关于在超步边界整洁地停止运行并在稍后恢复,请参阅优雅关闭。
INFO
单节点超时和节点级错误处理程序需要 @langchain/langgraph>=1.4.0。
重试(Retries)
重试策略根据异常类型和退避设置自动重试失败的节点尝试。
将 retryPolicy 传递给 addNode:
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。
参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
maxAttempts | number | 3 | 最大尝试次数(包括首次尝试)。 |
initialInterval | number | 500 | 首次重试前的毫秒数。 |
backoffFactor | number | 2.0 | 每次重试后应用于间隔的乘数。 |
maxInterval | number | 128000 | 重试之间的最大毫秒数。 |
jitter | boolean | true | 为间隔添加随机抖动。 |
retryOn | (error: unknown) => boolean | 内置处理程序(设置策略时) | 返回 true 表示可重试异常的可调用函数。仅在配置了 retryPolicy 时使用。 |
logWarning | boolean | true | 尝试重试时是否记录警告日志。 |
自定义重试逻辑
将可调用函数传递给 retryOn。与 Python 不同,JavaScript 没有导出的 defaultRetryOn 辅助函数——请实现自己的谓词:
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();检查重试状态
在节点内使用执行信息来检查当前的尝试次数。当主调用持续失败时,这对于切换到回退方案很有用:
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 暴露以下字段:
| 属性 | 类型 | 说明 |
|---|---|---|
nodeAttempt | number | 当前尝试次数(从 1 开始)。首次尝试为 1,首次重试为 2,依此类推。 |
nodeFirstAttemptTime | number | undefined | 首次尝试开始时的 Unix 时间戳(毫秒)。在重试之间保持恒定。 |
threadId | string | undefined | 当前执行的线程 ID。没有检查点时为 undefined。 |
runId | string | undefined | 当前执行的运行 ID。配置中未提供时为 undefined。 |
checkpointId | string | 当前执行的检查点 ID。 |
checkpointNs | string | 当前执行的检查点命名空间。 |
taskId | string | 当前执行的任务 ID。 |
即使没有重试策略,executionInfo 也可用——nodeAttempt 默认为 1。
超时(Timeouts)
INFO
需要 @langchain/langgraph>=1.4.0。
addNode 上的 timeout 参数限制单个节点尝试可以运行的最长时间。传递数字(毫秒)或 TimeoutPolicy 以分别指定运行和空闲限制:
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 是单次尝试的硬性挂钟上限。无论节点活动如何,它都不会被刷新:
const graph = new StateGraph(State)
.addNode("callModel", callModel, {
timeout: { runTimeout: 120_000 },
})
.compile();当超出限制时,LangGraph 会引发 NodeTimeoutError,清除失败尝试中的所有写入,并让重试策略决定是否重试。
空闲超时(Idle timeout)
idleTimeout 是进度重置上限。它仅在节点在指定的持续时间内停止产生可观察的进度时才会触发——与 runTimeout 不同,每当节点产生进度信号时,时钟就会重置:
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() 调用缩小刷新源。当你想要一个严格的空闲定义且不被频繁的下属活动重置时,这很有用:
const graph = new StateGraph(State)
.addNode("callModel", callModel, {
timeout: { idleTimeout: 30_000, refreshOn: "heartbeat" },
})
.compile();手动心跳
对于不会自然发出进度信号的长时间运行工作,请调用 runtime.heartbeat() 手动重置空闲时钟:
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,并带有关于达到哪个限制的结构化上下文:
| 属性 | 类型 | 说明 |
|---|---|---|
node | string | 执行超时的节点名称。 |
elapsed | number | 超时触发前经过的毫秒数。 |
kind | "idle" | "run" | 触发了哪种超时。 |
timeout | number | 触发的超时值(毫秒)。 |
idleTimeout | number | undefined | 配置的空闲超时(毫秒),如果有的话。 |
runTimeout | number | undefined | 配置的运行超时(毫秒),如果有的话。 |
在 TypeScript 中使用 isNodeTimeoutError(error) 来收窄捕获的错误。
NodeTimeoutError 默认是可重试的。将 timeout 与重试策略结合使用开箱即用——超时时钟在每次新尝试时重置,并且在下一次重试之前清除超时尝试的写入:
const graph = new StateGraph(State)
.addNode("callModel", callModel, {
timeout: { idleTimeout: 30_000 },
retryPolicy: { maxAttempts: 3 },
})
.compile();使用 Send 动态设置超时
当使用 Send 动态分发节点时(例如在 Map-Reduce 模式中),你可以直接在 Send 上传递超时,以针对该特定推送覆盖目标节点的静态超时:
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 类):
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 参数接收故障上下文:
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 是一个包含两个字段的类:
| 属性 | 类型 | 说明 |
|---|---|---|
node | string | 执行失败的节点名称。 |
error | Error | 失败节点引发的异常。 |
error: NodeError 参数是可选的。不需要故障上下文的处理程序可以省略第二个参数,仅接收 state。
使用 Command 进行路由
错误处理程序可以返回 Command 来更新状态并路由到特定节点,从而启用 Saga / 补偿模式:
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 在一个位置配置图范围的默认设置:
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():
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 默认设置特别有价值。当某一步骤需要自己的补偿逻辑时,单个节点处理程序仍然优先:
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):
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() 以发出应停止运行的信号:
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) 恢复已排空的运行:
const result = await graph.invoke(null, config);在节点内部读取排空状态
通过 runtime 参数访问排空状态,以在到达超步边界之前调整节点行为:
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 钩子模式
处理进程关闭的推荐模式:
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。 - 处理程序故障向外冒泡:如果错误处理程序本身抛出异常,该异常将像节点没有处理程序一样传播。
