LLM 缓存深入浅出:从 KV Cache 原理到命中率优化实战

很多人说"缓存命中了所以省钱",但如果你问他们命中了什么缓存、怎么命中的、为什么有时突然命中率暴跌——大概率答不上来。这篇文章帮你彻底搞懂。


引言:一个被严重低估的优化手段

当你看到 Claude API 返回 cached_tokens: 50000,或者 vLLM 日志打印 prefix cache hit 时,背后发生的事情远比"缓存命中"四个字复杂得多。

LLM 推理中,缓存是最容易被低估的成本优化手段。一个精心优化的 Agent 系统,通过 Prompt Caching 可以将 API 成本降低 90%,首 Token 延迟(TTFT)降低 85%。但现实中,很多团队的缓存命中率从最初的 72% 跌到 18% 却浑然不知。

本文将回答三个问题:

  1. LLM 有哪些缓存?它们分别做什么?
  2. 缓存命中的底层原理是什么?跨用户共享安全吗?
  3. 如何系统地提高缓存命中率?

第一部分:LLM 缓存全景图

先澄清一个常见误解:LLM 语境下的"缓存命中"通常指 Transformer 推理层的 KV Cache,不是 Redis,也不是 HTTP 缓存。但 LLM 生态中确实存在多个层级的缓存机制,容易混淆。

1.1 KV Cache —— 最核心的推理缓存

Transformer 架构中,每生成一个新 Token 都需要对前面所有 Token 做 Attention 计算。这意味着第 N 个 Token 的生成本质上需要"看"前 N-1 个 Token,计算它们的 Key(K)和 Value(V)向量。

如果不做任何优化,每次生成都要从头计算所有历史 Token 的 K/V——这对于一个 100K 上下文的请求来说,计算量是灾难性的。

KV Cache 的核心思想极其简单:把历史 Token 的 K/V 计算结果缓存起来,下一轮只计算新增 Token。

用一个例子说明:

第 1 轮请求:
┌─────────────────────────────────┐
│ System Prompt(10,000 tokens)    │  ← 需要计算全部 10K token 的 K/V
│ + 用户问题                        │
└─────────────────────────────────┘
→ 生成 KV Cache,存入 GPU 显存

第 2 轮请求:
┌─────────────────────────────────┐
│ System Prompt(10,000 tokens)    │  ← 命中缓存!跳过计算
│ + 历史对话                        │  ← 命中缓存!跳过计算
│ + 新用户问题                      │  ← 仅计算这部分的 K/V
└─────────────────────────────────┘
→ 复用已有 KV Cache,仅计算增量

结果:TTFT 更快、GPU 占用更小、成本更低。

1.2 缓存的四个层级

层级 名称 缓存内容 存储位置 举例
L1 KV Cache Attention 的 K/V 矩阵 GPU 显存 / CPU 内存 vLLM PagedAttention
L2 Prefix Cache 公共前缀的 KV Cache GPU 显存 SGLang RadixAttention
L3 Semantic Cache 语义相似查询的结果 Redis / 向量数据库 AI Gateway 缓存
L4 Exact Match Cache 完全相同输入的输出 Redis / 内存 HTTP 风格缓存

本文重点讨论 L1(KV Cache)L2(Prefix Cache),因为它们是推理引擎层面的核心优化,也是大多数人在讨论"LLM 缓存"时真正指代的东西。

1.3 缓存存在哪里?

GPU 显存是最常见的存储位置。KV Cache 的体积可以用这个公式估算:

KV Cache 大小 ≈ 2 × Token 数 × 层数 × Hidden Size × 精度字节数

对于一个 70B 参数的模型,100K Token 上下文的 KV Cache 可以轻松达到数十 GB。因此,现代推理框架(vLLM、TensorRT-LLM、SGLang、LMDeploy)都主要将 KV Cache 存放在 GPU VRAM 中。

当显存不够时,vLLM 的 PagedAttention 机制会将 KV Cache 分页管理,类似操作系统的虚拟内存,在显存和 CPU 内存之间按需换入换出。


第二部分:Prefill 与 Decode —— 理解为什么缓存如此重要

要理解缓存的价值,必须先理解 LLM 推理的两个阶段。

2.1 Prefill 阶段 —— 最贵的部分

