几天没上班,AI 又进步了不少。在向 Claude 询问最近的更新内容时,我注意到它的「缓存读取」降价非常明显($0.50 → $0.20 / 百万 token),而输入价只降了 20%。

这让我想起之前在做 Agent 开发时,为了省钱,自己写逻辑,截取部分信息再发给大模型。朋友发来一句灵魂拷问:「你这样频繁截取信息,考虑过缓存命中吗?」

这里梳理了一下 Prompt Caching(提示词缓存)的底层逻辑,简单做个笔记,提升下认知吧。

一、它是什么?

在传统 API 调用中,每次请求都必须发送完整的上下文(系统提示、工具定义、历史对话、参考文档等)。模型需要从头处理这些内容,并按完整的输入 Token 计费。

然而,在多轮交互中,绝大部分前置上下文是静态的。Prompt Caching 就是让服务端将这段重复的前缀存储起来,当后续请求的开头部分完全一致时,直接复用已处理的计算结果。

以 Claude Opus 5.5 的计费为例:

  • 缓存写入(Cache Write):首次存入,比常规输入贵(5 分钟缓存是输入价的 1.25 倍,1 小时缓存是 2 倍)。
  • 缓存读取(Cache Read):后续命中缓存,$0.20 / 百万 Token,只有常规输入($4)的 1/20。

二、为什么对 Agent 尤为重要?

Agent(如 Claude Code、Codex CLI)在执行复杂任务时,往往需要与大模型进行数十甚至上百轮的交互。每一轮都必须携带之前积累的所有上下文,上下文越滚越大,累计的 Token 消耗增长得非常快。

粗略算一笔账:假设上下文累积达 10 万 Token,交互 50 轮,按 Opus 5.5 的定价:

  • 无缓存机制:500 万输入 Token × $4/百万 = 约 $20
  • 充分命中缓存:首次写入约 $0.5,后续 49 轮读取约 $1,合计 = 约 $1.5

Agent 账单的大头往往源于「反复读取同一段长上下文」。因此,缓存读取的定价才是决定 Agent 运行成本的关键所在。

三、核心原理:前缀匹配(Prefix Matching)

系统会从第一个 Token 开始逐一比对,一旦遇到第一个不同的地方,从该位置往后的所有缓存将全部失效。

这是 Prompt Caching 的核心铁律,后续所有的优化策略均是基于此原则的推演。

四、提高命中率的 8 个核心策略

1. 静态内容靠前,动态内容置后

Claude 的拼接顺序是 tools → system → messages。应将固定内容(工具定义、系统提示、参考文档)放在最前面,用户的最新消息放在最后。

常见踩坑:在系统提示中注入当前时间、随机 ID 或动态用户名。这些变量一旦改变,后续缓存直接清零。这类信息应放在最后一条消息中。

2. 保证序列化的绝对稳定

不仅要求「语义一致」,必须做到「字节级一致」:

  • 工具列表的排列顺序必须固定,切忌动态排序或按条件增减。
  • JSON 对象的 Key 顺序保持一致。
  • 空格、换行等格式化拼接方式绝对统一。

3. 历史对话「只追加,不修改」

每轮交互只在末尾追加新消息。删除中间的某条对话、截断早期消息、或改写前文,都会导致对应位置之后的缓存全部失效。如果必须压缩上下文,建议一次性截断并压缩一大段,避免每轮进行微调。

这也回答了开头朋友的那个问题:我之前每轮都自己截取信息再发,看起来少发了 Token,但前缀每轮都在变,缓存每轮都要重新写入。省下的那点 Token,很可能还不如丢掉的缓存值钱。

4. 精准放置断点(针对 Claude)

调用 Claude API 时,需要通过 cache_control 手动标记缓存断点(最多支持 4 个)。常见的高效布局:

  • 系统提示的末尾放置 1 个。
  • 对话历史的最后一条消息放置 1 个,并在每轮交互中向后滚动(实现增量缓存)。
