draft · 仅 crs13 预览环境显示,正式站不会发布

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 层面要看的字段主要有两个:typettltype 表示这是临时缓存,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_tokenscache_creation.ephemeral_5m_input_tokens,分别表示本轮写入 1 小时缓存和 5 分钟缓存的输入 token。

Claude 对命中条件要求很严格:缓存断点之前的内容需要保持一致。哪怕语义差不多,只要文本、结构、顺序发生变化,都可能影响命中。

这对 Agent 框架提出了一个很直接的要求:稳定内容要真的稳定。

常见的失效来源包括:

这些变化单独看都不大,但它们如果出现在缓存断点之前,就会影响后面整段内容的复用。

Claude 的 usage 字段比较适合拿来读账:

{
  "usage": {
    "input_tokens": 842,
    "cache_creation_input_tokens": 18640,
    "cache_read_input_tokens": 73210,
    "output_tokens": 1260
  }
}

这里有三个输入相关字段:

所以读 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 工具调用。

如果前缀每轮都变,缓存机制再强,也只能不断写入新缓存。