Codex 工程实践(二):上下文如何压缩与拼接

Coding Agent 一轮又一轮地读文件、运行工具,上下文很快会超过一开始的任务描述。真正难的地方是:空间变少以后,哪些信息还能支撑下一步决策;压缩完成以后,哪些规则需要重新组装进去。

《Codex 源码分析》第 6、7 章分别从核心循环和 Prompt 组装解释了这两个问题。把它们连起来看,上下文管理包含两项工作:维护可继续执行的历史,以及为下一次模型请求准备正确的材料。

本系列日期按博客时间轴编排,资料统一于 2026-11-05 核对。参考章节基于 f31e90b79f594c606746626a56bdf1e49497b275;本文另核对 Codex 上游 86ac0916597244a2e67a197822a5483924660ff1 的压缩实现。示例算法用于说明设计,不是源码的逐行复刻。

一、上下文由哪些东西组成

把上下文理解成聊天记录,容易忽略真正占空间、也真正影响执行的部分。

内容作用管理方式
基础指令与运行约束定义行为、执行条件从可信配置和运行状态构造
项目规则与能力目录说明仓库约定、可用能力按路径发现,控制体积
当前任务和最新纠正决定这次要做什么保留明确目标与变化
工具调用及结果提供执行过程和证据保持关联,减少低价值输出
历史总结与近期交互支持跨阶段继续工作压缩旧内容,保留近期细节

这里还存在一种重要差别:有些内容可以从文件和运行状态重新取得;有些内容只存在于本次交互中,例如用户刚刚撤销了一个方案。后者在压缩时更容易丢失,也更需要被明确保存。

二、为什么压缩会出现在回合中途

第 6 章展示的核心循环是分层的:会话接收操作,任务驱动一个回合,一个回合里又会多次请求模型与执行工具。

1
2
3
4
5
6
接收用户任务
→ 准备本轮上下文
→ 请求模型
→ 执行工具并追加结果
→ 判断是否需要继续
→ 必要时压缩历史,再请求模型

所以,“一次用户输入”并不等于“一次模型调用”。代理在执行一个复杂任务时,可能已经读入数十份材料;即使用户没有再发消息,也可能需要在回合中途压缩。

容量预算也不能只看当前文本。下一次请求还需要容纳新工具结果、补入的上下文和输出。可以用下面这个设计关系理解:

1
当前有效历史 + 待拼接内容 + 预留输出空间 ≤ 可用上下文预算

这只是预算模型。实际计量方式和触发点受运行版本、模型元信息与实现路径影响,不宜把某个教程里的固定阈值当作所有模型的标准。

三、压缩的对象是历史表示

我会把“压缩”分成三种处理,避免混淆。

输出裁剪处理体积。例如把很长的日志保留首尾,注明中段省略,并留下原始文件路径。这会丢掉内容,适合处理暂时不需要逐行阅读的输出。

语义摘要处理信息密度。它把多个步骤转成当前状态:已经尝试什么、验证了什么、哪些方案已废弃、接下来做什么。摘要同样可能丢信息,也可能写错。

历史替换则让后续请求使用新的历史表示。它必须保持消息结构和上下文边界,不能只把一段摘要随意追加到原始历史末尾。

在本次核对的上游 compact.rs中,可以看到摘要构造、新历史生成、初始上下文组装与 replace_compacted_history 等步骤。实现还处理了待接续输入和上下文插入位置。这些细节说明,压缩不仅是让模型“总结一下”,还需要由运行时安装一份可继续使用的历史。

不同版本及本地、远程压缩路径可能采用不同表示;不能假定所有压缩结果都是用户可读的一段短文。

四、一份有用的摘要要保留什么

任务摘要应该优先支持恢复执行。下面是我建议的结构:

1
2
3
4
5
6
7
目标:修复跨时区日期排序,公共 API 保持兼容。
当前状态:修改了排序函数,尚未提交。
已验证:新增边界用例通过;完整构建尚未运行。
用户纠正:不要引入新的日期库。
已排除方案:按显示字符串排序会在跨月时出错。
证据入口:测试文件路径、失败用例名称、修改文件。
下一步:运行完整构建,检查 diff,再整理交付说明。

