LangGraph / Capabilities / Fault tolerance

原文链接:https://docs.langchain.com/oss/python/langgraph/fault-tolerance

容错能力

当节点发生故障时------无论是来自慢速的外部API、瞬态网络错误,还是未处理的异常------LangGraph 提供三种可组合的机制来应对:

  • 重试 ------ 根据异常类型和退避设置自动重新运行失败的尝试
  • 超时 ------ 限制单次尝试的运行时长
  • 错误处理 ------ 在所有重试用尽后运行恢复函数

使用 set_node_defaults 为所有节点一次性配置这些机制,而无需在每个 add_node 调用中重复设置。

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

如需在超级步边界干净地停止运行并在稍后恢复,请参阅优雅关闭

每个节点的超时和节点级错误处理器需要 langgraph>=1.2

重试

重试策略会在节点尝试失败时根据异常类型和退避设置自动重新运行。

add_node 中传递 retry_policy

python 复制代码
from langgraph.types import RetryPolicy

builder.add_node(
    "call_api",
    call_api,
    retry_policy=RetryPolicy(max_attempts=3),
)

默认行为

默认情况下,retry_on 使用 default_retry_on,它会重试除以下异常(及其子类)之外的所有异常:

  • ValueError
  • TypeError
  • ArithmeticError
  • ImportError
  • LookupError
  • NameError
  • SyntaxError
  • RuntimeError
  • ReferenceError
  • StopIteration
  • StopAsyncIteration
  • OSError

对于来自 requestshttpx 等流行 HTTP 库的异常,它只重试 5xx 状态码。NodeTimeoutError 默认可重试。

参数

参数 类型 默认值 描述
max_attempts int 3 最大尝试次数,包括首次。
initial_interval float 0.5 首次重试前的等待秒数。
backoff_factor float 2.0 每次重试后间隔的乘数。
max_interval float 128.0 重试间隔的最大秒数。
jitter bool True 为间隔添加随机抖动。
retry_on `typeException Sequencetype\[Exception] Callable\[Exception, bool]`

自定义重试逻辑

传递可调用对象或异常类型给 retry_on。导入 default_retry_on 以扩展默认行为:

python 复制代码
from langgraph.types import RetryPolicy, default_retry_on

def custom_retry_on(exc: BaseException) -> bool:
    if isinstance(exc, MyCustomError):
        return False
    return default_retry_on(exc)

builder.add_node(
    "call_api",
    call_api,
    retry_policy=RetryPolicy(max_attempts=3, retry_on=custom_retry_on),
)

检查重试状态

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

python 复制代码
from langgraph.graph import StateGraph, START, END
from langgraph.runtime import Runtime
from langgraph.types import RetryPolicy
from typing_extensions import TypedDict

class State(TypedDict):
    result: str

def my_node(state: State, runtime: Runtime) -> State:
    if runtime.execution_info.node_attempt > 1:
        return {"result": call_fallback_api()}
    return {"result": call_primary_api()}

builder = StateGraph(State)
builder.add_node("my_node", my_node, retry_policy=RetryPolicy(max_attempts=3))
builder.add_edge(START, "my_node")
builder.add_edge("my_node", END)

execution_info 暴露以下字段:

属性 类型 描述
node_attempt int 当前尝试次数(从1开始)。首次为1,第一次重试为2,依此类推。
node_first_attempt_time `float None`
thread_id `str None`
run_id `str None`
checkpoint_id str 当前执行的检查点ID。
task_id str 当前执行的任务ID。

即使没有重试策略,execution_info 也可用------node_attempt 默认为 1

超时

需要 langgraph>=1.2

add_nodetimeout= 参数限制单次节点尝试的运行时长。可以传递数字(秒)、timedeltaTimeoutPolicy 来分别设置运行时间和空闲时间限制:

python 复制代码
from datetime import timedelta
from langgraph.types import TimeoutPolicy

# 简单的墙上时钟上限
builder.add_node("call_model", call_model, timeout=60)
builder.add_node("call_model", call_model, timeout=timedelta(minutes=2))

