Codex Context Compaction 全过程深度解析:Agent 压缩后为何不失效仍能执行任务核心原因
*图:长程 Agent 真正需要保住的是现场碎片,而不只是文章主线。*
窗口隐藏了,不代表窗口不重要
Codex App 曾经把上下文窗口的使用比例放在主界面上。后来这个指示器被挪到了二级入口,只能通过/status 或 /compact 之类的命令查看。这个改动让不少重度用户很不爽。
原因很好理解。对短问答来说,上下文窗口只是一个技术参数;对长程编码来说,它更像油表。你不一定每秒盯着它,但你需要知道车还能跑多远。
Codex 团队当时给出的方向是 “vibe contexting”:用户不再需要主动管理上下文。
这句话如果放在一年前,很像产品经理的漂亮话。因为压缩一直是 Agent 链路里最脆弱的地方。它不是把文本缩短那么简单,而是在决定哪些事实还能活到下一轮,哪些事实永远丢掉。
Codex 敢把窗口感知降级,真正依赖的是下面几层机制:入口截流、远程压缩、系统脚手架重建,以及服务端和模型的配合。
真正进入压缩之前,很多内容已经被处理掉了
很多人以为上下文管理就是“满了以后压一下”。Codex 不是这么干的。 在完整压缩之前,它已经先把一批容易撑爆窗口的内容压住了:| 位置 | Codex 怎么处理 | 这意味着什么 |
|---|---|---|
| 工具输出 | 工具结果进入历史时就做中间截断,保留头尾 | 中间细节可能直接消失 |
| 终端输出 | PTY 原始输出只留头尾,总量有限 | 大日志不会完整进上下文 |
| Hook 结果 | 太大的 hook 输出写到临时文件,只给预览和路径 | 模型需要时可以再读,但不会默认吃满窗口 |
| 历史消息 | 清理不成对的工具调用/输出、孤儿输出和不支持的图片 | 保证请求结构干净 |
| 系统上下文 | 只追加变化部分,不每轮重复塞满配置 | 降低固定上下文的增长速度 |
本地压缩:一份交接文档能救简单任务,救不了复杂现场
Codex 压缩的第一条路叫 本地压缩。它的做法并不神秘:让当前模型给后续模型写一份 handoff summary。 流程大概是这样:
*图:本地压缩以明文摘要替换旧历史*
这套方案非常像人类工程师写交接文档。写得好,后面的人能接上;写漏了,后面的人只能猜。
本地压缩 最大的问题就在这里:压缩后的替换历史里,很多原始材料不会再出现。助手的中间判断、工具调用的原始结果、推理过程、网络结果,大多只能靠摘要间接留下来。
简单任务还能扛住。比如开一个 GitHub issue、补一条评论、确认几个标签,摘要抓住主线就行。
复杂任务不一样。长程编码里真正致命的常常是小事实:
- 当前在哪个分支;
- 哪个 stash 是临时绕过签名用的;
- 哪个测试 315/315 通过;
- 哪个构建失败是 x86_64 slice 缺失;
- 用户最后是否已经验收。
这些东西如果没被摘要写进去,下一轮模型不会“自然想起来”。它只能重新查,或者老老实实说不知道。
远程压缩:客户端看不懂,但效果明显不一样
第二条路是 远程压缩。这里开始变得有意思。 Codex 会根据 provider 判断能不能走远程压缩。OpenAI 原生 provider 和 Azure Responses provider 支持;常见第三方 provider、中转站、本地模型入口默认不支持。分流逻辑大致是:
*图:不同 provider 下的远程压缩路径*
远程 V1 走专用 /v1/responses/compact 端点。远程 V2 则把压缩触发器塞进普通 Responses 流:输入末尾多一个 CompactionTrigger,服务端返回一条特殊的压缩项。
两条远程路径最后都会产生类似的东西:encrypted_content。
客户端不会拿到一段可读摘要。它拿到的是一块 opaque state,一段加密内容。后续请求继续带上它,由服务端解密并组装给模型。
*图:encrypted_content 在服务端完成状态迁移*
这也是 远程压缩 和 本地压缩 的本质区别:本地留给客户端一份“文本交接”;远程留给服务端一份“状态块”。
*图:远程压缩的关键变化,是把压缩从文本摘要推进到服务端状态承载。*
实验最刺眼的地方:差距不是 5 分,而是一个等级
如果只看机制,很容易陷入猜测。真正有说服力的是同一批长程任务分别走本地、远程 V1、远程 V2 后的恢复效果。 一类简单任务里,远程比本地稳定一些,但差距还不算夸张:| 测试内容 | 本地 | 远程 |
|---|---|---|
| 主线和决策链恢复 | 89.7 | 96.6 |
| 反事实判断 | 88 | 100 |
| 低层事实定位 | 84.0 | 92.3 |
| 禁用工具后的续跑 | 86 | 92 |
| 允许工具后的最小验证 | 88 | 95 |
| 平均 | 87.1 | 95.2 |
| 压缩路径 | 客观评分 | 关键事实命中 |
|---|---|---|
| 本地压缩 | 约 28/100 | 5/24 |
| 远程 V1 | 约 94/100 | 23/24 |
| 远程 V2 | 约 92/100 | 23/24 |
服务端黑盒:提示词之外还有多少操作空间
有人用 prompt injection 研究过 远程压缩 的服务端流程。能看到的大致是这样:服务端有一个压缩器 LLM,它读入系统提示和会话历史,写出一份明文摘要;随后服务端把这份摘要加密成 blob。下一轮请求时,服务端再解密 blob,拼上交接提示,交给模型继续执行。 麻烦也在这里。泄露出来的压缩提示,和 本地压缩 开源出来的那段提示词非常接近。既然提示词差不多,为什么复杂任务里一个接近失忆,一个还能恢复大部分现场? 更合理的理解是,把 prompt injection 看到的东西理解成“表层文本”,不是完整机制。服务端真正能动手脚的地方,比那几行 prompt 多得多。
*图:远程压缩可能包含隐藏信号和上下文重组*
第一种可能,是服务端给压缩器的输入里夹了额外信号。比如工具调用的结构化摘要、关键实体列表、对话的元数据标注。它们不一定会以自然语言形式出现在 prompt 里,所以即使用注入方式套出了提示词,也看不到这些中间信号。
第二种可能,是加密 blob 里不只有一段普通摘要。表面实验看到的是交接文本,但服务端在加密前完全可能追加后处理结果:工具输出摘要、关键决策节点、甚至某种更适合恢复任务状态的结构化表示。客户端只拿到 encrypted_content,没法判断里面到底装了什么。
第三种可能,是远程的压缩器本来就不是用户当前正在用的模型。本地压缩 只能调用当前配置模型来写交接文档;远程压缩 的压缩器由服务端选择,它可能针对压缩任务做过优化,更擅长从一堆工具调用和对话碎片里抓住任务状态。这个猜测没法从客户端证实,但能解释为什么同一套提示词会拉开这么大差距。
第四种可能,发生在解密之后。能看到摘要被放在交接提示后面,但服务端组装最终上下文时是否还有检索、重排、裁剪或额外注入,外部很难观察。换句话说,压缩不是一次“模型写摘要”就结束了,后面还有上下文组装这一步。
所以 远程压缩 的强点未必是“提示词更好”。更可能是服务端掌握了从压缩、加密、保存、解密到重新组装上下文的整条链路。客户端只能看到加密 blob 进来、模型状态恢复出去,中间发生了什么基本不可见。
这也是它最让人不踏实的地方:效果确实强,但不可审计。你不知道它保住了哪些事实,也不知道哪些事实已经在黑盒里丢了。
只靠压缩还不够,系统规则必须重新搭起来
长程 Agent 压缩后容易出问题,还有一个原因:它不只会忘任务,也会忘规则。 比如项目里的 AGENTS.md 怎么要求,当前沙箱策略是什么,哪些工具可用,哪些技能已安装,工作目录和 git 状态是什么。这些东西如果也靠摘要传递,压几次以后迟早变形。 Codex 的做法是:系统约束不交给摘要。 压缩完成后,Codex 会清掉一个基线标记。下一轮开始时,它发现基线不存在,就重新构造初始上下文。这个脚手架会把开发规则、权限策略、可用工具、MCP、插件、环境信息、协作模式等重新注入。
*图:任务状态与系统脚手架在下一轮重新合流*
这点很关键。摘要只需要记住“任务发生了什么”。至于“当前系统要求模型怎么工作”,下一轮重新搭。
换句话说,Codex 的恢复不是一条腿走路:
- 压缩负责迁移任务状态;
- prompt scaffold 负责恢复执行环境和规则。
很多人只盯着摘要,就会低估第二层机制的作用。
Claude Code 的思路不同:它更像在维护一张视图
拿 Claude Code 对比,会更容易看出 Codex 的设计取舍。 Codex 像 checkpoint/rebase:压缩后生成一段替换历史,把旧历史换成新基线。 Claude Code 更像数据库视图:原始记录尽量追加写入,模型看到的是预算、外置、折叠、缓存编辑之后的投影视图。| 维度 | Codex | Claude Code |
|---|---|---|
| 大工具输出 | 入口截断,简单直接 | 外置到 sidecar 文件,保留读取路径 |
| 完整压缩前的减压 | 主要靠入口控制和历史清洗 | 工具预算、精简、微压缩、上下文折叠多层处理 |
| 压缩提示 | 极简交接 | 100+ 行审计清单式提示 |
| 服务端协作方式 | 返回 opaque encrypted state | cache_edits、context_management 等声明式能力 |
| 客户端透明度 | 低,服务端空间大 | 高一些,但客户端逻辑复杂 |
Prompt Cache 是这套设计里最现实的约束
为什么 Codex 不在历史中间做细粒度修改?为什么 Claude Code 那么在意外置决策“冻结”?绕不开 prompt cache。 长程会话里,单次请求十几万 token 并不稀奇。上一轮已经出现过的前缀,如果这一轮逐 token 一致,就可以命中缓存。命中的 token 便宜得多,也快得多。 但缓存很脆。你改了历史中间某个旧工具结果,从那里往后的 token 都可能失去命中。 所以两个系统都在避免“中间切一刀”:| 设计动作 | 背后的 cache 考虑 |
|---|---|
| Codex 在内容入史前截断 | 还没形成缓存,不破坏旧 prefix |
| Codex 完整压缩时替换整段历史 | 不做局部手术,直接建立新基线 |
| Claude Code 冻结外置决策 | 保证后续线格式不变 |
| Claude Code 用 cache_edits | 让服务端动缓存层,不改客户端消息内容 |
更大的变化:模型和 Harness 正在绑在一起
远程压缩 不是一个孤立功能。它其实暴露了 Agent 竞争的下一层:模型和 Harness 正在变成一套东西。 过去大家更关心“哪个模型更强”。现在越来越多体验差异来自模型外面的那层系统:工具协议、上下文压缩、缓存策略、权限模型、IDE 集成、历史状态恢复。 OpenAI 有 Codex 的encrypted_content 和 CompactionTrigger。Anthropic 有 Claude Code 的 cache_edits 和 context_management。这些能力并不是裸模型 API 的简单包装,而是服务端、客户端、训练数据一起配合出来的。
第三方 Agent 当然还能做得很好,但它要追的东西变多了:协议细节、服务端 beta 能力、模型后训练习惯、真实用户长程数据。任何一个环节慢半拍,体验都可能差一点。
这也是为什么 “Model + Harness = Agent” 会越来越像行业共识。只提供模型,不做 Harness,就等于把最终用户体验交给别人,也失去了最有价值的交互数据。
结语:上下文压缩已经不是摘要问题
Codex 多次压缩后还能继续跑,原因大概可以拆成三层: 1. 入口层先控制工具输出和历史膨胀,避免窗口太快被噪声吃掉。 2. 远程压缩 把任务状态交给服务端处理,用 opaque state 承载比明文摘要更强的恢复能力。 3. Prompt scaffold 在压缩后重建系统规则,让摘要不必背负所有环境约束。 这套机制当然还有黑盒问题。encrypted_content 里到底有什么?服务端压缩器是什么模型?解密后怎样重组上下文?外部只能猜个轮廓。
但实验结果已经足够说明一点:长程 Agent 的能力,很多时候不只取决于模型会不会推理,还取决于它怎么保存现场、怎么遗忘、怎么在遗忘后重新接上。
所谓 “vibe contexting”,表面上是用户不用管上下文了。底下其实是一堆很不 vibe 的工程。