这比“已经分析并修复了日期问题”更有用,因为它保留了约束、验证范围和未完成动作。

一个常见压缩错误是把状态写得过于肯定。“局部测试通过”被压成“测试通过”,后续代理可能跳过完整验证。另一个错误是把废弃方案留成当前决定,导致任务绕回原路。

因此,摘要中的事实应带上证据位置和状态;推测与未验证内容继续保持未验证。完整原始材料可以留在外部文件,摘要只保留检索入口。重新读取时还要检查文件是否被改过,路径本身不代表内容未变化。

五、拼接时保留角色、来源与顺序

第 7 章最有帮助的地方,是把 Prompt 展示为不同来源片段的组装结果。

参考版本中,基础指令可以来自显式配置、历史保存的信息或模型默认指令;Skill 的可用目录进入 developer 片段,被选中的 Skill 正文则包装为带名称和路径的 user 片段。这些是参考版本的具体实现,不是所有代理系统必须采用的角色分配。

可以把组装理解为:

1
2
3
4
5
6
7
8
模型请求
├─ 基础指令
├─ 当前运行约束与能力说明
└─ 输入项序列
├─ 项目上下文及选中的 Skill
├─ 压缩后保留的历史与近期交互
├─ 关联完整的工具调用和结果
└─ 当前用户任务及后续纠正

这是一张组成示意,真实序列需要遵循实现中的插入规则。核心原则是:拼接不能抹掉来源差异。用户授权、项目约定、工具返回的网页内容,各自承担不同角色。外部材料即使写着“忽略之前的要求”,也不应因此升级为运行策略。

工具调用与结果也需要保留标识关联。裁剪时留下结果却丢掉调用,或保留调用却丢掉结果,都可能让模型误解执行状态。恢复中断历史时,应明确记录未知结果,而不能凭一条调用推断操作已经成功。

六、Skill 为什么先给目录,再读取正文

安装的 Skill 越多,把全部正文放进每次请求的成本越高。参考第 7 章采用的思路是渐进加载:先展示名称、描述和路径,让代理知道有什么能力;命中任务需要的 Skill 后,再加载具体步骤。

在目录预算不足时,参考实现还区分三种情况:完整描述可以放下就保留;最小目录行可以放下时缩减描述;连最小行也放不下时只能保留部分目录并报告预算问题。

这里有两个实践启发。

一是目录应该便于选择。简短、能区分用途的描述,比把整份操作手册复制进描述更合适。二是“目录里可见”和“本轮已加载正文”必须分别观察,否则很容易把未命中、名称冲突或预算裁剪误判为模型不会使用工具。

Skill 的说明文本也不等于权限。它要求运行某个程序,仍需由工具系统按当前权限执行。

七、怎样检查压缩与拼接是否有效

如果在自己的 Harness 中实现这套机制,可以用下面的流程做设计参照:

1
2
3
4
5
6
7
统计历史、待注入片段与预留空间
→ 先减少低价值工具输出
→ 需要时生成历史摘要
→ 检查目标、约束、证据和未完成状态
→ 安装替换历史
→ 重新组装当前运行上下文
→ 检查调用与结果关联,发起下一次请求

检验重点不只是 token 降了多少。更关键的是:用户最新纠正是否保留,未完成动作是否仍能识别,摘要中的证据能否定位,下一步是否重复已经做过的副作用操作。

例如,在“修改文件完成、测试尚未运行”时触发一次压缩,再继续任务,观察代理是否会先运行测试,是否还记得禁止新增依赖。这类固定恢复任务,比只看摘要读起来是否流畅,更能判断压缩是否支持实际工作。

上下文越长,越需要把任务状态、来源和证据组织清楚。压缩负责让历史保持可用,拼接负责让当前规则与材料正确进入下一次请求;两者的交接处,往往就是长任务能否稳定继续的关键。

参考阅读

上一篇:个人与企业如何减少使用漂移。下一篇:工具生命周期、权限控制与沙箱。