# 分别设置运行时间和空闲时间限制
builder.add_node(
    "call_model",
    call_model,
    timeout=TimeoutPolicy(run_timeout=120, idle_timeout=30),
)

节点超时仅适用于异步节点。同步节点带有超时会在编译时被拒绝。要包装阻塞 I/O,请在异步节点内部使用 asyncio.to_thread

运行超时

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

python 复制代码
from langgraph.types import TimeoutPolicy

builder.add_node(
    "call_model",
    call_model,
    timeout=TimeoutPolicy(run_timeout=120),
)

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

空闲超时

idle_timeout 是一个进度重置型上限。它仅在节点在指定持续时间内停止产生可观察进度时触发------与 run_timeout 不同,每当节点产生进度信号时,时钟会重置:

python 复制代码
builder.add_node(
    "call_model",
    call_model,
    timeout=TimeoutPolicy(idle_timeout=30),
)

可以同时设置 run_timeoutidle_timeout。先触发的那个会取消尝试。

进度信号

在默认的 refresh_on="auto" 下,空闲时钟会在以下任何事件发生时重置:

  • 通过 CONFIG_KEY_SEND 进行状态写入
  • 流式输出(产生的异步流块)
  • 子任务调度
  • 运行时流写入器调用
  • 来自节点或其子节点的任何 LangChain 回调事件(LLM 令牌、工具调用、链开始/结束等)

心跳模式

设置 refresh_on="heartbeat" 将刷新源缩小为仅显式的 runtime.heartbeat() 调用。当您希望一个严格的空间定义,不被嘈杂的子节点重置时,这很有用:

python 复制代码
builder.add_node(
    "call_model",
    call_model,
    timeout=TimeoutPolicy(idle_timeout=30, refresh_on="heartbeat"),
)

手动心跳

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

python 复制代码
from langgraph.graph import StateGraph, START, END
from langgraph.runtime import Runtime
from langgraph.types import TimeoutPolicy
from typing_extensions import TypedDict

class State(TypedDict):
    result: str

async def long_running_node(state: State, runtime: Runtime) -> State:
    for batch in fetch_batches():
        process(batch)
        runtime.heartbeat()
    return {"result": "done"}

builder = StateGraph(State)
builder.add_node(
    "long_running_node",
    long_running_node,
    timeout=TimeoutPolicy(idle_timeout=30, refresh_on="heartbeat"),
)
builder.add_edge(START, "long_running_node")
builder.add_edge("long_running_node", END)

runtime.heartbeat() 在空闲计时尝试之外是无操作,因此可以无条件调用。

NodeTimeoutError

当超时触发时,LangGraph 抛出 NodeTimeoutError,其中包含关于触发了哪个限制的结构化上下文:

属性 类型 描述
node str 执行超时的节点名称。
elapsed float 超时触发前经过的秒数。
kind Literal["idle", "run"] 触发了哪个超时。
idle_timeout `float None`
run_timeout `float None`

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

python 复制代码
from langgraph.types import RetryPolicy, TimeoutPolicy

builder.add_node(
    "call_model",
    call_model,
    timeout=TimeoutPolicy(idle_timeout=30),
    retry_policy=RetryPolicy(max_attempts=3),
)

使用 Send 动态超时

当使用 Send 动态调度节点时(例如在 map-reduce 模式中),可以直接在 Send 上传递超时,以覆盖该特定推送的目标节点静态超时:

python 复制代码
from langgraph.types import Send, TimeoutPolicy

def fan_out(state: OverallState):
    return [
        Send("process_item", {"item": item}, timeout=TimeoutPolicy(idle_timeout=15))
        for item in state["items"]
    ]

如果在 Send 上省略超时,则应用目标节点的超时(在 add_node 时设置)。这允许在节点上设置默认超时,并为单个调用收紧它。

错误处理

需要 langgraph>=1.2

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

add_node 中传递 error_handler=

python 复制代码
from langgraph.errors import NodeError
from langgraph.types import Command, RetryPolicy
from langgraph.graph import StateGraph, START
from typing_extensions import TypedDict

