Part 2 · Serving & Cost 服务与成本

2.2 Prompt Caching 前缀缓存,与它对提示词结构的反向约束

难度 ★★☆ 阅读 ~13 min 面试出现频率 中高 前置 1.2 KV Cache
Core Concept 核心概念

为什么前缀可以被缓存

1.2 讲过:每个 token 的 K/V 向量算出来之后就不再改变。这里要补上关键的第二句——第 i 个 token 的 K/V 只取决于它自己和它前面的 token(因果注意力的定义)。后面来什么内容,完全不影响前面已经算好的 K/V。

这个性质有一个立刻可用的推论:两个请求只要开头的 token 序列完全相同,这段前缀的 KV cache 就是逐位相同的。既然如此,第二个请求何必重算?把第一个请求的这段 KV 留着,直接接上去就行。

这就是 prompt caching(提示词缓存,底层实现常称 prefix caching,前缀缓存)。它跳过的是 prefill 阶段对已缓存部分的全部计算,因此收益出现在两个地方:

TTFT 下降——prefill 只需处理没命中的那部分 token
输入成本下降——命中部分通常按显著折扣计费(自托管则是省下真实算力)

注意:输出 token 的生成速度(TPOT)完全不受影响。
缓存只作用于 prefill,不作用于 decode。

为什么这件事值得单独一章?因为 LLM 应用的输入有一个普遍结构:绝大部分内容在多次调用之间是重复的。system prompt、工具定义、few-shot 示例、检索到的文档、多轮对话的历史——真正变化的往往只有末尾那一句用户提问。1.1 的估算题算过一个 6 轮对话:11,800 个输入 token 里有 6,240 个是同一段 system prompt 被重复计费了 6 次。缓存要吃掉的正是这一部分。

唯一的规则:从第 0 个 token 开始的精确前缀匹配

整章最重要的一句话:缓存匹配是从头开始的、逐 token 精确的前缀匹配。不是模糊匹配,不是「内容差不多」,也不是「找到中间那段相同的」。一旦第 k 个 token 不一致,从 k 开始直到结尾的全部内容都必须重算——哪怕后面的内容和上次一模一样。

因为 KV 依赖于「它前面的所有 token」,前缀一变,后面所有 token 的 K/V 都随之改变,缓存自然全部失效。这个机制简单得近乎粗暴,但正是它派生出了整章的工程结论。

同样的内容,两种排列方式,缓存命中率天差地别 ✗ 把易变内容放在开头 当前时间戳 system prompt 工具定义 few-shot 示例 用户提问 第一个 token 就不同 → 后面全部作废 → 命中率 0% ✓ 稳定内容前置,易变内容后置 system prompt 工具定义 few-shot 示例 当前时间戳 用户提问 这段命中缓存,prefill 跳过 ————————————→ 仅此段重算 两种排列送给模型的信息完全相同,成本和 TTFT 却可能差出数倍。
图 1 · 缓存命中边界由第一个差异点决定。蓝色 = 命中缓存、无需计算的部分;橙色 = 必须重新 prefill 的部分。提示词的排列顺序从此不再只是可读性问题,而是成本问题。

反向约束:缓存机制开始决定你怎么写提示词

这是本章标题里「反向约束」的意思——一个底层的性能机制,反过来对上层的提示词工程提出了硬性要求。四条可以直接执行的规则:

① 按「变化频率」从低到高排列。最稳定的(system prompt、工具 schema、few-shot)放最前,会话级的(历史对话、检索文档)放中间,请求级的(用户当前提问)放最后。这是唯一一条真正重要的规则,其余三条都是它的推论。

② 把动态噪声赶出前缀。当前时间、请求 ID、随机排序的列表、用户昵称——这类内容一旦出现在靠前位置,会让缓存永久命中率为零,而且这个 bug 从功能测试上完全看不出来。如果业务确实需要当前时间,把它放到用户提问旁边。

③ 警惕「无害」的改动。给 system prompt 加一个空格、调整 JSON 字段顺序、更新版本号注释——任何一处 token 级变化都会让全体缓存失效。所以 system prompt 应该被当作需要版本管理和变更审批的资产,而不是随手可改的配置。

