s11:错误恢复(Error Recovery)

核心问题

生产环境中 API 错误是常态:输出被截断、上下文超限(压缩后还是太长)、临时故障(429 限流 / 529 过载)。错误恢复系统设计了一套多路径、带上限、可降级的恢复机制,让 Agent 在故障中尽可能继续工作,而不是崩溃退出。


RecoveryState — 恢复状态的"控制面板"

RecoveryState 是一个共享状态对象,其五个字段各管一条恢复路径的上限,被 agent_loopwith_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_delaybase × 2^attempt(上限 32s)+ 抖动,避免 thundering herd。
  • 双层异常:内层 with_retry 处理 429/529,外层 try/except 处理 prompt_too_longmax_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 仅一次