07 · KV Cache 与缓存友好架构:稳定前缀、命中率与成本
「AI-Agent 面试深度指南」· 模块二 · 上下文工程 · 第 7 篇 / 共 32 篇
引言
两个团队用同一个模型、做同一个客服 Agent,月度账单差了 4 倍。排查下来,差别不在模型,而在缓存命中率:A 团队的系统提示与工具定义被固定在上下文最前面,命中率 92%;B 团队把当前时间戳放在了 system 提示的第一行,命中率常年接近 0。
KV Cache 是 Agent 工程里少有的"既降成本又降延迟还提稳定性"的杠杆,但它的收益完全取决于上下文布局是否"缓存友好"。本文讲清原理、两个缓存层级、四个典型杀手,以及一套可落地的布局规范。
一、KV Cache 原理与两个层级
1.1 为什么需要缓存
自回归生成时,生成第 n 个 token 需要用到前 n−1 个 token 的 Key 和 Value。若不缓存,每生成一个 token 都要重算整个前缀,总复杂度约为 O(n²) 的矩阵运算------这在长上下文下完全不可接受。
缓存后:Prefill 阶段 一次性算完输入的 K/V 并存储;Decode 阶段只计算新 token 的注意力,其余从显存读取。这是典型的"空间换时间",也是为什么长上下文会显著消耗显存。
一个值得记住的量化感受:KV Cache 的显存占用 ≈ 2 × 层数 × 头数 × 维度 × 序列长度 × batch × 精度字节数。这就是为什么长上下文服务更容易 OOM,也解释了 MQA/GQA(共享 K/V 头)为什么能大幅降低显存。
1.2 关键性质:按前缀匹配
KV Cache 的复用单位是前缀 。只有当新请求的前缀与缓存中的某段完全一致(逐 token 相同)时,后续计算才能被复用。前缀中间任何一处不同,后面的缓存全部失效。
这条性质把"上下文布局"从一个风格问题变成了架构约束:稳定的内容必须放前面,易变的内容必须放后面。
1.3 两个层级的缓存
推理引擎层的 KV Cache:同一请求内 Decode 阶段复用,通常引擎自动处理,开发者无感。
服务层的 Prefix / Prompt Cache:跨请求复用公共前缀。多数厂商对缓存命中的 token 给予显著折扣(通常是输入单价的几分之一),对长系统提示 + 大工具集的 Agent 收益尤其明显。
二者的关系可以理解为:前者解决"一次生成别重复算",后者解决"不同请求别重复算"。Agent 场景的收益主要来自后者------因为同一个 Agent 的每一轮、每一个用户的请求,都共享同一份系统提示与工具定义。
二、四个典型的缓存杀手
2.1 动态信息插在开头
最常见的错误:在系统提示第一行写"当前时间:2026-09-07 11:23:52"。每过一秒,这个前缀就变一次,后面几万 token 的缓存全部作废。
解法:把时间、进度、剩余预算等动态信息统一放进尾部的"状态栏"区块。模型同样能读到,但缓存前缀不受影响。
2.2 工具列表顺序不稳定
若工具定义按"使用频率动态排序"或"从 dict 随机顺序生成",每次请求的顺序都可能不同。
解法 :工具顺序固定(按注册顺序或固定排序规则),变化时通过版本号显式管理------即工具集更新时整体换一版,而不是每次微调顺序。
2.3 系统提示中嵌入动态用户信息
"当前用户:张三(VIP,上次登录 3 天前)"这类内容若放在 system 提示里,等于给每个用户一个独立前缀。
解法:用户信息放在用户消息或尾部状态栏;系统提示只放与用户无关的规则。
2.4 消息历史被重排或改写
有些实现会在每轮对历史做"格式化整理"(重新编号、调整顺序、去除空格),导致前缀变化。
解法 :历史采用只增不改(append-only)策略,新增内容追加在尾部,已有内容不再改动。压缩操作也只在尾部区块内进行。
三、缓存友好的上下文布局规范
3.1 推荐的五段式布局
[1] 系统提示词 ← 完全静态,最长,最该缓存
[2] 工具定义 ← 静态,顺序固定
[3] 长期知识/索引 ← 半静态,按版本更新
[4] 消息历史 ← 只增不改,增量追加
[5] 状态栏 + 当前输入 ← 完全动态,放最后
这个布局同时满足三件事:缓存命中率最高、模型对首尾敏感区被有效利用(开头放规则、结尾放当前任务)、压缩与裁剪操作只需触碰尾部。
3.2 缓存与压缩的关系:看似矛盾,实则互补
一个常见疑问:压缩会改写历史,破坏前缀,那还能不能压?
答案是能,但要分区域:
- 前缀区(1--3):永不改动,保证命中;
- 历史区(4):采用"稳定段 + 活跃段"双层结构------较老的历史压缩一次后固定下来(此后成为稳定前缀的一部分),最近几轮保持原样增量追加;
- 尾部区(5):自由变化。
这样既获得了压缩带来的 token 节省,又保住了主要前缀的命中率。实践中往往能达到"压缩省下的成本 > 缓存损失的成本"。
3.3 可观测性:必须监控命中率
没有度量就没有优化。每次模型调用的返回里通常包含 cached_tokens 字段(不同厂商命名不同)。要做的三件事:
- 在 trace 中记录每次调用的 cached/total 比例;
- 按场景聚合,找出命中率低的调用方;
- 对命中率异常下跌设置告警------它通常意味着有人改动了系统提示或工具顺序。
四、成本与延迟的连带收益
4.1 成本账
假设某 Agent 的系统提示 + 工具定义共 4000 token,每次任务 10 轮,日活 1 万次任务:
- 无缓存:10 万次调用 × 4000 token × 输入单价;
- 命中 90%:仅 10% 的部分按原价,其余按折扣价。
在长系统提示场景,缓存通常能带来数倍的输入成本下降,且轮数越多收益越大。
4.2 延迟账
命中缓存的前缀跳过了 Prefill 的计算,直接降低首 token 延迟(TTFT)。对交互型应用,TTFT 是影响"快不快"主观感受的最关键指标。
4.3 与分层模型的协同
缓存与"小模型路由 + 大模型决策"并不冲突,但要注意:缓存是按模型实例绑定的,换模型即失效。因此切换模型要有节制,频繁在模型间跳会同时损失缓存收益与行为一致性。
五、面试考点与答题框架
5.1 高频真题
Q1:为什么 ReAct 循环的缓存读取量随轮数近似二次方增长?
答:每轮请求都要携带(并可能重新读取)越来越长的上下文前缀;轮数为 n 时,累计读取量约为 O(n²)。优化方向有三个:稳定前缀提高命中、增量追加避免重排、减少无效轮数(更好的规划与首次成功率)。
Q2:怎么设计上下文以最大化命中率?
答:五段式布局(系统提示 → 工具定义 → 长期知识 → 只增不改的历史 → 动态状态与当前输入);禁止在头部插入任何随时间或用户变化的内容;用版本号管理工具集变更。
Q3:缓存命中对成本影响有多大?命中不了怎么排查?
答:命中单价通常是输入单价的几分之一,长系统提示场景可省下数倍输入成本。排查方法:打印每次调用返回的 cached token 数;若为 0,逐段比对前后两次请求的原始文本,找出第一处不一致的位置------90% 的情况是时间戳或工具顺序。
5.2 答题加分点
- 能解释"前缀匹配"并推出"稳定前置、易变后置";
- 能说出缓存与压缩的分区域共存方案;
- 能把 TTFT 与缓存关联起来,说明你理解用户体验层面的收益。
小结
缓存友好架构可以浓缩成八个字:稳定前置、易变后置 ,再加四个字的执行纪律:只增不改。它同时改善成本、延迟与稳定性,是投入产出比最高的一项优化。做 Agent 而不做缓存治理,等于每次请求都在为同一份系统提示重复付费。