④ 检索结果的顺序要稳定。RAG 场景中,若把检索到的文档拼在提问之前,而检索结果的顺序在多次调用间不稳定(例如按分数排序但分数有微小抖动),缓存同样会大面积失效。可行的做法是固定排序规则,或干脆把文档放到提问之后。

一个容易踩的坑:缓存不是免费的。写入缓存通常有额外开销(自托管是显存占用,商业 API 常有写入溢价),且缓存有存活时间。因此只有被复用的前缀才划算:一次性的长文档分析、每个用户各不相同的超长上下文,可能写入了就再也没命中,反而更贵。判断标准很简单——这段前缀在缓存存活期内会被读几次?少于一两次就不值得。

Case Study 真实案例

RadixAttention:把前缀共享做成自动的

SGLang: Efficient Execution of Structured Language Model Programs

早期的前缀缓存需要应用显式声明「这一段是可复用前缀」。SGLang 的 RadixAttention 把它变成了自动机制:用一棵基数树(radix tree)组织所有请求的 KV cache,新请求进来时自动在树上匹配最长公共前缀,命中部分直接复用;配合 LRU 策略做淘汰。

树结构在这里是恰到好处的选择——它天然表达了「多个请求共享同一段前缀,然后各自分叉」的结构,而这正是 LLM 应用请求的典型形态:同一个 system prompt 下分出成千上万个不同的用户提问。

最高 5×
相对基线系统的吞吐提升
自动
跨请求前缀复用,无需应用显式声明
TTFT ↓
减少 prefill 计算,同时降低显存占用

条件:提升幅度高度依赖负载的前缀共享程度——多轮对话、few-shot、同一套 system prompt 服务大量用户的场景收益最大;而每个请求都完全不同的负载则几乎没有收益。这也再次印证了本章的判断标准:缓存的价值等于复用的程度。

值得注意的是它与 1.2 的 PagedAttention 的关系:分页让「多个逻辑块指向同一个物理块」成为可能,RadixAttention 则在其上回答「哪些块应该被共享、什么时候淘汰」。底层机制解锁能力,上层策略决定收益。

来源:Zheng et al., "SGLang: Efficient Execution of Structured Language Model Programs" 及项目公开说明。数字引自公开报告;示意图为本书重绘。
Tooling Sidebar 工具层
各家缓存机制的差异(会过期,正文不依赖本区块) 截至 2026-09

显式 vs 自动:缓存断点有两种给法——在请求里手动标记断点(对可缓存段的边界有精确控制),或由服务自动放在最新的可缓存位置(几乎不用改代码,但控制力弱)。同一家服务常常两种都支持,且因模型而异,接入前按所用模型确认,这决定了要不要改调用代码。

三个必查参数:① 最小可缓存长度——短于某个 token 数的前缀通常不予缓存;② 存活时间 TTL——从几分钟到数小时不等:有的默认 5 分钟、可付费延长到 1 小时,有的按模型或组织的数据策略决定默认值、最长可到 24 小时;请求间隔超过 TTL 就命中不了;③ 计费方式——缓存写入、缓存读取、普通输入三者单价各不相同,且各家差异很大。价格属于工具层,请以官方文档为准,本书不给具体数字。

自托管:vLLM 当前版本对支持的模型默认开启自动前缀缓存(需要关闭时用 --no-enable-prefix-caching);SGLang 默认启用 RadixAttention。自托管没有「缓存计费」,代价是显存——缓存占用的显存会挤压可用于并发的 KV 预算,需在 2.4 的容量模型里一并考虑。

怎么验证真的命中了:不要假设它生效了。商业 API 一般在响应里返回缓存读/写的 token 数,把它接进监控,做成「缓存命中率」指标;自托管则看框架暴露的 prefix cache 命中率。一次不慎的 system prompt 改动就可能让命中率归零,而账单要到月底才会告诉你。

Quiz 自测

章末自测

估算题客服机器人:system prompt + 工具定义 + few-shot 共 3000 token 固定不变,每轮用户提问约 100 token,日均 50 万次调用。假设缓存读取价格为普通输入的 1/10,开启缓存能省下多少比例的输入成本?

