🛡️ 容错与韧性

教学 Agent 假设一切顺利。生产 Agent 面对的是:网络抖动、API 限流、模型幻觉、工具超时、死循环。本章精读三大框架的容错机制——重试、降级、限流、超时、错误路由,构建一个"失败也能优雅恢复"的生产级 Agent。

本章目标

  • 识别生产 Agent 的五类失败模式
  • 精读 LangChain 三件套(RunnableRetry / with_fallbacks / InMemoryRateLimiter)
  • 精读 LangGraph 的 RetryPolicy / TimeoutPolicy / error_handler
  • 理解 OpenCode 区分"配额/限流/5xx"的业务级重试
  • 构建一个带完整容错的 Agent

生产 Agent 的五类失败

① 网络错误 超时/连接断 → 重试 ② API 限流 429 TooMany → 限流+退避 ③ 模型幻觉 输出错误/编造 → 验证+重生 ④ 工具失败 工具抛异常 → 降级/错误路由 ⑤ 死循环 反复调同工具 → 检测+打断 容错策略矩阵 重试(瞬时错误)· 降级(主失败转备)· 限流(防超载) 超时(防卡死)· 错误路由(异常变可恢复)· 检测(防死循环)
图 AD8.1 · 五类失败及对应的容错策略
💡 核心原则:区分"可重试"与"不可重试"

不是所有错误都该重试。网络抖动、5xx、限流是瞬时的,重试有意义;参数错误、权限拒绝、业务逻辑错是确定的,重试无用反而浪费。好的容错系统必须精确区分这两类。这正是 default_retry_onlanggraph/_internal/_retry.py:1)做的事。

LangChain 容错三件套

① with_retry:重试

📄 runnables/retry.py:48 · RunnableRetry python
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,
    让追踪能区分每次尝试。
    """
📄 base.py:2089 · with_retry 用户 API python
# 只对"易失败的子链"加重试,而非整个 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:降级

📄 runnables/fallbacks.py:37 · RunnableWithFallbacks python
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:限流

📄 rate_limiters.py:67 · InMemoryRateLimiter(令牌桶) python
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 的容错更系统——声明式策略 + 执行器分离

📄 types.py:416/449 · RetryPolicy + TimeoutPolicy python
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/state.py:274 · add_node 声明容错 python
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 负责实际执行
🧭 error_handler:异常变可恢复

这是 LangGraph 相对 LangChain 最大的演进——节点抛错时不致命退出,而是路由到 error_handler 节点_runner.py:147schedule_error_handler)。error_handler 可以记录错误、改状态、决定重试或跳过。这让 Agent 从错误中恢复,而非崩溃。

OpenCode 的业务级重试

OpenCode 的重试更精细——区分配额/限流/5xx 给不同处理

📄 session/retry.ts:68 · retryable 判定 typescript
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
}
📄 retry.ts:35 · delay 退避算法(遵从 retry-after) typescript
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
}
// 比固定退避更智能:尊重服务端的限流指示
⚠️ 遵从 retry-after 头是生产最佳实践

OpenCode 优先读 retry-after 头而非自己算退避——这是尊重服务端限流策略的最佳实践。服务端最清楚它什么时候能恢复,按它的指示等待比盲目退避更高效、更不容易被永久封禁。

实战:完整容错 Agent

🚀 组合所有容错策略

把重试、降级、限流、超时、错误路由组合到一个 Agent。

📄 ad8_resilient_agent.py · 完整容错 python
# 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 防止:

📄 recursion_limit 防死循环 python
# 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
🧭 OpenCode 的重试最精细

对比三方:LangChain 的 retry_if_exception_type 按异常类型区分;LangGraph 的 default_retry_on 按"瞬时/确定"区分;OpenCode 的 retryable() 按业务语义区分(配额/限流/5xx 给不同 action,甚至引导付费)。这是面向真实 LLM 提供商的业务级容错,最贴近生产。

小结

下一章

下一章 AD9 · 持久化与部署——容错让 Agent 不怕失败,持久化让 Agent 跨进程存活。精读 PostgresSaver、RemoteGraph,把 Agent 部署成 API 服务。