class State(TypedDict):
    status: str

def charge_payment(state: State) -> State:
    raise RuntimeError("payment gateway timeout")

def payment_error_handler(state: State, error: NodeError) -> Command:
    return Command(
        update={"status": f"compensated: {error.error}"},
        goto="finalize",
    )

def finalize(state: State) -> State:
    return state

graph = (
    StateGraph(State)
    .add_node(
        "charge_payment",
        charge_payment,
        retry_policy=RetryPolicy(max_attempts=3, retry_on=ConnectionError),
        error_handler=payment_error_handler,
    )
    .add_node("finalize", finalize)
    .add_edge(START, "charge_payment")
    .compile()
)

处理器仅在重试策略用尽后触发,如果未配置重试策略则立即触发。重试策略和错误处理器保持解耦:独立配置何时重试和何时补偿。

NodeError

错误处理器通过类型注解注入的 NodeError 参数接收失败上下文(与 runtime: Runtime 模式相同):

python 复制代码
from langgraph.errors import NodeError

def my_handler(state: State, error: NodeError) -> Command:
    print(f"Node {error.node} failed with: {error.error}")
    return Command(update={"status": "recovered"}, goto="next_step")

NodeError 是一个冻结数据类,包含两个字段:

属性 类型 描述
node str 执行失败的节点名称。
error BaseException 失败节点抛出的异常。

error: NodeError 参数是可选的。不需要失败上下文的处理器可以使用更简单的签名,如 (state)(state, runtime)

使用 Command 路由

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

python 复制代码
from langgraph.errors import NodeError
from langgraph.types import Command, RetryPolicy
from langgraph.graph import StateGraph, START
from typing_extensions import TypedDict

class State(TypedDict):
    status: str

def reserve_inventory(state: State) -> State:
    return {"status": "reserved"}

def charge_payment(state: State) -> State:
    raise RuntimeError("payment timeout")

def payment_error_handler(state: State, error: NodeError) -> Command:
    return Command(
        update={"status": f"compensated_after_{error.node}: {error.error}"},
        goto="finalize",
    )

def finalize(state: State) -> State:
    return state

graph = (
    StateGraph(State)
    .add_node("reserve_inventory", reserve_inventory)
    .add_node(
        "charge_payment",
        charge_payment,
        retry_policy=RetryPolicy(max_attempts=3, retry_on=ConnectionError),
        error_handler=payment_error_handler,
    )
    .add_node("finalize", finalize)
    .add_edge(START, "reserve_inventory")
    .add_edge("reserve_inventory", "charge_payment")
    .compile()
)

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

可恢复的故障

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

与 interrupt() 的行为

节点内部抛出的 interrupt() 不会 被路由到错误处理器。中断使用 GraphBubbleUp 机制暂停图执行以进行人机交互工作流,绕过重试策略和错误处理器。图照常暂停。

子图故障

如果节点包装了子图,且子图抛出未处理的异常,该异常会冒泡到父节点。如果父节点有错误处理器,处理器会以子图的异常作为 error.error 触发。

图默认值

需要 langgraph>=1.2

不必在每个 add_node 调用上重复相同的 retry_policy=error_handler=timeout=cache_policy=,使用 set_node_defaults 在一个地方配置图级默认值:

python 复制代码
from langgraph.errors import NodeError
from langgraph.types import RetryPolicy, TimeoutPolicy
from langgraph.graph import StateGraph, START
from typing_extensions import TypedDict

class State(TypedDict):
    status: str

def default_error_handler(state: State, error: NodeError) -> State:
    return {"status": f"handled: {error.error}"}

graph = (
    StateGraph(State)
    .set_node_defaults(
        retry_policy=RetryPolicy(max_attempts=3),
        error_handler=default_error_handler,
        timeout=TimeoutPolicy(run_timeout=30),
    )
    .add_node("step_a", step_a)
    .add_node("step_b", step_b)
    .add_edge(START, "step_a")
    .compile()
)