当你发送一个请求时,模型首先需要"阅读"整个 Prompt,计算所有 Token 的 Attention,生成 KV Cache。这个阶段叫做 Prefill

Prefill 的计算复杂度接近 O(n²)(n 为输入 Token 数)。对于 100K Token 的 Agent Prompt,Prefill 是 GPU 资源消耗最大的环节。

2.2 Decode 阶段 —— 逐 Token 输出

KV Cache 生成后,模型进入 Decode 阶段,逐个生成输出 Token。每个新 Token 只需要与已有的 KV Cache 做 Attention,计算量远低于 Prefill。

2.3 缓存如何改变游戏规则

如果没有缓存,每一轮对话都要重新 Prefill 整个上下文(包括 System Prompt、工具定义、历史对话)。对于一个 100K Token 的 Agent 请求:

  • 无缓存:每轮都做 100K Token 的 Prefill → 成本极高
  • 有缓存:仅 Prefill 新增的几 K Token → 成本降低 90%+,TTFT 降低 85%+

这就是为什么缓存被称为 LLM 推理的"免费加速器"。


第三部分:Prefix Cache —— 跨用户共享的秘密武器

3.1 什么是 Prefix Cache?

KV Cache 的一个强大变体是 Prefix Cache(也叫 Prompt Cache)。它不仅在同一会话内复用缓存,还能在不同用户之间共享公共前缀的 KV Cache

原理很简单:如果两个请求的前 N 个 Token 完全相同,那么前 N 个 Token 的 KV Cache 就可以共享。

用户 A 的请求:
┌──────────────────────────────┐
│ System Prompt(固定)           │ ← 公共前缀
│ Tool Schema(固定)            │ ← 公共前缀
│ 用户问题:如何部署 Redis?      │ ← 差异部分
└──────────────────────────────┘

用户 B 的请求:
┌──────────────────────────────┐
│ System Prompt(固定)           │ ← 命中 A 的缓存
│ Tool Schema(固定)            │ ← 命中 A 的缓存
│ 用户问题:怎么部署 K8s?        │ ← 需要新计算
└──────────────────────────────┘

用户 B 可以直接复用用户 A 已计算好的公共前缀 KV Cache,跳过绝大部分 Prefill 计算。

3.2 安全吗?会串数据吗?

这是最常被问到的问题。答案是:不会

KV Cache 存储的是 Attention 计算的中间矩阵(K 和 V 向量),不是用户的聊天文本。共享的条件是 Token 序列完全一致——哪怕多一个标点符号,缓存都会失效。而且,只有公共前缀会被共享,每个用户自己独有的输入部分始终独立计算。

3.3 主流框架的支持情况

框架 缓存机制 特点
SGLang RadixAttention(Radix Tree) 专门为跨请求前缀共享设计,吞吐量极高
vLLM Automatic Prefix Caching 基于 Block 级别的 KV 复用,自动匹配公共前缀
TensorRT-LLM Paged KV Cache + Shared Prefix NVIDIA 官方方案,支持前缀复用
LMDeploy Cache-aware 前缀复用 支持多轮对话和跨请求复用
OpenAI API Prompt Caching 自动启用,API 无状态天然就是跨请求共享
Anthropic Claude Prompt Caching 支持显式缓存控制,cache hit 成本降低 90%

3.4 一个被忽略的事实

Copilot、Cursor、Claude Code、ChatGPT、Devin 这类大规模 AI 产品的实际 GPU 成本,远低于按 Token 单价简单计算的结果。核心原因就是海量用户共享了巨大的公共前缀 Cache。如果不共享,这些产品的 GPU 账单会爆炸。


第四部分:如何判断缓存命中了?

不同平台的监控方式各不相同:

OpenAI API

OpenAI 在 2024 年底默认启用了 Prompt Caching。在 API 响应的 usage 字段中,可以看到命中的缓存 Token 数:

{
  "usage": {
    "prompt_tokens": 12000,
    "cached_tokens": 10000
  }
}

cached_tokens 就是命中的缓存 Token 数量。

Anthropic Claude

Anthropic 的返回更加详细,区分了"创建缓存"和"读取缓存":

