2694 字
约 8 分钟
14
S23-A17 · KV 缓存复用:为什么"同一段前缀"能省一半

一个客服机器人每轮都要带 3000 token 的系统提示与流程说明,而用户问题通常只有 50 token。逐轮重算意味着99% 的算力花在"重复读同样的前缀"上。启用前缀缓存后,首 token 延迟从 900ms 降到 200ms,成本降了一半。

KV 缓存复用的前提看起来很严格——"前缀必须完全一致"——但它的收益极其可观,因为系统提示、工具描述、few-shot 示例都是天然的固定前缀。这个失败模式有个共同特征:它不报错,只是"没省下来"。所以团队常常"加了缓存但没感觉",然后把原因归到"模型就这样"。
而多数系统的失败原因是"前缀里混进了会变的东西"。


1. 复用的三个必要条件

条件 说明
前缀逐 token 相同 差一个字符就失效
同一模型(含版本) 不同模型不共享
同一部署实例/会话 缓存通常不跨实例

第二项常被忽略:"模型名相同"不等于"权重相同",版本更新后旧缓存不可用。

第三项在集群里是硬约束:缓存在进程内存里,请求打到另一个实例就 miss。第三项在实践中表现为"命中率忽高忽低":并发一上来,请求被分散到多个实例,每个实例各自的缓存都只命中一部分。所以命中率与"实例数"负相关。
所以"提高命中率"往往需要"按会话粘住实例"。

2. 前缀里最容易被破坏的东西

破坏源 例子
时间戳/日期 "今天是 2026-11-10"
用户名/ID "你是用户 12345 的助手"
随机排序的示例 few-shot 顺序随机
动态工具清单 按权限过滤后的工具列表
检索结果 放在系统提示之后、用户问题之前

前四项都很直接,第五项最隐蔽:很多人把 RAG 片段拼在"系统提示之后",于是用户问题之前的整段都成了"可变前缀",复用率归零。

// 两种排布,差别巨大
差:system + 【检索片段】+ user      → 片段变了,前缀就变了,永不命中
好:system + user + 【检索片段】      → 系统提示稳定复用,只有尾部变化
// 前提:模型能正确处理"片段在问题之后"的排布(多数可以)

3. 缓存的结构与代价

类型 机制
进程内 KV 随请求生命周期,最简单
前缀缓存(服务端) 按前缀 hash 复用
会话缓存(多轮) 复用上一轮的整段 KV
持久化缓存 跨请求、跨会话(较少用)

第一项在多轮对话里天然存在:同一会话的下一轮可以复用上一轮的全部 KV,只需要为新内容增量计算。

第二项(前缀缓存)的实现细节值得知道:它按"前 N 个 token 的 hash"索引,所以"前 1000 token 相同"就能命中共用段——不必整段相同。这让"部分复用"成为可能。
第三项的成本在内存:一个 3000 token 的前缀缓存可能占几百 MB,而它的淘汰策略决定了"复用"能否持续。

// 前缀缓存的典型配置(示意)
prefix_cache:
  enabled: true
  max_entries: 64              # 缓存多少个不同前缀
  max_total_tokens: 200_000     # 总 token 预算
  eviction: lru
  # 关键:前缀按 hash 索引;同一前缀的后续请求直接复用

4. 命中率怎么量

指标 含义
前缀命中率 请求中有多少比例命中了已有前缀
复用 token 数 实际省下的计算量
首 token 延迟 复用最直接的效果
缓存内存占用 成本侧

第二项比第一项更真实:命中 90% 但每次只复用 10 个 token,等于没命中。第三项(首 token 延迟)是最容易量化的收益:它是"用户感知最快"的指标,因为每轮对话的第一句话出现得越早,用户越觉得"系统灵敏"。
所以应当统计"被复用的 token 占输入 token 的比例"。

这也解释了"命中但没变快"的一种情况:用户感知的慢主要来自"生成长"而不是"prefill"。此时优化缓存没有用,该优化的是"输出长度"或"流式返回"。
第三项是用户能感知的:复用主要改善首 token 延迟(prefill 阶段),而对生成阶段(decode)没有帮助。

// 一次请求的账(3000 固定 + 50 用户)
不复用:prefill 3050 token ≈ 900ms
复用:  prefill 50 token   ≈ 200ms(省掉 3000 token 的 prefill)
// 注意:decode 阶段(生成)时间不受影响