step_astep_b 现在共享相同的重试策略、错误处理器和超时,无需任何重复。

优先级

直接传递给 add_node() 的每个节点值始终覆盖 set_node_defaults() 设置的默认值。默认值在 compile() 时解析,因此可以以任何顺序调用 set_node_defaults()add_node()

python 复制代码
graph = (
    StateGraph(State)
    .set_node_defaults(error_handler=default_error_handler)
    .add_node("step_a", step_a)                                     # 使用 default_error_handler
    .add_node("step_b", step_b, error_handler=custom_error_handler) # 使用 custom_error_handler
    .add_edge(START, "step_a")
    .compile()
)

默认错误处理器

error_handler 默认值特别有价值,当每个图运行映射到外部进程(例如后台任务行)时,任何未处理的节点故障都应将该进程标记为失败,而无需在每个 add_node 上重复 error_handler=。当步骤需要自己的逻辑时,每个节点的处理器仍然优先:

python 复制代码
from langgraph.errors import NodeError
from langgraph.graph import StateGraph, START
from langgraph.types import Command, RetryPolicy
from typing_extensions import TypedDict

class State(TypedDict):
    process_id: str
    status: str

def fetch_data(state: State) -> State:
    return {"status": "fetched"}

def charge_payment(state: State) -> State:
    raise RuntimeError("payment timeout")

def finalize(state: State) -> State:
    return state

def mark_process_failed(state: State, error: NodeError) -> State:
    # 在由 process_id 键控的外部进程行上持久化失败。
    return {"status": f"failed at {error.node}: {error.error}"}

def refund_payment(state: State, error: NodeError) -> Command:
    return Command(
        update={"status": f"compensated after {error.node}"},
        goto="finalize",
    )

graph = (
    StateGraph(State)
    .set_node_defaults(
        retry_policy=RetryPolicy(max_attempts=3),
        error_handler=mark_process_failed,
    )
    .add_node("fetch_data", fetch_data)  # 使用 mark_process_failed
    .add_node(
        "charge_payment",
        charge_payment,
        error_handler=refund_payment,  # 覆盖图级默认值
    )
    .add_node("finalize", finalize)
    .add_edge(START, "fetch_data")
    .add_edge("fetch_data", "charge_payment")
    .compile()
)

如果 fetch_data 在重试后失败,运行 mark_process_failed。如果 charge_payment 在重试后失败,运行 refund_payment,因为每个节点的处理器覆盖了默认值。

处理器接受与错误处理中描述的相同的 (state, error: NodeError) 签名。如果需要访问 thread_id 等配置值,它还可以接受 RunnableConfig 作为可选的第三个参数:

python 复制代码
from langchain_core.runnables import RunnableConfig

def mark_process_failed(
    state: State, error: NodeError, config: RunnableConfig
) -> State:
    thread_id = config["configurable"].get("thread_id")
    return {"status": f"failed on thread {thread_id}: {error.error}"}

适用性矩阵

并非所有默认值都适用于所有节点类型。错误处理器节点(通过 add_node(error_handler=...) 注册的)被排除在某些默认值之外,以防止不安全行为:

set_node_defaults 参数 适用于常规节点 适用于错误处理器节点 原因
retry_policy 处理器应在瞬态故障上重试
timeout 卡住的处理器应像卡住的常规节点一样被取消
error_handler 处理器绝不能捕获自己
cache_policy 缓存处理器结果不安全

作用域

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

函数式 API

相同的 timeout=retry_policy= 参数在函数式 API 的 @task@entrypoint 上可用:

python 复制代码
from langgraph.func import entrypoint, task
from langgraph.types import RetryPolicy, TimeoutPolicy

@task(
    timeout=TimeoutPolicy(idle_timeout=30),
    retry_policy=RetryPolicy(max_attempts=3),
)
async def call_api(url: str) -> str:
    response = await fetch(url)
    return response.text

@entrypoint(timeout=60)
async def my_workflow(inputs: dict) -> str:
    result = await call_api("https://api.example.com/data")
    return result