{
  "usage": {
    "input_tokens": 12000,
    "cache_creation_input_tokens": 10000,
    "cache_read_input_tokens": 9000
  }
}
  • cache_creation_input_tokens:首次创建缓存(写入)
  • cache_read_input_tokens:从缓存读取(命中)

自建推理引擎

vLLM 日志:  "prefix cache hit" 或 "KV cache usage: 85%"
SGLang 控制台:"Radix cache hit rate: 82%"

第五部分:缓存的匹配规则 —— 为什么"差不多"不行?

这是一个极其关键的认知:缓存匹配基于 Token 的完全一致性,不是语义相似性。

"你是助手"  → Token 序列: [1234, 5678, 9012]
"你是助手。" → Token 序列: [1234, 5678, 9012, 3456]  ← 多了一个句号
"你是助手"   → Token 序列: [1234, 5678, 9013]        ← 全角空格,Token 变了

三种写法的含义几乎相同,但 Token 序列不同,缓存全部失效。

两种匹配粒度

Prefix Cache(前缀缓存):只要求前缀 Token 一致。例如 System Prompt + Tool Schema + 历史对话的前 N 轮相同即可命中。这是最实用的模式。

Exact Match Cache(完全匹配缓存):整个 Prompt 完全一致,直接复用最终输出结果。这更接近 HTTP 缓存的概念,适合问答机器人等确定性场景。


第六部分:为什么 Agent / Tool Calling 特别依赖缓存?

Agent 系统是 Prompt Cache 的最大受益者,原因如下:

  1. 超长 System Prompt:一个典型的 Coding Agent 可能有 5K-30K Token 的 System Prompt
  2. 超长 Tool Schema:MCP 工具定义、API Schema 加起来可能 10K-50K Token
  3. 多轮对话上下文:每次请求都携带完整的历史对话
  4. 大量并发请求:不同用户同时使用同一个 Agent

这意味着一个 Agent 请求可能有 100K+ Token 的输入,其中 80%+ 是固定的公共前缀。如果不缓存,每轮 Prefill 的成本是天文数字。

这正是为什么很多 Agent 框架会要求固定工具排序、固定 JSON Schema、避免动态 Prompt——目的就是最大化缓存命中率。


第七部分:如何系统地提高缓存命中率?

这是本文最实用的部分。缓存命中率的优化可以分为四个层面。

7.1 Prompt 层面:稳定前缀

固定 System Prompt 的内容和顺序。 不要根据请求动态调整 System Prompt 的结构。如果必须变化,把可变部分放在 Prompt 的末尾,让公共前缀尽量长。

✅ 正确做法:可变部分放末尾
┌─────────────────────────────┐
│ System Prompt(固定)         │ ← 所有用户共享
│ Tool Schema(固定)           │ ← 所有用户共享
│ ─────────────────────────── │
│ 用户个性化上下文(可变)       │ ← 放在最后
│ 用户问题(可变)              │
└─────────────────────────────┘

❌ 错误做法:可变部分穿插在中间
┌─────────────────────────────┐
│ System Prompt(固定)         │
│ 用户个性化上下文(可变)       │ ← 打断了公共前缀!
│ Tool Schema(固定)           │ ← 无法命中缓存
│ 用户问题(可变)              │
└─────────────────────────────┘

7.2 Tool Schema 层面:固定排序和格式

固定工具定义的顺序和 JSON key 的排序。 哪怕是 {"a": 1, "b": 2}{"b": 2, "a": 1} 这样仅顺序不同的 JSON,Token 化之后也可能完全不同。

✅ 固定顺序:
tools:
  - read_file
  - write_file
  - grep
  - edit_file

❌ 动态顺序(每次请求顺序不同):
tools:
  - grep
  - edit_file
  - read_file
  - write_file

7.3 RAG 层面:控制检索结果的稳定性

这是最容易踩坑的地方。 很多人以为 RAG 只是"检索文档然后拼接到 Prompt 里",但动态拼接严重破坏缓存:

  • 每次检索返回的文档顺序不同 → 前缀不同 → 缓存失效
  • 文档 Chunk 的边界不同 → Token 化结果不同 → 缓存失效
  • 动态生成的上下文模板 → 每次格式微变 → 缓存失效

