用条件边构建有限循环和错误边界

本章目标
构建“生成—检查—返工”循环,并区分业务重试与技术重试。
完整代码:examples/state_and_routing.py
def route_after_review(state: ReviewState) -> Literal["rewrite", "finish"]:
if state["approved"] or state["attempts"] >= 3:
return "finish"
return "rewrite"
builder.add_conditional_edges(
"review",
route_after_review,
{"rewrite": "write", "finish": END},
)
每个循环必须同时具有:
- 成功退出条件。
- 最大次数或预算限制。
- 失败后的用户可理解结果。
- 不会重复副作用的设计。
为网络超时等暂时性异常配置节点技术重试:
from langgraph.types import RetryPolicy
builder.add_node(
"query_order",
query_order,
retry_policy=RetryPolicy(max_attempts=3, retry_on=TimeoutError),
)
技术重试不会改变业务决策;业务重试是图中的显式循环,需要写入 State 并出现在 Trace 中。

还应设置运行时兜底:
graph.invoke(inputs, config={"recursion_limit": 50})
recursion_limit 是熔断器,不应代替业务退出条件。

先分类异常,再决定是否重试
“失败就重试三次”会放大故障。生产代码至少区分以下类别:
| 异常 | 是否重试 | 建议处理 |
|---|---|---|
| 连接超时、临时 503 | 是 | 指数退避、抖动和最大次数 |
| 429 限流 | 条件性 | 尊重 Retry-After,同时降低并发 |
| 参数校验失败 | 否 | 修正输入或进入失败分支 |
| 401/403 | 否 | 停止运行并记录安全事件 |
| 业务拒绝,如订单已发货 | 否 | 转换为明确业务结果 |
| 未知异常 | 默认否 | 保留因果链,进入人工或告警 |
LangGraph 的 RetryPolicy 已提供退避和抖动参数:
RetryPolicy(
initial_interval=0.5,
backoff_factor=2.0,
max_interval=8.0,
max_attempts=3,
jitter=True,
retry_on=(TimeoutError, ConnectionError),
)
总重试时间必须小于节点和整次运行的 deadline。否则“最多三次”仍可能让请求挂起数分钟。
重试必须和幂等一起设计
技术重试会再次进入同一个节点,业务循环也会再次调用执行节点。只读查询通常安全,退款、发券和消息发送必须携带稳定业务键。键不能包含随机数或每次变化的时间戳,应由租户、业务单号、动作类型和动作版本构成。
tenant-a:T-100:refund:v1
如果调用超时,下一次尝试应先用幂等键查询执行结果,再决定是否重发。仅在 Agent State 中记录 executed=True 无法抵抗进程在外部成功后、写 Checkpoint 前崩溃。
故障注入而不是只测成功路径
本项目的 OrderService 可以让订单查询先超时再成功。以下测试验证节点技术重试发生两次,但不会进入业务退款循环:
python -m pytest \
tests/test_production_graph.py::test_order_lookup_technical_retry_recovers \
tests/test_production_graph.py::test_order_lookup_technical_retry_exhaustion_raises
还应测试:永久参数错误不重试、连续超时达到上限、业务重试耗尽后转人工、退款响应丢失后使用同一幂等键恢复。
本章验收
- 为每种外部依赖写出可重试异常白名单。
- 能从 Trace 区分节点技术重试和图中的业务循环。
- 所有副作用节点都有稳定幂等键和结果查询方式。
- 重试耗尽后能给用户明确状态,而不是只抛出堆栈。