02 Claude 的缓存,控制很强,也很容易被打碎
Claude 的 prompt caching 比较适合拿来讲 Agent 成本,因为它的机制足够明确,usage 字段也比较细。
Claude 缓存看的是请求里的有序前缀。按照 Anthropic 文档,缓存会按这个顺序引用内容:
tools → system → messages
这意味着,前面的内容变化,会影响后面内容的缓存。
如果 tool definition 变了,后面的 system 和 messages 缓存都可能受到影响。如果 system prompt 变了,后面的 messages 缓存也会受到影响。到了 Agent 场景,这一点很关键,因为 tools 和 system 往往是每轮请求里最靠前、也最容易被框架动态拼接的部分。
Claude 的缓存通过 cache_control 指定缓存位置。可以理解成告诉 API:到这里为止,这一段前缀值得缓存。
一个简化示例:
{
"model": "claude-sonnet-4-5",
"system": [
{
"type": "text",
"text": "You are a coding assistant. Follow the project rules below..."
}
],
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Here is the repository context and coding rules...",
"cache_control": {
"type": "ephemeral"
}
},
{
"type": "text",
"text": "Now fix the failing test."
}
]
}
]
}
这里的重点是位置。cache_control 放在稳定内容之后,后面的用户新问题可以变化,前面的稳定内容还有机会被复用。
默认情况下,{"type":"ephemeral"} 对应 5 分钟 TTL。需要 1 小时 TTL 时,原始 API 请求里要显式加上 ttl 字段:
{
"cache_control": {
"type": "ephemeral",
"ttl": "1h"
}
}
所以,Claude API 层面要看的字段主要有两个:type 和 ttl。type 表示这是临时缓存,ttl 决定缓存保留默认 5 分钟还是 1 小时。5 分钟是默认路径,1 小时需要明确配置,写入成本也更高。
但到了 Claude Code CLI,情况会多一层封装。
我查了 Claude Code changelog 和本机更新后的 Claude Code 2.1.160 代码。changelog 里在 2.1.108 提到:新增 ENABLE_PROMPT_CACHING_1H 环境变量,用来启用 1 小时 prompt cache TTL;同时新增 FORCE_PROMPT_CACHING_5M,用来强制 5 分钟 TTL。后续 changelog 还修复过“1 小时 prompt cache TTL 被静默降级成 5 分钟”的问题。
在 2.1.160 的 CLI 代码里,cache_control 并非全局固定成 5m 或 1h。Claude Code 会先生成一个基础缓存配置:
function Ta({ scope, ttl } = {}) {
return {
type: "ephemeral",
...ttl && { ttl },
...scope === "global" && { scope }
}
}
是否给某些缓存块传入 ttl: "1h",由另一个内部判断决定:
function yyH(querySource) {
if (process.env.FORCE_PROMPT_CACHING_5M) return false
if (process.env.ENABLE_PROMPT_CACHING_1H) return true
// 之后再看账号状态、overage、querySource 和内部 allowlist
}
也就是说,Claude Code 发给 API 的 block 仍然是 cache_control,但 ttl: "1h" 是否出现,会受 CLI 自己的策略影响。ENABLE_PROMPT_CACHING_1H 可以打开 1 小时路径,FORCE_PROMPT_CACHING_5M 可以压回 5 分钟路径;没有强制环境变量时,CLI 还会按 query source、账号状态和内部配置决定哪些请求适合 1 小时缓存。
这也解释了为什么在 Claude Code 的用量里,看到的 TTL 不一定像手写 API 请求那样单一。最终响应 usage 里还可能拆出 cache_creation.ephemeral_1h_input_tokens 和 cache_creation.ephemeral_5m_input_tokens,分别表示本轮写入 1 小时缓存和 5 分钟缓存的输入 token。
Claude 对命中条件要求很严格:缓存断点之前的内容需要保持一致。哪怕语义差不多,只要文本、结构、顺序发生变化,都可能影响命中。
这对 Agent 框架提出了一个很直接的要求:稳定内容要真的稳定。
常见的失效来源包括:
- tool schema 变化
- tool 列表顺序变化
- system prompt 每轮加入当前时间
- system prompt 每轮加入动态 token budget
- 项目规则的位置前后移动
- messages 中工具结果结构变化
- JSON key 顺序变化
- 切换模型或平台参数
这些变化单独看都不大,但它们如果出现在缓存断点之前,就会影响后面整段内容的复用。
Claude 的 usage 字段比较适合拿来读账:
{
"usage": {
"input_tokens": 842,
"cache_creation_input_tokens": 18640,
"cache_read_input_tokens": 73210,
"output_tokens": 1260
}
}
这里有三个输入相关字段:
cache_creation_input_tokens:本轮写入缓存的输入 tokencache_read_input_tokens:本轮从缓存读取的输入 tokeninput_tokens:缓存断点之后,仍按普通输入处理的 token
所以读 Claude 账单时,不能只看 input_tokens。完整输入规模要把这三项合起来看。真正影响成本的是它们分别落在哪种计价档位。
如果一轮请求里 cache_read_input_tokens 很高,说明大量重复上下文被复用。
如果连续多轮 cache_creation_input_tokens 很高、cache_read_input_tokens 很低,说明系统可能一直在写缓存,却没有有效读回来。
这类情况在 Agent 里不罕见。
比如一个框架每轮都把当前时间、剩余 token、cwd、git branch 写进靠前的 system prompt;或者 MCP 工具 schema 每轮顺序不稳定。用户看到的只是继续对话,Claude 看到的前缀已经变了,缓存自然很难稳定命中。
Claude 的强项在于控制感很强。你可以通过 breakpoint 明确告诉它哪些内容值得缓存,也可以通过 usage 字段看到缓存读写情况。
代价是,框架和应用必须认真管理上下文结构。
如果前缀管理做得好,Claude 的缓存非常适合长会话、代码仓库上下文、Agent 工具调用。
如果前缀每轮都变,缓存机制再强,也只能不断写入新缓存。