用户已经说“不做了”,Agent为什么还在继续?
上下文压缩可以减少 Token,却可能让已取消需求重新出现。本文结合三个公开源码项目,分析摘要、最近原文与结构化状态如何协同。
摘要:上下文压缩不是把旧对话缩短那么简单。本文从 Codex、OpenCode 与 Pi 的公开源码出发,分析它们如何触发压缩、保留最近原文和更新摘要,并解释为什么“取消需求”仍可能在下一次压缩后被错误复活。
用户先说:“增加 A 功能。”
开发进行到一半,用户又说:“A 不做了,先完成 B。”
Agent 当时答应了,甚至停止了 A。但对话变长并触发上下文压缩后,它又开始补 A 的测试、恢复 A 的接口,或者在下一轮计划里把 A 当成未完成任务。
这不是一个罕见的语言理解问题,而是上下文架构问题。
压缩器既要减少 Token,又要回答一个更难的问题:哪些旧信息仍然有效,哪些已经被新指令覆盖,哪些必须作为明确的禁止项继续保留?
结合 Codex、OpenCode 和 Pi 的公开源码,可以先得到一个结论:三个项目都把 Token 计算、切分边界和最近消息保留做成了确定性逻辑,但“新增 A”与“取消 A”之间的语义合并,仍主要依靠模型生成摘要。
这对通用编码 Agent 是合理的工程起点,却不是高风险业务 Agent 的终点。更可靠的方案应该把压缩拆成两部分:模型负责理解语言,状态机负责裁决当前任务状态。
一、上下文压缩真正压缩的不是文本,而是任务状态
模型每次调用看到的上下文,通常不只有聊天记录,还可能包含:
- 系统指令与开发规则;
- 用户历史消息;
- 工具定义与工具结果;
- 已读取、修改的文件;
- 当前计划、审批和子任务状态;
- 环境信息、权限和可用能力;
- 前一次压缩生成的摘要。
当总量接近模型窗口时,最直接的办法是删除最旧消息。但旧消息中可能有架构约束、失败方案和用户偏好,直接截断会让 Agent 重新犯错。
因此,主流实现通常采用:
旧历史 → 摘要
最近历史 → 保留原文
系统与环境状态 → 重新注入
这个结构解决了“装不下”的问题,却没有天然解决“哪个决定现在有效”。

