先说一个核心判断:缓存命中便宜,不命中才叫贵。省的不是token本身,而是模型把prompt重新跑一遍预计算(prefill)的算力。命中了,就是复用上次算好的中间成果,跳过重复计算;没命中,就得从头算一遍。同一段system prompt,第一次老老实实算,后面每次都能白嫖上一轮的成果——账单的差距就在这里。

## 一、一个比喻,把这件事说明白
把大模型想象成一个赶作业的学霸。你每次交的“卷子”,开头都要写一段一模一样的2000字个人陈述——这就是你的system prompt。
- 笨办法:每次都把那2000字从头手写一遍。
- 聪明的办法:第一次写完后,复印一份(photocopy)订在卷子头上,以后每次直接把复印件订上去,只写后面不一样的部分。
这个“复印件”,就是缓存(KV cache)——模型算好的中间成果。
- 命中:这次卷子开头和上次复印件一字不差 → 直接订复印件,省掉重写 → 这部分按低价计费。
- 不命中:开头改了一个字 → 复印件用不了,2000字重写 → 按原价计费。
为什么只有“开头”能复印?因为模型是逐字往后读的,后面每个字的理解都依赖前面的字。只要前面一样,前面的计算成果就能整个搬来用;前面一改,后面全得重来。所以,缓存只认前缀。
### 这笔账有多夸张
拿Anthropic的定价举例(base $5/百万token):一段2000 token的system prompt,每次再带50 token的用户问题。
| 方式 | 单次成本 | 10次总成本 |
|------|----------|------------|
| 不缓存 | 2050 token全原价 ≈ $0.0103 | ≈ $0.103 |
| 缓存(首写1.25x,后续读0.1x) | 首写$0.0128,后续$0.0013 | ≈ $0.024 |
省了3/4以上。请求越多越省,上百次能砍掉85%+。这还只是2000 token的system prompt——如果你的prompt里塞的是一整份几万token的文档(RAG、长文档问答),差距会直接拉到10倍。
## 二、硬核原理:KV Cache与为什么前缀能复用
### 2.1 预计算(prefill)到底算了什么
Transformer处理你的prompt时,会把整段文字过一遍所有层。每一层都给每个token算出两组向量:Key(K)和Value(V),合称KV。生成回答时,模型靠attention去“回顾”前面所有token的K和V。为了不每次重算,推理框架把算过的KV留在显存里——这就是KV cache。
关键点在于:attention是单向的,只往后看。第1到第N个token的KV向量,只取决于这前N个token自己,跟后面跟了什么话没关系。所以只要两个请求的前N个token一模一样,它们前N个位置的KV cache就分毫不差。第二个请求直接把这块KV捞出来用,省掉前N个token的预计算。
这块显存其实挺贵。拿Llama-2 70B算一笔账:每个token每层KV约4KB(FP16,GQA后8个KV head),80层就是320KB/token。一个2000 token的system prompt,KV cache要占大约640MB显存。每次请求都重算再扔掉,纯属浪费——prefix caching就是要把这块显存留住、复用。
### 2.2 实际是按“块”复用,不是按token
推理框架不会傻到逐token比对,而是把KV切成固定大小的块(vLLM用PagedAttention,16 token一块;SGLang用RadixAttention,前缀树组织)。匹配靠链式哈希:
```
key_0 = hash(tokens[0 : 16])
key_1 = hash(key_0 || tokens[16 : 32])
key_2 = hash(key_1 || tokens[32 : 48])
...
```
为什么搞链式?因为两个prompt可能末尾一样、前面不一样。比如“A国首都是巴黎,2+2=?”和“B国首都是巴黎,2+2=?”,最后那句“2+2=?”的KV其实不同——前面上下文变了,它attend到的东西就不同。链式哈希保证:只要前面某一块对不上,后面所有块的key全变,自然不会误命中。
```
flowchart LR
subgraph A["请求1: system + 问题X"]
A1["Token 1..2000
system prompt"] --> A2["Token 2001..2010
问题X"]
end
subgraph B["请求2: system + 问题Y"]
B1["Token 1..2000
system prompt"] --> B2["Token 2001..2012
问题Y"]
end
KV[("GPU 显存
Token 1..2000 的 KV Cache")]
A1 -->|"首次计算并驻留"| KV
B1 -->|"命中前缀·直接复用"| KV
```
### 2.3 从调用栈看,这事儿分四层
```
flowchart TD
App["应用层: 你的代码
cache_control / prompt_cache_key"] --> Product["产品层: 厂商 API
Prompt Caching / Context Caching
按命中差价计费"]
Product --> Engine["系统层: 推理引擎
vLLM PagedAttention / SGLang RadixAttention
按块哈希命中前缀"]
Engine --> GPU["物理层: GPU 显存
KV Cache
每层每 token 的 K/V 向量"]
```
我们平时说的“token缓存命中”,落到最底下,就是物理层那块KV被复用了。
## 三、命中 / 不命中,流程上差在哪
```
flowchart TD
Req["新请求到达"] --> Hash["对 prompt 前缀做哈希"]
Hash --> Match{"缓存里有
相同前缀?"}
Match -->|"命中"| Reuse["复用已算好的 KV
只算尾部新增 token
按 cache read 低价计费"]
Match -->|"不命中"| Full["整段 prefill 重算
按正常 input 价计费"]
Full --> Store["把前缀 KV 写入缓存
供后续请求复用
部分厂商收写入费"]
```
- **命中**:前缀的KV直接复用,这截token不进预计算,计费走“cache read”低价档。延迟(TTFT,首token时延)也跟着降——缓存越长,首token越快。OpenAI实测过,长prompt(150K+)命中能比不命中快约67%的TTFT。
- **不命中**:整段重算。Anthropic和Gemini还会顺手把前缀写进缓存(收“cache write”费);OpenAI在GPT-5.6之前的模型写入免费,GPT-5.6起写入收1.25x。
两个反直觉、但必须记住的点:
1. **缓存只对“前缀”生效**。一旦prompt从某个位置开始跟缓存版本不一样(换了user消息、换了一份RAG文档),那个位置之后的缓存就废了。
2. **字节级精确匹配**。多一个空格、改个标点,缓存直接失效。不是“意思差不多就行”。
## 四、三家实现和定价,差别比想象大
| 厂商 | 触发方式 | 最小可缓存 | 命中折扣 | TTL | 写入费 |
|------|----------|------------|----------|-----|--------|
| Anthropic | 显式 `cache_control` | 1024 token | 90% off(0.1x) | 5min(默认)/ 1h(付费) | 5min写1.25x / 1h写2x |
| OpenAI | 全自动(>1024) | 1024 token,128递增 | 50%~90%不等,越新越深 | 5–10min,1h内清 | 老模型免费;GPT-5.6+写1.25x |
| Gemini | 隐式自动 + 显式API | 隐式1024 / 显式32768 | 隐式75% off / 显式约90% | 显式可配,默认1h | 存储$1.00/M token/小时 |
几个踩过或观察到的坑:
- **Anthropic的写入费是真实成本**。5min TTL写一次收1.25x。盈亏平衡点在1~2次命中:写贵25%,但每次命中省90%,第二次命中就回本。所以“5分钟内请求≥2次同一前缀”的负载,闭眼开。低于这个频率,缓存老过期,你一直在交写入费——这时要么上1h TTL(写2x),要么干脆别用。
- **OpenAI全自动,但你不控制路由**。它按前缀哈希把请求路由到“最近算过同样前缀的机器”,必须落到同一台才命中。多租户高峰期,缓存可能被挤掉,没有保证。共享长前缀的流量可以传`prompt_cache_key`帮它路由命中。另外,它返回`usage.prompt_tokens_details.cached_tokens`,这是验证命中的唯一真实信号,别只看账单。
- **Gemini显式缓存按小时收存储费**。32K起,你开一个缓存对象,每小时按token数收钱,跟命中多少次无关。低频请求可能“省了计算,赔了存储”。它的`cachedContent`是显式资源,TTL默认1小时,适合“一份大文档被反复问”的场景。
## 五、实战:怎么更容易命中(按性价比排序)
回到开头的“抄作业”比喻——你想photocopy的那段,必须每次都一模一样、且放在最前面。
1. **静态内容放最前,动态内容放最后**。这是铁律。system prompt、few-shot示例、工具定义、RAG文档这些不变的,全堆在prompt头部;用户每次不同的输入放尾巴。缓存只看前缀,尾巴随便变不影响前缀命中。
2. **顺序要稳定**。system → tools → 记忆 → 历史对话 → 当前消息,这个顺序每次保持一致。工具定义按名字排序(确定性顺序)。OpenAI的匹配是“最长精确前缀”,你reorder一下前面,整段缓存就断了。
3. **别在缓存前缀里塞“会变的量”**。时间戳、请求ID、随机种子、当次日期——这些只要出现在被缓存的前缀段里,每次都不同,缓存必失。需要的话,把它们挪到尾部动态区。我见过有人把`当前时间:{now()}`写进system prompt,结果缓存一次都没命中过,排查了半天才发现。
4. **Anthropic把breakpoint打在稳定边界上**。最多4个:system prompt结尾、工具定义结尾、静态文档结尾、历史对话前缀结尾。每个breakpoint之前到上一个breakpoint之间的内容被缓存。让“会变的部分”恰好落在最后一个breakpoint之后。
5. **低频负载的处理**。5分钟TTL对低频接口是诅咒——请求间隔一长,缓存早过期,你每次都交写入费。两个办法:用1h TTL(写贵一倍,但命中赚更多);或者搞一个缓存warmer,定时用同一前缀打一个低成本请求,把缓存焐热。
6. **一定要监控命中率**。别凭感觉。Anthropic看`cache_read_input_tokens` / `cache_creation_input_tokens`;OpenAI看`cached_tokens`;Gemini看缓存对象的复用统计。命中率为0但你以为开了——多半是前缀没对齐或没过最小token阈值(1024)。
7. **不在每次都变的内容上浪费缓存**。缓存只对“会被重复的前缀”有意义。一份每请求都不同的RAG上下文塞进缓存,等于每次都写、从不读,纯亏写入费。判断标准一句话:这个前缀,后面还会有请求用一模一样的开头吗?不会,就别缓存。
## 六、收尾
Token缓存不是玄学,就是“相同前缀别重复算”这一件事,只是被三家包装成了不同的API和价目表。要把它用明白,记住三件事:**静态前置、字节一致、盯着命中率**。做到了,长system prompt、多轮对话、大文档问答这类负载,账单砍掉60%~90%很常见;做不到,你可能一直在交1.25x的写入费,还以为自己省了。