---
url: /practices/prompt-caching.md
description: >-
  提示缓存按前缀哈希命中，改一个字就全废。这篇讲清分界点（cache_control /
  prompt_cache_breakpoint）怎么放、各家写入与读取的价差与最小 token 门槛，以及复用几次才回本的计算。
---

# 缓存为什么总不命中？提示缓存的原理和省钱方法

一个五万 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，超了就放弃。

## 命中率一直是 0？先查前缀有没有被动过

我上次把一个日期变量放在 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

长响应也吃 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%，就回去翻日志，看前缀里到底哪一段在变。
