🛡️ 容错与韧性 ★
教学 Agent 假设一切顺利。生产 Agent 面对的是:网络抖动、API 限流、模型幻觉、工具超时、死循环。本章精读三大框架的容错机制——重试、降级、限流、超时、错误路由,构建一个"失败也能优雅恢复"的生产级 Agent。
本章目标
- 识别生产 Agent 的五类失败模式
- 精读 LangChain 三件套(RunnableRetry / with_fallbacks / InMemoryRateLimiter)
- 精读 LangGraph 的 RetryPolicy / TimeoutPolicy / error_handler
- 理解 OpenCode 区分"配额/限流/5xx"的业务级重试
- 构建一个带完整容错的 Agent
生产 Agent 的五类失败
不是所有错误都该重试。网络抖动、5xx、限流是瞬时的,重试有意义;参数错误、权限拒绝、业务逻辑错是确定的,重试无用反而浪费。好的容错系统必须精确区分这两类。这正是 default_retry_on(langgraph/_internal/_retry.py:1)做的事。
LangChain 容错三件套
① with_retry:重试
class RunnableRetry(RunnableBindingBase):
"""重试的核心类。基于 tenacity 库。
字段:
- retry_exception_types (:114):哪些异常才重试
- wait_exponential_jitter (:124):指数退避+抖动
- max_attempt_number=3 (:132):最多重试次数
_patch_config (:158):每次重试给子 callback 打 "retry:attempt:N" tag,
让追踪能区分每次尝试。
"""
# 只对"易失败的子链"加重试,而非整个 Agent
model = ChatOpenAI(model="gpt-4o-mini")
retry_model = model.with_retry(
stop_after_attempt=3, # 最多3次
wait_exponential_jitter=True, # 指数退避+随机抖动(避免雪崩)
retry_if_exception_type=(TimeoutError, ConnectionError), # 只重试这些异常
)
# 重试时机:网络错误重试(值得),参数错误不重试(重试也失败)
retry_model.invoke("...") # 失败会自动等1s→2s→4s重试
② with_fallbacks:降级
class RunnableWithFallbacks(RunnableSerializable):
"""降级链:主失败时依次尝试备用。
exceptions_to_handle:哪些异常触发降级
fallbacks:备用 Runnable 序列
"""
# 主模型挂了用备用模型
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
primary = ChatOpenAI(model="gpt-4o")
backup = ChatAnthropic(model="claude-3-5-sonnet")
model_with_fallback = primary.with_fallbacks([backup])
# gpt-4o 调用失败(如 OpenAI 宕机)→ 自动切到 claude
model_with_fallback.invoke("...")
③ InMemoryRateLimiter:限流
class InMemoryRateLimiter(BaseRateLimiter):
"""令牌桶限流:控制每秒请求数。
requests_per_second (:123):每秒允许的请求数
available_tokens:可用令牌,按时间增量补充 (:182-183)
"""
from langchain_core.rate_limiters import InMemoryRateLimiter
rate_limiter = InMemoryRateLimiter(
requests_per_second=2, # 每秒最多2次(防超 API 配额)
)
model = ChatOpenAI(model="gpt-4o-mini", rate_limiter=rate_limiter)
# 超速的请求会自动等待,而非被 API 拒绝
LangGraph 容错:声明式策略
LangGraph 的容错更系统——声明式策略 + 执行器分离:
class RetryPolicy(NamedTuple): # :416 节点重试策略
initial_interval: float = 0.5 # 初始间隔
backoff_factor: float = 2.0 # 退避因子
max_interval: float = 128.0 # 最大间隔
max_attempts: int = 3 # 最多尝试
jitter: bool = True # 抖动
retry_on: Callable = default_retry_on # :432 哪些异常重试
class TimeoutPolicy: # :449 超时策略(协作式取消)
run_timeout: float | None # :465 硬墙超时
idle_timeout: float | None # :471 无进度超时
refresh_on: str = "auto" # :474 何时刷新
# default_retry_on(_internal/_retry.py:1):
# 网络错误/5xx → 重试(瞬时)
# ValueError/TypeError → 不重试(编程错误,重试无用)
graph.add_node(
"my_node",
node_fn,
retry_policy=RetryPolicy(max_attempts=5), # 重试
error_handler="error_handler_node", # ★ 错误路由到另一节点
timeout=TimeoutPolicy(run_timeout=30), # 超时
)
# 声明式:把容错配置作为参数,而非在节点函数里写 try/except
# 执行器 pregel/_retry.py:573 run_with_retry 负责实际执行
这是 LangGraph 相对 LangChain 最大的演进——节点抛错时不致命退出,而是路由到 error_handler 节点(_runner.py:147 的 schedule_error_handler)。error_handler 可以记录错误、改状态、决定重试或跳过。这让 Agent 从错误中恢复,而非崩溃。
OpenCode 的业务级重试
OpenCode 的重试更精细——区分配额/限流/5xx 给不同处理:
function retryable(error, provider) {
// ① ContextOverflow → 不重试(重试也溢出,该压缩上下文)
if (error instanceof ContextOverflowError) return false // :70
// ② 5xx → 必重试(服务器临时问题)
if (error.status >= 500) return true // :75
// ③ 免费额度用完 → 不重试,给 upsell action(引导付费)
if (error instanceof FreeUsageLimitError) { ... } // :76-121
// ④ 付费配额用完 → 不重试,给配额提示
if (error instanceof GoUsageLimitError) { ... }
// ⑤ 限流(rate_limit/too_many_requests 文本)→ 重试但更长退避
if (/rate_limit|too_many_requests/i.test(error.message)) // :129-150
return true
}
function delay(attempt, error) {
// 优先读服务端的 retry-after / retry-after-ms 响应头
const header = error.headers?.["retry-after-ms"] ?? error.headers?.["retry-after"]
if (header) return parseRetryAfter(header) // :39-58 服务端说等多久就等多久
// 否则指数退避:2000 * 2^(attempt-1)
return Math.min(2000 * Math.pow(2, attempt - 1), 30000) // 上限30s
}
// 比固定退避更智能:尊重服务端的限流指示
OpenCode 优先读 retry-after 头而非自己算退避——这是尊重服务端限流策略的最佳实践。服务端最清楚它什么时候能恢复,按它的指示等待比盲目退避更高效、更不容易被永久封禁。
实战:完整容错 Agent
把重试、降级、限流、超时、错误路由组合到一个 Agent。
# pip install langgraph langchain-openai langchain-anthropic
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.prebuilt import ToolNode
from langchain_core.tools import tool, ToolException
from langchain_core.rate_limiters import InMemoryRateLimiter
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from typing import Annotated
from typing_extensions import TypedDict
class State(TypedDict):
messages: Annotated[list, add_messages]
# ① 限流:防超 API 配额
rate_limiter = InMemoryRateLimiter(requests_per_second=1)
# ② 重试 + 降级:主模型失败转备用
primary = ChatOpenAI(model="gpt-4o-mini", rate_limiter=rate_limiter)
backup = ChatAnthropic(model="claude-3-5-haiku")
# 主模型:重试3次 + 失败降级到 claude
resilient_model = primary.with_retry(
stop_after_attempt=3,
retry_if_exception_type=(TimeoutError, ConnectionError),
).with_fallbacks([backup])
# ③ 工具:失败抛 ToolException(让 Agent 知道并换策略)
@tool
def risky_api(query: str) -> str:
"""调用外部API(可能失败)。"""
import random
if random.random() < 0.3:
raise ToolException("API 暂时不可用,可重试或换方法")
return f"结果: {query}"
tools = [risky_api]
model_with_tools = resilient_model.bind_tools(tools)
# ④ 构建图,带错误处理
def call_model(state):
return {"messages": [model_with_tools.invoke(state["messages"])]}
def call_tools(state):
return ToolNode(tools).invoke(state)
def should_use_tools(state):
last = state["messages"][-1]
return "tools" if getattr(last, "tool_calls", None) else END
# error_handler 节点:节点失败时不崩溃,记录后继续
def error_handler(state, error):
return {"messages": [{"role": "tool", "content": f"[工具出错: {error},已捕获]"}]}
graph = StateGraph(State)
graph.add_node("agent", call_model, error_handler=error_handler) # ★ 错误路由
graph.add_node("tools", call_tools, error_handler=error_handler)
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", should_use_tools,
{"tools": "tools", END: END})
graph.add_edge("tools", "agent")
# ⑤ checkpointer 让中断可恢复
app = graph.compile(checkpointer=InMemorySaver())
# 这个 Agent 容错能力:
# - LLM 调用:重试3次 → 失败降级到 claude
# - 限流:每秒1次,防超配额
# - 工具失败:抛 ToolException,Agent 收到错误可换策略
# - 节点异常:error_handler 捕获,不崩溃
# - 中断:checkpointer 支持 resume
config = {"configurable": {"thread_id": "t1"}}
result = app.invoke({"messages": [("user", "查询天气")]}, config)
死循环检测(回顾)
F3 章预告、OC4 章讲过 OpenCode 的 doom_loop 检测。LangGraph 用 recursion_limit 防止:
# LangGraph 的 recursion_limit:限制图执行的总步数
app = graph.compile(checkpointer=InMemorySaver())
result = app.invoke(inputs, {"recursion_limit": 25})
# 超过25步强制停止(防 agent↔tools 无限循环)
# 对比 OpenCode(OC4):
# - doom_loop 检测:连续3次同名同参工具 → 触发 ask
# - 更智能:不只看步数,还看"是否在原地打转"
与生产实践对照
| 容错机制 | OpenCode 对应 | |
|---|---|---|
| with_retry(指数退避) | → | retry.ts:delay(+retry-after 头) |
| with_fallbacks(降级) | → | ModelFallbackMiddleware |
| InMemoryRateLimiter(限流) | → | retry.ts 识别 rate_limit 错误 |
| error_handler(错误路由) | → | processor.ts:537 cleanup 容错 |
| recursion_limit(防死循环) | → | agent.steps + doom_loop 检测 |
| 区分可重试/不可重试 | → | retryable() 区分配额/限流/5xx |
| TimeoutPolicy(超时) | → | AbortController |
对比三方:LangChain 的 retry_if_exception_type 按异常类型区分;LangGraph 的 default_retry_on 按"瞬时/确定"区分;OpenCode 的 retryable() 按业务语义区分(配额/限流/5xx 给不同 action,甚至引导付费)。这是面向真实 LLM 提供商的业务级容错,最贴近生产。
小结
- 五类失败:网络→重试、限流→退避、幻觉→验证、工具失败→降级/路由、死循环→检测。
- 核心原则:区分可重试(瞬时)与不可重试(确定)。
- LangChain 三件套:
with_retry(重试)/with_fallbacks(降级)/InMemoryRateLimiter(限流)。 - LangGraph 声明式:
RetryPolicy/TimeoutPolicy/error_handler(异常变可恢复)。 - OpenCode 最精细:区分配额/限流/5xx,遵从 retry-after 头。
下一章 AD9 · 持久化与部署——容错让 Agent 不怕失败,持久化让 Agent 跨进程存活。精读 PostgresSaver、RemoteGraph,把 Agent 部署成 API 服务。