上下文窗口经济学与缓存
专栏:Agent 工程 · 第 6 / 18 篇从这篇起进入专栏的重头戏:上下文工程。先建立两条硬约束,再看两个系统如何围绕它们做设计。
:::info 学习目标 完成本篇后你能够:计算一个 agent 任务的理论成本曲线;说出四条缓存友好的上下文设计纪律;给现有 agent 做一次”前缀稳定性”体检。 前置:第 4 篇的 mini-agent 可用。预计时长:45 分钟。 :::
:::note 本章术语速查(新手建议先读)
- 上下文经济学:把上下文窗口当”预算”管理——每一步花多少、还能花多少、怎么省着花。
- KV Cache(键值缓存):模型服务方缓存的”阅读进度”。你发的前缀如果和上次一模一样,这部分不用重算——省钱又快。
- 前缀(prefix):请求开头的公共部分。前缀必须逐字节一致才能命中缓存,改一个字就全失效。
- cache_control:Anthropic API 的标记,告诉服务方”从这个位置之前都可以缓存”。
- 截断协议:工具结果太长时”留头留尾、中间省略”的约定,让模型知道被省略了还能续读。 :::
两条硬约束
约束一:上下文是按次全量重发的。 agent 每步都带着全部历史请求模型,第 N 步的成本 ∝ 前缀长度。历史只增不减,成本单调上涨。
约束二:服务端有 KV cache,但前缀必须逐字节稳定。 模型服务方会缓存注意力的中间结果(KV 张量)——前缀相同的请求直接复用,命中部分成本降 50~90%、延迟显著下降(官方文档)。但缓存按前缀精确匹配:改一个字符,从此处往后全部失效、按全价重算。
从这两条约束推出的设计纪律,就是所谓「上下文工程」的一半:
- system 提示词逐字节固定:不放时间戳、不放随机 ID、不放会话可变状态;
- 消息只追加,不改写;
- 动态内容追加在历史尾部,而不是插进 system;
- 工具 schema 与顺序固定——工具列表也进前缀,顺序抖动 = 缓存失效。
上下文预算分配策略
把窗口当成一张预算表来分配(以 128K 窗口的 coding agent 为例):
预算表的意义:每一项超支都挤占其他项。两个系统各有一个”预算可视化”机制——Codex 给模型 get_context_remaining 工具让它自己看余量;dsh 的压缩(第 7 篇)由 token 状态驱动。你自己的 mini-agent 至少要在每步打印当前前缀大小。
dsh 的实现:把确定性做成机制
第 5 篇讲过 dsh 提示词的稀疏 order 确定性排序——现在你能看清它的动机:注册顺序不影响最终文本,是为了前缀逐字节稳定。dsh 把这条约束做成了三道机制:
- request/header 落日志:每次请求的 system 文本与工具 schema 都作为事件写入会话日志,任何一次请求可精确重建,前缀一致即可复用缓存;
- 动态上下文走消息尾部:runtime 上下文快照(时间、工作区状态)不进 system,只在发生变化时作为一条带来源的
user/message追加到历史末尾——前缀不动,只长尾巴; - 运行时不变量:每次发请求前,把实际请求与
session.deriveMessages()逐 JSON 比较——“请求是日志的纯函数”,改写历史这种破坏缓存的操作在架构上就不可能发生。
Codex 的实现:预算可见 + 压缩兜底
Codex 的做法侧重让预算变得可操作:
- 每次轮次记录
TokenUsageRecord进 rollout 日志,用量可审计; - 内建工具
get_context_remaining——模型自己能看到还剩多少上下文预算,学会在长任务里主动收敛; - 超预算时触发压缩(下一篇详讲),甚至提供 token 预算型压缩:跳过模型总结、直接安装新上下文窗口。
一个值得玩味的细节:Codex 的 capture_step_context 在每步开始时一次性捕获工具清单与权限视图,保证一次请求内的视图一致——视图漂移同样是前缀稳定性的敌人。
框架对照:LangGraph 里的缓存友好设计
LangGraph 不管缓存(provider 侧行为),但状态设计决定命中率:
# ✓ 好:system 常量 + 消息追加
SYSTEM = SystemMessage(content=[{
"type": "text",
"text": "你是代码助手……", # 固定文本
"cache_control": {"type": "ephemeral"}, # Anthropic 缓存断点
}])
state["messages"].append(new_msg) # 只追加
# ✗ 坏:每轮往 system 塞当前时间 / 改写历史消息 → 缓存全失
本篇产出(带验收标准)
- 成本体检:给
chat()加记录——每次请求前算sum(len(json.dumps(m)) for m in messages),跑一个 10 步任务画出”步数 vs 前缀大小”曲线。验收:能说出第 10 步的请求体是第 1 步的几倍; - 前缀体检:找 system 里”每次运行会变”的内容(时间、随机 ID)并移到消息尾部。验收:连续两次运行,system 与首条消息逐字节一致;
- (可选)用 Anthropic key 在 system 上打
cache_control,对比连续五步的计费 token,观察缓存命中的价格曲线; - 读
packages/core/system-prompt/src/index.ts的assemble()与codex-rs/history/src/lib.rs的TokenUsageRecord,体会”确定性”与”可审计”如何互相成就。
上下文有涨就有跌——下一篇讲硬币的另一面:记忆与压缩。