乐于分享
好东西不私藏

AI 服务挂了,Claude Code 用户没感觉——是因为做对了这 10 件事

AI 服务挂了,Claude Code 用户没感觉——是因为做对了这 10 件事

如果你做过 AI 产品,你应该见过这种场景——

:用户量上来了!

OpenAI:今晚 3 点 ~ 6 点 API 不可用。

用户:你这玩意儿挂了!

:这不是我的锅啊,是上游……

用户:我管你上游下游,反正你这破玩意儿挂了。

挂在这种事情上的产品我见过太多。

朋友圈三天两头有 AI 创业公司发"由于上游服务波动,我们暂停服务"的公告

但你注意一件事—— Anthropic 的 API 也会挂。 但 Claude Code 用户基本没感觉

这一章讲的就是——Claude Code 是怎么把"上游挂了"这件事,藏到用户感觉不到的深度


重试这件事,没你想得简单

先讲一个数字。

Claude Code 的 API 通信层,一次请求最多重试 10 次

听起来很多对吧?但更精彩的是——重试不是无脑重试

源码里有一个常量叫 DEFAULT_MAX_RETRIES = 10,但旁边还有一个常量叫 MAX_529_RETRIES = 3

什么意思?

全局重试预算 10 次,但 529 错误(服务过载)独立计数——上限 3 次

为什么 529 要单独算? 因为 529 是"服务端忙不过来"的信号——你越重试,越是火上浇油

源码注释里有一句话让我看完后背一凉:

"每一次重试都是对过载后端的 3-10 倍放大效应。"

什么意思?

假设上游服务挂了。1 万个客户端在请求。每个客户端遇到 529,立刻重试。

1 万个变 2 万个。

2 万个变 4 万个。

4 万个变 8 万个。

3 次重试之后,原本的 1 万个流量变成了 8 万个流量。 本来只是过载,现在是雪崩。

所以 Claude Code 设计了 4 条规矩:

  1. 529 单独计数,最多 3 次——别再火上浇油了
  2. 背景任务(标题生成、摘要)遇到 529 直接放弃——只有用户在等的请求才重试
  3. 3 次 529 之后,立刻降级模型——从 Opus 切到 Sonnet——避开过载的那条路
  4. 指数退避——重试间隔 0.5s、1s、2s、4s……加上 0~25% 抖动——避免所有客户端在同一秒重试

这 4 条规矩,是 Anthropic 工程师用一次次真实雪崩磨出来的

我那家挂了的初创公司,曾经因为一次重试雪崩,让上游 API 停了 4 小时。 我们当时用的是最朴素的 try { ... } catch { retry() }——就这。 那 4 小时,我们贡献了上游服务被打挂的一半流量


"慢不等于死"——这一句话价值几百万

第二个让我重新理解 AI 工程的细节。

流式响应这事——AI 模型一边说话你一边看——很常见对吧?

但流式响应有个隐藏问题:它可能挂在中间

你看到模型说了一半,然后……没了。 不是结束了。是中间断了。

怎么判断流是"挂了"还是"在思考"?

这是个哲学问题。慢和死,外观看起来一样。

Claude Code 给出了一个让我服气的答案——双重看门狗

| 看门狗 | 阈值 | 行为 | |---|---|---| | Idle 看门狗 | 90 秒没任何动静 | 断流 | | Stall 看门狗 | 两次事件之间间隔超过 30 秒 | 只记日志,不断 |

Idle = 完全死了。90 秒一个字都没出来——这条命没了,断流,启动 fallback。

Stall = 只是慢了。中间停顿了 30 秒,但还在往外蹦字——别动它,让它继续,但记一笔账。

这一组定义就是 4 个字——慢不等于死

源码注释里写:

"Idle = no events at all (connection dead);
Stall = events are arriving but with gaps (server is just slow).
Slow is not dead."

为啥这件事重要?

因为如果你把"慢"当成"死",你会浪费一大笔钱

每次断流,你前面流过来的那些 token 都白付钱了。 每次断流,你要重新从 0 开始,再付一次钱。 每次断流,用户看到的是"刚才那段没了",他要重发问题——又一次完整的钱。

我那家初创公司的产品,就是把"慢"当成"死"——超过 5 秒没响应就断流重试。 结果有一阵子 Anthropic API 在国内访问慢,每个请求都要 8~12 秒。 我们 5 秒就掐断,每次都得重新跑一遍。

那个月烧出去的钱是平时的 2.7 倍

如果当时我们写的是"30 秒只记不断、90 秒才断",可能还在。


缓存感知重试:等 20 秒还是切模型?

第三个细节——这个让我对工程的理解又上了一个台阶。

API 重试的时候,服务器经常在响应里告诉你"等 X 秒再试"——这个东西叫 Retry-After

正常的客户端是怎么处理的?

等 X 秒,再试。

就这。

Claude Code 的处理多了一步:

如果 X < 20 秒——原地等,保留缓存 如果 X ≥ 20 秒——别等了,切模型

为什么 20 秒是分水岭? 因为 prompt cache 的 TTL 大约就是几十秒。

