主题
10.11 持久执行与容错:崩溃复活与自动重试
长流程跑一半,最怕三件事:进程崩了、第三方接口超时、机器重启。普通脚本只能从头来,前面烧掉的 Token 和时间全部作废。LangGraph 的 Durable Execution(持久执行) 把长流程变成可以断点续跑的工程:只要挂了会落盘的 Checkpointer,节点执行完的那一刻进度就落袋;崩了,从最近一张快照复活。
可以把它想成登山时每隔一段打一根冰锥。滑坠了不会掉回山脚,只会挂在最后一根冰锥上——抖抖雪,接着爬。
1. 断点续跑:恢复粒度是“整个节点”
python
from langgraph.checkpoint.memory import MemorySaver # 生产换 SqliteSaver / PostgresSaver
graph = builder.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "order_1001"}}
try:
graph.invoke(inputs, config)
except Exception:
# 同一 thread_id 再次 invoke:已完成的节点不会重跑
graph.invoke(None, config)两个反直觉但必须记住的细节:
- 半路上没跑完的那个节点会整个重跑。 所以官方反复强调:节点内部的网络请求、写库,要尽量幂等——重复执行一次和执行多次结果相同。写入用 upsert,扣款先查状态再改,发邮件带去重键。
MemorySaver/InMemorySaver只活在内存里。 进程一崩,冰锥等于打进雪里。想抗重启,必须换SqliteSaver或PostgresSaver。
幂等键应来自一次业务请求的稳定标识(订单号、请求 ID),而不是每次重试 uuid4() 一个新值。否则“重试”会变成“再下一单”。
故障演练很值得做:在节点执行前后主动抛异常,观察恢复后哪些步骤再次发生。只有验证了外部副作用没有重复,才能说“断点续跑”适用于该业务。
2. RetryPolicy:只对值得再试的错误出手
网络抖动、限流 429、偶发超时,这些瞬时故障不该让整张图报废。往 add_node 上挂 RetryPolicy:
python
from langgraph.types import RetryPolicy
builder.add_node(
"call_flight_api",
call_flight_api_node,
retry_policy=RetryPolicy(
max_attempts=3, # 含第一次,一共最多 3 次
initial_interval=0.5, # 首次重试前等 0.5 秒
backoff_factor=2.0, # 之后 1s、2s……
retry_on=(TimeoutError, ConnectionError), # 只对这两类异常重试
),
)像客服热线没打通:隔一会儿再拨,而且间隔越来越长,别在一秒钟内狂拨二十次。retry_on 必须精确。默认策略会跳过 ValueError、TypeError 这类“再试也没用”的编程错误。把参数写错放进重试名单,只是在交同样的学费。
要区分暂时故障和永久错误:
| 错误 | 要不要重试 | 为什么 |
|---|---|---|
| 超时、连接断开、429 | 要,指数退避 | 过几秒可能就好了 |
| 400 参数错误、403 没权限 | 不要 | 同一请求再发也不会过 |
| 业务规则拒绝(库存不足) | 不要,走错误分支或人工 | 这是结果,不是故障 |
重试次数、间隔和最终状态应写入运行记录,方便事后对账。
3. 超时与缓存:两道省钱省时的保险
- 超时: 1.x 支持给节点声明
timeout(超时当异常,可与重试叠加)。节点内部的 HTTP 调用也建议设客户端超时,防止单个下游把整张图拖死。 - 缓存: 相同输入的昂贵节点(尤其是 LLM)可以复用上次结果。编译时挂缓存实现,节点上声明策略:
python
from langgraph.cache.memory import InMemoryCache
from langgraph.types import CachePolicy
builder.add_node(
"expensive_llm_node",
node_fn,
cache_policy=CachePolicy(ttl=120),
)
graph = builder.compile(cache=InMemoryCache(), checkpointer=memory)缓存只适合“输入确定、输出可复用”的节点。实时报价、验证码、带随机性的生成,缓存会帮倒忙。TTL 要和业务时效对齐:航班余票缓存 2 分钟可以,缓存 2 天不行。
4. 容错组合拳
| 手段 | 防什么 | 关键配置 |
|---|---|---|
| Checkpointer 落盘 | 进程崩溃、机器重启 | SqliteSaver / PostgresSaver |
| 节点幂等 | 恢复后重跑造成重复副作用 | upsert、先查再改、业务幂等键 |
| RetryPolicy | 瞬时网络故障、限流 | max_attempts + retry_on + 退避 |
| 超时 | 下游把全图拖死 | 节点 timeout + HTTP 客户端 timeout |
| 缓存 | 重复的昂贵调用 | CachePolicy + TTL |
recursion_limit | 死循环持续烧钱 | config={"recursion_limit": 25} |
RemainingSteps | 接近上限时主动收尾 | 写在 State 里,路由函数提前走 END |
组合逻辑可以记成三句话:Retry 管“再试一次能不能成”,Checkpointer 管“实在不行下次从哪继续”,缓存管“能不能根本不用试”。 三者叠加,才是完整的“跑到一半崩了能续跑”。
02 节提过的 RemainingSteps 在这里特别有用:与其在外层抓 GraphRecursionError,不如让图在还剩两步时自己总结、收工。
📁 本节示例代码:code/examples/11_durable_execution_demo.py —— 真实重试三次 + 崩溃后从
step_1之后复活。
扩展阅读
- Fault tolerance:fault-tolerance
- Persistence:persistence
- Checkpointers:checkpointers
下一节: 旅行助手采用平铺节点与真子图混合。先拆开“图中有图”,再按当前官方分类看清 Subagents、Handoffs、Skills、Router 与 Custom workflow。