5.3 KV Cache 与成本的关系
一句话结论:系统提示里加一行当前时间,可能让延迟涨 6 倍、账单翻倍。 因为缓存要求"前缀字节级不变"——而动态内容一注入,缓存全线失效。所以上下文设计不只是"写什么",还有"什么能动、什么不能动"。
读这篇之前
需要先读 5.1(上下文装了六类东西)、3.4(什么该放 system)。这一篇是全系列技术密度最高的一篇,但结论极其实用。
一、先建立直觉:模型在"回头看"
模型每生成一个 Token,都要回头看一遍前文所有 Token 的中间计算结果。
如果每轮都从头算一次,开销会随上下文长度爆炸式增长。
KV Cache 的做法:把前文的中间计算结果缓存下来,下一轮只需计算新增 Token 的部分。
前提是:要复用的那部分前缀保持不变。若 Token 序列从某个位置开始不同,首个不同 Token 及其后的缓存状态都要重新计算;此前的位置不受影响。[原文 4568]
三个向量:Query、Key、Value
想理解"为什么缓存的是 Key 和 Value",需要知道注意力怎么工作。
假设模型正在处理"北京 的 天气 怎么样"。当读到"怎么样"时,它要决定:前面哪些词对理解"怎么样"最重要?
| 向量 | 含义 | 在这个例子里 |
|---|---|---|
| Query(查询) | 当前词发出的"搜索请求" | "怎么样"问:哪个词和我最相关? |
| Key(键) | 每个词的"标签",被搜索匹配 | "北京"标签偏"地名","天气"偏"气象" |
| Value(值) | 每个词的"内容",匹配后提取 | 匹配到"天气"后,提取它的语义信息 |
计算分三步:[原文 4568]
- "怎么样"生成自己的 Query 向量(代表"我在找什么")
- Query 与每个词的 Key 做点积(匹配度打分),得到注意力权重
- 用权重对所有词的 Value 加权求和
关键洞察:前面每个词的 Key 和 Value 一旦算好就不变了——因为它们的文本没变。所以可以缓存起来复用。而 Query 是当前这个词的,每次都要新算。
这就是 KV Cache 的名字来源:缓存的正是 K 和 V。
二、一个真实事故:一行时间戳,账单翻倍
这是原文里最值得记住的案例:
某团队的客服 Agent 每天处理 10 万次对话,原本一切正常。某天工程师为了让 Agent "知道"当前时间,在系统提示词里加了一行:
Current time: {{now}}
第二天监控告警:所有对话的首 Token 延迟从 0.5 秒涨到 3~5 秒,月度推理账单几乎翻了一倍。 代码看起来完全没问题,模型也没换。[原文 4568]
原因:那一行时间戳使每次请求的 Token 序列从时间戳所在位置开始不同,因此该位置及其后的 KV 状态都无法复用。
而系统提示词位于上下文最前部——所以它后面几乎全部内容都要重算。
这就是"无形成本":开发者写下的一行看似无害的代码,可能让整条推理链路慢一个量级。
修复方法
差:system: "你是一个助手。Current time: {{now}}"
好:system: "你是一个助手。"(固定不变)
后续 user 消息里:【当前时间】2026-10-25 14:30
把动态内容追加到末尾,前缀就稳定了。
三、三条必须记住的结论
原文贴心地给了"可以跳过原理,只记这三条"的总结:[原文 4568]
| # | 结论 | 原因 |
|---|---|---|
| 1 | 系统提示词和工具定义一旦确定就不要改 | 任何改动(哪怕一个空格)都可能改变 Token 序列,使首个不同 Token 及其后缓存失效;越靠前,影响越大 |
| 2 | 动态信息永远追加到末尾 | 时间戳、用户状态等,作为新消息追加,而不是修改已有的系统提示 |
| 3 | 使用标准 API 格式,不要自行拼接消息 | 结构化消息会被 Chat Template 翻译成模型训练时见过的格式;手拼 USER: ... ASSISTANT: ... 偏离训练格式,会削弱多步思考能力 |
第 3 条容易被误解,需要补充说明:
- 缓存层面:它只认 Token 字节序列,手拼也能命中缓存(只要前缀字节级稳定)
- 模型效果层面:偏离 Chat Template 的训练格式,会影响模型表现
所以"用标准格式"主要不是为了缓存,而是为了模型本身.
四、两个层级的缓存
一个容易混淆的点:KV Cache 和 Prompt Cache 不是一回事。
| KV Cache | Prompt Cache | |
|---|---|---|
| 层级 | 推理引擎内部 | API 服务商层面 |
| 作用范围 | 单次请求内部 | 跨请求复用 |
| 谁在用 | 推理引擎 | 云服务商 |
| 你能控制 | 间接(通过稳定前缀) | 间接(同上) |
Prompt Cache 是构建在 KV Cache 之上的跨请求缓存。[原文 4568]
对你的实际影响:只要前缀稳定,两者都受益;前缀一乱,两者都失效。
五、把缓存当架构约束,而不只是优化
多数人把缓存当成"性能优化"。但从上面的事故可以看出,它是架构约束:
| 设计决策 | 缓存友好吗 | 为什么 |
|---|---|---|
| 系统提示固定、版本化管理 | ✅ | 前缀不变 |
| 动态内容追加到末尾 | ✅ | 前缀不受影响 |
| 系统提示里插当前时间/用户名 | ❌ | 前缀每次都变 |
| 每轮重新生成工具描述 | ❌ | 工具定义位置在最前 |
| 按阶段切换工具集 | ⚠️ | 切换时缓存失效,但优于每轮都变 |
最后一行值得注意:按阶段加载工具(4.5 推荐)会让缓存失效。 这是一个真实权衡:
全量工具:缓存命中率高,但上下文长、选择易错
按阶段加载:上下文短、准确率高,但每次切换要重算
判据:如果同一阶段内会跑很多轮,按阶段加载更划算;如果每轮都要换阶段,全量更省。
六、KV Cache 的另一面:可编辑的"笔记"
原文提到一个进阶视角:KV Cache 未必是一次性的——可以设计成可编辑、可组合的"笔记"。[原文 4568]
这个思路的含义是:如果一段上下文在多个请求间重复使用(比如一个长的参考文档),可以把它作为稳定的前缀,反复命中缓存。相当于把"参考资料"变成了一个可复用的缓存块。
实践中的对应做法:
| 做法 | 效果 |
|---|---|
| 把长文档放最前,问题放后面 | 文档部分可复用 |
| 多个请求共享同一份系统提示 | 系统提示部分可复用 |
| 按"会话"组织,避免跨会话混用前缀 | 减少缓存污染 |
七、常见误区
误区一:缓存是运维的事,跟设计无关。
恰恰相反。上下文怎么排,直接决定缓存能不能命中。
误区二:改系统提示只是内容变化,不影响性能。
会显著影响。它位于最前部,改动代价最大。
误区三:把时间/用户名放进系统提示方便。
这是最典型的性能陷阱。放末尾。
误区四:手拼消息和标准格式等价。
缓存层面近似,但模型效果会下降(偏离训练格式)。
误区五:缓存命中与否不影响模型质量。
KV Cache 影响延迟与成本,Prompt Cache 同理——它们不直接改变输出质量,但通过"能不能跑更多轮"间接影响效果。
误区六:按阶段加载工具一定更好。
它会破坏缓存。要看同一阶段的轮数(第五节已给出判据)。
八、一个实操:把缓存命中性变成可测指标
5.3 讲了原理,但怎么知道自己的系统有没有命中缓存?
三个可观测信号(若 API 返回)
| 字段 | 含义 |
|---|---|
cached_tokens / prompt_tokens_details.cached |
命中缓存的 Token 数 |
cache_creation_input_tokens |
本次写入缓存的 Token 数 |
| 首 Token 延迟 | 命中时显著下降 |
一个自测方法
① 用完全相同的系统提示连续调用 3 次
② 对比第 1 次与第 3 次的延迟和成本
③ 若第 3 次明显更便宜/更快 → 命中
若三次一样 → 没有命中,检查前缀
三次一样是明确的故障信号,而多数团队从来没测过。
一个常见漏点:工具定义顺序不稳
# 错:字典遍历顺序在不同进程/版本可能不同
tools = [build(t) for t in tool_dict.values()]
# 对:显式排序
tools = [build(t) for t in sorted(tool_dict.values(), key=lambda x: x.name)]
这一行改动,能修复"缓存永不命中"的问题,而它极难被发现——因为系统"能跑,只是慢且贵"。
一个检查清单
| # | 检查 | 命中条件 |
|---|---|---|
| 1 | 系统提示是否含动态内容(时间、用户 ID)? | 必须无 |
| 2 | 工具定义顺序是否固定? | 必须固定 |
| 3 | 工具描述是否每次重新生成? | 应缓存为常量 |
| 4 | 前缀是否至少达到服务商要求的最小长度? | 查官方文档 |
| 5 | 调用之间间隔是否过长(缓存过期)? | 注意 TTL |
第 4 条常被忽略:多数服务商的缓存有最小前缀长度门槛(如 1024 Token)。系统提示太短反而不命中。
九、动手:KV Cache 的成本实测
5.3 讲了原理。这里给一个可以自己跑的测量方法。
一个三行测法
import time
def measure(call, n=3):
times, costs = [], []
for i in range(n):
t0 = time.time()
r = call()
times.append(time.time() - t0)
costs.append(r.usage.prompt_tokens_details.cached_tokens)
return times, costs
times, cached = measure(lambda: llm.chat(SAME_LONG_PROMPT))
print("延迟:", [f"{t:.2f}s" for t in times])
print("命中缓存 Token:", cached)
三种结果对应三种问题
| 现象 | 原因 | 修法 |
|---|---|---|
延迟三次数值接近,cached 均为 0 |
完全没命中 | 检查前缀是否稳定 |
| 第 1 次慢、第 2-3 次快 | 正常命中 | —— |
| 三次数值都慢且都 0 | 前缀长度未达门槛 | 加长稳定前缀 |
第二种是正常状态:第一次写缓存,后续命中。
一个常见误判
很多人看到"第 2 次变快了",就以为缓存生效了。但可能是模型侧的负载波动。
正确判断依据是 cached_tokens 字段,不是延迟。
如果 API 不返回这个字段,就只能用"完全相同的请求连发 5 次,看延迟曲线是否收敛"来间接判断。
一个立竿见影的实验
把系统提示里的 当前时间:{{now}} 删掉,挪到 user 消息里,再测一次。
多数情况下,cached_tokens 会从 0 变成一个接近系统提示长度的数字 —— 这一行改动就是 5.3 讲的真实收益。
十、下一篇
现在有了两块拼图:压缩(管内容量)和缓存(管重复计算)。
还剩一个同样重要、但更难的问题:上下文里的内容,会不会"污染"模型?
工具返回的网页、文件、日志里,可能夹带着误导性的文字——它们一旦进入上下文,就可能改变模型的行为。下一篇讲上下文污染与遗忘。
一个量化结论
把系统提示从 3000 Token 缩到 800 Token,缓存命中率可能从 0 提升到 80%。
收益计算:
假设每轮输入 15000 Token,其中稳定前缀 800
命中部分单价 0.1x:
每轮成本 = 800×0.1 + 14200×1 = 14280 等效 Token
未命中:
每轮成本 = 15000 等效 Token
节省 ≈ 5%
但如果稳定前缀有 6000 Token(含工具定义):
命中后 = 6000×0.1 + 9000 = 9600,节省 36%
结论:前缀越大,缓存收益越大。 所以"把稳定的东西集中到前面"比"精简系统提示"的收益更明显。
十三、动手:什么时候该上 KV Cache 优化
不是所有系统都值得做缓存优化。三条判据:
| 判据 | 值得做 | 不值得 |
|---|---|---|
| 系统提示长度 | > 2000 Token | < 500 Token |
| 调用频次 | 每天上千次 | 每天几十次 |
| 任务轮数 | 多轮(>3) | 单轮 |
三条全中,优化收益最明显。
一个反例
场景:一次性脚本,每天跑 20 次,系统提示 300 Token
做法:花两天做前缀稳定改造
收益:每天省 20 × 300 × 0.9 = 5400 等效 Token ≈ 几分钱
投入两天换几分钱,不值得。
反面:一个客服 Agent,每天 10 万次,系统提示 4000 Token,平均 6 轮
→ 每天省下几百万 Token,半天改造就回本。
判据的本质
缓存优化是"规模化收益" —— 单次省得少,靠次数放大。
所以在做之前,先算一下"每天的调用次数 × 可缓存 Token 数"。 这个乘积小于 10 万,就别急着做。
一句话
缓存优化的门槛是"规模",不是"难度"。 规模化系统不做是浪费,小规模系统做了是浪费。