Skip to content

4.7 静默错误修复:用户看不见的「免疫系统」

本节学习目标

  • 能对错误做 粗分类:API 错误 / 429 速率限制 / 网络超时 / 解析失败。
  • 理解 退避(backoff)+ 抖动(jitter)+ 上限(cap) 三件套。
  • 区分 用户应知应静默 的边界,避免「沉默式失败」的伦理风险。

为何需要「静默」?

QueryEngine 面向 交互式 CLI:用户心理模型是「在和助手聊天」,而不是「在盯 nginx 日志」。

若每次都抛堆栈体验后果
打断心流用户以为产品坏了
暴露内部路径信息泄露风险
重复可恢复错误显得「不智能」

生活类比:优秀服务员不会在客人面前 大喊厨房着火了;他会说「请稍等,我们为您重新上一份」——背后可能是 重试一次出餐


错误分类矩阵

类型典型信号默认可恢复?常见策略
429 速率限制HTTP 429、retry-after尊重 header 等待
5xx 服务端502/503/504指数退避重试
网络超时ETIMEDOUT重试 + 降并发
DNS / 断网ENOTFOUND视情况有限次重试后提示
401/403 鉴权密钥错、组织策略必须明确提示用户
400 请求体消息结构不合法多半否记录上下文,可能需开发介入
压缩熔断连续失败计数终止并解释原因

重试策略:指数退避 + 抖动

公式(教学)

[ \text{delay} = \min\left(\text{cap},\ \text{base} \times 2^{\text{attempt}} + \text{jitter}\right) ]

参数典型取值(示意)
base200ms~1s
cap30s~120s
jitter随机 0~250ms

为何要有 jitter? 大量客户端同时 429 时,若都 精确同一秒重试,会造成 重试风暴

教学伪代码

typescript
async function withSilentRetries<T>(
  fn: () => Promise<T>,
  opts: { maxAttempts: number; baseMs: number; capMs: number },
): Promise<T> {
  let attempt = 0;
  // eslint-disable-next-line no-constant-condition
  while (true) {
    try {
      return await fn();
    } catch (e) {
      attempt += 1;
      if (attempt >= opts.maxAttempts || !isRetriableError(e)) {
        throw e;
      }
      const delay = computeBackoffWithJitter(attempt, opts.baseMs, opts.capMs);
      await sleep(delay);
      yieldRetryTelemetry(e, attempt); // 用户不可见
    }
  }
}

「静默」不是「隐瞒」:可观测性必须存在

渠道内容
本地日志文件完整 stack、request id
遥测(若启用)错误码聚合
UI一句「正在重试…」或进度条

伦理边界:若错误导致 任务未完成,最终仍应 明确告知「未完成的原因」,而不是无限重试后假装成功。


与流式解析错误的交织

阶段错误处理
半条 SSE 行JSON.parse 失败丢弃坏行或整轮重试
message_stop 未到达连接断开视为可恢复,重试本轮
tool_use JSON 坏输入非法生成 tool_result 描述错误,让模型自纠

与八步循环的挂钩

八步静默修复发生点
2~3API 流、解析
5工具执行超时(可重试一次)
1压缩子请求失败(计入熔断)

小结

  • 分类决定 重试还是停机429/5xx/超时 常走静默恢复。
  • 退避 + jitter + cap 是工业标准,防止 重试风暴
  • 静默面向 UI,日志/遥测面向真相——二者缺一不可。

重试次数与退避:推荐配置表(教学)

下表 不是 泄露源码的硬编码值,而是 工业 CLI 常见量级,便于你做实验或读代码时对照。

错误族建议 maxAttemptsbaseMscapMs备注
4295~850060000优先读 retry-after
503/5023~530030000可略激进
网络抖动320015000配合 DNS 缓存
解析半包1~200重拉整流 而非死 parse

isRetriableError 判别伪代码

typescript
function isRetriableError(e: unknown): boolean {
  if (isHttpError(e)) {
    if (e.status === 429) return true;
    if (e.status >= 500 && e.status <= 599) return true;
    if (e.status === 408) return true; // Request Timeout
    return false;
  }
  if (isNodeNetError(e)) {
    return ["ECONNRESET", "ETIMEDOUT", "EAI_AGAIN"].includes(e.code);
  }
  return false;
}
返回 false 的典型情况处理
401引导用户检查 ANTHROPIC_API_KEY
400 + invalid_request_error记录 messages 摘要到日志,避免上传密钥

与 QueryEngine 八步的「挂钩点」速查

步骤可静默?典型用户感知
2~3常可「稍等」或无明显提示
5部分可工具卡片显示重试中
1有限压缩失败或触发熔断文案

可观测性字段:建议 always-on

字段用途
request_id对齐 Anthropic 支持工单
attempt区分首次与重试
error_class429 / 5xx / net / parse
backoff_ms复盘退避是否合理

常见误解

误解澄清
「静默 = 吞掉错误」错:应 记录 + 聚合,只是 默认不刷屏
「所有 400 都可重试」错:多数 400 需 修请求体
「重试越多越好」错:会放大 雪崩账单

下一篇:4.8 预算三重关卡

本项目仅用于教育学习目的。Claude Code 源码版权归 Anthropic, PBC 所有。