优化策略:

  1. 对检索结果按固定规则排序(如按文档 ID、相关性分数的确定映射)
  2. 固定 Chunk 大小和分词策略
  3. 将 RAG 内容放在 Prompt 的固定位置(如 System Prompt 之后、用户问题之前)
  4. 缓存检索结果本身——如果用户问了相似的问题,直接复用检索结果,避免重新 Embedding 和检索

7.4 架构层面:利用平台特性

选择支持跨请求缓存共享的平台或推理引擎:

  • 使用 Anthropic Claude 时,显式标记缓存断点(cache_control),将大块静态内容标记为可缓存
  • 使用 OpenAI API 时,Prompt Caching 默认启用,但要注意缓存有效期为 5 分钟(Ephemeral)
  • 自建推理服务时,优先选择 SGLang 或 vLLM(启用 --enable-prefix-caching),利用 Radix Tree 或 Block 级别的前缀共享

建立缓存命中率监控:

关键指标:
- Cache Hit Rate = cached_tokens / total_prompt_tokens
- 目标值:> 70%(Agent 系统)
- 预警值:< 50%(说明 Prompt 设计有问题)

根据生产环境的观察,缓存命中率的变化通常遵循这样的模式:刚启用时 70%+,随着功能迭代逐渐跌到 18%。这是因为每次功能更新都可能悄悄改变 Prompt 结构,而团队没有持续监控命中率。


第八部分:缓存的成本模型

“缓存命中了所以不计费”——这是另一个常见误解。

不同平台的定价策略不同:

平台 Cache 写入价格 Cache 读取价格 节省比例
Anthropic 基础价格 × 1.25(贵 25%) 基础价格 × 0.1(便宜 90%) 90%
OpenAI 与普通输入同价 约普通输入的 50% 50%
自建(vLLM/SGLang) GPU 显存 + 计算成本 仅 GPU 显存成本 80%+(取决于显存利用率)

关键结论:

  1. 缓存不是免费的,但命中率越高,综合成本越低
  2. Anthropic 的缓存经济模型最激进——写入贵 25%,但读取便宜 90%,适合高并发、大前缀的场景
  3. 自建推理引擎的缓存"成本"体现为显存占用——KV Cache 会占用大量 VRAM,需要在显存预算和命中率之间做平衡

第九部分:输出为什么不缓存?

你可能注意到了,以上讨论的都是输入侧的缓存。为什么输出不缓存?

因为 LLM 的输出是非确定性的:

  • Temperature > 0 时,每次输出的 Token 概率分布不同
  • Top-p、Top-k 采样引入随机性
  • 同一个 Prompt 多次调用,输出可能完全不同

所以 KV Cache 缓存的是 Attention 的中间计算结果(K/V 矩阵),而不是最终的输出文本。

不过,有一种特殊情况叫 Semantic Cache

用户问:"北京今天天气?"
→ Embedding → 查找语义相似的已缓存问答
→ 命中:"北京天气 25℃ 晴"(直接返回,不走模型)

这种 Semantic Cache 通常部署在 AI Gateway 层,用 Redis + 向量数据库实现。它与 KV Cache 是完全不同的东西,但可以作为补充优化手段。


第十部分:总结与行动清单

核心结论

LLM 的缓存 = 把历史 Token 的 Attention 计算结果(KV)存到 GPU/内存中复用

"大模块缓存命中率高" = 大量相同前缀 Token 能复用已有 KV Cache

效果 = 降低推理成本 + 提高 TTFT + 提高 GPU 吞吐量

立即行动清单

  • 检查当前缓存命中率:查看 Anthropic 的 cache_read_input_tokens 或 vLLM 的 prefix cache 日志
  • 固定 Prompt 结构:System Prompt、Tool Schema 的内容和顺序保持稳定
  • 固定 JSON key 排序:工具定义、配置文件等使用确定性的序列化顺序
  • RAG 结果排序稳定化:检索结果按固定规则排序,避免每次拼接顺序不同
  • 可变内容放末尾:用户个性化内容、动态上下文放在 Prompt 最后
  • 建立监控告警:缓存命中率低于 50% 时自动告警
  • 评估平台选择:高并发大前缀场景优先考虑 Anthropic(90% 缓存节省)或自建 SGLang/vLLM

延伸阅读


理解缓存,是理解现代 LLM 推理经济学的第一步。希望这篇文章帮你建立了完整的认知框架。