我第一次同时看到 KV Cache、Prefix Cache、Prompt Cache 和 Semantic Cache 时,直觉上觉得它们只是同一个缓存技术的四种叫法。真正把链路拆开以后才发现:它们甚至不一定保存同一种东西,也不在同一个系统层工作。
有的缓存发生在一次生成内部,避免每生成一个 Token 都重算历史;有的让不同请求共享已经算过的前缀;有的只是云服务商把这种能力包装成 API、计费与可观测指标;还有一种直接返回旧答案,连模型都不再调用。
这篇文章从 Avi Chawla 的长文 KV, Prefix, Prompt and Semantic Caching in LLMs, clearly explained 出发,但不按原文顺序逐段复述。我更想解决一个实际问题:
当延迟或 Token 账单异常时,怎样判断到底是哪一层缓存没有工作;当缓存命中时,又怎样确认它没有把错误答案复用出去?
| 项目 | 说明 |
|---|---|
| 内容类型 | LLM 推理基础与生产缓存设计笔记 |
| 适合读者 | 使用过模型 API,但容易混淆四类缓存的开发者 |
| 阅读时间 | 约 18-22 分钟 |
| 资料基础 | Avi Chawla 原文、OpenAI、Anthropic、Hugging Face、vLLM 与 GPTCache 官方资料 |
| 资料核对日期 | 2026-09-04 |
| 可带走产物 | 四层缓存对照表、缓存失效排查表、上线前检查清单 |
| 边界 | 本文解释机制与工程判断,不给出跨模型通用的性能承诺 |
先说明来源处理方式
原文提供了很好的四层划分和可运行代码。本文保留其核心问题与实验启发,但重新组织了结构,并按当前官方文档修正 Provider 接口差异。原文中的英文 RAG 图没有直接搬运;文中图片均为中文重绘,用来解释本文对应段落。
一分钟概览
如果只想先记住结论,可以带走这八点:
- KV Cache 保存一次增量生成所需的 Key/Value 张量。 它减少 Decode 时的重复计算,却增加显存读写压力。
- Prefix Caching 把可复用的 KV Block 留到后续请求。 命中条件不是“文本意思相近”,而是渲染后的 Token 前缀精确一致。
- Prompt Caching 是托管服务提供的产品接口。 底层通常仍复用前缀 KV 状态,但断点、TTL、计费与指标由 Provider 定义。
- Semantic Cache 保存的是答案。 它通过 Embedding 找相似问题,命中后跳过 LLM,因此会引入错误答案复用风险。
- 前三层主要优化 Prefill,不会替你生成同一个答案。 Semantic Cache 才可能直接返回旧结果。
- 稳定内容放前面,动态内容放后面。 时间戳、用户 ID、工具顺序和重新摘要都可能让精确前缀提前分叉。
- 缓存命中率不是共同指标。 精确缓存看 Token 命中、TTFT 和成本;语义缓存还必须看错误命中率与答案新鲜度。
- 先测逐字节重复,再考虑语义缓存。 Exact Response Cache 没有模糊匹配的 False Positive,往往是更稳的第一步。
图 1:LLM 请求从应用层语义缓存进入推理服务,再经过 Prompt/Prefix Cache 与单次请求 KV Cache
图 1:四层缓存不是四个同义词。越靠近模型内部,复用的越是计算状态;越靠近应用层,复用的越接近最终答案,正确性责任也越重。
1. 先用一张表把四层分开
判断一种“缓存”到底是什么,先问四个问题:
保存了什么?
键是什么?
命中后跳过哪段工作?
命中错误会不会改变答案?
| 层次 | 保存对象 | 典型键 | 命中后省掉什么 | 是否仍运行 LLM | 主要风险 |
|---|---|---|---|---|---|
| KV Cache | 当前序列各层的 K/V 张量 | 当前请求与 Token 位置 | Decode 时重算历史 K/V | 是 | 显存、带宽、生命周期管理 |
| Prefix Caching | 跨请求保留的 KV Block | 父块哈希 + 当前 Token Block + 附加身份 | 相同前缀的 Prefill | 是 | 前缀分叉、驱逐、租户隔离 |
| Prompt Caching | Provider 管理的前缀 KV 状态 | 完整渲染前缀与 Provider 路由条件 | 托管模型的部分 Prefill 与输入成本 | 是 | 接口、TTL、断点和模型差异 |
| Semantic Cache | 已生成响应及其元数据 | Prompt Embedding + 过滤条件 | 整次模型调用和输出生成 | 否 | 错误命中、过期、越权复用 |
这张表里最关键的一列是“是否仍运行 LLM”。前三种缓存命中后,模型仍会根据当前后缀生成新答案;Semantic Cache 命中后则直接把旧答案交给用户。
所以,前三种缓存未命中通常只让系统更慢、更贵;Semantic Cache 错误命中会让系统更快地给出错误答案。
2. 从 Prefill 与 Decode 理解 KV Cache
一个 Decoder-only LLM 的生成过程可以先粗略拆成两段:
Prefill:一次处理输入 Prompt,建立每层注意力状态
Decode:每次生成一个新 Token,并继续追加状态
自注意力会为每个 Token 计算 Query、Key 与 Value。到了增量 Decode 阶段,最新 Token 的 Query 需要与此前所有 Token 的 Key 做匹配,再按权重聚合 Value。
此前 Token 的 Query 已经完成使命,不会被新 Token 再次使用;此前的 Key 和 Value 却要被后续每一步读取。因此,标准增量解码会保存 K/V,而不保存历史 Q。
图 2:Prefill 一次计算历史 Token 的 K/V,Decode 每步只追加一个新 Token 的 K/V
图 2:KV Cache 把“每一步重算全部历史”改成“读取历史 K/V,再为新 Token 追加一格”。计算减少了,缓存读取却随上下文增长。
2.1 它省的是计算,付的是内存
不使用 KV Cache 时,每生成一个 Token 都要再次计算历史 Token 的 K/V 投影。使用后,旧 K/V 可以直接读取,但缓存会随序列长度线性增长。
对常见全注意力结构,可以用下面的近似式理解一批请求的 KV Cache 总大小;计算单条序列时令 Batch Size = 1:
KV bytes ≈
层数
× Token 数
× KV Head 数
× Head Dimension
× 2(Key + Value)
× 每元素字节数
× Batch Size
假设一个模型有 80 层、8 个 KV Head、Head Dimension 为 128,使用 BF16 保存 128K Token,并且 Batch Size = 1:
80 × 131072 × 8 × 128 × 2 × 2 bytes
≈ 40 GiB
这里刻意写出结构参数,而不是说“70B 模型一定占 40 GB”。KV Cache 大小由层数、KV Head、维度、精度和上下文共同决定,不能只看模型总参数量。
Hugging Face 的缓存策略文档 目前把常用选择分为 DynamicCache、StaticCache、Offloaded Cache 与 QuantizedCache:它们分别在动态内存、编译友好、CPU/GPU 传输和低精度存储之间取舍。量化缓存可以省内存,却可能因量化/反量化开销在短上下文上更慢。
2.2 为什么 Decode 容易受带宽限制
每生成一个 Token,模型都要读取此前的大量 K/V。上下文越长,单步读取量越大。计算 Kernel 可能很快完成,GPU 却还在等待 HBM 把缓存搬进来,于是瓶颈从纯计算转向内存带宽。
这也解释了为什么“减少 KV Cache 精度”“减少 KV Head”“限制注意力窗口”和“把缓存分层存储”会成为推理系统的重要方向:它们不只是为了塞进更长上下文,也是在改变每一步需要搬运的数据量。
3. Prefix Caching:把一次请求的 KV 留给下一次
KV Cache 默认属于当前运行。请求结束后,如果服务端仍保留其中可复用的完整块,并允许后续请求查找,就进入 Prefix Caching。
这里最容易出现的误解是:
两个 Prompt 有一大段相同文本,所以一定能复用。
真正比较的是 Token 序列和相关渲染状态。系统消息、Chat Template、工具定义顺序、特殊 Token、换行、图像哈希、LoRA 身份都可能成为键的一部分。
3.1 为什么要链式哈希
vLLM 的 Automatic Prefix Caching 设计 会把 Prompt 切成完整 KV Block,并让每个块的哈希包含:
父块哈希
+ 当前块的 Token ID
+ LoRA / 多模态输入 / cache_salt 等附加值
因此,第 3 块的键不只代表第 3 块,还代表“前两块完全匹配之后的第 3 块”。调度器从左向右查找,在第一个 Miss 处停止;后面即使出现相同文本,也不再是同一个前缀位置。
图 3:Prefix Cache 使用链式块哈希复用最长公共前缀,并在第一个 Miss 处停止
图 3:请求 B 复用请求 A 的前两个完整块;第三块不同后,剩余后缀全部重新 Prefill。只有完整块进入索引,块大小会影响查找开销与尾部浪费。
当前 vLLM 文档还强调两点:
- v0.11 起默认使用
sha256,并提供可复现序列化与更快非加密哈希的不同选项;非加密哈希需要评估碰撞和多租户风险。 - 可以传入
cache_salt,让只有持有相同 Salt 的请求共享缓存,从而缓解共享 Prefix Cache 暴露的时序侧信道。多租户部署应使用不可预测的用户级或租户级秘密值,不要直接把公开的tenant_id当作 Salt。详见 vLLM 安全文档。
3.2 Prefix Caching 不会加速所有请求
vLLM 功能文档 对边界写得很直接:APC 只减少 Query 的 Prefill,不减少新 Token 的 Decode。
因此它更适合:
- 同一份长文档后接不同问题;
- 多轮对话持续追加历史;
- 大量请求共享稳定系统指令与工具 Schema。
它不太适合:
- 每个 Prompt 从开头就不同;
- 输出很长,绝大多数时间花在 Decode;
- 缓存占用挤压了并发 Batch,驱逐又让命中率不稳定。
不能只看“开启 APC 后单个请求快了多少”,还要同时看 TTFT、并发吞吐、GPU Cache 使用率和驱逐后的冷启动比例。
4. Prompt Caching:同一机制进入托管 API 后
托管 API 不会把 Provider 的 Block Table、调度器和驱逐队列交给调用者。它暴露的是另一组东西:是否自动启用、最小可缓存长度、断点、TTL、缓存键、Usage 指标以及计费方式。
底层仍可能是 Prefix KV 状态复用,但 Prompt Caching 是产品契约,不是一个跨 Provider 完全统一的协议。
图 4:稳定前缀写入 Prompt Cache,动态问题放在断点之后;下一次请求复用最长匹配前缀
图 4:稳定工具定义、系统规则与参考资料放前面;时间戳、用户变量和当前问题放在断点之后。缓存键帮助路由,但不等于强制命中。
4.1 OpenAI 当前的口径
按 2026-09-04 的 OpenAI Prompt Caching 文档:
- 支持的模型默认启用 Prompt Caching,复用的是 KV 张量,不是原始 Prompt 文本。
- 完整渲染前缀必须匹配,包含 OpenAI 提供的指令、Developer Message、Tool Definition 和对话历史。
- GPT-5.6 及后续模型支持隐式与显式断点,最小可缓存前缀为 1,024 个可见输入 Token。
- 对这组较新模型,缓存写入按普通输入费率的
1.25×计费,读取为0.1×;prompt_cache_options.ttl当前支持30m。 - 更早模型的最小长度、断点能力、写入计费和保留参数不同,不能把新模型规则反推到全部模型。
prompt_cache_key影响相关请求的路由分组,不会把请求钉死在某台机器上,也不保证 Hit。
原文把 1.25× 写入与 0.1× 读取概括成 OpenAI 当前模型的统一规则。这个解释对较新模型很直观,但写生产代码时必须先检查目标模型的差异表。
4.2 Anthropic 当前的口径
按当前 Anthropic Prompt Caching 文档:
- 可以在请求顶层加入
cache_control使用自动缓存,也可以在具体内容块设置显式断点。 - 缓存前缀按
tools → system → messages的顺序形成层级,前方变化会让后续部分失效。 - 默认 5 分钟写入通常为基础输入价的
1.25×,1 小时写入为2×;读取通常为0.1×,但新模型可能采用不同读取倍率,因此应以实时价格页为准。 - 显式断点 Miss 后会向前寻找较早写入项,当前回看窗口最多为 20 个内容块。
- Usage 中的
cache_creation_input_tokens与cache_read_input_tokens才是是否真正写入、命中的证据。
Anthropic 目前还提供 Beta 状态的 Cache diagnostics,可比较连续请求在模型、系统指令、工具或消息历史中的分叉位置。按本文核对日期,调用时需要使用文档指定的 Beta Header,因此不能把响应中的诊断字段当作长期稳定接口。它说明了一个朴素事实:日志里“看起来没变”,不代表最终发送给模型的字节前缀没变。
4.3 Provider 不同,工程原则相同
尽管接口不同,常见策略是一致的:
稳定且高复用:工具 Schema、系统规则、共享参考资料
变化较慢:项目配置、会话历史
每次变化:时间戳、用户变量、当前问题、实时工具结果
把稳定层放前面,动态层放后面;只有在知道前缀会再次使用时,才主动承担缓存写入成本。
4.4 用两次请求确认缓存真的命中
不要仅凭第二次响应更快就判断缓存生效。对支持显式断点的模型,可以准备一段从未使用过、且超过目标模型最小缓存长度的稳定测试前缀,在稳定部分之后设置断点,然后连续发送两次请求:
请求 1:相同模型 + 稳定长前缀 + 问题 A
请求 2:相同模型 + 同一稳定前缀 + 问题 B
两次请求的动态问题可以不同,但断点之前的最终渲染内容必须一致。预期证据如下:
| Provider | 第一次请求 | 第二次请求 | 可以得出的结论 |
|---|---|---|---|
| OpenAI GPT-5.6+ | cache_write_tokens > 0、cached_tokens = 0 | cached_tokens > 0 | 前缀先写入,再被后续请求读取 |
| Anthropic | cache_creation_input_tokens > 0、cache_read_input_tokens = 0 | cache_read_input_tokens > 0 | 缓存项已创建,并在后续请求中命中 |
如果第二次仍然没有读取 Token,按下面的顺序排查:
- 两次请求是否使用相同模型、缓存模式和 TTL。
- 断点之前的系统指令、工具顺序、消息与空白字符是否一致。
- 稳定前缀是否达到目标模型当前要求的最小长度。
- 第二次请求是否在缓存生命周期内开始。
这个实验验证的是“Provider 报告的缓存命中”,不是笼统的延迟下降。网络、排队与 Decode 长度都可能让端到端耗时产生噪声。
5. Semantic Cache:复用答案,而不是计算状态
Semantic Cache 位于应用层。一次请求大致经历:
Prompt
→ Embedding
→ 向量检索
→ 过滤与相似度阈值
→ Hit:返回旧答案
→ Miss:调用 LLM,再保存 Prompt、答案与元数据
它与前三层有三个本质区别:
- 保存的是响应,而不是注意力张量。
- 键是近似语义,而不是精确 Token 前缀。
- 命中后完全不运行 LLM,因此会直接继承旧答案的错误和时效。
图 5:同义改写可以命中语义缓存,但否定词和业务字段变化也可能被误判为相似
图 5:相似度高不等于答案可复用。“如何重置密码”是合理同义改写;“是否限流”与“是否不限流”需要相反答案;年度与月度套餐则可能受不同政策约束。
原文用一个小实验展示了这个问题。同义改写与只多一个否定词的句子,余弦相似度可能非常接近。提高阈值会牺牲大量有效 Hit,降低阈值又会放大 False Positive。
这不是“找到一个万能阈值”就能解决的问题。Embedding 表示的是统计语义相近,不自动编码:
- 答案是否仍然有效;
- 两个用户是否有相同权限;
- 年度与月度套餐是否使用同一政策;
- 一个
not是否翻转了业务结论; - 原答案是否经过审核。
GPTCache 的结构也体现了这些决策:Embedding、向量存储、相似度评估、阈值、驱逐策略和后处理都需要配置。其 ACL 论文 说明了用缓存先拦截请求、Miss 后再调用 LLM 的基本架构,但生产安全性仍取决于具体流量和验证策略。
5.1 哪些场景不该直接做模糊答案缓存
以下请求默认不适合只凭相似度返回旧答案:
- 金融价格、仓位、交易与投资建议;
- 医疗、法律和安全处置;
- 权限判断与用户私有数据;
- 订单、退款、库存等快速变化状态;
- 带版本号、日期、地域、套餐或金额的政策问题;
- Agent 的工具调用计划和外部副作用结果。
更稳妥的 Semantic Cache 至少要把这些字段纳入精确过滤条件:
tenant_id / user_scope
policy_version / data_version
locale / region
model_and_prompt_version
tool_or_data_freshness
answer_validation_status
expires_at
向量相似度只负责找候选,结构化约束负责判断它是否有资格成为 Hit。
6. RAG 为什么容易破坏 Prefix Cache
原文保存下来的配图展示的是标准 RAG:文档进入向量库,查询检索相关片段,片段与问题一起发给 LLM。对 Prefix Cache 而言,麻烦发生在“检索片段拼进 Prompt”这一步。
图 6:RAG 检索到相同文档但排序变化时,链式前缀在第一个不同块处失效
图 6:两次查询都拿到 A、B、C 三个片段,但第二次顺序变成 B、A、C。链式哈希要求此前块也匹配,因此普通 Prefix Cache 不能把三个片段任意重排后继续复用。
把每个文档片段单独 Prefill,再把 KV 张量简单拼起来也不可靠:位置编码不同,片段之间没有发生完整交叉注意力,直接拼接会改变模型真正看到的状态。
CacheBlend 原始论文提出了一种非前缀 KV 复用方法,随后进入 LMCache 的工程实现:它在移动片段位置时复用部分缓存,并选择性重算一部分 Token 来恢复跨片段关系。这里最值得带走的不是某个固定加速倍数,而是一个边界:Prefix Cache 天然适合稳定顺序,非前缀复用需要额外算法维持注意力正确性。
7. 六种常见的缓存失效
| 现象 | 根因 | 排查方式 | 修复方向 |
|---|---|---|---|
每次 cached_tokens 都接近 0 | 时间戳、Request ID 或用户名出现在前缀开头 | 比较最终 Token ID 分叉点 | 稳定内容前置,动态字段后移 |
| 改了一个工具后全部冷启动 | Tool Schema、顺序或 Tool Choice 改写了前缀 | 记录排序后的完整 Tool 定义 | 保持定义和顺序稳定,用允许列表控制可调用项 |
| 长对话摘要后突然 Miss | Compaction 重写了历史 | 对比摘要前后的渲染上下文 | 接受一次冷启动,再从新前缀继续追加 |
| 切换模型后缓存消失 | KV 状态与模型/配置绑定 | 按模型拆分指标 | 不期待跨模型复用 |
| RAG 明明命中同一批文档却没缓存 | 检索片段顺序或边界变化 | 记录 Chunk ID 和排序 | 稳定排序,或采用专门的非前缀方案 |
| Semantic Hit 很高但投诉增加 | 相似问题并不共享答案 | 审核命中样本,统计错误命中 | 加结构化过滤、版本、TTL 与验证器 |
如果使用本地 Tokenizer,可以用下面的最小逻辑定位两个输入的公共前缀:
def shared_prefix_length(a: list[int], b: list[int]) -> int:
shared = 0
for left, right in zip(a, b):
if left != right:
break
shared += 1
return shared
不要只 Diff 自己编写的 Prompt 字符串。真正输入还可能经过 Chat Template、工具序列化、特殊 Token 和 Provider 系统内容处理。
8. 指标要按层观察
四类缓存不能共用一个“Hit Rate 看板”。
| 层次 | 至少记录什么 | 不足以说明什么 |
|---|---|---|
| KV Cache | 每请求 KV bytes、活跃 Token、读带宽、驱逐/Offload | 单独请求的答案是否正确 |
| Prefix Cache | 可复用 Token、完整块命中、TTFT、驱逐率、Salt 作用域 | Decode 是否更快 |
| Prompt Cache | Cache Read/Write Token、普通输入 Token、TTFT、实际费用、模型与 TTL | 请求一定落到缓存机器 |
| Semantic Cache | 候选相似度、结构化过滤结果、命中来源、答案年龄、错误命中率 | 高 Hit Rate 等于高质量 |
语义缓存尤其应该把 false_hit_rate 放在 hit_rate 前面。一个 60% 命中、几乎不返回错误答案的系统,往往比 90% 命中、偶尔跨权限或跨政策版本复用答案的系统更可靠。
9. 应该从哪一层开始
可以按下面的顺序做决策:
单次生成显存或 Decode 成本高?
→ 先看 KV Cache 策略、GQA/MLA、量化与 Offload
大量请求共享精确长前缀?
→ 看 Prefix Caching,并验证最长公共 Token 前缀
使用托管 API,希望降低重复输入成本和 TTFT?
→ 按目标 Provider 配置 Prompt Caching,并读 Usage
大量问题逐字节完全相同?
→ 先做 Exact Response Cache
只有语义相近、业务也允许复用旧答案?
→ 最后才评估 Semantic Cache,并建立错误命中测试集
这几层也可以同时存在:应用先查语义或精确答案缓存;Miss 后请求 Provider;Provider 查 Prompt Cache;推理进程内部再使用 KV Cache。此时 Trace 必须标出最终在哪一层 Hit,否则“请求很快”并不能说明是哪项优化生效。
10. 上线前检查清单
KV / Prefix
- 计算过真实模型结构下的 KV Cache 大小,而不是按参数量猜测。
- 明确缓存是每请求、跨轮次还是跨请求保存。
- 记录 Prefill 与 Decode 延迟,不把整段加速都归给 Prefix Cache。
- 验证完整块大小、驱逐策略和并发 Batch 的内存竞争。
- 多租户场景评估 Salt、命名空间和时序侧信道。
Prompt Caching
- 以目标模型的当前官方文档核对最小长度、断点、TTL 与价格。
- 工具定义、系统规则与共享资料保持稳定顺序。
- 时间戳、用户变量和当前问题放在稳定前缀之后。
- 读取 Provider 返回的 Cache Read/Write Token,而不是凭延迟猜命中。
- 模型切换、Compaction 和工具变更后允许一次可解释的冷启动。
Semantic Cache
- 先测量 Exact Response Cache 能覆盖多少流量。
- 为租户、权限、地区、政策和数据版本设置精确过滤。
- 缓存项保存来源、生成时间、模型/Prompt 版本、验证状态和 TTL。
- 用同义句、否定句、数字、日期和业务字段变化构造 False Hit 测试集。
- 高风险请求默认绕过模糊缓存。
- 对命中样本持续抽检,而不是上线后只看 Hit Rate。
结语
“缓存”这个词掩盖了四个完全不同的问题:
KV Cache:当前生成怎样避免重算历史?
Prefix Cache:后续请求怎样复用精确前缀?
Prompt Cache:Provider 怎样把前缀复用变成产品契约?
Semantic Cache:应用是否敢直接复用旧答案?
前三层越做越好,通常意味着相同任务使用更少的 Prefill 计算;第四层越激进,则越需要为正确性、新鲜度和权限负责。
下次再看到“缓存命中率提升”时,我会先追问:命中的究竟是 Token 状态,还是用户最终看到的答案? 这两个结果在监控图上可能都叫 Hit,在工程风险上却相差很远。
参考资料
- Avi Chawla:KV, Prefix, Prompt and Semantic Caching in LLMs
- OpenAI:Prompt caching
- Anthropic:Prompt caching
- Anthropic:Cache diagnostics
- Hugging Face Transformers:Cache strategies
- vLLM:Automatic Prefix Caching design
- vLLM:Automatic Prefix Caching feature guide
- vLLM:Prefix Cache security and cache salting
- CacheBlend:非前缀 KV Cache 复用论文
- LMCache:CacheBlend 工程实现文档
- GPTCache:An Open-Source Semantic Cache for LLM Applications