Skip to content

第 2 章 上下文成本与缓存 ​

本章要解决的问题

Agent 每调用一次模型,都要再次提供系统要求、工具定义和必要历史。任务越长,重复输入越多,等待时间和费用也会累积。

本章集中回答三个问题:

  • 上下文的时间和费用花在哪里;
  • 缓存怎样复用相同前缀,能省什么、不能省什么;
  • 怎样组织请求,提高缓存命中率。

上下文该放什么、太长以后怎样剪裁和压缩,分别在第 6、12、13 章展开。

2.1 Agent 为什么会反复支付上下文成本 ​

第 1 章已经说明,模型不会自动记住上一轮。为了让它继续任务,程序需要重新提供必要历史:

text
第 1 次输入 = 系统要求 + 用户问题
第 2 次输入 = 第 1 次输入 + 读文件调用 + 文件内容
第 3 次输入 = 第 2 次输入 + 测试调用 + 测试输出
……

工具循环中上下文怎样逐步增长

模型处理一次请求通常分成两段:

阶段在做什么对 Agent 的直接影响
处理输入读取这次请求中的全部上下文,为生成做准备输入越长,首个输出通常等得越久
生成输出逐个产生新 token输出越长,等待和输出费用越高

多数模型服务会分别统计输入、缓存输入和输出 token;推理模型还可能单独统计推理消耗。具体字段与价格会变化,实现时应读取 API 返回的 usage,不要在程序里写死假设。

长上下文带来的问题不只有费用:

  1. 首个输出更慢: 模型要先处理输入,才开始生成。
  2. 总费用更高: Agent 往往把相同前缀重复发送很多轮。
  3. 重点更难突出: 窗口装得下,不等于所有内容都值得放进去。

缓存主要解决前两项。第三项仍要靠检索、分层加载、剪裁和压缩。

2.2 先分清两种缓存 ​

“KV Cache”和“前缀缓存”经常被混在一起,但它们解决的范围不同。

一次请求内的 KV Cache ​

模型逐个生成 token 时,前文不会变化。KV Cache 保存已经处理过的前文计算结果,避免每生成一个 token 都从头计算整段输入。

这主要是模型服务的推理实现。调用 API 的应用通常不直接管理它,但上下文越长,KV Cache 占用的显存也越多。

多次请求之间的前缀缓存 ​

Agent 连续多轮请求时,开头的大段内容往往相同。模型服务可以保存这段前缀的计算结果,下一轮命中后直接复用:

提示词缓存:相同前缀有机会复用已有计算

常见产品形态有两类:

无论采用哪种方式,关键条件都一样:后一次请求必须保留可复用的相同前缀。 最低长度、有效期、隔离范围和计费规则由服务商决定,并且会变化。

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、延迟和费用为准。

本章小结 ​

  1. Agent 多轮运行会反复发送相同前缀,缓存可以减少这部分输入计算、等待和费用。
  2. KV Cache 主要服务一次生成,前缀缓存复用多次请求的共同开头;两者不是回答缓存。
  3. 稳定内容放前面、动态内容放后面,并保持顺序、序列化和工具清单稳定。
  4. 缓存不能扩大窗口、删除无关信息或保证答案正确;先减少上下文,再优化命中率。
  5. 是否有效只看真实 usage、首个输出耗时和每个成功任务的费用。

想继续了解模型内部原理,可以阅读下一篇选读章;按工程主线阅读则直接进入第 3 章,用一次 API 调用和 while 循环做出最小 Agent。

基于 MIT 协议发布,欢迎转载与贡献。