一次 Claude Code 缓存崩溃的完整溯源——从 99% 暴跌到 0.4% 的数据→源码→验证之旅
背景
用 Claude Code 对接 DeepSeek V4 的 Anthropic 兼容接口,发现缓存命中率偶尔脉冲式突变:同一轮对话中从 99%+ 暴跌到 0.5%,下一轮自动恢复。
1 | 10:11:23 cache=122,368/123,641 99.0% |
根因
Claude Code 的 system prompt 末尾嵌入了 git status 快照:
1 | system[0]: "You are Claude Code..." ← cache_control 断点 |
两条相邻请求用 DeepSeek tokenizer 做逐 token diff 验证:
1 | Token 总量: req1=361,655 req2=361,789 (+134) |
仅仅因为 git status 多了一行 ?? temp_debug.json(26 字节),后续 36 万个 token 的 KV cache 全部作废。
源码追溯
定位到 Claude Code 源码中的完整调用链:
1 | // context.ts:36 |
关键:getGitStatus 和 getSystemContext 都是 lodash memoize(Map-based,无 TTL),缓存永不自动过期。只在三处被清除:
| 位置 | 触发场景 |
|---|---|
commands/clear/caches.ts:54 |
用户执行 /clear |
main.tsx:3111 |
--continue |
main.tsx:3362 |
--resume / --from-pr / teleport |
正常逐轮 REPL 对话中,即使你用 Write 工具创建了文件,getGitStatus 返回的仍是会话开始时的快照——memoize 劫持了所有后续调用。
但 --resume 触发 clearSessionCaches() → getGitStatus.cache.clear() → 重新执行 git status --short → 磁盘上所有文件变化被拍进 system prompt → KV cache 崩盘。
验证实验
| 轮次 | 操作 | gitStatus | 命中率 |
|---|---|---|---|
| 1 | 同一 session 内 Write 创建文件 | 未变 | 99.8% ✅ |
| 2 | --resume 重启 |
已更新 | 0.8% 🔴 |
| 3 | 不重启,继续对话 | 未变 | 99.8% ✅ |
暴跌后自动恢复的机制:暴跌那轮把新前缀重新写入缓存 → 下一轮前缀不变 → 全部命中 → 直到下次 --resume 或 /clear → 再次暴跌。形成"脉冲式突变"。
解决方案
1 | export CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS=1 |
system prompt 不再嵌入 git status,前缀永不因文件变化而变。代价:Claude Code 不再自动感知 git 状态。
实践建议
排查过程中发现多个会破坏缓存前缀的操作,汇总如下:
1 | # 1. 禁用 billing header |
运行时行为方面的注意事项:
- 不要对话中途修改 CLAUDE.md 或 MEMORY.md。这两个文件通过
prependUserContext()注入 messages 前缀,compact 或/clear后重新读取磁盘 → 内容变了 → 缓存全碎。 - 不要对话中途增删 MCP 服务器。MCP 工具列表变化会改变 system prompt 中的工具定义段,触发缓存重建。
- 减少不必要的
--resume。正常逐轮 REPL 对话中getGitStatus被 memoize 劫持、永不刷新,缓存非常稳定。每次--resume都会触发全量刷新。 - 把临时文件加入
.gitignore。即使不禁用 git status,忽略临时文件也能减少 gitStatus 段的变化频率。