一个 Coding Agent 连续工作十轮时,真正变化的内容往往只占 Prompt 的一小部分。System Prompt、工具定义、项目规则、固定 Few-shot 示例以及前几轮历史消息会一遍遍出现在后续请求中。如果推理服务把每轮请求都当成一段全新的 Token 序列,那么这些已经计算过的前缀仍会重新经过 Transformer Prefill,再生成一套内容相同的 Key 和 Value。
这种浪费在短聊天里不一定突出,但当固定前缀达到数千甚至数万 Token,并且 Agent 持续调用模型时,Prefill 会反复占用 GPU 计算、显存带宽和调度时间。Prefix Caching,也就是前缀缓存,解决的不是“如何让模型少生成几个 Token”,而是“如何确认一段历史前缀已经计算过,并安全复用它对应的 KV Cache”。因此它最直接影响的是首 Token 之前的工作量,而不是后续逐 Token Decode 的计算规律。
Prefix Caching 缓存的是 Transformer 状态,不是 Prompt 字符串
一个请求从文本进入模型,大致经历 Tokenization、Embedding、逐层 Transformer 计算和 Decode。对于自回归模型,Prefill 处理完整输入序列,并在每个 Attention 层产生历史 Token 对应的 K/V 状态。后续每生成一个新 Token,只需要为新位置计算新的状态,再通过 KV Cache 访问此前上下文。
如果两个请求拥有完全相同的 Token 前缀,那么在模型、位置、Attention 配置等条件一致时,这段前缀经过各层计算得到的 KV 状态也可以复用。Prefix Cache 保存的正是这种已经完成 Prefill 的中间状态,而不是简单保存原始字符串。
flowchart TD
A[新请求到达] --> B[分词得到 Token]
B --> C[查找最长可复用前缀]
C --> D{是否命中缓存}
D -->|是| E[挂接已有 KV 块]
D -->|否| F[从头执行 Prefill]
E --> G[只计算未命中后缀]
F --> H[生成新的 KV 块]
G --> H
H --> I[进入 Decode]
I --> J[返回首个及后续 Token]
这里有一个重要边界:文本“看起来相同”不一定意味着可以直接复用。缓存匹配通常发生在 Token 或缓存 Block 层面。Tokenizer、模板拼接、特殊 Token、工具定义顺序、模型版本以及位置相关执行条件发生变化,都可能让最终 Token 序列或 KV 语义不同。可靠的 Prefix Cache 必须使用能够代表实际模型输入与缓存上下文的键,而不能只对原始字符串做模糊比较。
对于 Agent 来说,这意味着 Prompt 构造是否稳定会直接决定缓存命中率。相同的工具 JSON 如果每轮字段顺序、空白、动态时间戳或随机 ID 都发生变化,逻辑上仍是“同一组工具”,但 Token 前缀已经被打断,缓存系统无法把它当作相同路径复用。
为什么它主要改善 TTFT,而不是 Decode 每 Token 延迟
大模型在线请求通常可以拆成 Prefill 和 Decode 两个主要阶段。Prefill 一次处理输入 Prompt,构造后续生成所需的 KV Cache;Decode 则在每轮增加一个或少量新 Token,并持续读取已有 KV。
Prefix Caching 跳过的是“已经存在的前缀 Prefill”。假设一个 Agent 请求由 8000 个固定 Token 加 200 个新 Token 组成,命中完整固定前缀后,服务只需要处理新增的后缀,而不必再次为前面 8000 个 Token 重建所有层的 K/V。首 Token 出现之前需要完成的计算因此减少,TTFT 通常是最直观的收益指标。
一旦进入 Decode,模型仍然必须为每个新生成 Token 执行正常的层计算。Prefix Cache 并没有让模型跳过生成阶段,也没有降低一个新 Token 本身所需的模型权重计算。因此,把 Prefix Caching 描述成“整个回答都会按命中比例同比变快”是不准确的。
| 请求阶段 | Prefix Cache 能否直接减少工作 | 主要原因 | 更适合观察的指标 |
|---|---|---|---|
| 长 Prompt Prefill | 能 | 已命中前缀无需重新生成对应 KV | TTFT、Prefill 时间、Prefill Token 吞吐 |
| 未命中后缀 Prefill | 不能跳过 | 新 Token 尚无可复用 KV | 新增 Token 数、部分命中长度 |
| 逐 Token Decode | 通常不直接减少 | 新生成 Token 仍要经过模型计算 | TPOT、Decode 吞吐、Batch 效率 |
| 跨请求调度 | 间接影响 | 节省 Prefill GPU 时间后可能释放更多容量 | 请求吞吐、队列时间、GPU 利用率 |
这一区分也解释了为什么不同业务看到的收益差异很大。长 System Prompt、短回答的 Agent 请求,TTFT 在体验中占比高,缓存命中可能非常有价值;如果 Prompt 很短但模型需要生成数千 Token,那么 Decode 才是主要成本,Prefix Cache 只能优化请求前半段。
Agent 与 RAG 为什么天然容易形成可复用前缀
Agent 的 Prompt 往往由多个稳定模块拼接而成:System Prompt 定义角色与约束,Tool Schema 描述函数或 MCP 能力,项目规则提供长期上下文,Memory 或历史轨迹又把前几轮内容带入下一轮。用户的新消息通常追加在最后。
当请求序列保持“旧内容在前、新内容在后”的追加模式时,前缀缓存与 Agent 的数据形态天然匹配。第一轮计算出固定 System Prompt 和工具定义的 KV 后,第二轮可以复用这部分;随着对话继续,已经确认的历史消息也可能继续构成长前缀。缓存不是只服务一个固定模板,它可以随着请求演进逐步延长可复用路径。
RAG 也可能具有类似模式,但需要区分文档组织方式。若同一份长文档始终放在 Prompt 前部,不同问题只追加在文档之后,那么文档前缀很适合缓存。若每次检索出来的文档集合、顺序或模板结构都不同,公共前缀可能很快分叉,命中长度就会下降。
例如两个请求分别是:
System + 文档 A + 文档 B + 问题 1
System + 文档 A + 文档 B + 问题 2
它们可以共享较长前缀。但如果第二个请求变成:
System + 文档 B + 文档 A + 问题 2
即使内容集合相同,传统 Prefix Caching 也不能直接把 文档 A + 文档 B 的 KV 当成 文档 B + 文档 A 使用,因为自回归 Attention 中 Token 的位置和前序上下文已经改变。
因此,真正决定收益的不是业务是否被称为“Agent”或“RAG”,而是线上请求的 Token 序列是否形成稳定、重复且足够长的公共前缀。
vLLM 的 Block Hash:把前缀匹配落到 KV Block 上
在高并发服务中,逐 Token 把新请求与所有历史请求比较并不现实。推理引擎本来就需要按 Block 管理 KV Cache,因此一种自然做法是以完整 Block 为缓存与匹配粒度。
vLLM 的 Automatic Prefix Caching 设计采用基于 KV Block 的 Hash。可以把一段 Token 想成连续 Block:第一个 Block 根据自身 Token 及必要上下文生成键;后续 Block 的键还需要反映前序路径,使“内容相同但出现在不同前缀之后”的块不会被错误复用。
这解决了两个问题。第一,查找从长 Token 序列比较转为哈希索引查询;第二,缓存键能够表达 Block 的前缀语义,而不是只判断这一小段 Token 内容是否相同。
Block 粒度也带来边界。若两个请求只在一个 Block 内的最后几个 Token 发生分叉,那么通常只能复用此前完整匹配的 Block,剩余部分仍需要重新 Prefill。Block 越大,元数据和调度可能更简单,但细粒度复用能力会下降;Block 越小,索引、引用计数和管理开销又会增加。实际实现需要在 GPU kernel、Paged KV 管理和缓存命中粒度之间取平衡。
哈希碰撞同样属于正确性边界。缓存命中意味着系统准备把一段已经计算过的模型状态交给另一个请求,因此错误命中的后果不是“统计数据稍微不准”,而是直接改变模型后续计算上下文。工程上需要根据部署场景选择足够可靠的哈希策略,并确保缓存键覆盖会影响 KV 结果的必要输入条件。
RadixAttention:把共享前缀变成一棵动态维护的树
SGLang 的 RadixAttention 从另一种数据结构视角管理复用关系。大量请求往往不是只共享一个全局固定前缀,而是在不同位置不断分叉和再次形成长公共路径。Radix Tree 适合表示这种“前面相同、后面分支”的序列集合。
假设服务器先后看到:
System + Tools + Project A + 问题 1
System + Tools + Project A + 问题 2
System + Tools + Project B + 问题 3
树的上层可以保存 System + Tools,随后分成 Project A 与 Project B 两条路径,Project A 下又继续分叉到不同问题。树节点或路径片段与相应 KV 状态关联,新请求到来后沿树寻找最长匹配前缀,就能得到可复用的缓存范围。
这种结构对结构化 LLM 程序尤其有价值。程序可能执行多次生成、分叉、回到共同上下文再继续,或者让不同请求共享同一组 Few-shot 示例。此时“当前请求与上一条请求是否相同”并不是最有用的问题,更关键的是整个服务器已经保存的前缀集合里,哪一条路径与新输入拥有最长公共部分。
Block Hash 与 Radix Tree 并不是简单的“谁更先进”。它们解决的是同一问题的不同实现层面:如何为大量 KV 状态建立可查找的身份和共享关系。实际系统还要结合缓存块分配、引用计数、淘汰策略、调度器和 Attention backend,才能把数据结构上的命中转化成端到端收益。
缓存命中率不够:还要看命中了多少 Token、节省了多少工作
只记录“命中 / 未命中”会掩盖 Prefix Cache 的真实价值。一个请求命中 64 个 Token,另一个请求命中 50000 个 Token,在二值统计里都算一次命中,但节省的 Prefill 工作完全不同。
生产环境更适合同时观察:缓存请求命中率、命中 Token 数、命中长度占输入比例、避免的 Prefill Token 数、缓存占用显存,以及这些变化最终带来的 TTFT 改善。对于 Agent,还可以按 System Prompt、Tool Schema、历史对话、检索文档等前缀类型分桶,判断是哪一部分真正提供复用价值。
缓存的价值还受到请求到达时间影响。一个 20000 Token 的前缀理论上很贵,但一个月只复用一次,把它长期留在昂贵的 GPU HBM 中未必划算。反过来,一个 3000 Token 的工具定义若每秒被数百个 Agent 请求复用,即使单次节省较小,也可能具有很高的总收益。
因此,淘汰策略真正面对的是一个资源分配问题:有限缓存容量应该留给哪些前缀,才能最大化未来避免的计算。LRU 以“最近是否被访问”作为近似信号,优点是简单、成本低;更复杂的研究会尝试考虑前缀长度、重算成本、访问频率、请求类别甚至预测未来复用概率。但策略越复杂,元数据、预测和调度成本也越高。
| 缓存决策信号 | 能回答的问题 | 优点 | 局限 |
|---|---|---|---|
| 最近访问时间 | 哪些前缀近期还在使用 | 实现简单,维护成本低 | 不直接反映前缀长度和重算价值 |
| 命中频率 | 哪些前缀反复出现 | 能发现高复用模板 | 可能长期保留大量短前缀 |
| 前缀长度 | 一次命中可能跳过多少 Prefill | 与潜在节省量相关 | 长前缀不一定会再次访问 |
| 重算成本估计 | 淘汰后未来重新计算有多贵 | 更接近系统目标 | 估计模型和调度复杂度更高 |
| 复用概率预测 | 哪些缓存未来更可能再次命中 | 可适应业务模式 | 预测错误会反向浪费容量 |
一个好的淘汰算法不能只提高“缓存命中率”这个数字,还要证明它在相同显存预算下减少了更多 Prefill 计算,并且没有引入足以抵消收益的管理开销。
单机命中不等于集群命中:路由决定 KV 是否真的可复用
单 GPU 示例容易让 Prefix Caching 看起来像一个本地哈希表问题。真实推理集群中,请求可能被负载均衡器分配到不同实例,而 KV Cache 通常与具体 Worker、GPU 或缓存节点绑定。
假设请求 A 在 GPU 1 上完成了 10000 Token 的 Prefill并留下缓存。用户下一轮请求 B 被无状态负载均衡器送到 GPU 7,即使 B 的前缀与 A 完全一致,GPU 7 本地也可能没有这份 KV。系统要么重新计算,要么跨节点搬运缓存;后者又会引入网络传输、缓存目录和一致性管理成本。
因此大规模 Agent 服务常需要把 Prefix Cache 与请求路由一起设计。会话粘性、前缀感知路由、缓存目录服务或状态化调度,本质上都在回答同一问题:新请求应该送到哪个计算节点,才能既保持整体负载均衡,又尽可能靠近已经存在的 KV 状态。
这里存在明显冲突。只追求缓存命中可能不断把热门前缀请求压到同一 GPU,形成热点;只追求 GPU 负载均衡又会把请求打散,降低本地 KV 复用。生产调度器需要同时考虑队列长度、可用 KV Block、请求优先级和预期复用收益,而不是把 Prefix Cache 当作与负载均衡无关的独立开关。
这也是前缀缓存与 PagedAttention 的连接点。PagedAttention 解决 KV Block 如何分配和管理,Prefix Caching 决定哪些 Block 可以跨请求共享,而集群路由决定请求能否到达拥有这些 Block 的位置。三层任何一层失配,都可能让理论上的公共前缀无法转化为实际节省。
从 GPU HBM 到多级缓存:容量扩大后,延迟层级也随之出现
GPU HBM 是最适合直接参与 Attention 的缓存位置,但容量昂贵且有限。随着上下文长度和并发增加,仅在 GPU 上保存所有可能复用的 KV 很快会与活跃请求的 KV Cache、模型权重和工作空间争夺显存。
多级缓存的思路是把 KV 状态按照访问热度放在不同介质:高频前缀留在 GPU,更冷的数据可以移到 CPU DRAM,甚至进一步进入容量更大的存储层。SGLang 的 HiCache 体现了这种方向:GPU 层不再是缓存生命周期的唯一位置,被淘汰的 KV 可以有机会进入更大的 Host Memory 层,而不是立即完全丢失。
这种设计扩大了有效缓存容量,但也改变了“命中”的含义。GPU 命中可以直接跳过 Prefill并继续计算;CPU 命中还需要把 KV 搬回 GPU;更慢介质上的命中则可能具有更高恢复延迟。如果加载缓存所需时间接近甚至超过重新 Prefill,复用就失去价值。
因此多级 Prefix Cache 应该把命中分层观测:HBM hit、Host hit、远端 hit 分别有不同的字节搬运成本和恢复延迟。淘汰策略也不再只是“保留还是删除”,而变成“应该放在哪一层”。这确实越来越接近数据库 Buffer Pool 或操作系统内存层级的问题,但 KV Cache 还有模型层数、Token 位置、GPU kernel 消费格式等额外约束,不能直接照搬传统页缓存策略。
Prefix Caching 的边界:相同内容不等于相同前缀
传统 Prefix Caching 的强假设是:可复用内容必须处于相同的前缀路径。自回归 Transformer 中,一个 Token 的 K/V 不是只由这个 Token 自身决定,它还受到此前上下文和位置编码影响。因此,把一个文档片段从 Prompt 第一个位置移动到第三个位置后,原来计算得到的 KV 通常不能原样拿过来使用。
这对 RAG 是一个明显限制。检索结果可能在每个请求中重新排序,文档集合也会增删。两个请求虽然共享大量文档文本,却没有形成足够长的连续公共前缀。传统 Prefix Cache 看到的是早期分叉,而不是“后面还有很多相同片段”。
用户提供的 Position-Independent Caching 与 HYPIC 等较新研究正试图扩展这一边界,让缓存复用不再完全依赖连续前缀。由于这类方法涉及位置处理、Attention 结构以及缓存状态如何重用,不能简单理解成“给每个文档算一次 KV,以后放到任意位置都能拼接”。在没有针对具体模型和算法验证之前,传统前缀缓存仍是更清晰、成熟的工程假设:只有能够证明计算语义一致的状态才可以复用。
对 Agent 系统而言,这反而给出一个立刻可执行的设计原则:在不改变语义的前提下,让稳定内容尽量靠前,并保持序列化结果稳定。例如固定 System Prompt、固定 Tool Schema 顺序、避免在前缀中注入每轮变化的时间戳,把真正动态的任务输入放在靠后位置。很多时候,提高缓存命中率首先是 Prompt 构造和请求路由问题,而不是更换缓存算法。
上线时应该怎样证明 Prefix Cache 真的有效
最简单的验证方式不是看“缓存功能已经开启”,而是构造与真实业务相同的重复前缀负载。对于 Coding Agent,可以准备三组请求:完全冷启动、相同工具和规则但不同用户任务、持续追加历史的多轮会话。分别记录未启用缓存和启用缓存后的 Prefill Token 数、命中 Token 数、TTFT、GPU 时间和显存占用。
随后再加入并发。单请求下命中缓存通常容易表现出优势,但高并发时缓存 Block 会与活跃 KV 争夺容量,热门前缀还可能影响调度分布。需要观察 P50、P95、P99 TTFT,而不是只看一次最佳结果;同时记录缓存淘汰次数、缓存层级迁移、Block 使用率和请求排队时间。
对于集群,还应单独统计“逻辑可命中但因路由到其他节点而未命中”的请求。如果本地缓存命中率很低,而同一用户或同一 Agent 模板在整个集群中频繁出现,问题可能不在 Hash 或 Radix Tree,而在入口负载均衡策略。相反,如果请求已经稳定路由到相同节点,但命中长度仍短,就应该检查 Prompt 是否存在动态前缀、模板漂移或 Token 化差异。
最后要验证输出正确性。Prefix Cache 是对中间模型状态的复用,一旦缓存键、Block 生命周期、模型版本隔离或引用计数出错,可能产生难以察觉的跨请求污染。上线前应使用固定输入对比冷 Prefill 与缓存命中的生成结果,并覆盖缓存驱逐、Block 复用、模型重载和并发共享等边界。性能优化只有在状态语义可靠时才成立。
Prefix Caching 最值得关注的并不是“缓存”这个名字,而是它改变了推理服务看待 Prompt 的方式。传统服务把每个请求当成一次独立计算;前缀缓存把历史请求留下的 KV 状态视为可共享的计算资产。对于携带长 System Prompt、工具定义、Memory 和会话轨迹的 Agent,这种资产可能在数十轮调用中重复出现。
因此,评估 Prefix Caching 时最有价值的问题不是“框架是否支持 Automatic Prefix Caching”,而是:真实请求中有多少 Prefill Token 可以稳定复用,这些状态能在缓存里存活多久,请求能否被路由到持有它们的位置,以及恢复缓存的成本是否低于重新计算。只有这几项同时成立,公共前缀才会从文本层面的重复,真正转化为 TTFT 和系统吞吐上的收益。
资料来源
- Automatic Prefix Caching - vLLM Documentation
- Automatic Prefix Caching - vLLM Features
- SGLang: Efficient Execution of Structured Language Model Programs
- Learned Prefix Caching for Efficient LLM Inference
- HiCache System Design and Optimization
- HYPIC: Accelerating Hybrid-Attention LLM Serving with Position-Independent Caching
- SGLang: Efficient Execution of Structured Language Model Programs - NeurIPS 2024
- vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention
- 基于 Amazon SageMaker 有状态路由优化大规模推理集群下的 KV Cache 复用方案
- 自动前缀缓存 - vLLM 中文站