3661 字
约 12 分钟
14
5.3 KV Cache 与成本的关系
2026-10-25
无标签

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]

  1. "怎么样"生成自己的 Query 向量(代表"我在找什么")
  2. Query 与每个词的 Key 做点积(匹配度打分),得到注意力权重
  3. 用权重对所有词的 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 万,就别急着做。

一句话

缓存优化的门槛是"规模",不是"难度"。 规模化系统不做是浪费,小规模系统做了是浪费。

5.3 KV Cache 与成本的关系
https://clxhxhhr.top/posts/4889/
作者
clxstart
发布于
2026-10-25
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。