没有 KV Cache 时开销如何增长?
- 场景: Agent 第 6 轮对话已累积 2000 个 token;
- 无缓存做法: 每生成一个新 token 都重新计算这 2000 个 token 的 K、V 向量,相当于重跑整个前缀的前向计算;
- 悖论: 前 5 轮内容完全没变,第 6 轮仍要像第 1 轮那样从头计算,而且前缀更长、代价更大;
- 量级: prefill 阶段(生成回复之前处理全部输入 token 的阶段)的注意力计算量随上下文长度平方级增长;
- 后果: 几十轮工具调用的 Agent 任务中,延迟与成本会急剧攀升到不可接受;
KV Cache 如何工作?
- 例子: 上下文为 [A, B, C, D],准备生成第 5 个 token E;
- 注意力核心: 来自 D 的 Query 与 A、B、C、D 的 Key 做点积算匹配度,再对四者的 Value 加权求和,得到 D 位置的输出表示;
- 缓存规则: 每个 token 的 K、V 只在第一次进入上下文时计算一次,之后常驻缓存;
- 加速: 生成 E 时 A、B、C、D 的 K、V 直接取用;E 被采样出来后只计算 E 自身的 K、V 并追加,供生成第 6 个 token 使用;
- 累计量: 无缓存时总计算量与序列长度 N 的平方成正比,有缓存时把历史 K、V 投影重算省掉;
- 剩余成本: 每个新 token 的注意力仍要遍历全部缓存的 K、V,注意力计算量随上下文长度线性增长;
- 瓶颈: 长上下文解码越来越慢,KV Cache 的显存与带宽成为推理瓶颈;
为什么修改前缀会让后续缓存失效?
- 结构: 大模型由数十到上百层 Transformer 串联,每一层独立生成自己的 K、V 缓存;
- 逐层传递: 第 1 层输出喂给第 2 层,第 2 层再加工,像流水线工序;
- 依赖范围: 第 1 层处理每个词时会综合该词及其前面所有词,因此第 k 个 token 变化会使其后所有 token 的表示逐层受影响;
- 复用边界: 缓存只能保留到首个不同 token 之前,从该位置起必须重新计算;
- 代价与位置: 变动点越靠前,需重算与重新计费的 token 越多,延迟影响通常越大(实测可达数倍);
- 纪律: 这就是“系统提示词一旦定下来就不要改”的根本原因;
对缓存友好的三条核心原则是什么?
- 静态前缀不改: 系统提示词和工具定义一旦确定就不要改,多一个空格也可能使首个不同 token 及其后的缓存无法复用;
- 动态信息追加末尾: 时间戳、用户状态等变化内容作为新消息追加到对话末尾,而不是修改已有系统提示词;
- 用标准 API 格式: 结构化消息由 Chat Template 翻译成训练时见过的固定 token 序列;自行拼成
"USER: ... ASSISTANT: ..." 偏离训练格式,会削弱多步思考能力;
- 缓存视角澄清: 缓存只认 token 字节序列,只要拼出的前缀字节级稳定照样能命中;但拼接方式不稳定时缓存同样失效;
- 直觉总结: 模型会把前面处理过的内容缓存起来,下次只需处理新增部分;
一行时间戳如何让推理成本翻倍?
- 背景: 某客服 Agent 每天处理 10 万次对话,原本正常;
- 改动: 工程师在系统提示词里加一行
Current time: {{now}} 注入实时时间戳;
- 现象: 第二天所有对话的首 token 延迟从 0.5 秒涨到 3-5 秒,月度推理账单几乎翻倍;
- 原因: 每次请求的 token 序列从时间戳位置开始不同,该位置及其后的 KV 状态无法复用,而系统提示词位于上下文前部;
- 教训: 开发者一行看似无害的代码,可能让整条推理链路慢一个量级;
- 正解: 把时间信息作为用户消息追加到对话末尾,或只在真正需要时通过工具调用获取;