主题
提示缓存缓存的是前缀,不是整段对话
一个五万 token 的系统提示,用户只改了最后一句,账单却按整段重算。这是很多人第一次看 LLM 账单时的困惑。
提示缓存(prompt caching)就是冲着这个场景来的。规则比名字麻烦,先记住一句话:缓存的对象是前缀,不是提示。
缓存按前缀哈希命中
服务端不存你的整段对话,只存某一段前缀的哈希。
Anthropic 的拼接顺序是 tools、system、messages 三段依次排,缓存断点打在哪个 content block 上,就只算到这个 block 为止(cache_control)。OpenAI 用 prompt_cache_breakpoint 标,也可以让它自己挑位置。
命中要求前面部分逐字节相同。你在系统提示最上面插一个当天日期,后面所有内容一起作废。
官方文档举过一个例子:第 1 轮 10 个 block,断点落在第 10 块。第 2 轮涨到 15 块,第 15 块没有缓存记录,系统往回逐块找,发现第 10 块写过,于是前 10 块直接命中。这个回看窗口是 20 个 block,超了就放弃。
命中不了,先查前缀有没有动
我上次把一个日期变量放在 system 的最开头,压测下来命中率一直是 0。
把它挪进 user 消息之后,命中率立刻上去了。会变的部分越靠后,能复用的前缀越长。
常见的几个破坏源:
- 排序不稳定的内容(当天日期、随机 ID、每次 JSON key 顺序不同的序列化结果)
- 每次请求都重新拼一遍的 tool 定义
- 会话压缩(OpenAI 的 compaction)替换掉了前面的上下文
写入加价,读取才打折
缓存不是免费的。写入要加价,读取才便宜,两笔账要分开算。
Anthropic 的乘数:5 分钟档写入 1.25 倍,1 小时档写入 2 倍,读取 0.1 倍。拿 Sonnet 5 的基础价 2 美元/百万 token 算,写入是 2.50,读取是 0.20。缓存被读取时自动续期,续期不另收费。
OpenAI 的缓存读取折扣最高 95%,写入乘数同样是 1.25。用量从 usage.input_tokens_details.cached_tokens 和 cache_write_tokens 两个字段读,能自己对着账单核。
Gemini 走隐式缓存,2.5 之后命中部分打九折计费,2.5 Flash 的最低门槛是 1,024 token,2.5 Pro 是 2,048。
太短的前缀进不了缓存
前缀长度不够,缓存直接跳过,而且不报错。
Anthropic 的最小长度按模型分:Opus 4.8、Sonnet 5、Sonnet 4.6 是 1,024 token,Opus 4.7 是 2,048,Opus 4.6、Opus 4.5 和 Haiku 4.5 都是 4,096。
OpenAI 在 GPT-5.6 之后统一成 1,024 个可见输入 token,更早的模型按请求设置浮动。
想知道有没有真的命中,看 response 里的 cache_creation_input_tokens 和 cache_read_input_tokens。两个都是 0,就是没缓存上,多半是撞了门槛。
复用几次才回本
写入付 1.25 倍,读取付 0.1 倍。复用次数少的时候,缓存比不缓存还贵。
OpenAI 文档算过交叉点:按写入 1.25、读取 0.1 的假设,跨 10 次请求,把 221 token 的前缀扩到 1,024 token 就开始划算;103 token 的前缀要累计 1,963 次请求才回本;102 token 及以下永远不回本。
Anthropic 的算法更直接:5 分钟档被读一次就回本,1 小时档要读两次。
一次性任务、每天跑一次的低频脚本,老老实实别开缓存。
几个容易忽略的边界
长响应也吃 TTL。Anthropic 的倒计时从请求开始算,不是从响应结束算。
如果一次响应流式输出了 4 分钟,后续请求得在响应结束后约 1 分钟内发起才命中,5 分钟档很容易踩空。
缓存隔离级别不一样。Claude API 和 Microsoft Foundry 是 workspace 级隔离,Bedrock 和 Google Cloud 只做到组织级。
OpenAI 的 TTL 默认 30m,也可以用 prompt_cache_options.ttl 显式指定。更早的模型是另一套参数 prompt_cache_retention,24h 那档约保留 30 分钟、最长 24 小时。
我的落地做法
我现在所有项目都按三段拼提示,前两段打断点,最后一段不打。
python
messages = [
{"role": "system", "content": STATIC_INSTRUCTIONS}, # 规则,永不改
{"role": "system", "content": TOOL_DOCS}, # 工具文档,按版本改
{"role": "user", "content": user_input}, # 每次都变
]上线前压测一轮,把 cached_tokens / input_tokens 打出来看一眼。我去年踩过一次坑:把工具文档从 800 token 扩到 1,200 token,以为更清楚,结果门槛没撞上却因为中间插了时间戳,命中率掉到 12%,花了两天才定位。
命中率长期低于 50%,就回去翻日志,看前缀里到底哪一段在变。