Appearance
第 2 章 上下文成本与缓存
本章要解决的问题
Agent 每调用一次模型,都要再次提供系统要求、工具定义和必要历史。任务越长,重复输入越多,等待时间和费用也会累积。
本章集中回答三个问题:
- 上下文的时间和费用花在哪里;
- 缓存怎样复用相同前缀,能省什么、不能省什么;
- 怎样组织请求,提高缓存命中率。
上下文该放什么、太长以后怎样剪裁和压缩,分别在第 6、12、13 章展开。
2.1 Agent 为什么会反复支付上下文成本
第 1 章已经说明,模型不会自动记住上一轮。为了让它继续任务,程序需要重新提供必要历史:
text
第 1 次输入 = 系统要求 + 用户问题
第 2 次输入 = 第 1 次输入 + 读文件调用 + 文件内容
第 3 次输入 = 第 2 次输入 + 测试调用 + 测试输出
……模型处理一次请求通常分成两段:
| 阶段 | 在做什么 | 对 Agent 的直接影响 |
|---|---|---|
| 处理输入 | 读取这次请求中的全部上下文,为生成做准备 | 输入越长,首个输出通常等得越久 |
| 生成输出 | 逐个产生新 token | 输出越长,等待和输出费用越高 |
多数模型服务会分别统计输入、缓存输入和输出 token;推理模型还可能单独统计推理消耗。具体字段与价格会变化,实现时应读取 API 返回的 usage,不要在程序里写死假设。
长上下文带来的问题不只有费用:
- 首个输出更慢: 模型要先处理输入,才开始生成。
- 总费用更高: Agent 往往把相同前缀重复发送很多轮。
- 重点更难突出: 窗口装得下,不等于所有内容都值得放进去。
缓存主要解决前两项。第三项仍要靠检索、分层加载、剪裁和压缩。
2.2 先分清两种缓存
“KV Cache”和“前缀缓存”经常被混在一起,但它们解决的范围不同。
一次请求内的 KV Cache
模型逐个生成 token 时,前文不会变化。KV Cache 保存已经处理过的前文计算结果,避免每生成一个 token 都从头计算整段输入。
这主要是模型服务的推理实现。调用 API 的应用通常不直接管理它,但上下文越长,KV Cache 占用的显存也越多。
多次请求之间的前缀缓存
Agent 连续多轮请求时,开头的大段内容往往相同。模型服务可以保存这段前缀的计算结果,下一轮命中后直接复用:
常见产品形态有两类:
- 自动匹配: 服务端发现请求具有相同前缀后自动复用,例如 OpenAI 提示词缓存和 vLLM 自动前缀缓存。
- 显式指定: 客户端标记缓存边界或创建可复用的缓存对象,例如 Claude 提示词缓存和 Gemini 上下文缓存。
无论采用哪种方式,关键条件都一样:后一次请求必须保留可复用的相同前缀。 最低长度、有效期、隔离范围和计费规则由服务商决定,并且会变化。
2.3 缓存能省什么,不能省什么
前缀缓存不是“回答缓存”。命中后,模型仍会读取新增内容并重新生成回答,所以相同请求仍可能得到不同输出。
| 问题 | 缓存能否改善 | 原因 |
|---|---|---|
| 重复前缀的输入计算 | 能 | 命中部分不必完整重算 |
| 首个输出等待时间 | 通常能 | 跳过一部分输入处理 |
| 重复输入费用 | 可能降低 | 许多服务对缓存输入采用不同计费,但不是免费 |
| 新增输入和输出费用 | 不能 | 新内容仍要处理,回答仍要生成 |
| 上下文窗口上限 | 不能 | 缓存内容通常仍计入窗口 |
| 无关内容干扰模型 | 不能 | 被缓存的无关信息仍然会被模型看到 |
| 错误历史传播 | 不能 | 缓存只复用计算,不会判断内容是否正确 |
显式缓存还可能保存一段时间的数据。涉及私有代码或零数据保留要求时,要同时检查服务商的数据保留规则,不能只看价格。
2.4 提高缓存命中率的七种常规做法
核心原则很简单:稳定内容放前面,变化内容放后面。
text
稳定前缀
系统要求
项目指令
工具定义
已确认且不再改写的历史
动态尾部
当前任务状态
最新工具结果
用户刚补充的要求具体可以按下面七项检查。
1. 固定稳定内容的顺序
系统要求、项目指令和工具定义即使文字完全相同,顺序变化也会产生不同前缀。工具注册表、JSON schema 字段和项目指令的拼接顺序都应保持确定。
2. 把动态状态放在末尾
时间、当前分支、剩余预算、最新任务状态会频繁变化。把它们放在前缀中间,每次更新都会让后面的内容失去复用机会。
3. 不要把随机值写进稳定前缀
请求 ID、精确到秒的时间戳、随机 nonce 和每轮变化的调试信息会直接改变前缀。它们应进入请求元数据、日志或动态尾部。
4. 追加历史,不要重写已经发送的消息
新一轮通常只需在末尾追加 assistant、tool 和 user 消息。为了润色而改写早期消息,或者每轮重新生成不同格式的摘要,都会提前截断可复用前缀。
5. 同一会话内少改工具清单和核心配置
增加、删除或重新排序工具,切换模型、provider、系统提示词版本,都可能创建新的缓存边界。确需变更时,应按轮次或会话统一切换,不要在一次工具批次中反复变化。
6. 不要过于频繁地压缩历史
压缩会用摘要替换旧历史,旧前缀从替换位置开始失效。应该在接近预算边界时做一次完整压缩,而不是每轮都改写摘要。第 13 章会说明怎样选择安全切点。
7. 使用服务商提供的缓存控制能力
如果 API 提供 cache key、缓存边界、显式缓存对象或有效期选项,应根据业务重复模式使用。不要自行猜测服务端一定会把两次相似请求路由到同一份缓存。
2.5 用数据判断缓存是否真的有效
只看“请求内容好像相同”没有意义。至少记录下面五项:
| 指标 | 用来判断什么 |
|---|---|
| 总输入 token | 上下文本身是否还在无节制增长 |
| 缓存命中 token | 有多少输入真正复用了缓存 |
| 首个输出耗时 | 缓存是否改善用户等待时间 |
| 每轮费用 | 命中是否真的带来降本 |
| 前缀版本 | 哪次提示词、工具或配置变更打断了缓存 |
一个基础指标是:
text
缓存输入占比 = 缓存命中 token / 总输入 token但占比高不代表设计已经合理。一次请求携带十万 token,即使命中九万,剩余成本和信息干扰仍可能过高。正确顺序是:先删除不需要的上下文,再提高剩余稳定内容的命中率。
评估改动时,用同一组任务对比修改前后的总输入、缓存输入、首个输出耗时、任务成功率和每个成功任务的费用。只比较一次请求容易被网络和服务端调度波动误导。
2.6 四个 Agent 实际做了什么
截至审阅时,四个实现都不会在本地重算模型的 KV Cache。它们共同做的是两件事:尽量让请求前缀保持稳定,并记录模型服务返回的缓存读取量;服务端提供缓存写入量时,也一并记录。相同部分不再逐家重复,下面只列差异:
| 项目 | 与缓存直接相关的实现 | 取舍 |
|---|---|---|
| codex | 请求可以携带会话级缓存键,并分别统计缓存读取与写入 token;回归测试会检查连续请求的基础说明和工具顺序一致 | 与 OpenAI Responses 接口结合较深;工具配置、项目指令或压缩历史变化后,仍会形成新前缀 |
| grok-build | 统计缓存读取与写入;部分复用主会话历史的辅助调用会沿用主会话缓存键,并直接复用原历史 | 对不同后端是否转发缓存键做了兼容判断;会话回顾等旁路请求要专门保持原前缀 |
| dsh | 系统说明和工具按确定顺序装配;计划模式保留原工具清单,只增加规则;模型适配层把缓存命中量交给统一 token 统计 | 插件化装配很灵活,但切换 preset、插件或作用域会改变请求内容 |
| pi | 针对多种模型接口处理缓存键、缓存边界和有效期,统一统计缓存读取与写入,界面还会显示命中率和异常丢失的缓存成本 | 兼容面最广,相应地要维护更多服务商差异和降级路径 |
这些做法能提高复用机会并让问题可观测,但不能保证服务端一定命中。最终仍要以 provider 返回的 usage、延迟和费用为准。
本章小结
- Agent 多轮运行会反复发送相同前缀,缓存可以减少这部分输入计算、等待和费用。
- KV Cache 主要服务一次生成,前缀缓存复用多次请求的共同开头;两者不是回答缓存。
- 稳定内容放前面、动态内容放后面,并保持顺序、序列化和工具清单稳定。
- 缓存不能扩大窗口、删除无关信息或保证答案正确;先减少上下文,再优化命中率。
- 是否有效只看真实 usage、首个输出耗时和每个成功任务的费用。
想继续了解模型内部原理,可以阅读下一篇选读章;按工程主线阅读则直接进入第 3 章,用一次 API 调用和 while 循环做出最小 Agent。