Prompt 缓存按前缀复用,缓存命中只看请求开头的字节级相同性。一个早期差异(即使 12 token 的 session id)会使整个 40k reference block 完全失去缓存价值。按内容稳定频率排序:最稳定的放前面,最易变的放后面。
Prompt caching 不是一种开关设置。它是 prompt 本身的一种属性——要么有,要么没有——一个错误位置的时间戳就会让它在后续所有请求中彻底失效。
它是一个前缀,这就是全部设计
缓存 token 的定价和机制属于 token 集群的范畴。唯一支配组装逻辑的结构性事实是:缓存操作基于前缀。Provider 可以复用你请求中从开头到第一个与之前请求产生差异的位置为止的计算状态,超出这个位置一个 token 都无法复用。
这比「重复内容更便宜」要严苛得多。内容重复但发生了位移则毫无价值。如果在两个请求之间插入了 12 token 的 session id,那么即便有 40,000 token 的参考块完全相同,也不会有任何折扣。没有部分计费,没有模糊匹配,prompt 后续也不会有任何补救——前面一处差异就让你损失了所有剩余 token 的缓存。
这使得缓存优化变成一个只有机械答案的问题:我实际发送的请求中,最长的字节级相同的前缀是什么?其余一切由此推导。
按变更频率排序
将各个块按变化频率排序,最稳定的排在前面。这样缓存与不缓存的边界就能尽可能地靠后。
tier 0 永不变化 system prompt、persona、output contract
tier 1 部署时变化 tool schemas、静态参考资料
tier 2 每次会话变化 用户 profile、账户设置、压缩后的记录
tier 3 每轮变化 对话历史(仅追加:前缀稳定)
tier 4 每次请求变化 检索到的文档、tool 结果、问题本身
^ 缓存边界落在这里
Tier 3 是有意思的部分。对话历史每轮都在变化,但它通过追加变化,所以它的前缀是稳定的——尽管内容本身不稳定:第 12 轮的历史是第 11 轮的历史加上两条消息。这意味着历史记录在上一轮结束为止的部分是可缓存的,前提是 tier 3 以下的内容没有排在它之后。这正是历史记录应该优先于检索文档的具体原因——反过来排序会让每一轮的历史都因为把检索稍微提前而变得不可缓存。
压缩是一个必须规划的例外。当压缩器重写历史块时,tier-2 的记录会发生变化,前缀就会断裂——一次昂贵的未缓存请求,之后恢复稳定。只要压缩是在达到阈值时触发而非持续进行,这就是可接受的;如果压缩器每轮只修剪一条消息,就会产生一个永远冷的缓存。
七种破坏前缀的情况
这些都是真实发生过的,所有这些在代码审查中都不可见,而且每一个都会让你损失整个缓存。
System prompt 中的时间戳。「当前日期时间是 2026-08-03T11:42:07Z。」每秒都在变化。如果模型需要日期,给到分钟或天级别就行,或者把它移到 prompt 末尾。
非确定性序列化。Tool schemas 来自 key 顺序不稳定的字典,或者两次运行之间 JSON 序列化时使用了不同的空白符。字节级相同就是字面意思的字节级相同;要显式排序 key 并固定序列化器。
顶部注入的每用户个性化信息。Name、plan tier 或 locale 被注入 system 块,会让每个用户拥有私有缓存。有时这是可接受的——重度用户填满了他们自己的缓存——但这必须是一个明确决策,对于长尾流量而言则意味着完全没有缓存。
请求 id 或 trace id。由中间件注入,从不被人审查,本质上肯定是唯一的。这是最纯粹的 bug 形式。
打乱顺序的 few-shot 示例。随机化示例顺序以减少偏差是一种合理的技巧,但与前缀缓存完全不兼容。选择一个固定顺序。
分配器漂移。如果你的分配器这轮产生了 41,318 个 token 的参考内容,下一轮产生了 41,290 个,那么这个块在尾部有差异,后续所有内容都未被缓存。将分配粒度四舍五入到较粗的 granularity,使它们保持稳定。
模型或参数变化。缓存的 key 不只是文本。切换模型,或者在某些实现中切换采样参数或 tool 定义,都会产生不同的缓存条目。
设 C 为可缓存前缀的 token 数,V 为不稳定的剩余部分,P 为未缓存的输入价格,r 为缓存读取倍数,w 为缓存写入倍数(写入时收取费用),h 为命中率。每请求:
无缓存 : (C + V) * P
缓存命 中: h*(C*P*r) + (1-h)*(C*P*w) + V*P
节省 : C * P * [ 1 - h*r - (1-h)*w ]
盈亏平衡命中率(当 w > 1 时):h > (w - 1) / (w - r)
盈亏平衡点是值得牢记的部分。如果写入需要支付溢价——比如 w = 1.25,r = 0.1,都是假设值,换成你 provider 的实际数字——那么缓存只有在命中率超过 0.25/1.15 ≈ 22% 时才划算。低于这个值,你就是在为没有人复用的前缀支付写入溢价。当写入不单独计费时,任何大于零的命中率都是收益。
这个表达式中的另一个杠杆是 C 本身,它是线性的:稳定前缀长度翻倍,节省也翻倍。这正是静态与检索决策比缓存出现之前更倾向于静态的原因——放在前缀中的内容按 P × r 计费,而不是 P。
命中率也是流量的函数,不仅仅是布局的函数。缓存有生命周期,间隔几秒复用的前缀保持温热,而每小时复用一次的前缀通常会变冷。无论 prompt 排序多么完美,低流量应用都应该按接近零的 h 来计算。
验证你实际命中了缓存
缓存优化有一种独特的静默失败倾向:prompt 看起来排序良好,代码里有个注释说前缀是稳定的,但命中率是零——因为在两层以下注入了一个 header。什么都不假设,直接检查。
记录前缀哈希。对每个渲染后请求的前 C 个 token 做哈希并记录。如果本应共享前缀的连续请求之间哈希发生了变化,你就找到了问题所在,完全不需要 provider 的遥测数据。
对比两个渲染后的请求。不是对比输入——是对比最终序列化的 payload。第一个不同的字节就是答案,而且它几乎从不在你期望的位置。
关注报告的缓存 token 数量。Provider 会告诉你有多少 token 是从缓存服务的。这个数字与 C 的比值才是你真正的命中率,这是唯一值得信赖的数字。
对此设置告警。命中率是一个会静默降到零的指标——当有人往 system prompt 里加了一个字段时。它值得拥有自己的仪表板折线和阈值,因为这种失败是一种成本上升但不附带任何错误。
最后两个检查需要 provider 返回的单个数字,每请求返回而非月度聚合:缓存 token 数量与输入输出数量一起返回,这才能把上面公式里的 h 从假设变成实际观测。
Structuring Context for Retrieval Within the Window
What to Put in the System Prompt vs Retrieve on Demand
Token Accounting Across a Long Session