s11:错误恢复(Error Recovery)
s11:错误恢复(Error Recovery)
核心问题
生产环境中 API 错误是常态:输出被截断、上下文超限(压缩后还是太长)、临时故障(429 限流 / 529 过载)。错误恢复系统设计了一套多路径、带上限、可降级的恢复机制,让 Agent 在故障中尽可能继续工作,而不是崩溃退出。
RecoveryState — 恢复状态的"控制面板"
RecoveryState 是一个共享状态对象,其五个字段各管一条恢复路径的上限,被 agent_loop 和 with_retry 共享读写。
class RecoveryState:
"""Track recovery attempts across the loop."""
def __init__(self):
# 路径1:是否已升级过 max_tokens(bool,只允许一次)
self.has_escalated = False
# 路径1:续写次数(最多 3 次)
self.recovery_count = 0
# 路径3:连续 529 计数
self.consecutive_529 = 0
# 路径2:是否已压缩过
self.has_attempted_reactive_compact = False
# 路径3:当前使用的模型(可能被切换)
self.current_model = PRIMARY_MODEL
| 字段 | 类型 | 管哪条路径 | 上限 |
|---|---|---|---|
has_escalated |
bool |
路径1:max_tokens 升级 |
只允许一次 |
recovery_count |
int |
路径1:续写次数 | 最多 3 次 |
consecutive_529 |
int |
路径3:连续 529 | ≥3 切换备用模型 |
has_attempted_reactive_compact |
bool |
路径2:紧急压缩 | 只允许一次 |
current_model |
str |
路径3:当前模型 | 可切换为 FALLBACK_MODEL |
指数退避 retry_delay
处理瞬时故障(429/529)时,不能立即重试——会加剧服务器负载。采用带抖动的指数退避。
# attempt:当前重试次数;retry_after:服务器建议的等待时间
def retry_delay(attempt, retry_after=None):
"""带抖动的指数退避。Retry-After 头部优先遵循。"""
if retry_after:
return retry_after
# - 基础延迟:`500ms × 2^attempt`,上限 32 秒
base = min(BASE_DELAY_MS * (2 ** attempt), 32000) / 1000
# 抖动:在基础上加 0~25% 的随机值
# 作用:避免多个 agent 同一时刻集体重试(thundering herd)
jitter = random.uniform(0, base * 0.25)
return base + jitter
抖动(Jitter)的作用:如果每个 Agent 的退避时间完全相同,它们会在同一时刻集体重试,形成"惊群效应(thundering herd)"再次压垮服务器。加入随机抖动后,重试时间被错开。
with_retry — 瞬时故障的"护城河"
它把 LLM 调用包装在一个**只处理瞬时故障(429/529)**的 retry 循环里;非瞬时故障(如 prompt_too_long)直接 raise 穿透给外层 try/except。
三种错误处理路径
| 路径 | 错误 | 处理方式 |
|---|---|---|
| 路径 3a | 429 限流:请求太快了(API 有调用频率限制) | 指数退避重试 |
| 路径 3b | 529 过载:服务器当前负载过高,无法处理请求 | 退避 + 切换备用模型 |
| 非瞬时 | 不是暂时的(如 prompt_too_long、鉴权失败、网络断开) |
raise 抛出异常,上层处理 |
# fn:要执行的 LLM 调用函数(通常是 lambda 或函数引用)
# state:恢复状态对象,跟踪错误计数和当前使用的模型
def with_retry(fn, state: RecoveryState):
"""瞬态错误(429/529)的指数退避。
非瞬态错误会重新抛出,交由外层处理器处理。"""
for attempt in range(MAX_RETRIES): # 最多 10 次
try:
result = fn() # 执行 LLM 调用
state.consecutive_529 = 0 # 成功 → 清零连续 529 计数
return result
except Exception as e:
name = type(e).__name__
msg = str(e).lower()
# ── 路径3a:429 限流 → 指数退避重试 ──
if "ratelimit" in name.lower() or "429" in msg:
delay = retry_delay(attempt)
time.sleep(delay)
continue # ← 循环内重试
# ── 路径3b:529 过载 → 退避重试 + 切换备用模型 ──
if "overloaded" in name.lower() or "529" in msg or "overloaded" in msg:
state.consecutive_529 += 1
# 连续 529 ≥ 3 次 → 切换到备用模型
if state.consecutive_529 >= MAX_CONSECUTIVE_529:
if FALLBACK_MODEL:
state.current_model = FALLBACK_MODEL
state.consecutive_529 = 0
else:
state.consecutive_529 = 0
delay = retry_delay(attempt)
time.sleep(delay)
continue # ← 循环内重试
# ── 非瞬时故障 → 抛给外层处理 ──
# prompt_too_long、鉴权失败、网络断开等不属于瞬时故障
# raise 会直接穿透 for 循环,被 agent_loop 的外层 try/except 捕获
raise
# for 循环正常结束(10 次全部重试失败)→ 彻底失败
raise RuntimeError(f"Max retries ({MAX_RETRIES}) exceeded")
is_prompt_too_long_error — 错误类型判断
判断是否提示词过长:四个条件用 or 连接,覆盖 Anthropic API 在不同版本中可能返回的不同措辞。
def is_prompt_too_long_error(e: Exception) -> bool:
"""检查 API 错误是否表示提示词/上下文过长"""
msg = str(e).lower()
# 检查异常消息中是否包含特定关键词
return (("prompt" in msg and "long" in msg)
or "prompt_is_too_long" in msg
or "context_length_exceeded" in msg
or "max_context_window" in msg)
agent_loop 里的核心集成
双层异常处理
- 内层
with_retry:自动重试 429/529,最多 10 次 - 外层 try/except:捕获
with_retry抛出的非瞬时错误,执行压缩或退出
模型输出超出限制:max_tokens 的两阶段处理
| 阶段 | 条件 | 行为 |
|---|---|---|
| 阶段 1 | has_escalated=False |
提升至 64K,不追加截断内容(避免重复,用更大窗口重新生成完整内容) |
| 阶段 2 | has_escalated=True |
追加截断内容 + 续写提示,最多 3 次 |
agent_loop(messages: list, context: dict):
system = get_system_prompt(context)
state = RecoveryState()
max_tokens = DEFAULT_MAX_TOKENS # 初始 8000
while True:
# ── 内层:with_retry 处理 429/529 最多 10 次 ──
try:
response = with_retry(
lambda: client.messages.create(
# 可能已切换到备用模型
model=state.current_model,
system=system,
messages=messages,
tools=TOOLS,
max_tokens=max_tokens), # 可能已提升至 64K
state)
# ── 外层:处理 prompt_too_long 和 unrecoverable ──
except Exception as e:
# 路径2:紧急压缩(仅一次)
if is_prompt_too_long_error(e):
if not state.has_attempted_reactive_compact:
messages[:] = reactive_compact(messages)
state.has_attempted_reactive_compact = True
continue # 回到循环开头重试
# 压缩后仍然超长,终止
return
# 不可恢复的错误
return
# ── 路径1:max_tokens 处理 ──
# 模型达到 max_tokens 上限停止生成触发
if response.stop_reason == "max_tokens":
# 第一次:提升上限,不保存截断内容(阶段1的输出可能不完整或逻辑断裂,不如用更大窗口重新生成一段完整的)
if not state.has_escalated:
max_tokens = ESCALATED_MAX_TOKENS
state.has_escalated = True
continue # 用新上限重新请求
# 64K 仍然截断:保存输出 + 续写提示
messages.append({"role": "assistant", "content": response.content})
# 续写次数上限内,对话历史添加一条续写 prompt,继续循环,模型接着上次内容继续生成
if state.recovery_count < MAX_RECOVERY_RETRIES:
messages.append({"role": "user", "content": CONTINUATION_PROMPT})
state.recovery_count += 1
continue
return # 续写次数耗尽
# 正常完成:保存响应
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return # 模型认为任务完成,退出循环
# 执行工具调用,结果附加到消息列表
# ... (工具执行代码)
本模块知识点
- 生产环境 API 错误常态:截断 / 超限 / 限流 429 / 过载 529。
RecoveryState控制面板:5 字段各管一条恢复路径上限,跨循环共享。- 指数退避
retry_delay:base × 2^attempt(上限 32s)+ 抖动,避免 thundering herd。 - 双层异常:内层
with_retry处理 429/529,外层 try/except 处理prompt_too_long与max_tokens续写。
| 路径 | 错误 | 处理 |
|---|---|---|
| 路径 3a | 429 限流 | retry_delay 指数退避重试 |
| 路径 3b | 529 过载 | 退避 + 连续 ≥3 次切换备用模型 |
| 非瞬时 | prompt_too_long 等 |
raise 穿透给外层 try/except |
| 路径 1 | max_tokens |
阶段1升上限,阶段2追加续写 |
| 路径 2 | prompt_too_long 重试 |
reactive_compact 仅一次 |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 茯茶养生人的博客!










