Claude Code 源码就像红楼梦,不同的人可以从中看到不同的东西。安全从业人员可以看到它复杂的 shell 注入防护;Agent 开发者可以看到它巧妙的用户交互;而作为 AI 网关开发者,我更多地关注它是怎么高效地与 Anthropic 模型提供商打交道的,尤其是关注它是如何组织 prompt cache。
在进入正题之前,让我们先绕过去,先不看 Claude Code 是如何使用私有 API 的。我们先来看看,它是如何让整个缓存的使用更加高效的。和其他模型厂商不同,Anthropic 是要用户额外为缓存付费的。它有两档:缓存 5 分钟,cache write (生成缓存)部分按 1.25x input cost 计价;缓存 1 小时 2x 计价。所以 Anthropic 默认是不会启用缓存。至于缓存命中的计费,倒是和主流厂商一样有 90% 的折扣。总体来说,Anthropic 认为有缓存的话,其实成本会更低。因为在 Claude Code 里面默认就会为订阅用户(包括 PAYG 的二等 Enterprise 订阅用户)把缓存打开,并设置为一小时的缓存。而对于使用 API Key 的用户,它则没有打开缓存。我觉得这更多是因为它可能觉得,如果价格和用量看起来不一致,用户可能会感到疑惑,所以才选择不开启。有趣的是,关于 Bedrock 还有个 ENABLE_PROMPT_CACHING_1H_BEDROCK 环境变量来开启 prompt cache。
那么这个缓存的 key 是怎么决定的?Anthropic API 里面没有显式输入 key 的接口。从代码上看,key 的组成估计是和服务端约定的一个范围,包括下面内容:
- system
- tools
- model
- messages prefix
- thinking config
- betas header
Claude Code 做了很多事情来减少内容变化,提高命中率。比如 search tool;将部分内容做一层映射在保持 prompt 不变的同时让用户感觉有所变化;delta attachment。前两者在 Anthropic 公开的博客文档里面就有。这里说一下第三个。Claude Code 在禁用/启用某个工具时并不是重新将 tools 生成一遍 - 因为 tools 也是缓存结构的一部分。它会 append 一条由 <system-reminder>...</system-reminder> 包裹起来的消息,告诉 LLM 哪些工具被禁用/启用了。这样一来,原本的消息依然能命中 prompt cache。
Okay,在铺垫了那么多之后,我们终于可以进到主题了。和主流的厂商相比,Anthropic 有着复杂的缓存管理 API。除了一个全局的 cache_control 字段,它还支持在某个 content block 中加上 cache control breakpoint,这样缓存策略只会应用于开头到 breakpoint 部分(或 breakpoint 之间)。Anthropic 支持最多打四个 breakpoint,但 Claude Code 绝大多数情况下只会打两个 breakpoint,一个在 system 里面打(system 通常都能复用),另一个在最后一个 block 上打(或者倒数第二个 block,如果最后一个 block 触发了 subagent)。
以上都是官方文档里能找到的部分。
下面就是私有 API 了。Claude Code 有个功能开关,支持在 main agent 里使用额外的 cache_reference / cache_edit 标记做 microcompact。
前者是给旧的 content block 打上 cache_reference。形状大概是这样:
{
"type": "tool_result",
"tool_use_id": "toolu_123",
"content": "...large output...",
"cache_reference": "toolu_123"
}后者是在后续请求里追加一个 cache_edits block:
{
"type": "cache_edits",
"edits": [
{ "type": "delete", "cache_reference": "toolu_123" },
{ "type": "delete", "cache_reference": "toolu_456" }
]
}这两个东西放在一起,语义就很清楚了:
- 先给旧块一个可引用的名字
- 再告诉服务端,后续请求里这些引用对应的块应该被删掉
注意这个“删掉”不是真的删掉,而是删服务端缓存视图里的那一部分,是逻辑删除。cached microcompact 这条路径明确写着:不修改本地消息内容。也就是说,用户在 UI 里看到的那段历史还是原来的样子,消息本身没有被直接替换成 [Old tool result content cleared] 之类的占位符。真正变化发生在请求序列化阶段。
下面是一个最小例子,假设原始对话长这样:
system:
[sys static]
[sys dynamic]
messages:
U1 user asks a question
A1 tool_use(read file)
U2 tool_result(toolu_1, "... very large file ...")
A2 assistant answer
U3 continue
A3 next answer
^
last message-level cache_control marker如果 Cloud Code 认为 toolu_1 这块历史结果已经没有必要一直带着了,它不会去改 U2 的本地内容。它做的是在这次发请求时,改成下面这样:
messages:
U1 user asks a question
A1 tool_use(read file)
U2 tool_result(toolu_1, "... very large file ...", cache_reference="toolu_1")
A2 assistant answer
U3 [
text("continue"),
cache_edits(delete cache_reference="toolu_1"),
text(".")
]
A3 next answer
^
last message-level cache_control marker所以这里发生的不是“把历史消息删短了”,而是“在原有历史上追加一个 provider 能看懂的删除脚本”。换句话说,它是这样的过程:
第一次请求时,服务端拿到的是完整前缀:
Turn N
------
Client sends:
[1][2][3][cache_control]
Server caches:
P0 = [1][2][3]下一次请求,客户端没有去重写 2,而是这样发:
Turn N+1
--------
Client sends:
[1{cache_reference=1}]
[2{cache_reference=2}]
[3{cache_reference=3}]
[cache_edits(delete 2)]
[tail...]
[cache_control]这就是“逻辑删除”的含义:
- 本地历史没删
- 用户看到的内容没变
- 服务端缓存前缀的有效视图变了
这种逻辑删除使得 microcompact 成为可能。用过 coding agent 的人都知道,在实际开发中难免会走一些弯路。这些弯路会留在上下文里。有些时候我们会希望压缩这部分不在主线上的内容,但传统的 /compact 会改写上下文,导致缓存不命中。而 microcompact 可以根据上下文语义,由客户端决定需要淘汰哪些内容。理论上客户端将不再被 prompt cache miss 束缚着,极大地释放管理上下文的潜能。当然作为实验性功能,应该有一些限制(比如像 cache breakpoint 最多只能放四个一样,cache edit 的个数应该是有限的)。
目前 Claude Code microcompact 针对的不是所有消息,而是高体积、价值衰减快的 tool_result。src/services/compact/microCompact.ts 的 compactable 工具集合包括:
- Read
- Bash
- Grep
- Glob
- WebFetch
- WebSearch
- FileEdit
- FileWrite
注意 Anthropic 并没有完全打破 kvcache 默认按前缀复用的规则:
- 它仍然围绕一个主要 cached prefix 工作
- 只是允许在这个 prefix 内部,通过 edit script 做局部删除
- 而且不是引用计数(所以只能在 main agent 里面开启该功能)
也就是 “cached prefix + edit script”,而不是更强的“支持非连续缓存块”语义。这个区别很重要,因为客户端还有一个行为细节能说明问题:旧的 cache_edits 不是发一次就完了,而是会在后续请求里继续按原位置重发。如果这套协议只是“一次删除,永久改写服务端缓存对象”,那客户端没有必要一直带着旧的 cache_edits。但 Claude Code 实际上不是这么做的。它会把已经发过的 cache_edits pin 住,然后每次后续请求都重新插回原来的位置。这个行为非常值得注意,因为它基本说明了两件事:
- cache_edits 不是一次性后台命令
- 它本身已经变成了“稳定请求形状”的一部分
换言之,服务端复用的不是一个“已经被永久改写过的缓存对象”,而更像是“原始前缀加一组持续生效的编辑脚本”。所以传统 kvcache aware routing 通过前缀树匹配找到最佳的推理引擎,应该还是适用的。不过局部删除的部分,仍然还会释放 kvcache block。但释放了 kvcache block 之后,下一个请求过来是否需要重新计算,这个光看客户端代码证明不了。我个人倾向于不需要重新计算,否则 compact 删掉的内容还是会生成新的 kvcache block,那么在本次释放 kvcache block 的意义就不大了。无论是否需要重新计算,其他内容因为位置不变,还是可以复用 kvcache block 的。
当下的潮流是,大模型公司把产品 + infra + 模型作为一套组合拳,紧密结合起来优化。cache reference / cache edit 就是一个很好的例子。如果不是 agent,而是 chatbot,恐怕没多少内容需要通过 cache edit 给 microcompact 掉。同时如果 infra 不支持,也不可能有 microcompact 这套方案。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。