Claude Code 源码拆解(四):为了不让连接静默死掉,他们写了一个 90 秒看门狗
「Claude Code 源码拆解」系列 · 第四篇
上一篇拆完了 Agent 核心循环,这篇往下钻一层——API 调用层。一个 123KB 的 claude.ts 和一个 28KB 的 withRetry.ts,里面有为了省 50K token 缓存而精心设计的降级策略,有为了抓住”连接静默断开”而写的看门狗,还有一个因为 SDK 官方封装性能太差被绕过的骚操作。看完你会对”调一次 API”这件事有全新的认识。
直接进。
一、绕开 SDK 官方封装:因为 O(n²)
先说最骚的那个操作。
Anthropic 自己的 SDK 提供了 BetaMessageStream 封装,用起来很方便——创建流、遍历事件、自动组装消息。但 Claude Code 没用它,直接走了原始 Stream:
// Use raw stream instead of BetaMessageStream to avoid O(n²)// partial JSON parsing. BetaMessageStream calls partialParse()// on every input_json_delta, which we don’t needconst result = await anthropic.beta.messages .create({ …params, stream: true }, { signal }) .withResponse()
为什么?BetaMessageStream 对每个 input_json_delta 事件都调 partialParse()——每次都从头解析已收到的 JSON 片段。工具调用的 input 可能很大(几千 token),随着片段累积,每次解析的工作量线性增长,总复杂度变成 O(n²)。
Claude Code 的解法:自己攒字符串,只在 content_block_stop 时做一次完整解析。代价是几百行手写事件处理代码。但对一个动辄调几十次 API 的 Agent 来说,O(n) 和 O(n²) 的差距是用户能感知到的。
二、重试引擎:11 次机会 + 三层 Fast Mode 降级
基本面
withRetry 是个 AsyncGenerator,最多尝试 11 次,通过 yield 向 UI 推送重试等待进度,通过 return 返回最终结果。
退避策略:指数退避 + 25% jitter,上限 32 秒:
constBASE_DELAY_MS = 500// 退避序列:500ms, 1s, 2s, 4s, 8s, 16s, 32s, 32s…const baseDelay = Math.min(BASE_DELAY_MS * Math.pow(2, attempt – 1), maxDelayMs)const jitter = Math.random() * 0.25 * baseDelayreturn baseDelay + jitter
三层 Fast Mode 降级
Fast Mode(speed=’fast’)是 Opus 4.6 的加速模式,费用翻倍($30/$150 vs $5/$25 per Mtok)。遇到 429/529 时的降级策略特别精巧:
第一层:检查 overage 头 → 永久禁用 fast mode
第二层:短 retry-after(< 20 秒) → 保持 fast mode 重试。这个设计很关键——切模型名会让 prompt cache 失效(cache key 包含模型名),浪费已缓存的 ~50K token。
第三层:长 retry-after(≥ 20 秒) → 进入 cooldown,切标准模型。cooldown 最少 10 分钟,防止反复切换。
一句话总结这个策略的核心思路:能不丢缓存就不丢缓存。
529 连续 3 次 → 换模型
连续 3 次 529(过载),直接抛 FallbackTriggeredError,让上层切到备选模型。而且——后台任务不重试 529。
// Non-foreground sources bail immediately on 529 — no retry// amplification during capacity cascades. User never sees// these fail.
摘要生成、标题生成、分类器这些后台任务,遇到 529 直接放弃。因为在过载级联期间,每次重试都是 3-10 倍的网关放大。
三、90 秒看门狗:抓住”静默死亡”的连接
这是我觉得最值得学习的设计。
流式传输有个阴险的故障模式:连接没有断开,TCP 正常,但服务端就是不发数据了。SDK 的超时只管初始连接,不管 body 接收。没有看门狗的话,你的 session 会永远挂在那里。
constSTREAM_IDLE_TIMEOUT_MS = 90_000// 90 秒无数据 → 中止constSTREAM_IDLE_WARNING_MS = 45_000// 45 秒先警告// 每收到一个 chunk 就重置计时器for await (const part of stream) {resetStreamIdleTimer() // ← 这一行// 处理事件…}
除了看门狗,还有一套”断流检测”——记录每两个 chunk 之间的间隔,超过 30 秒就记录一次 stall。这和看门狗是两套机制——stall 检测只在收到下一个 chunk 时才触发(被动),看门狗用 setTimeout 主动杀(主动)。两套配合覆盖所有场景。
四、非流式降级:一个导致工具重复执行的 bug
流式传输中途出错时,有个降级路径:切到非流式模式重试。但这引入了一个 bug(inc-4258):
// The mid-stream fallback causes double tool execution// when streaming tool execution is active: the partial// stream starts a tool, then the non-streaming retry// produces the same tool_use and runs it again.// See inc-4258.
流式模式下,工具在 content_block_stop 到达时就开始执行了。如果此时流挂了,降级到非流式,非流式模式返回的响应里又包含同一个工具调用——结果同一个工具被执行了两次。
五、持久重试模式:6 小时不放弃
为 CI/CD 无人值守场景设计的 CLAUDE_CODE_UNATTENDED_RETRY 模式,429/529 无限重试:
constPERSISTENT_MAX_BACKOFF_MS = 5 * 60 * 1000// 最大退避 5 分钟constPERSISTENT_RESET_CAP_MS = 6 * 60 * 60 * 1000// 最大等待 6 小时constHEARTBEAT_INTERVAL_MS = 30_000// 心跳 30 秒
关键设计:把长等待切成 30 秒的小块,每块都 yield 一条消息给宿主环境。因为 CI/CD 平台会把静默超过一定时间的 session 标记为空闲并杀掉。每 30 秒输出一条”还在等”就能续命。
// TODO(ANT-344): the keep-alive via SystemAPIErrorMessage// yields is a stopgap until there’s a dedicated// keep-alive channel.
用错误消息来保活——不够优雅但确实有效。
六、Prompt Cache:为了省钱的精密手术
1h TTL 缓存:默认缓存 5 分钟,但 Anthropic 员工和订阅用户可以开启 1 小时 TTL——同一个系统提示词和工具定义,缓存命中能省掉 ~50K token 的重复传输。
Session 级锁存:1h TTL 资格和 allowlist 在 session 启动时”锁定”,整个 session 内不变。为什么?因为如果中途配置变了,cache_control TTL 从 1h 变成 5min,就会 bust 服务端缓存——相当于白花了之前写缓存的钱。
Fast Mode header 锁存:一旦发送过 fast mode beta header,后续请求即使进入 cooldown 也继续发送 header。因为 header 是 cache key 的一部分,去掉 header 就等于换了 cache key。cooldown 只禁止 speed=’fast’ 参数,不禁止 header——用最小的变化保住缓存。
写在最后
拆完 API 层,我最大的感受是:调一次 API 这件事,远比你想象的复杂。
重试不是简单的”失败了再来一次”——你得区分前台后台(后台不重试 529 避免级联放大),得三层降级保住缓存,得在连续 3 次过载时果断换模型。流式传输不是”开一个连接读到底”——你得防静默断连(90 秒看门狗),防重复执行(工具已经跑了但流挂了),防代理返回 200 但实际没数据。
而所有这些复杂性的终极目标只有一个:让用户觉得 AI 工具”就是稳”。最好的基础设施就是你感知不到它的存在。
📄 完整代码分析报告(1.5万字)
我让 Claude Code 用 4 个 Agent 并行,对它自己的源码做了一份完整分析报告,覆盖 22,068 个文件,包含:核心架构分层、40+ 工具清单、状态管理详解、Hook 生命周期、权限系统、设计模式识别…
后台私信「claude code」即可获取 👇另有一份更详细的架构流程图正在整理中,敬请期待
一个问题留给大家:
Claude Code 为了保住 ~50K token 的 prompt cache,设计了三层 Fast Mode 降级 + header 锁存。你觉得这种”为了省钱不惜增加系统复杂度”,是值得学习的工程纪律,还是过度优化?评论区聊聊 👇
下一篇,拆权限系统——300KB 的 BashTool 安全代码、自研 Shell 解析器,以及那个让 AI “不能 rm -rf / 但能 echo rm -rf /” 的语义级命令分析。
本文基于 npm 包 @anthropic-ai/claude-code v2.1.88 的反编译源码进行分析仅用于技术学习 · 源码版权归 Anthropic 所有
夜雨聆风