错误处理与自愈:AI 如何从失败中恢复
深入 Claude Code 的错误恢复机制——query.ts 恢复状态机、工具失败处理、网络重试、用户中断、/doctor 自检
问题引入
一个 AI Agent 在真实环境中运行时,失败是常态而非例外。网络超时、API 过载、文件权限不足、模型输出截断、用户突然按下 Esc——这些不是边缘情况,而是每天发生数百万次的日常事件。
Claude Code 的核心设计哲学是:错误不应该终止会话,而应该触发恢复。query.ts 中的主循环不是线性的请求-响应,而是一个包含多种恢复路径的状态机。当 API 返回 max_output_tokens 错误时,系统会自动重试并注入 "continue" 指令;当 prompt 超长时,系统会触发响应式压缩然后重试;当用户按 Esc 中断时,系统会生成合成的 tool_result 保持消息格式合法。
本文深入分析这个恢复状态机的每一条路径。
query.ts 恢复状态机
状态定义
query 循环维护一个可变状态对象,在每次迭代间传递:
关键的恢复状态字段:
maxOutputTokensRecoveryCount— 已尝试的输出截断恢复次数(上限 3)hasAttemptedReactiveCompact— 是否已尝试过响应式压缩maxOutputTokensOverride— 当前覆盖的 max output tokenstransition— 上一次迭代继续的原因(用于防止重复恢复)
循环初始化
恢复路径总览
max_output_tokens 恢复
当模型输出被截断(stop_reason: max_output_tokens)时,系统不是直接报错,而是尝试让模型继续:
错误抑制
流式循环中,max_output_tokens 错误被抑制(不发送给 SDK 消费者):
升级重试
如果使用的是默认 8K max output tokens,先升级到 64K 重试同一请求——不注入 continue 消息,不增加恢复计数:
如果 64K 也不够,进入多轮恢复——注入一条用户消息("你的输出在这里被截断,请从截断处继续"),然后循环回 API 调用:
恢复上限为 3 次——防止无限循环(模型可能在某些情况下持续产生超长输出)。
Prompt Too Long 恢复
当上下文超过模型限制时,系统有两级恢复:
第一级:Context Collapse 排空
Context Collapse 是一种轻量级压缩——将旧的消息折叠成摘要,但保留粒度。排空(drain)是把所有已准备好的折叠一次性提交:
注意 state.transition?.reason !== 'collapse_drain_retry' 检查——如果上一次迭代就是 collapse drain 且仍然 413,说明排空不够,需要更激进的措施。
第二级:Reactive Compact
如果 collapse 排空不够(或未启用),触发完整的响应式压缩:
关键的安全措施:
hasAttemptedReactiveCompact: true确保只尝试一次——防止"压缩→重试→413→压缩"死循环- 不执行 stop hooks——模型没有产生有效响应,hooks 无法评估
executeStopFailureHooks是不同的函数——它只做最基本的失败通知
前置阻断
在进入 API 调用前,如果 auto-compact 关闭且 token 已到阈值,直接阻断:
注意跳过条件——当 reactive compact 或 context collapse 启用时,不做前置阻断,因为它们能在 API 错误发生后恢复。前置阻断会阻止错误发生,也就阻止了恢复机会。
模型降级恢复
当流式传输过程中触发 FallbackTriggeredError:
特别值得注意的是 stripSignatureBlocks——protected thinking blocks 带有模型特定的加密签名,在降级到不同模型后会导致 API 400 错误。
用户中断处理
用户按 Esc 或 Ctrl+C 时,系统需要优雅地停止:
中断优先级:
- 活跃任务 — 设置 abort signal,取消 API 调用和工具执行
- 命令队列 — 如果 Claude 空闲但有排队命令,弹出最后一条
- 回退 — 清空权限确认队列
中断后的消息清理
在 query.ts 中,中断后需要为所有未完成的 tool_use 生成合成的 tool_result:
yieldMissingToolResultBlocks 确保消息格式合法——API 要求每个 tool_use 后必须有对应的 tool_result:
Ctrl+C vs Esc 的区别
Ctrl+C 额外处理了查看 teammate 的场景——停止所有后台 Agent 并返回主线程。
Kill All Agents (二次确认)
3 秒确认窗口防止误操作——后台 Agent 可能正在执行重要任务。
工具执行失败反馈
当工具执行失败时,错误信息作为 tool_result 的 is_error: true 内容反馈给模型。这让模型能理解发生了什么并决定下一步——是重试、换方法、还是向用户报告:
这是 Claude Code 的核心自愈模式——错误不是系统终止信号,而是模型的输入信号。模型看到 bash 命令失败后,通常会修改命令重试。看到文件不存在后,会先用 ls 检查。
/doctor 环境自检
/doctor 命令提供系统级的诊断:
诊断覆盖:
- 安装类型检测 — npm-global/npm-local/native/package-manager/development
- 多安装检测 — 发现系统中多个 Claude Code 安装
- 权限检查 — 自动更新是否有写权限
- ripgrep 状态 — 搜索引擎是否正常工作
- Shell 配置 — alias 和环境变量是否正确
安装类型检测逻辑相当详尽:
检测覆盖了所有主流包管理器——Homebrew、winget、mise、asdf、pacman、deb、rpm、apk——确保在任何 Linux/macOS/Windows 环境下都能正确识别安装方式。
恢复路径的互动关系
各恢复路径之间有复杂的互动关系,理解这些关系是理解系统韧性的关键:
关键的互动规则:
- 前置阻断与恢复互斥 — 启用 reactive compact 或 context collapse 时,不做前置阻断(否则恢复路径永远不会被触发)
- Collapse → Reactive 级联 — collapse 排空失败后才尝试 reactive compact
- 同类型只尝试一次 —
hasAttemptedReactiveCompact防止 reactive compact 死循环 - transition 防止重复 —
state.transition?.reason检查防止同一恢复策略连续执行 - 错误抑制与恢复必须一致 — 流式循环中抑制的错误,必须在恢复检查中有对应处理;否则错误会被吞掉
流式错误抑制的一致性要求
Feature flag 的值在流式传输的 5-30 秒内可能变化(GrowthBook 缓存刷新)。如果流开始时抑制了错误,但流结束时恢复检查看到 flag 关闭,错误就丢失了。所以在 turn 开始时一次性提取 flag 值,全程使用同一个值。
小结
Claude Code 的错误恢复系统体现了几个核心原则:
- 错误是输入,不是终止信号 — 工具执行失败变成
tool_result(is_error: true)反馈给模型 - 分级恢复 — 从轻量(collapse drain)到重量(reactive compact),逐级升级
- 有限重试 — 每种恢复路径都有明确的尝试上限,防止死循环
- 状态完整性 — 中断后生成合成 tool_result,保持消息格式合法
- Flag 一致性 — 抑制和恢复必须看到相同的 feature flag 值
- 环境自检 — /doctor 提供系统级诊断,帮助用户定位环境问题
这个系统的复杂性直接来源于"永远不应该终止会话"的设计目标。在一个 AI Agent 可能连续运行数小时的世界里,每一种故障模式都需要一条恢复路径——不是因为工程师喜欢复杂性,而是因为现实就是复杂的。