LLM 提示词缓存不命中的排查思路
文章围绕提示词前缀复用机制,解释缓存命中、过期和读写成本,并梳理常见失效原因。建议固定工具定义及其顺序,将稳定内容前置、动态内容后置,并观察缓存用量指标。
AI 深度解读
Prompt caching 开了,账单却没降,cached_tokens 还一直是 0,问题可能出在请求的组织方式上。原文围绕 OpenAI、Claude 和 Gemini,解释了前缀缓存如何匹配,以及缓存始终不命中的六类原因。
作者给出的结论是:稳定内容前置、动态内容后置,再逐次记录缓存读写量。重复部分最高可便宜 90%,但缓存写入、有效期和最小长度都有条件。对维护 AI 功能的程序员,这篇最有用的是一套排查顺序。
为什么内容一样,缓存还是认不出来
Prompt caching 复用的是从请求开头连续匹配的前缀,不是任意位置的重复文本。
按照原文的描述,匹配要求内容逐字节一致。一旦某处出现差异,后面的部分就无法继续复用。即使两次请求包含同一份长文档,只要文档前面的时间戳变了,也可能丢掉这段缓存。
Claude 按 tools、system、messages 的顺序读取请求;OpenAI 和 Gemini 同样从请求起点匹配。因此,模板应该按以下顺序组织:
- 工具定义,顺序和序列化方式保持固定。
- 系统指令,移除时间戳、用户名和请求 ID。
- 共享资料,例如 schema、风格指南和少量示例。
- 会话上下文,例如用户正在阅读的页面。
- 用户本次提出的问题。
前三部分可以跨用户复用,第四部分主要在同一会话内复用。这里缓存的是输入提示词,答案仍然重新生成。收益来自重复输入更便宜,以及首个 token 更早返回。
六类不命中,先查开头,再查模型门槛
第一类是固定前缀太短。总请求超过 1,024 tokens,不代表满足缓存条件。作者举的例子是:400-token 固定系统提示词后接很长的动态消息,能参与匹配的仍然只有前面那 400 tokens。
第二类是靠前的位置发生变化。除了日期时间,JSON 键顺序变化、从 Set 构建出的不稳定工具顺序,也可能破坏匹配。含义相同不够,请求表示也要稳定。
第三类是工具或设置变化。原文称,Claude 的工具定义变化会使整段缓存失效,tool_choice、图片或 thinking 设置变化会使消息部分失效。作者建议保留固定工具列表,用 tool_choice 限制工具选择。但这个参数自身也有影响,不能把它理解为随便修改都无代价。
第四类是过期。Claude 的 5 分钟窗口从请求开始计算,流式输出耗时也算在内。回答生成得越久,留给后续请求复用的时间越少。
第五类是缓存隔离。原文称缓存按组织隔离,Claude 还按 workspace 隔离,OpenAI 还有机器级缓存因素。排查时要看请求实际进入哪个缓存范围,不能仅凭提示词相同就认定能够共享。
第六类是模型门槛变化。原文列出的最小前缀范围是 512 至 4,096 tokens,并称迁移到 Gemini 3.8 Flash 后门槛升至 4,096 tokens,部分旧 Claude 模型也需要 4,096。长度不足可能直接不缓存,也不报错。
缓存能否省钱,要同时看读取和写入
作者以 2026 年 10 月为口径讨论价格,并提醒按具体模型核对。他给出的一个计算条件是:缓存写入价格为普通输入的 1.25x,读取为 0.1x。按此前提,同一前缀使用两次,成本是 1.25 + 0.1 = 1.35,低于两次普通输入的 2。
原文进一步称,5 分钟缓存使用两次即可回本,1 小时缓存需要三次;Gemini 显式缓存另收按小时计算的存储费,更适合每小时被读取多次的大文档。不过,当前摘录没有提供完整价格对照,无法独立复核所有模型和有效期的回本条件。
落到代码里,先统一各家的统计字段,才能比较不同功能的缓存表现:
// Normalize cache usage across providers: { input, read, write }
export function cacheStats(provider, res) {
switch (provider) {
case 'anthropic': {
const { input_tokens, cache_read_input_tokens: r = 0, cache_creation_input_tokens: w = 0 } = res.usage;
return { input: input_tokens + r + w, read: r, write: w }; // input_tokens excludes cached parts
}
case 'openai': {
const d = res.usage.input_tokens_details || {};
return { input: res.usage.input_tokens, read: d.cached_tokens || 0, write: d.cache_write_tokens || 0 };
}
default: { // gemini: promptTokenCount already includes cached tokens
const m = res.usageMetadata;
return { input: m.promptTokenCount, read: m.cachedContentTokenCount || 0, write: 0 };
}
}
}
最要留意 Anthropic 分支的加法:这里的 input_tokens 不包含缓存部分,需要加上读取量和写入量。Gemini 的 promptTokenCount 已经包含缓存 token,不能照搬同一算法。
作者建议按功能、按天观察 read / input,并记录 prompt 版本。这个指标实际衡量的是输入 token 中被缓存读取覆盖的比例,不是命中请求数占比。部署后突然接近零,应触发排查,而不是等账单出来再发现。
我的判断
这篇最可靠的价值是排查框架:把缓存问题拆成内容稳定性、长度、有效期、隔离范围和模型差异,程序员可以逐项检查。
但原文没有给出他们团队的生产命中率、延迟对照或账单实验。最高便宜 90% 指向重复输入部分,不能直接当成整个接口降本 90%;两次或三次回本也依赖具体写入价格和有效期。这些结论需要保留条件。
与应用层直接缓存完整答案相比,前缀缓存仍会重新生成回答,适合共享长指令、资料相近而问题不同的请求。若请求本来就短、复用间隔又长,为了跨过门槛硬塞内容,未必划算。另外,发布后读取比例下降也可能来自流量结构变化,不能只凭这一项指标断定有人改坏了提示词开头。
能拿来做什么
- 在 Node 请求构建层固定工具顺序和 JSON 序列化方式,把日期、用户信息及本次问题放到共享资料之后,检查最终请求,而不只检查模板文件。
- 给每次 AI 调用记录功能名、模型、prompt 版本以及
input、read、write,按功能观察read / input的变化,避免总体均值掩盖某个入口失效。 - 更换模型前重新核对最小前缀长度、有效期和读写价格,用真实请求的重复次数估算收益,再决定是否启用需要额外写入或存储费用的缓存。