例如,下面两段话不是可以同时保留的普通事实:
第 12 轮:实现 A 功能。
第 38 轮:A 不做了。
第二条是对第一条的覆盖。如果摘要写成“用户要求实现 A,后续讨论过取消 A”,恢复后的 Agent 仍可能把 A 当成目标。正确的当前状态应该是:
A:已取消,不得继续实现
所以,压缩质量不能只用“摘要是否流畅”衡量,还要检查任务状态是否正确归并。
二、本文分析的源码范围
本文以 2026 年 8 月 24 日获取的三个公开仓库快照为依据:
- Codex,提交
e3609f2d,主要分析compact.rs; - OpenCode,提交
105b398c,主要分析两套session/compaction.ts; - Pi,提交
dcd46192,主要分析 harness 中的compaction.ts。
仓库仍在快速演进。本文讨论的是这些固定提交所展示的架构,不把某个内部函数推断成产品永远不变的承诺。
三、Codex:摘要之外,还保留最近的用户原话
Codex 在每轮采样前检查 Token 状态。如果当前上下文达到自动压缩限制或模型完整窗口上限,run_pre_sampling_compact 会触发压缩。
压缩过程调用模型生成一份 handoff summary,默认提示词要求保留:
- 当前进度与关键决策;
- 重要上下文、约束和用户偏好;
- 剩余工作和下一步;
- 继续任务所需的数据、示例和引用。
但 Codex 没有只留下摘要。build_compacted_history_with_limit 会从后向前选择真实用户消息,最多保留约 20000 Token,然后把压缩摘要追加到新历史中。
这意味着,如果“A 不做了”仍在最近的用户消息范围内,恢复后的模型不仅能看到摘要,还能看到用户原话。它降低了摘要遗漏最新否定指令的概率。
Codex 还会处理另一类容易被忽视的内容:初始上下文。压缩后,模型指令、AGENTS.md、权限、工具和其他 world state 不能随旧历史一起消失。源码通过重新构建或再次注入初始上下文,让新窗口继续拥有当前环境约束。
这个设计可以概括为:
当前系统状态
+ 最近用户原文
+ 模型生成的历史摘要
它的边界也很清楚:
第一,最近原文有预算上限。当否定指令足够旧,仍要依赖摘要正确携带。
第二,默认摘要结构要求保留“关键决策”,但没有一个通用的、确定性的需求状态机去证明 A 已从 active 变为 cancelled。
第三,源码会提示用户:长线程和多次压缩可能降低准确性。这是一个诚实的工程判断——压缩不是无损编码。
四、OpenCode:明确规定新对话覆盖旧摘要
OpenCode 的当前仓库同时包含正在演进的会话实现。其压缩路径先计算模型可用上下文:为输出预留空间,当已使用 Token 达到可用上限时触发自动压缩。
在 packages/opencode 的会话压缩实现中,系统会为最近历史保留一段预算。默认预算约为可用上下文的四分之一,同时限制在 2000 到 15000 Token 之间;其余较旧内容进入摘要。
更值得关注的是 OpenCode 的摘要更新规则。packages/core/src/session/compaction.ts 明确告诉摘要模型:
旧摘要与新对话冲突时,新对话优先;
写入修正后的事实,并删除旧说法。
这条规则直接覆盖了本文的问题。如果旧摘要写着“实现 A”,新对话说“A 不做了”,摘要应更新为取消 A,而不是并列保留两种说法。
OpenCode 还用 tail_start_id 标记最近历史的保留起点。模型侧看到的是压缩摘要加保留的最近消息,而完整会话记录仍以消息和压缩事件的形式存在。压缩失败时,不应把一个未完成摘要当成新的有效边界。
此外,旧工具输出可以被单独裁剪,摘要输入中的工具结果也会限制长度。这体现了一个重要思想:上下文治理不只有“整段摘要”一种动作。工具输出通常体积最大,也最适合先做确定性瘦身。
OpenCode 的新旧冲突规则比“保留关键决策”更具体,但它仍然是一条写给模型的自然语言指令。模型是否把“不用 A 方案”正确理解为“取消方案但保留目标”,仍属于语义判断。
五、Pi:保留最近尾部,并把文件操作单独结构化
Pi 的默认自动压缩条件是:
contextTokens >
contextWindow - reserveTokens
当前源码中的默认配置为预留 16384 Token,并保留约 20000 Token 的最近上下文。压缩器从后向前寻找切分点,尽量在完整轮次边界切开;如果单轮工具调用本身过长,也有单独的 split turn 处理。
Pi 把压缩结果保存为结构化对象,其中包括:
summary
tokensBefore
retainedTail
details
retainedTail 保存最近原始消息。重复压缩时,上一轮保留的尾部会重新进入下一轮切分计算,而不是只更新一段越来越抽象的摘要。
Pi 还有一个很有价值的设计:文件读写记录不是完全交给摘要模型。源码从 read、write 和 edit 工具调用中确定性提取文件路径,分别维护已读文件和已修改文件,再把它们作为详情与标签写入压缩结果。
换句话说,Pi 已经在实践一种“语义摘要 + 结构化事实”的混合压缩:
模型摘要:目标、进度、决策、下一步
确定性提取:读过哪些文件、改过哪些文件
原文保留:最近消息尾部
但在需求否定方面,Pi 的更新提示词主要要求保留旧摘要、增加新进展,并允许移除不再相关的内容。它没有像 OpenCode 那样明确写出“新对话与旧摘要冲突时,新对话获胜”。因此,如果旧摘要强调 A,新消息取消 A,最终仍取决于摘要模型是否正确理解和改写。