// 假设 msg.content 是字符串;如果已经是数组(带图片、工具结果等),
// 应把 cache_control 加在数组最后一个元素上
messages: history.map((msg, i) =>
i === history.length - 1
? {
...msg,
content: [
{
type: "text",
text: msg.content,
cache_control: { type: "ephemeral" },
},
],
}
: msg,
);

5. 关注缓存过期时间(TTL)

Claude 默认缓存有效期为 5 分钟,命中后自动续期。如果 Agent 等待人工确认的时间可能超过 5 分钟,建议配置为 1 小时缓存模式。

6. 维持会话参数一致

在同一会话中,切换模型型号、修改工具列表、调整推理思考(Reasoning)参数或增减图片,都会打破原有状态,导致缓存无法复用。

7. 并发请求前的「预热」

如果同时发出 10 个相同前缀的请求,由于首个缓存尚未建立,这 10 个请求都会触发「缓存写入」。正确做法是:先发 1 个请求建立缓存(预热),收到响应后再并发剩余请求。

8. 留意最小长度阈值

极短的提示词通常不会被缓存。各大模型均有最小长度要求(一般在 1000 Token 以上,具体以官方文档为准)。

五、如何验证缓存是否命中?

平台 监控字段
Claude usage.cache_creation_input_tokens(写入)
usage.cache_read_input_tokens(命中)
OpenAI usage.prompt_tokens_details.cached_tokens

建议通过以下 5 个测试来验证代码的缓存逻辑是否严密:

  1. 基础命中:发送相同请求两次 → 第二次应显示大量命中。
  2. 动态变量测试:在系统提示中加入时间戳 → 命中应归零(证明动态内容会破坏前缀)。
  3. 多轮对话测试:第 N 轮的命中 Token 数,应约等于第 N-1 轮的总输入量。
  4. 过期重置测试:等待超过有效时间后再发送 → 应显示重新写入。
  5. 一致性测试:调换工具列表中两个工具的声明顺序 → 命中应归零。

六、各家厂商的区别

虽然底层原理(前缀匹配)完全一致,上述策略也是通用的,但在开启方式和计费逻辑上存在差异:

维度 Claude OpenAI (Codex / GPT)
启用方式 需手动在特定区块标记 cache_control 默认自动开启(也支持显式设置断点)
写入成本 比普通输入贵 无额外费用(按普通输入计价)
过期机制 5 分钟 / 1 小时 内存模式 / 扩展模式(最长 24 小时)
特殊控制 — prompt_cache_key 优化路由

OpenAI 生态的几个关键点:

  • prompt_cache_key 的重要性:OpenAI 会把请求路由到最近处理过相同前缀的服务器上。这个 Key 会和前缀哈希组合起来影响路由,让相关请求更容易落到同一处,从而提高命中率。在 GPT-5.6 及之后的模型上,必须设置这个 Key 才能使用更可靠的匹配方式。一般按用户或会话分配一个固定值即可。
  • 核心逻辑差异:简单来说,Claude 是显式命令「把这里缓存下来」,而 OpenAI 的 Key 是暗示「这几个请求是同一组的」。
  • 接口选择:在多轮 Agent 场景下,优先使用 Responses API。据 OpenAI 内部测试,它的缓存利用率比 Chat Completions 更高。

注:Gemini、DeepSeek、Kimi 等大多也内置了自动前缀缓存。Gemini 额外提供了需要手动创建并按存储时长计费的 Context Caching 服务。

避坑指南:如果通过某些第三方 OpenAI 兼容接口(中转网关)调用 Claude,可能用不上 Claude 的缓存。重度依赖缓存的场景,建议直连原生接口。

七、总结

  1. 官方工具省心:如果你使用的是 Claude Code、Codex CLI 等官方 Agent,它们已经处理好了缓存,开箱即用。
  2. 自己调 API:保持前缀的绝对稳定是最高准则。Claude 记得在代码里精准标记断点,OpenAI 务必设置 prompt_cache_key。
  3. 重新评估成本模型:在当前的 Agent 开发框架下,计算成本时应重点核算「缓存读取(Cache Read)」单价,而不能仅仅对比各家名义上的标准输入/输出价格。