背景

用 Claude Code 对接 DeepSeek V4 的 Anthropic 兼容接口,发现缓存命中率偶尔脉冲式突变:同一轮对话中从 99%+ 暴跌到 0.5%,下一轮自动恢复。

1
2
3
4
10:11:23  cache=122,368/123,641  99.0%
10:11:45 cache= 1,536/119,826 1.3% ← 暴跌
10:11:57 cache= 6,528/123,795 5.3%
10:12:18 cache=124,288/124,430 99.9% ← 恢复

根因

Claude Code 的 system prompt 末尾嵌入了 git status 快照:

1
2
3
4
5
6
system[0]: "You are Claude Code..."         ← cache_control 断点
system[1]: CLAUDE.md + 工具定义 + ... ← cache_control 断点
gitStatus: ← 此处变化!
M src/config.ts
?? demo.py ← 文件变更 → 多了/少了几行
messages[0..986]: 完整对话历史 ← 无 cache_control,字节偏移全乱 → 全碎

两条相邻请求用 DeepSeek tokenizer 做逐 token diff 验证:

1
2
3
Token 总量:   req1=361,655   req2=361,789   (+134)
分叉位置: token[1490] ← 正好落在 gitStatus 段
前缀命中: 1490 / 361,655 = 0.41% ← 与 API 报告的 0.45% 几乎一致

仅仅因为 git status 多了一行 ?? temp_debug.json(26 字节),后续 36 万个 token 的 KV cache 全部作废。

源码追溯

定位到 Claude Code 源码中的完整调用链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// context.ts:36
export const getGitStatus = memoize(async (): Promise<string | null> => {
// ...执行 git status --short, git log, git branch 等
return [
`This is the git status at the start of the conversation...`,
`Current branch: ${branch}`,
`Status:\n${truncatedStatus}`,
`Recent commits:\n${log}`,
].join('\n\n')
})

// context.ts:116
export const getSystemContext = memoize(async () => {
const gitStatus = await getGitStatus() // ← memoize 一级缓存
return { ...(gitStatus && { gitStatus }) } // ← 返回 { gitStatus: "..." }
})

// utils/api.ts:437
export function appendSystemContext(systemPrompt, context) {
return [...systemPrompt,
Object.entries(context)
.map(([key, value]) => `${key}: ${value}`)
.join('\n')
] // → "gitStatus: This is the git status..."
}

关键getGitStatusgetSystemContext 都是 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
2
3
4
5
# 1. 禁用 billing header
export CLAUDE_CODE_ATTRIBUTION_HEADER=0

# 2. 禁用 git status 注入(治本,避免 --resume 时缓存重建)
export CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS=1

运行时行为方面的注意事项:

  1. 不要对话中途修改 CLAUDE.mdMEMORY.md。这两个文件通过 prependUserContext() 注入 messages 前缀,compact 或 /clear 后重新读取磁盘 → 内容变了 → 缓存全碎。
  2. 不要对话中途增删 MCP 服务器。MCP 工具列表变化会改变 system prompt 中的工具定义段,触发缓存重建。
  3. 减少不必要的 --resume。正常逐轮 REPL 对话中 getGitStatus 被 memoize 劫持、永不刷新,缓存非常稳定。每次 --resume 都会触发全量刷新。
  4. 把临时文件加入 .gitignore。即使不禁用 git status,忽略临时文件也能减少 gitStatus 段的变化频率。