六、三个项目都没有把所有判断交给模型
说“上下文压缩完全依赖模型”并不准确。
三个项目的确定性部分至少包括:
- 何时触发压缩;
- 为模型输出预留多少空间;
- 哪些旧消息进入摘要;
- 保留多少最近消息;
- 工具结果如何裁剪;
- 压缩失败是否生效;
- 压缩后怎样重建模型可见上下文。
真正主要依赖模型的是:
- 哪个目标仍然有效;
- 新指令是否覆盖旧指令;
- “不做 A”是取消、延期还是更换方案;
- 哪个失败经验仍值得保留;
- 哪些细节可以安全丢弃。

这也解释了为什么“保留最近原文”很重要。最近消息在压缩后仍以原始形式进入模型,可以给摘要错误留一条纠正路径。但它只是降低风险,不是确定性保证。
七、“不做了”至少对应五种不同状态变化
如果要把否定指令从摘要中独立出来,第一步不是增加一个 cancelled 布尔值,而是区分否定的作用域。
1. 取消目标
A 功能不做了。
目标本身从 active 变为 cancelled。
2. 延期目标
A 先不做,等 B 完成再说。
A 不是永久取消,而是 deferred。后续规划不能立即执行,但可以在条件满足后重新评估。
3. 替换实现方案
A 还是要做,但不要用 Redis。
取消的是 solution=Redis,不是 goal=A。如果压缩器只抓住“不要”,很容易误删整个目标。
4. 撤销本次实现
需求保留,先把刚才对 A 的代码改动回滚。
业务目标仍然 active,只是当前 implementation revision 被撤销。
5. 覆盖约束
之前要求兼容旧接口,现在不需要兼容了。
改变的是约束集合,而不是功能目标。
“撤回刚才那个”“还是按原来的来”还包含指代解析。如果系统不能确定用户指的是目标、方案还是代码改动,就不应该自信地更新状态,而应该保留原文并请求确认。
八、可靠架构:摘要负责叙事,状态机负责当前事实
更稳妥的上下文架构可以分为五层:
不可变事件日志
↓
语义事件提取
↓
确定性状态归并
↓
模型历史摘要
↓
上下文组装器
第一层:保留不可变事件
用户每次新增、取消和修改都写成新事件,不直接删除旧记录。
{
"id": "evt_38",
"turn": 38,
"kind": "CANCEL_GOAL",
"target": "feature_A",
"supersedes": ["evt_12"],
"sourceText": "A 不做了"
}
这里的取消事件就是一块“语义墓碑”。它告诉后续系统:A 不是从历史中消失,而是被明确否定。即使旧摘要或代码注释再次提到 A,也不能自动把它复活。
第二层:模型只提交受限事件候选
模型可以把自然语言映射为有限操作:
type DecisionKind =
| "ADD_GOAL"
| "CANCEL_GOAL"
| "DEFER_GOAL"
| "RESTORE_GOAL"
| "REPLACE_SOLUTION"
| "UNDO_IMPLEMENTATION"
| "UPDATE_CONSTRAINT";
模型不能直接重写整个任务状态,只能提交候选事件。服务端验证目标是否存在、引用是否明确、事件顺序是否合法,并对低置信度或高影响操作要求用户确认。
第三层:状态机确定性归并
function reduce(
state: TaskState,
event: DecisionEvent,
): TaskState {
const item = state.goals[event.target];
switch (event.kind) {
case "CANCEL_GOAL":
return markCancelled(state, item, event);
case "DEFER_GOAL":
return markDeferred(state, item, event);
case "RESTORE_GOAL":
return restoreGoal(state, item, event);
case "REPLACE_SOLUTION":
return replaceSolutionOnly(state, item, event);
default:
return applyValidatedEvent(state, event);
}
}
这段代码表达的是架构边界,不是可直接安装的库。关键在于:相同事件序列必须得到相同当前状态,不能因为换了模型或摘要措辞而改变。
第四层:模型生成叙事摘要
摘要仍然有价值。它适合保留为什么换方案、排查过什么、哪些尝试失败以及下一步如何继续。这些信息很难全部结构化。
但摘要不再是需求真相的唯一来源。
第五层:按优先级组装新上下文
1. 系统与安全指令
2. 当前有效目标和约束
3. 明确取消、延期和禁止事项
4. 当前代码与工具状态
5. 历史摘要
6. 最近原始对话
7. 本轮用户消息
如果摘要写着“继续 A”,结构化状态却是 A=cancelled,组装器应该拒绝继续 A,并记录摘要冲突,而不是让模型自行选择相信哪一个。