什么是 prompt cache? 你每次发请求,前面那一坨几万 token 的系统提示词,Anthropic 会缓存,下次再发同一个就便宜 90%。

但这个缓存有时效——几十秒后过期

所以 Claude Code 是这么算账的:

  • 等 < 20 秒:缓存还在,原地等划算。等完直接用同一个模型,享受 90% 折扣。
  • 等 ≥ 20 秒:缓存反正要失效了,再等也没意义。直接切到备用模型——重新建立缓存——避开过载的那条路。

这个 20 秒不是拍脑袋的数字。 是一个用钱算出来的精算结果——

  • 等 20 秒能省 X 块 token 钱
  • 切模型成本 Y 块 token 钱
  • 当 X ≈ Y 时——拐点出现

Claude Code 在这个拐点上做了一行代码的决策


25 种错误分类——每一种都是一段血泪史

第四个让我服气的细节——错误分类

正常的 API 客户端,错误分类大概是这样的:

- 4xx:客户端错误 - 5xx:服务端错误

就这。

Claude Code 的错误分类有 25 种以上

  • aborted:用户主动取消
  • repeated_529:连续 529 过载
  • prompt_too_long:请求超长
  • pdf_password_protected:PDF 加密了
  • token_revoked:API key 被吊销
  • bedrock_model_access:AWS Bedrock 模型访问权限问题
  • tool_use_mismatch:工具调用配对错误(暗示上下文管理 bug)
  • ssl_cert_error:SSL 证书问题(检查代理)
  • ……

每一种错误背后都是一次具体的故障。 某个用户传了加密 PDF,PDF 库挂了——pdf_password_protected 诞生。 某次工具调用的 input 和 output 对不上——tool_use_mismatch 诞生。 某个 AWS 用户的 IAM 权限没配对——bedrock_model_access 诞生。

为什么要分这么细?

因为分得越细,修起来越快

监控告警显示"500 错误率上升"——你不知道是哪儿挂了,得花 2 小时排查。 监控告警显示"tool_use_mismatch 错误率从 0.01% 跳到 5%"——你 5 分钟知道是某次发布把工具调用的配对逻辑搞挂了。

源码注释里有一句话——

"这套系统的每一行代码都映射着一个曾经发生过的故障场景。"

这种分类不是天上掉下来的,是一次次半夜被叫起来磨出来的


心跳保活——长睡眠切成 30 秒一节

最后一个细节——这个真的让我服气。

Claude Code 有一个"持久重试模式"——开启之后,重试时间最长能等 5 分钟。

正常的客户端是这么写的:

sleep(retryAfterMs)  // 一觉睡到要重试的时候

就这。

Claude Code 是这么写的:

let remaining = retryAfterMs while (remaining > 0) {   yield 一个心跳事件   await sleep(min(remaining, 30秒))  // 最多睡 30 秒   remaining -= 30秒 }

长睡眠切成 30 秒一节

为什么?两个原因:

第一个原因——容器。 很多用户的 Claude Code 跑在容器里——比如 Docker、CCR 这种。容器有个习惯——5 分钟空闲它就把你回收了。 你睡 5 分钟,醒来发现容器已经被杀了,整个会话挂了。 切成 30 秒一节,每 30 秒动一下,容器就觉得你还在——不会被回收。

第二个原因——用户。 用户可能突然想 Ctrl+C 取消。 你睡 5 分钟,用户按 Ctrl+C 你听不到——他得等你睡醒。 切成 30 秒一节,每 30 秒检查一次"用户是不是要取消"——用户最多等 30 秒就能取消。

这个设计叫"心跳保活"。 名字朴素,背后是两次半夜被叫起来的故事—— "为什么我们的会话莫名挂了?"——容器被回收。 "为什么用户取消了还在跑?"——主流程在 sleep。

每一次半夜被叫起来,就多一行代码


你的 AI 应用为什么不稳,Claude Code 为什么稳

回到开头——

Claude Code 是怎么把"上游挂了"藏起来的

10 个细节:

  1. 重试 10 次,但 529 错误单独 3 次——避免火上浇油
  2. 背景任务遇到 529 直接放弃——别给过载的服务再加流量
  3. 3 次 529 之后立刻降级模型——切一条没堵车的路
  4. 指数退避 + 0~25% 抖动——避免所有客户端在同一秒重试
  5. 双重看门狗——慢不等于死,30 秒只记 90 秒才断
  6. 缓存感知重试——20 秒以内保留缓存,超过 20 秒切模型
  7. 25 种错误分类——每一种都是一次故障的具体修复
  8. 心跳保活——长睡眠切成 30 秒一节,容器不杀、用户能停
  9. fallback 链——主模型挂了切备用模型,备用挂了切再次备用
  10. 全程遥测——每次调用三个事件(query/success/error),出问题能查

每一项都对应着一个真实发生过的雪崩

我以前以为 AI 应用的核心竞争力是"调用对的 API + 写好的 prompt"。 读完这一章我知道——核心竞争力是"上游挂了你能不挂"

模型有三家在做。提示词全网都在抄。

唯独"上游挂了你能不挂"——是钱和时间堆出来的