上下文窗口经济学与缓存

专栏:Agent 工程 · 第 6 / 18 篇
Agent上下文工程KV Cache

从这篇起进入专栏的重头戏:上下文工程。先建立两条硬约束,再看两个系统如何围绕它们做设计。

:::info 学习目标 完成本篇后你能够:计算一个 agent 任务的理论成本曲线;说出四条缓存友好的上下文设计纪律;给现有 agent 做一次”前缀稳定性”体检。 前置:第 4 篇的 mini-agent 可用。预计时长:45 分钟。 :::

:::note 本章术语速查(新手建议先读)

  • 上下文经济学:把上下文窗口当”预算”管理——每一步花多少、还能花多少、怎么省着花。
  • KV Cache(键值缓存):模型服务方缓存的”阅读进度”。你发的前缀如果和上次一模一样,这部分不用重算——省钱又快。
  • 前缀(prefix):请求开头的公共部分。前缀必须逐字节一致才能命中缓存,改一个字就全失效。
  • cache_control:Anthropic API 的标记,告诉服务方”从这个位置之前都可以缓存”。
  • 截断协议:工具结果太长时”留头留尾、中间省略”的约定,让模型知道被省略了还能续读。 :::

两条硬约束

约束一:上下文是按次全量重发的。 agent 每步都带着全部历史请求模型,第 N 步的成本 ∝ 前缀长度。历史只增不减,成本单调上涨。

约束二:服务端有 KV cache,但前缀必须逐字节稳定。 模型服务方会缓存注意力的中间结果(KV 张量)——前缀相同的请求直接复用,命中部分成本降 50~90%、延迟显著下降官方文档)。但缓存按前缀精确匹配:改一个字符,从此处往后全部失效、按全价重算。

图表(context-economics-and-caching.md)

从这两条约束推出的设计纪律,就是所谓「上下文工程」的一半:

  1. system 提示词逐字节固定:不放时间戳、不放随机 ID、不放会话可变状态;
  2. 消息只追加,不改写
  3. 动态内容追加在历史尾部,而不是插进 system;
  4. 工具 schema 与顺序固定——工具列表也进前缀,顺序抖动 = 缓存失效。

上下文预算分配策略

把窗口当成一张预算表来分配(以 128K 窗口的 coding agent 为例):

图表(context-economics-and-caching.md)

预算表的意义:每一项超支都挤占其他项。两个系统各有一个”预算可视化”机制——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 塞当前时间 / 改写历史消息 → 缓存全失

本篇产出(带验收标准)

  1. 成本体检:给 chat() 加记录——每次请求前算 sum(len(json.dumps(m)) for m in messages),跑一个 10 步任务画出”步数 vs 前缀大小”曲线。验收:能说出第 10 步的请求体是第 1 步的几倍;
  2. 前缀体检:找 system 里”每次运行会变”的内容(时间、随机 ID)并移到消息尾部。验收:连续两次运行,system 与首条消息逐字节一致;
  3. (可选)用 Anthropic key 在 system 上打 cache_control,对比连续五步的计费 token,观察缓存命中的价格曲线;
  4. packages/core/system-prompt/src/index.tsassemble()codex-rs/history/src/lib.rsTokenUsageRecord,体会”确定性”与”可审计”如何互相成就。

上下文有涨就有跌——下一篇讲硬币的另一面:记忆与压缩。

← 返回文章列表