九、把三个项目的优点组合起来
从三个项目可以提炼出一套更完整的实现策略:
- 借鉴 Codex:保留最近用户原文,并在压缩后重新注入当前系统状态;
- 借鉴 OpenCode:明确规定最近对话覆盖旧摘要,压缩失败不推进有效边界;
- 借鉴 Pi:保留最近消息尾部,把文件操作等可验证事实从摘要中独立出来;
- 额外增加:为目标、方案、约束和撤销操作建立结构化决策事件与状态机。
这四部分分别解决不同问题:
- 最近原文:减少摘要遗漏最新指令的风险;
- 新消息优先:处理新对话与旧摘要的冲突;
- 结构化事实:防止文件、工具和业务状态被摘要写错;
- 取消墓碑:阻止已否定需求被旧上下文重新激活。
任何一项都不能单独替代其他项。
十、如何验证压缩后不会“复活”旧需求
不要只测试 Token 是否下降。至少要建立以下用例:
基础覆盖
新增 A → 取消 A → 压缩 → 继续任务
预期:A 保持 cancelled
方案与目标分离
实现 A,使用 Redis
→ 不用 Redis,A 保留
→ 压缩
预期:A active,Redis 方案 cancelled
延期与取消分离
A 先不做 → 压缩
预期:A deferred,而不是 done 或 cancelled
多轮压缩
新增 A → 第一次压缩 → 取消 A → 第二次压缩
预期:第二次摘要不能继续携带“A 待实现”
旧证据污染
A 已取消 → 代码注释和旧工具结果仍提到 A
预期:不能根据旧证据自动恢复 A
评测指标至少包括:
- 取消需求复活率;
- 当前约束保留率;
- 压缩后的任务续接成功率;
- 摘要与结构化状态冲突数;
- Token 压缩率和压缩调用成本。
其中“取消需求复活率”应该作为阻断指标,而不是普通质量分。对于会发邮件、改配置或执行交易的 Agent,一次错误复活就可能产生真实副作用。
十一、上下文架构是否值得做得这么复杂
如果 Agent 只负责一次性问答,结构化决策状态可能得不偿失。最近对话加一次摘要通常足够。
但下面这些场景值得增加状态层:
- 任务跨越多天、多轮压缩或多个 Agent;
- Agent 会修改代码、数据或外部系统;
- 用户经常调整范围、撤销需求和替换方案;
- 需要恢复、审计或解释“为什么这样做”;
- 错误执行的成本明显高于一次澄清。
实现时也不必一开始结构化所有内容。可以先挑选高价值状态:目标、禁止项、审批、资源权限和外部副作用;其余历史继续由模型摘要。
结语:压缩可以有损,当前决策不能含糊
Codex、OpenCode 和 Pi 的源码说明,上下文压缩早已不是粗暴截断:它们会计算 Token 预算、保留最近消息、生成滚动摘要、处理工具输出,并在压缩后重建上下文。
但“用户现在到底要什么”仍是最难压缩的信息。
可靠的设计应该坚持四条原则:
原始事件可以追溯;
最近指令优先于旧摘要;
取消不是删除,而是带状态的覆盖;
模型理解语义,规则裁决当前事实。
当用户已经说“A 不做了”,系统不能只期待下一次摘要“记得写进去”。它应该让 A 进入一个可验证的 cancelled 状态,并确保任何旧摘要、缓存或工具结果都无法悄悄把它复活。