Files
langgraph-learning-examples/09_retry_and_errors.py

186 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""LangGraph 容错示例:重试、错误恢复与循环保护。
本文件不调用真实 LLM/API,可以直接运行并稳定复现实验:
1. transient:前两次 ConnectionError,第三次成功
2. permanent:ConnectionError 重试耗尽后进入 error_handler
3. invalid:ValueError 不重试,直接进入 error_handler
4. loop:故意无限循环,由 recursion_limit 终止
需要 langgraph >= 1.2(节点级 error_handler / NodeError 是 1.2+ 能力)。
运行:
python 09_retry_and_errors.py
"""
from typing import Literal, TypedDict
from langgraph.errors import GraphRecursionError, NodeError
from langgraph.graph import END, START, StateGraph
from langgraph.runtime import Runtime
from langgraph.types import Command, RetryPolicy
# -----------------------------------------------------------------------------
# 1. 节点重试 + 重试耗尽后的错误恢复
# -----------------------------------------------------------------------------
class ApiState(TypedDict):
mode: Literal["transient", "permanent", "invalid"]
result: str
status: str
error: str
def call_unstable_api(
state: ApiState,
runtime: Runtime,
) -> Command[Literal["finalize"]]:
"""模拟一个不稳定的外部 API。
注意:失败尝试产生的 State 更新不会提交,因此不要用 State 字段记录
当前重试次数。LangGraph 已通过 execution_info 提供 node_attempt。
"""
attempt = runtime.execution_info.node_attempt
mode = state["mode"]
print(f" call_unstable_api:第 {attempt} 次尝试,mode={mode}")
if mode == "transient" and attempt < 3:
raise ConnectionError("模拟网络抖动")
if mode == "permanent":
raise ConnectionError("模拟上游服务持续不可用")
if mode == "invalid":
# ValueError 通常代表参数/业务错误,重复请求不会自行恢复。
raise ValueError("模拟无效请求参数")
return Command(
update={
"result": "API 调用成功",
"status": "success",
"error": "",
},
goto="finalize",
)
def api_error_handler(
state: ApiState,
error: NodeError,
) -> Command[Literal["finalize"]]:
"""在不重试或重试耗尽后,将异常转成可控的业务结果。"""
print(
f" error_handler:节点 {error.node} 最终失败:"
f"{type(error.error).__name__}: {error.error}"
)
return Command(
update={
"result": "已启用降级结果,请稍后重试",
"status": "recovered",
"error": f"{type(error.error).__name__}: {error.error}",
},
goto="finalize",
)
def finalize(state: ApiState) -> dict:
"""统一收尾节点;成功和降级路径都会到达这里。"""
print(f" finalize:status={state['status']}, result={state['result']}")
return {}
api_builder = StateGraph(ApiState)
api_builder.add_node(
"call_api",
call_unstable_api,
retry_policy=RetryPolicy(
max_attempts=3, # 包含第一次,共最多执行 3 次
initial_interval=0.1, # 第一次重试前等待 0.1 秒
backoff_factor=2.0, # 后续等待时间指数增长
max_interval=1.0,
jitter=False, # 教学输出保持可预测
retry_on=ConnectionError,
),
error_handler=api_error_handler,
)
api_builder.add_node("finalize", finalize)
api_builder.add_edge(START, "call_api")
api_builder.add_edge("finalize", END)
api_graph = api_builder.compile()
def run_api_case(mode: Literal["transient", "permanent", "invalid"]) -> None:
"""运行一个重试/错误恢复场景,并打印最终状态。"""
print(f"\n=== 场景:{mode} ===")
result = api_graph.invoke(
{
"mode": mode,
"result": "",
"status": "pending",
"error": "",
}
)
print(" 最终状态:", result)
# -----------------------------------------------------------------------------
# 2. 循环保护:recursion_limit
# -----------------------------------------------------------------------------
class LoopState(TypedDict):
count: int
def loop_forever(state: LoopState) -> dict:
"""故意不提供终止条件,用于演示 GraphRecursionError。"""
next_count = state["count"] + 1
print(f" loop:count={next_count}")
return {"count": next_count}
loop_builder = StateGraph(LoopState)
loop_builder.add_node("loop", loop_forever)
loop_builder.add_edge(START, "loop")
loop_builder.add_edge("loop", "loop")
loop_graph = loop_builder.compile()
def run_loop_case() -> None:
"""用较小的递归限制安全终止错误设计的循环图。"""
print("\n=== 场景:loop / recursion_limit ===")
try:
loop_graph.invoke(
{"count": 0},
config={"recursion_limit": 4},
)
except GraphRecursionError as exc:
print(f" 已捕获 {type(exc).__name__}")
print(" 原因:图在限制步数内没有到达 END。")
print(" 正确做法:为循环增加业务终止条件;recursion_limit 只是最后防线。")
# -----------------------------------------------------------------------------
# 3. 入口
# -----------------------------------------------------------------------------
def main() -> None:
print("=== LangGraph Retry & Errors ===")
run_api_case("transient")
run_api_case("permanent")
run_api_case("invalid")
run_loop_case()
print("\n=== 观察结论 ===")
print("1. ConnectionError 会按照 RetryPolicy 自动重试。")
print("2. ValueError 不符合 retry_on,第一次失败后立即交给 error_handler。")
print("3. 重试耗尽后,error_handler 可用 Command 更新状态并走降级路径。")
print("4. recursion_limit 能阻止失控循环,但不能替代正确的终止条件。")
if __name__ == "__main__":
main()