行为与 add_node 相同:超时时抛出 NodeTimeoutError,缓冲写入被清除,重试策略决定是否重试。

优雅关闭

协作式关闭允许在当前超级步完成后停止正在进行的图运行,并保存可恢复的检查点。这对于处理 SIGTERM 信号或任何需要在不清除工作的情况下回收资源的外部监督者很有用。

需要 langgraph>=1.2

创建 RunControl 并将其作为 control= 传递给 invokestream。从任何线程调用 request_drain() 以信号通知运行应停止:

python 复制代码
from langgraph.runtime import RunControl
from langgraph.errors import GraphDrained

control = RunControl()

# 在信号处理器或监督者中:
# control.request_drain("sigterm")

try:
    result = graph.invoke(inputs, config, control=control)
except GraphDrained as e:
    # 图提前停止并保存了检查点。
    # 稍后使用相同的配置恢复。
    print(f"Drained: {e.reason}")

语义

Drain 是协作式的,在超级步之间操作,从不会抢占已经在运行的工作:

场景 行为
节点执行中 运行至完成。Drain 在下一个超级步生效。
具有重试策略且正在重试的节点 重试循环运行至用尽或成功。Drain 在之后生效。
图在与 drain 相同的 tick 上自然完成 正常返回。检查 control.drain_requested 以与正常运行区分。
还有更多超级步 抛出 GraphDrained(reason)。检查点已保存且可恢复。
子图请求 drain GraphDrained 冒泡到父图,并在其自己的下一个超级步边界停止它。

Drain 后恢复

使用相同的 thread_id 通过 invoke(None, config) 恢复已 drain 的运行:

python 复制代码
result = graph.invoke(None, config)

在节点内部读取 Drain 状态

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

python 复制代码
from langgraph.runtime import Runtime

async def my_node(state: State, runtime: Runtime) -> State:
    if runtime.drain_requested:
        # 跳过昂贵的工作并返回最小结果
        return {"status": "skipped", "reason": runtime.drain_reason}
    return {"status": await do_work()}

SIGTERM 钩子模式

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

python 复制代码
import signal
from langgraph.runtime import RunControl
from langgraph.errors import GraphDrained

control = RunControl()
signal.signal(signal.SIGTERM, lambda *_: control.request_drain("sigterm"))

try:
    result = graph.invoke(inputs, config, control=control)
except GraphDrained as e:
    log.info("graph drained: %s", e.reason)
    # 下次启动时使用相同的配置恢复

request_drain() 不会取消正在运行的 asyncio 任务或杀死线程。对于硬性上限,将 drain 与优雅超时和任务取消配对使用。

限制

  • 超时仅限异步:带有超时的同步节点在编译时被拒绝。
  • 每个节点一个处理器 :每个节点最多只能有一个 error_handler
  • 处理器失败冒泡:如果错误处理器本身抛出异常,该异常会像节点没有处理器一样传播。
  • set_node_defaults 不会被子图继承:每个图独立管理自己的默认值。
相关推荐
ryan_9961 小时前
一次讲清 A2A 协议与 MCP 边界:从 Agent Card 到 Task 生命周期
agent·mcp·a2a·json-rpc·agent通信
IvanCodes1 小时前
Python 基础语法(一):变量、数据类型与运算符
python
安逸sgr2 小时前
AI 应用怎么评测?离线评测、人工评估和线上反馈如何结合?
人工智能·ai·大模型·agent·智能体
程序员三藏2 小时前
浅谈性能测试
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·性能测试
l1258652 小时前
# RAG向量数据库优化实战:HNSW索引调参与生产级性能设计
数据库·python·mysql·langchain
demo007x3 小时前
DSH harness 中的上下文管理探秘
程序员·agent·deepseek
meilindehuzi_a3 小时前
Python 基础语法(1):常量、变量、类型、输入输出与运算符
开发语言·python
Zane19944 小时前
调用了 async 函数却没执行?一文讲透协程、event loop 与 await
后端·python
OPEN-F4 小时前
Python进阶教程:装饰器与生成器深入
开发语言·python