AI Token 缓存:命中省 10 倍,不命中白扔钱

所谓"缓存命中便宜、不命中贵",省的不是 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.0128,后续 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,前缀树组织)。匹配靠链式哈希

ini 复制代码
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[&#34;请求1: system + 问题X&#34;] A1[&#34;Token 1..2000<br/>system prompt&#34;] --> A2[&#34;Token 2001..2010<br/>问题X&#34;] end subgraph B[&#34;请求2: system + 问题Y&#34;] B1[&#34;Token 1..2000<br/>system prompt&#34;] --> B2[&#34;Token 2001..2012<br/>问题Y&#34;] end KV[(&#34;GPU 显存<br/>Token 1..2000 的 KV Cache&#34;)] A1 -->|&#34;首次计算并驻留&#34;| KV B1 -->|&#34;命中前缀·直接复用&#34;| KV

2.3 从调用栈看,这事儿分四层

flowchart TD App[&#34;应用层: 你的代码<br/>cache_control / prompt_cache_key&#34;] --> Product[&#34;产品层: 厂商 API<br/>Prompt Caching / Context Caching<br/>按命中差价计费&#34;] Product --> Engine[&#34;系统层: 推理引擎<br/>vLLM PagedAttention / SGLang RadixAttention<br/>按块哈希命中前缀&#34;] Engine --> GPU[&#34;物理层: GPU 显存<br/>KV Cache<br/>每层每 token 的 K/V 向量&#34;]

我们平时说的"token 缓存命中",落到最底下就是物理层那块 KV 被复用了


三、命中 / 不命中,流程上差在哪

flowchart TD Req[&#34;新请求到达&#34;] --> Hash[&#34;对 prompt 前缀做哈希&#34;] Hash --> Match{&#34;缓存里有<br/>相同前缀?&#34;} Match -->|&#34;命中&#34;| Reuse[&#34;复用已算好的 KV<br/>只算尾部新增 token<br/>按 cache read 低价计费&#34;] Match -->|&#34;不命中&#34;| Full[&#34;整段 prefill 重算<br/>按正常 input 价计费&#34;] Full --> Store[&#34;把前缀 KV 写入缓存<br/>供后续请求复用<br/>部分厂商收写入费&#34;]
  • 命中 :前缀的 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 的写入费,还以为自己省了。

相关推荐
65岁退休Coder4 小时前
LangChain v1.3.4 笔记 - 04 Agent 中间件
后端
神奇小汤圆5 小时前
一个接口多个实现,Spring 怎么"适配多场景"?
后端
花开彼岸天~5 小时前
鸿蒙原生开发手记:徒步迹 - 自定义组件开发规范
后端·华为·harmonyos·鸿蒙系统
songroom6 小时前
Kimi K3:Rust封装XTP接口详细教程实践
开发语言·后端·rust
kebeiovo7 小时前
游戏服务端开发:Actor模型详解(Go语言)
开发语言·后端·golang
红烧大青虫7 小时前
setInterval 倒计时实现:60s 验证码发送逻辑
后端·华为·harmonyos·鸿蒙系统
小村儿8 小时前
连载14-实战篇--一个半月,我一个人和 Claude Code 搭出一套数字人工程
前端·后端·ai编程
你驴我8 小时前
WhatsApp 多账号下消息已读回执的实时聚合与推送实践
后端·python
用户208046804568 小时前
Python3 注释编写完全指南:从基础规范到高效实践
后端