从零构建 Coding Agent
English

09. 上下文工程与压缩

上下文窗口再大也会被 Agent 用完。Coding Agent 会读文件、跑命令、输出 diff、遇到错误、接收用户插话。真正的问题不是“如何塞更多 token”,而是“如何保留继续工作的必要状态”。压缩是把旧上下文投影成可继续执行的摘要,而不是把聊天记录变短。

什么时候压缩

压缩触发应该基于模型窗口和预留预算。假设模型窗口是 200k token,你不能等到 199k 才压缩,因为下一次请求还需要输出、工具参数和安全余量。一个简单策略是:

if used_tokens > context_window - reserved_output - safety_margin:
  compact()

预留预算要保守。Coding Agent 的下一次响应可能包含长工具调用参数或解释。压缩太晚会导致 provider 直接拒绝请求,Agent 没机会自己整理状态。

压缩切点

不要在任意消息中间切。安全切点通常是一个完整 turn 之后,也就是 assistant 消息和它请求的所有 tool result 都已经落日志。切断半个 tool batch 会让模型看到“我调用了工具,但结果消失了”,恢复后很容易重复执行或误判。

压缩 entry 应记录:

  • summary 文本。
  • 压缩前 token 数。
  • 第一条仍完整保留的 entry id。
  • 读过的关键文件。
  • 改过的文件。
  • 未完成任务和用户约束。

这些 details 不一定都进模型,但应该进日志,方便 UI 和扩展使用。

好摘要的结构

面向 Agent 的摘要不是会议纪要。它应该帮助模型继续工作。建议包含:

Goal:
- User wants ...

Current status:
- Done ...
- Still failing ...

Important constraints:
- Do not edit tests.
- Keep public API unchanged.

Files observed:
- src/parser.ts: contains ...

Files modified:
- src/parser.ts: changed ...

Open tool results:
- Last test run failed with ...

Next step:
- Inspect ...

摘要里最重要的是约束、文件事实和下一步。不要让模型在压缩后重新发现所有东西。

压缩后的失忆测试

每个压缩实现都应该做一个失忆测试:构造长会话,让 Agent 读一个文件、发现约束、修改另一个文件,然后触发压缩。压缩后问模型“下一步是什么”。如果模型忘记用户约束或刚刚修改的文件,说明摘要不可用。

这个测试不需要真实模型。你可以让 faux provider 检查压缩后 context 是否包含这些关键词:目标、约束、已修改文件、最近失败、下一步。真实模型 smoke test 只用来检查摘要是否自然可读。

上下文不是越多越好

很多新手会倾向于保留尽可能多的旧消息。这样看似安全,实际上会让模型注意力被历史噪声稀释。尤其是工具输出:一次失败测试可能有几千行日志,其中真正有用的是失败名称、错误行、堆栈尾部和命令退出码。

上下文工程的目标是高信噪比。对 Agent 来说,好的压缩不是“无损”,而是“保留继续完成任务所需的状态,并明确哪些信息被丢弃”。当摘要无法覆盖某个旧事实时,它应该告诉模型重新读取文件,而不是假装记得。

生产化取舍

压缩本身也会调用模型,因此它会失败、花钱、被限流。运行时要决定压缩失败时怎么办。常见策略是:

  • 如果还有空间,继续一轮并稍后再压缩。
  • 如果已经接近上限,暂停任务并要求用户确认。
  • 如果压缩模型失败,降级到较短的本地摘要模板,但标记质量较低。

压缩摘要应该写入日志。不要只存在内存里。恢复会话时,context builder 必须能看到压缩 entry,并据此跳过更早消息。

练习

给 session log 增加 compaction entry 和 context builder。

验收标准:

  • 压缩只发生在完整 turn 边界。
  • 压缩 entry 包含 summary 和 firstKeptEntryId。
  • 构建上下文时,旧消息被 summary 替代,不从日志删除。
  • 压缩后模型仍能看到目标、约束、已读文件、已改文件和下一步。
  • 当 provider 报上下文超长时,Agent 能触发压缩流程,而不是直接退出。