prompt caching 到底能省多少钱?机制、验证脚本和六种让缓存失效的写法(2026)
缓存省的不是一点点,但大多数人以为自己开了缓存、其实一次都没命中,原因几乎总是把会变的东西放进了前缀。这篇讲清前缀匹配、块粒度、写入溢价、TTL 四个机制,给一段连发三次就能看出命中率的脚本,以及两个协议 usage 字段的口径差异——算错会让你把命中率高估一倍或低估一半。
1. 先给结论:省多少、什么场景省、为什么你可能一次都没命中
第一,缓存省的不是"一点点"。在缓存命中的部分上,输入价通常会掉到标准输入价的十分之一量级(以各厂商当期文档为准)。也就是说,如果你的调用里有 80% 的输入 token 是重复前缀,理论上能把输入侧的账单压到原来的三成上下。第二,省得最多的场景不是长对话,而是"长系统提示词加短问题"的高频调用。AI 编程客户端几乎全是这个形状:一份几千到几万 token 的系统提示词加工具定义,后面挂一句几十个 token 的用户指令,一天跑几百次。这类场景是缓存的最佳受益者,也是最容易被自己写坏的地方。第三,大多数人以为自己开了缓存,其实一次都没命中,原因几乎总是同一个:把会变的东西放进了前缀。在系统提示词开头放一个当前时间戳,能让你全年零命中,而账单上看不出任何异常,它只是比应该的数字大了两三倍,你还以为这就是正常价格。这篇文章讲清机制、给一段可以直接跑的验证脚本,以及一份让缓存失效的写法清单,你不需要相信任何供应商的宣传,自己跑一遍就知道。
2. 缓存是怎么工作的:四个要点
前缀匹配。缓存是从消息序列的最开头开始逐 token 比对的,匹配到第一个不同的位置就停止。前面一万个 token 完全一样、第 10001 个不一样,那么前一万个命中,之后全部按标准价重算。注意这个顺序:是"前缀",不是"包含",你在中间插一句话,后面所有内容的缓存全部作废。块粒度。缓存不是逐 token 存的,内部有最小块的概念,并且各家都有一个最小可缓存长度门槛(通常是上千 token 量级),低于门槛的前缀根本不进缓存,所以一个两百 token 的系统提示词你怎么测都测不出命中,不是坏了,是不够长。写入溢价。第一次把内容写进缓存是要额外花钱的,按 Anthropic 公开的定价结构,短 TTL 的缓存写入约为标准输入价的 1.25 倍,缓存读取约为十分之一,这意味着一段前缀至少要被复用两次以上缓存才开始赚钱,只用一次的内容加缓存标记是纯亏。TTL。缓存有生存时间,常见是分钟级,并且每次命中通常会续期,所以缓存对高频连续调用最有利,对每天跑一次的定时任务几乎没有价值。
3. 什么场景真的省钱
把上面那张表读成一句话:缓存的收益等于前缀有多长乘以它在 TTL 内被复用了几次,两个因子任意一个为零,收益就是零。编程客户端和 Agent 是最典型的受益者,长系统提示词加工具定义几乎不变,后面挂的用户指令又短又频繁,前缀复用率很高,首选开启。多轮长对话也高,但要注意别在中间改历史。同一份文档问多个问题同理,把文档放前面、问题放后面就行。反过来,批量处理不同文档的场景基本没有价值,每次换文档意味着前缀几乎全变,只有那几句指令是相同的,长度又达不到门槛,加上写入溢价还可能倒亏。每天只跑一次的定时任务是最彻底的零收益,因为两次调用之间早就超过 TTL 了,每次都在重新写入,每次都在多付那 1.25 倍。判断你自己的场景该不该开,不用问别人,把这两个因子代进去就有答案。
4. 两个协议怎么开、怎么看
Anthropic 原生的 messages 接口需要显式打标记:在你想缓存的位置放一个断点,断点之前的所有内容会被缓存,之后的不会。所以顺序必须是稳定的在前、变动的在后,断点打在稳定部分的末尾。OpenAI 兼容侧多数是自动前缀缓存,不需要你加任何字段,但你得知道去哪看结果。usage 字段的位置是这样的:Anthropic 侧,命中的 token 数在 usage.cache_read_input_tokens,写入的在 usage.cache_creation_input_tokens,未命中的输入在 usage.input_tokens;OpenAI 侧,命中的 token 数在 usage.prompt_tokens_details.cached_tokens,写入通常不单列,输入在 usage.prompt_tokens。这里有一个非常容易算错的差异,务必看清:Anthropic 侧的 input_tokens 是不含缓存部分的,总输入等于 input_tokens 加 cache_read 加 cache_creation;而 OpenAI 侧的 prompt_tokens 是含缓存部分的,cached_tokens 是其中的一个子集,命中率等于 cached_tokens 除以 prompt_tokens。按错误的口径算,你会把命中率算高一倍或者低一半,这是最常见的一个误算。
{
"model": "你的模型名",
"max_tokens": 256,
"system": [
{
"type": "text",
"text": "这里是很长的系统提示词……(要超过最小长度门槛)",
"cache_control": { "type": "ephemeral" }
}
],
"messages": [{ "role": "user", "content": "这里是每次都不同的短问题" }]
}5. 可以直接跑的验证脚本
下面这段脚本把同一份前缀连发三次,打印每次的缓存字段变化,把地址、key、模型名换成你自己的就能跑。期望看到的输出形状是这样的:第一次,写入是一个大数、命中为 0,这次是在建缓存,并且比不用缓存还贵一点,这就是写入溢价;第二次和第三次,写入为 0,命中变成那个大数,命中率跳到很高,这就是缓存在生效。如果三次的命中始终是 0,先别怀疑供应商,按下一节那份清单逐条排查,尤其是前缀长度有没有过门槛。脚本只用标准库,不需要装 SDK,单次调用的输出限在 64 个 token,整个测试花掉的钱可以忽略。想测 OpenAI 兼容侧就把请求换成 chat completions,并去看 prompt_tokens_details 里的 cached_tokens,注意口径差异见上一节。
import json, urllib.request
BASE = "https://cocodot.co/api/ai" # Anthropic 原生,不带 /v1
KEY = "你的-key"
MODEL = "你的模型名"
# 造一段足够长的稳定前缀,确保超过最小可缓存长度门槛
PREFIX = ("你是一个严谨的代码审查助手。以下是团队的编码规范,请严格遵守。\n"
"规范条目:每个函数必须有类型注解;禁止裸 except;日志必须结构化。\n") * 220
def call(question):
body = {
"model": MODEL,
"max_tokens": 64,
"system": [{"type": "text", "text": PREFIX,
"cache_control": {"type": "ephemeral"}}],
"messages": [{"role": "user", "content": question}],
}
req = urllib.request.Request(
BASE + "/v1/messages",
data=json.dumps(body).encode(),
headers={"x-api-key": KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json"},
)
with urllib.request.urlopen(req, timeout=120) as r:
return json.load(r)["usage"]
for i in range(3):
u = call(f"第 {i+1} 次:用一句话回答 1+1 等于几")
read = u.get("cache_read_input_tokens", 0)
write = u.get("cache_creation_input_tokens", 0)
plain = u.get("input_tokens", 0)
total = read + write + plain
rate = read / total * 100 if total else 0
print(f"第{i+1}次 未缓存={plain:6} 写入={write:6} 命中={read:6} 命中率={rate:5.1f}%")6. 让缓存失效的六种写法
第一,前缀里有时间戳。"当前时间是 2026-09-08 14:23:11"这种句子每次都不同,后面的一切全部失效,要放就放到消息的最末尾。第二,前缀里有随机 id、会话 id、请求 id,同理。第三,把动态检索到的资料放在了系统提示词里。RAG 场景最容易踩这个坑:检索结果每次都变,却被拼在了最前面,正确顺序是稳定的指令和工具定义在前、检索片段在后。第四,工具定义的顺序不稳定。如果你的工具列表是从一个字典或集合里遍历出来的,顺序可能每次都不一样,序列化出来的字节就不一样。用固定顺序的列表。第五,JSON 序列化不稳定。键的顺序、空格、Unicode 转义方式变了,字节就变了,序列化时固定参数,比如把 sort_keys 打开并固定分隔符。第六,前缀太短,没到最小可缓存长度门槛,永远不命中——这种情况下命中率是 0 且完全正常,不是 bug。这六条里,前三条占了实际案例的绝大多数,而且都能在十分钟内改好。
7. 怎么估算能省多少
不要用感觉估,用一次真实调用的 usage 反推。设标准输入价为 1 个单位,读取倍率为 r(常见约 0.1),写入倍率为 w(短 TTL 常见约 1.25),缓存前缀长度为 P,一段缓存在 TTL 内被复用 N 次,每次的变动部分为 V。那么不开缓存的输入成本是 N 乘以 P 加 V;开缓存的输入成本是 w 乘以 P,加上 N 减 1 次的 r 乘以 P,再加上 N 乘以 V。把你自己的 P、V、N 代进去,一分钟就能算出这个场景该不该开。规律是:N 越大、P 相对 V 越大,收益越接近上限;N 等于 1 时开缓存一定亏。倍率数字请以你所用模型厂商和供应商的当期文档为准,这里只给结构不给死数。顺带一提,这个公式也解释了为什么"把缓存断点打在哪"很关键:断点越靠后,P 越大,但被变动内容打断的概率也越高,实践中的做法是把断点打在最后一段确定不会变的内容末尾。
8. 供应商这一层会不会把缓存弄丢
缓存能不能生效,取决于链路上每一环都没把它丢掉。中间任何一层如果把 Anthropic 请求翻译成别的协议再转发,cache_control 这种专有字段就没有对应位置,会被静默丢弃:不报错,请求照样返回 200,只是你永远命中不了。所以判断标准很简单——能不能在 usage 里看到缓存字段,以及第二次调用的命中数是不是真的跳上去了。上面那段脚本就是干这个的,跑一次三十秒。cocodot 的 API 会在 usage 里如实返回缓存字段(Anthropic 侧的 cache_read 与 cache_creation,OpenAI 侧的 cached_tokens),你可以直接拿上面的脚本自己核,不用听任何人的口头承诺。实测同一份前缀连发,命中率能到 60% 到 90% 这个区间,但这句话你不用信,脚本跑一遍你自己就有答案了。充值走支付宝,主体是海外注册公司。如果你想顺手把模型身份、上下文完整性一起体检,probe.cocodot.co 可以一次跑完,免费、不需要注册、key 不落库。
不同场景的缓存收益差别
| 场景 | 典型形状 | 前缀复用率 | 缓存价值 |
|---|---|---|---|
| 编程客户端 / Agent | 长系统提示词 + 工具定义 + 短指令,高频 | 很高 | 最高,首选开启 |
| 多轮长对话 | 历史越滚越长,每轮追加在末尾 | 高 | 高,但要注意别在中间改历史 |
| 文档问答(同一文档多问题) | 大文档在前,问题在后 | 高 | 高 |
| 批量处理不同文档 | 每次换文档,只有指令相同 | 低 | 基本无价值,写入溢价可能倒亏 |
| 每日一次的定时任务 | 调用间隔远超 TTL | 零 | 无价值 |