上下文压缩要消失了吗:Codex 正在把记忆搬到窗口外 codex context external memory Codex Agent LLM Context AgentEngineering AI工程
2521 字
13 分钟

上下文压缩要消失了吗: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 WikiAgent runtime state
目标沉淀语义知识恢复一次执行现场
主要读者人和后续 Agentruntime 和当前模型
数据形态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#

我的默认策略会是:

  1. 原始事件 append-only:不要只保留 summary,至少保留可定位的事件和 cursor
  2. 运行时状态独立于模型上下文:快照是机器恢复用的,不要把所有字段直接拼进 prompt
  3. 压缩是投影,不是删除:压缩后仍然可以回到原始事件重放
  4. 每个 turn 有明确边界:输入、工具调用、fork、取消和恢复都要有归属
  5. 用量跟着会话走:resume、fork、compaction 后都能解释数字从哪里来
  6. 恢复路径要可测试:同一条 rollout 用 full replay 和 bounded replay,结果应该一致
  7. 给状态设失效条件:provider、workspace、权限和用户消息变化后,旧 baseline 不能盲目复用

这套设计不一定适合一个很小的聊天机器人,但只要 Agent 会持续工作、会调用工具、会被暂停和恢复,就值得尽早做。

最后#

上下文窗口变长,解决的是“能装下多少内容”;上下文管理变好,解决的是“系统能不能知道哪些内容仍然有效”。这不是同一个问题。

所以我不认为上下文压缩会突然消失。更可能发生的是:summary 逐渐退到模型体验层,真正的记忆交给可持久化、可验证、可回放的 runtime state。

对长程 Agent 来说,记住一段话不够。它得记住自己做过什么,以及为什么可以相信这件事。

参考资料#

上下文压缩要消失了吗:Codex 正在把记忆搬到窗口外
https://bangwu.me/posts/codex-context-external-memory/
作者
棒无
发布于
2026-09-01
许可协议
CC BY-NC-SA 4.0