有一个"结构化"的做法能让稳定性变成"编译期可检":把提示拆成"静态模板 + 变量槽",静态部分单独维护、单独 hash,变量按固定顺序填充。

// 模板分层(前两段可复用,第三段随请求变)
L1 角色与规则      (永不变)        → 可复用
L2 工具与示例      (按版本变,不按请求变)→ 可复用
L3 用户问题 + 检索  (每次变)        → 不可复用
// 关键:L2 的版本号变化应当与"发布"绑定,而不是与"请求"绑定

5. 让前缀稳定的五个做法

做法 效果
固定部分放最前 最大化可复用段
变量后置 尾部变化不影响前缀
示例固定顺序 避免随机
时间信息后置或省略 避免每天失效
工具清单稳定排序 避免权限变化打乱

第四项有个折中:需要"今天日期"时,把它放在用户消息里第三项(示例固定顺序)有个反直觉之处:"随机 few-shot"在某些任务上能提升效果(减少位置偏差),而它与前缀复用直接冲突。取舍是:"提升效果" vs "降低成本",要按任务重要性选。
(如"今天是 11-10,问题是…"),而不是系统提示里。这样前缀不动。

第五项的替代方案是"按角色分组":把工具清单按角色(管理员/普通/只读)预先算好几套前缀,每个角色一套。这样"角色内的用户"共享前缀,而前缀数量可控。
第五项在多租户工具系统里很常见:按权限过滤工具列表,导致每个用户的系统提示都不同。做法是"全量工具描述固定在前缀,权限约束用一句话说明"。

监控上要能区分三种 miss,否则无法改进:

// 三种 miss 的区分(日志里各打一个标记)
miss_cold     首次请求,缓存为空           → 正常
miss_changed  前缀内容变了(提示更新/变量混入) → 可优化
miss_routed   路由到别的实例(无粘性)      → 可优化(改架构)
// 只看"总命中率"会把三种混在一起,而它们的修法完全不同

6. 排错:五种"缓存没生效"

现象 成因 处理方向
命中率为 0 前缀含时间戳 移出前缀
偶尔命中 负载均衡打散 会话粘性
命中但没变快 复用的量太小 检查前缀长度
内存涨得快 缓存条目太多 限制条目与总 token
更新提示后变慢 前缀全变 接受一次性冷启动

第五项是"预期管理":每次修改系统提示,都会让整个前缀缓存失效,第一波请求变慢。第三项(命中但没变快)还有一个"小前缀"陷阱:固定 200 token、可变 300 token 时,复用只省 40%,而延迟改善有限。所以"前缀占总输入的比例"才是关键数。
这属于正常代价,不必当故障。

最后一条"安全"边界:缓存是共享的,而提示里可能含敏感信息。多租户系统里,如果前缀里塞了"当前用户的权限说明",那它就既破坏了复用,也带来了跨用户泄漏的风险。所以规律是:前缀只放"对所有用户都一样"的内容。

// 前缀里能放 / 不能放
能放:角色定义、输出格式、通用规则、工具描述
不放:用户名、权限、租户 ID、检索片段、日期
// 判据:这段话"对另一个用户是否也成立"

7. 边界:什么情况不必做前缀复用

场景 为什么 做法
单轮、短提示(<200 token) 省不下多少 不做
提示极短且高频变化 无稳定前缀 不做
自建小模型(prefill 很快) 收益小 不做
离线批处理(不在乎延迟) 只看吞吐 视成本决定

最后一条与"评测"有关的边界:缓存不该影响输出质量,但"检索片段位置改变"会影响(放在问题之后可能降低相关性)。所以启用复用时要重跑一次评测,而不是假设"只是变快"。
判断口径一句话:固定前缀的长度是否远大于可变部分(比如 10 倍)。是,复用就是几倍的收益;不是,收益被"缓存管理成本"吃掉。


8. 自测

  1. 复用的三个必要条件里,哪一个在集群部署中成为硬约束?为什么需要会话粘性?
  2. 为什么把检索片段放在"系统提示之后、用户问题之前"会让复用率归零?
  3. "前缀命中率"与"复用 token 数"哪个更真实?为什么?
  4. 复用改善的是 prefill 还是 decode?这解释了"命中但没变快"的哪种情况?
  5. 需要"今天日期"时该怎么放?为什么按权限过滤工具清单会破坏前缀?
  6. 判断"是否值得做前缀复用"的那个问题是什么?
S23-A17 · KV 缓存复用:为什么"同一段前缀"能省一半
https://www.clxhxhhr.top/posts/6521/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。