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

循环必须能退出

本章目标

构建“生成—检查—返工”循环,并区分业务重试与技术重试。

完整代码: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 区分节点技术重试和图中的业务循环。
  • 所有副作用节点都有稳定幂等键和结果查询方式。
  • 重试耗尽后能给用户明确状态,而不是只抛出堆栈。