上下文压缩要消失了吗:Codex 正在把记忆搬到窗口外
先说结论
最近看到一个说法:Codex CLI 要取消上下文压缩,改成硬切窗口 + 外部记忆。
这句话抓到了方向,但说得太满了。
我看了 OpenAI Codex 仓库最近合并的几组改动,更准确的判断是:Codex 没有简单地把 compaction 删除,而是在把上下文管理从“反复总结聊天记录”,往“保留可回放状态、建立边界、按需重建上下文”推进。
这不是一个小修复。对长程 Agent 来说,真正重要的记忆不只有聊天内容,还有:
- 当前工作目录和环境
- 哪些工具已经执行过
- 哪个 turn 正在进行
- 子 Agent 从哪里 fork 出来
- 当前额度和 token 用量
- 最近一次压缩之后,哪些状态仍然有效
如果这些东西只藏在一次 summary 里,Agent 很容易出现一种很具体的失忆:它记得“我们讨论过要修 bug”,却不再知道已经改过哪个文件、哪个检查失败过、接下来应该从哪里继续。
先把“取消压缩”纠正一下
截至 2026 年 9 月 1 日,我没有看到 OpenAI 发布一份可以直接解读为“Codex 已全面取消上下文压缩”的产品公告。这个说法主要来自社区对仓库改动的归纳,OKP 也把它记成了一个架构变化信号,而不是稳定版 changelog。
但仓库里确实有一条很清楚的演进线:
- PR #41901:在空 wake turn 之后加载 bounded context,把最近一次仍然有效的完整 world-state snapshot 当成 durable baseline
- PR #41424:在 nested agent fork 中保留 context baseline,忽略被 compaction 替代的旧 snapshot
- PR #41429:保留每个 turn 最近一次被选中的 StepContext
- PR #41912:把 response token usage 作为可持久化的 rollout item,resume 时不用无限向历史回扫
- PR #41940:让 transcript 的布局缓存跨 backtrack 选择继续生效,避免每次选择都重新排整条历史
这些改动共同说明一件事:上下文不再只是一个字符串数组,而是一个带边界、快照、回放和归属关系的状态系统。
压缩为什么会让 Agent 失忆
最简单的 Agent 上下文大概是这样:
用户消息 → 助手回复 → 工具调用 → 工具结果 → 下一次模型请求当窗口快满时,系统把前面的内容压缩成一段摘要:
很长的历史 → summary → summary + 最近几轮消息这对聊天产品通常够用。对 coding Agent,问题在于历史里有很多不能只靠语义概括的东西:
- 工具返回的精确错误字符串
- 某个文件在第几次尝试后被改成了什么状态
- 用户在审批框里做过的决定
- 子 Agent 的 fork 边界
- 某个配置是在压缩前还是压缩后生效
- 一次失败请求是否已经消耗了额度
Summary 可以表达“发生过一次测试失败”,却不一定保留测试命令、退出码和失败文件。模型下一次看见摘要,会很自然地重新做一遍已经做过的工作。
更麻烦的是,压缩不是一次性的。长任务可能经历:
原始历史 → 压缩 1 → 压缩 2 → 压缩 3后面的 summary 可能建立在前面的 summary 上。每多一层,细节就多一层损失,错误也会被“总结得很合理”。
Codex 最近改的其实是什么
我更愿意用下面这个模型理解它:
raw rollout / transcript │ ├── compaction checkpoint │ ├── world-state baseline │ ├── turn context │ └── token usage │ └── bounded replay │ └── 当前模型上下文这里有三个变化。
1. Summary 不再是唯一真相
压缩后的内容仍然可以作为重建上下文的入口,但系统会同时保留一个更结构化的 baseline。它记录的不是“这段对话大意”,而是某个时间点上系统认为仍然有效的世界状态。
这两者的职责不一样:
- summary 适合给模型读
- baseline 适合给运行时恢复
把这两个层次混在一起,是很多 Agent 状态 bug 的来源。
2. 历史扫描有边界了
PR #41901 处理的是一种很容易被忽略的场景:一个 paginated thread 里可以积累很多没有用户消息边界的 wake turn。旧的 reverse scan 找不到安全的停止点,就可能一直往更早的历史扫描。
有了和 turn context 配对的完整 snapshot 后,系统可以在这里停下来:
最新 compaction ↓兼容的 turn context + 完整 snapshot ↓bounded replay 截止 ↓不再扫描无关的旧 wake turns这不只是省一点读取时间,也减少了重放时误把旧状态带回当前 turn 的概率。
3. 用量也成为状态的一部分
PR #41912 把 token usage 做成 durable rollout item。这个细节很关键:如果用量只存在进程内存里,resume 或 fork 之后就要重新推导;重新推导又可能跨过压缩边界,得到不一致的数字。
现在至少有机会把下面这些东西一起恢复:
response usage → turn attribution → thread total → root-turn lineage → compaction checkpoint对一个有额度限制的 Agent,这比在界面上显示一个漂亮的 token counter 更重要。计数必须能跟着会话一起活下来。
这和 LLM Wiki 不是一回事
我之前写过 LLM Wiki:把上下文编译成会生长的知识库,里面讲的是把原始资料加工成可读、可链接、可维护的知识页面。
Codex 这条线解决的是另一层问题:运行时如何记住自己处于什么状态。
可以这样区分:
| LLM Wiki | Agent runtime state | |
|---|---|---|
| 目标 | 沉淀语义知识 | 恢复一次执行现场 |
| 主要读者 | 人和后续 Agent | runtime 和当前模型 |
| 数据形态 | Markdown 页面、链接、索引 | snapshot、turn、rollout、usage |
| 更新方式 | 归纳、合并、修订 | append、checkpoint、replay |
| 失败代价 | 知识页面过时 | Agent 重复操作或做错决策 |
两者可以配合,但不能互相替代。Wiki 告诉 Agent“这个项目通常怎么部署”,runtime baseline 告诉它“这一次部署已经做到哪一步”。
两种上下文管理方式
旧式 compaction 可以抽象成:
async function compact(history: Message[]): Promise<Message[]> { const summary = await summarize(history); return [ { role: "system", content: summary }, ...history.slice(-recentMessageCount), ];}这段代码短,但它把“理解历史”和“恢复状态”都交给了模型。
更稳的方式应该把运行时状态单独持久化:
type Checkpoint = { worldState: WorldState; turnContext: TurnContext; usage: TokenUsage; transcriptCursor: string;};
async function resume(checkpoint: Checkpoint): Promise<Message[]> { const replay = await replayAfter(checkpoint.transcriptCursor); return projectModelContext({ baseline: checkpoint.worldState, turn: checkpoint.turnContext, replay, });}这里的关键不是代码长了,而是职责拆开了:
worldState负责运行时事实turnContext负责当前执行边界usage负责资源归属transcriptCursor负责从哪里开始回放- 最后才由 projection 决定模型看到什么
外部记忆也不是免费午餐
把记忆搬到窗口外,当然会引入新的问题。
检索错误
模型不再自动看到所有历史,而是依赖 runtime 选择正确的片段。选错了,系统可能比 summary 更安静地丢信息。
Snapshot 过期
一个 snapshot 只有在它的依赖仍然成立时才有效。provider 换了、fork 改了用户消息、compaction 产生了新基线,旧 snapshot 就应该失效。PR #41424 专门处理的就是“被压缩替代的 snapshot 不应该继续当基线”。
磁盘和隐私
raw transcript、工具结果和 token usage 都可能包含源代码、凭证引用和内部数据。外部记忆不是一句“写到本地文件”就结束了,还要考虑:
- 哪些结果可以长期保存
- 哪些字段需要脱敏
- fork 是否复制全部历史
- 删除会话时能否真的删除
- 多个 workspace 是否会串数据
可观察性
如果 Agent 说“我记得之前做过”,你需要能回答:它是从哪个 checkpoint、哪个 transcript item、哪个 note 得到的。否则外部记忆只是另一种不可解释的 prompt magic。
如果是我来设计长程 Agent
我的默认策略会是:
- 原始事件 append-only:不要只保留 summary,至少保留可定位的事件和 cursor
- 运行时状态独立于模型上下文:快照是机器恢复用的,不要把所有字段直接拼进 prompt
- 压缩是投影,不是删除:压缩后仍然可以回到原始事件重放
- 每个 turn 有明确边界:输入、工具调用、fork、取消和恢复都要有归属
- 用量跟着会话走:resume、fork、compaction 后都能解释数字从哪里来
- 恢复路径要可测试:同一条 rollout 用 full replay 和 bounded replay,结果应该一致
- 给状态设失效条件:provider、workspace、权限和用户消息变化后,旧 baseline 不能盲目复用
这套设计不一定适合一个很小的聊天机器人,但只要 Agent 会持续工作、会调用工具、会被暂停和恢复,就值得尽早做。
最后
上下文窗口变长,解决的是“能装下多少内容”;上下文管理变好,解决的是“系统能不能知道哪些内容仍然有效”。这不是同一个问题。
所以我不认为上下文压缩会突然消失。更可能发生的是:summary 逐渐退到模型体验层,真正的记忆交给可持久化、可验证、可回放的 runtime state。
对长程 Agent 来说,记住一段话不够。它得记住自己做过什么,以及为什么可以相信这件事。