186 lines
5.8 KiB
Python
186 lines
5.8 KiB
Python
"""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()
|