不开缓存:每次输入 3100 token,按单价 1 计,日成本 = 50万 × 3100 = 15.5 亿单位。

开缓存(假设命中率接近 100%,忽略写入开销):命中部分 3000 token 按 0.1 计价 = 300;未命中 100 token 按 1 计价 = 100。每次 = 400 单位,日成本 = 50万 × 400 = 2 亿单位。

节省 ≈ 87%。同时 TTFT 也会明显下降,因为 3000 token 的 prefill 被跳过了。

但要检查两个前提:① 调用频率必须高于缓存 TTL——若 TTL 是 5 分钟而调用稀疏,大量请求会落在缓存过期后,实际命中率远低于 100%,收益按比例缩水;② 那 3000 token 必须真的一字不变——如果里面嵌了当前日期,命中率是 0 而不是 100%。

这道题的真正考点:缓存收益 = 可缓存前缀占比 × 命中率 × 折扣力度。三个因子任何一个塌掉,收益都会消失,而只有第一个因子是设计时可见的。

场景判断团队开启了 prompt caching,但账单几乎没变。排查方向按什么顺序展开?

先量再猜:第一步永远是确认命中率指标(响应里的缓存读取 token 数)。若命中率本身就接近 0,往下查;若命中率很高但账单没变,说明可缓存部分占比太小,是设计问题而非配置问题。

命中率为 0 的常见原因,按发生频率排序:

① 前缀里有动态内容——最常见。当前时间、请求 ID、用户名、随机排序的检索结果出现在靠前位置。检查方法:把连续两次请求的完整 prompt 逐字符 diff,第一个差异点之后的全部内容都是没命中的。

② 顺序放反了——用户提问放在了 system prompt 前面,或每次拼接顺序不稳定。

③ 没达到最小可缓存长度——前缀太短,服务端根本不缓存。

④ TTL 过期——调用间隔大于缓存存活时间。这类情况命中率会呈现明显的时间相关性(高峰期高、低谷期低)。

⑤ 没真正开启——显式缓存机制需要在请求中标记,只改了配置没改调用代码。

概念题为什么 prompt caching 能降低 TTFT,却对 TPOT 毫无帮助?

因为缓存复用的是已经算好的 KV cache,它替代的是 prefill 阶段的计算。TTFT 主要由 prefill 时间构成,跳过大部分 prefill,TTFT 自然下降。

而 TPOT 由 decode 阶段决定:每生成一个新 token,都要完整读一遍模型权重、并对全部 KV cache 做注意力计算。这部分工作与前缀是算出来的还是缓存来的毫无关系——缓存省的是「算 K/V」,不是「用 K/V」。

甚至要注意反向效应:命中缓存意味着上下文里有一段很长的前缀,decode 每一步都要对它做注意力计算,所以长前缀下的 TPOT 反而略高于短前缀。缓存让你「更便宜地拥有长上下文」,但长上下文本身的解码成本依然存在。

架构评审某 RAG 系统的 prompt 结构为:`[检索到的 5 篇文档] + [system prompt] + [对话历史] + [用户提问]`,检索结果按相关性分数降序排列。指出其中的缓存设计问题。

问题一:最易变的内容被放在了最前面。检索结果因查询而异,几乎每次都不同——这直接导致命中率恒为 0,后面的 system prompt 和对话历史(本来是最该被缓存的部分)全部陪葬。这是整个设计里唯一致命的错误。

问题二:即使把文档后移,排序抖动仍是隐患。相关性分数相近的文档,顺序可能在多次检索间发生变化;若文档段本身希望被缓存(比如同一批文档服务多轮追问),不稳定的排序会让它失效。修法是按文档 ID 等稳定键做二次排序。

问题三:对话历史放在用户提问之前是对的,但要注意历史的截断策略。如果采用「保留最近 N 轮」的滑动窗口,那么每多一轮,历史的开头就会被切掉一段,前缀随之改变、缓存全部失效。更缓存友好的做法是从头累积、只在超限时做一次性压缩(见 4.3),把失效频率从「每轮」降到「偶尔」。

建议结构:[system prompt] + [工具定义] + [对话历史(累积式)] + [检索文档] + [用户提问]——严格按变化频率从低到高排列。