1. 为什么"压缩完就断片"是编码代理绕不过去的坎
做过 AI 编码代理的人,大概率都经历过这个场景:一个任务跑到一半,上下文窗口快满了,系统自动触发压缩,把前面几十轮对话揉成一段摘要。压缩本身没问题,问题是压缩完之后,代理就像换了个人——刚才还在改src/auth/session.ts的第 47 行,压缩后它开始重新问"你想让我做什么",或者更糟,它自信满满地去改一个根本不存在的文件。
我这次做的事情,就是拿一个真实的编码代理,连续跑 10 天,把每一次"压缩—恢复"的过程完整记录下来,最后攒了 430 条公开可查的记录。目的只有一个:搞清楚压缩之后,代理到底丢了什么,以及怎么让它接得上。
先说结论,免得你看到一半才发现方向不对:上下文压缩丢掉的从来不是"信息量",而是"结构"。摘要能把"我们在做一个登录重构"这件事保留下来,但它保不住"当前正在编辑的文件路径 + 光标附近的代码 + 上一步刚跑完的测试结果 + 下一步打算做什么"这一整套工作状态。前者是叙事,后者是状态机。代理恢复靠的是状态机,不是叙事。
这也是为什么很多团队觉得"我摘要写得挺全的啊,怎么还是接不上"。因为你补的是故事,它缺的是现场。
关键词里出现的long_mem、engram这些词,本质上都在试图回答同一个问题:怎么把"现场"用一种比原始 token 更省、但比自然语言摘要更结构化的方式存下来。engram这个词借的是神经科学里"记忆痕迹"的概念,挺贴切——它不是完整记忆,是能触发完整记忆的那一小块钩子。这个思路后面会展开讲。
至于Codex相关的那些热搜词,比如codex cli、codex 安装、codex 使用教程,说明大量人正在把这类代理往本地工作流里塞。而一旦你真的天天用它干活,压缩恢复这个问题就会从"理论问题"变成"每天都要骂一次的现实问题"。我这 430 条记录里,超过六成的失败恢复都跟这个直接相关。
这篇文章适合三类人:正在自建编码代理的工程师、在用现成代理但总觉得它"记性差"的重度用户、以及想理解长上下文管理到底难在哪的技术负责人。我会把实验设计、观察到的失败模式、我试过的几种接续方案、以及最终跑通的那套做法,全部摊开讲。不吹方案,只讲哪些真的管用、哪些看着优雅实际一跑就崩。
2. 430 条记录是怎么攒出来的:实验设计与记录口径
2.1 代理配置与任务选取
我用的代理是一个基于 CLI 的编码助手,跑在本地仓库上,工具集包括文件读写、shell 执行、代码搜索这几类基础能力。任务不是随便挑的,我刻意选了三类难度递进的工作:
- 单文件小改:改一个函数签名、修一个类型错误。这类任务通常 5 到 15 轮就结束,压缩很少触发,主要用来做基线对照。
- 跨文件重构:比如把一个散落在 6 个文件里的配置读取逻辑收敛到一个模块。这类任务动辄 40 轮以上,压缩必然触发,是观察重点。
- 带验证的修复:改完必须跑测试,测试挂了要回读日志再改。这类任务的状态最复杂,因为它同时涉及"代码状态"和"验证状态"两条线。
10 天里我总共跑了 87 个任务,平均每个任务触发 4 到 5 次压缩,累计下来就是 430 条左右的压缩—恢复记录。每条记录我固定采集这几个字段:压缩前的最后 3 轮动作、压缩摘要的实际内容、压缩后代理的第一个动作、这个动作是否正确、以及如果错了,错在哪。
提示:记录"压缩后第一个动作"是关键。因为恢复是否成功,几乎在第一个动作就能看出来。如果它第一件事是重新读文件、重新确认路径,那基本是没接上;如果它直接去改正确的行,那就是接上了。
2.2 什么叫"接得上":我给恢复成功下的定义
这里必须先把标准定死,否则 430 条记录没法统计。我定义的"恢复成功"要同时满足三条:
- 目标一致:压缩后代理执行的动作,和压缩前它正在推进的目标是同一个,没有跑偏到别的子任务。
- 位置正确:它操作的文件和代码位置是对的,不需要人类再指一遍。
- 无需重述:我没有手动补一句"你刚才在改 XX",它自己就接上了。
三条里任意一条不满足,我就记为一次失败恢复。按这个口径统计,430 条里成功接上的只有 158 条,成功率大概 37%。剩下 272 条失败里,我进一步分了类,这个分类是后面所有分析的骨架。
| 失败类型 | 占比 | 典型表现 |
|---|---|---|
| 目标漂移 | 34% | 跑去处理摘要里提到的次要话题 |
| 位置丢失 | 29% | 知道要改什么,但改错文件或改错函数 |
| 状态错乱 | 22% | 以为测试已经过了,其实没跑 |
| 完全重置 | 15% | 重新问"你想做什么" |
这张表值得盯着看一会儿。目标漂移排第一,说明摘要的"叙事性"反而是个陷阱——摘要里但凡提到一个次要的、没做完的小事,代理恢复后很容易被它勾走。位置丢失排第二,印证了我开头说的:摘要保不住"现场坐标"。
2.3 记录过程中踩的第一个坑:摘要本身不可见
实验刚开始时我犯了个错:我以为能直接拿到压缩摘要的原文。结果发现很多代理的压缩是"黑盒"的,摘要生成后直接进上下文,不落盘、不打印。我不得不改用一个笨办法——在压缩触发前后各打一次完整的上下文快照,然后 diff 出摘要那一段。
这个坑值得单独说,因为它影响了很多人的调试。你想优化恢复,第一步得能看见摘要长什么样。如果连摘要都拿不到,你根本不知道代理恢复时手里攥的是什么牌。我后来在代理外面套了一层日志,把每次请求的完整 messages 数组 dump 下来,才拿到干净的摘要文本。
3. 摘要为什么保不住现场:三种失败模式的根因拆解
3.1 目标漂移:摘要的"平权"问题
摘要生成有个天然倾向:它会把所有提到过的事情按"重要性"排个序,然后平等地写进去。但代理的工作记忆不是平权的——当前正在推进的那一条,权重应该远高于其他所有条目。
我举个记录里的真实例子。一个重构任务进行到第 30 轮,代理正在把config/loader.ts里的读取逻辑往config/index.ts搬。摘要里是这么写的:"用户要求重构配置模块;已识别 6 个相关文件;loader.ts的读取逻辑待迁移;另外用户提到想顺便加个环境变量校验。"
压缩后代理的第一个动作是什么?它去加环境变量校验了。因为摘要里"另外"那半句,和主线任务在文本上权重差不多,代理没有理由认为主线更重要。
这就是根因:自然语言摘要没有"当前焦点"这个字段。它是一份静态清单,不是一份带指针的状态。人读摘要能靠常识判断哪条是主线,代理不能,它只能按文本显著性来。
3.2 位置丢失:坐标是结构,不是语义
第二个高频失败是位置丢失。摘要能写"正在修改 session 相关代码",但写不出"src/auth/session.ts第 47 行,refreshToken函数体内,刚把expiresIn从硬编码改成读配置"。
有人会说,那我把路径写进摘要不就行了?我试过,效果有限。因为路径只是坐标的一半,另一半是"在这个位置上的具体意图"。你写"正在改session.ts",代理恢复后打开文件,面对 300 行代码,它还是不知道该动哪。它需要的是"改到哪了 + 下一步动哪",这是一个带方向的坐标,不是静态地址。
我在记录里做过一个对比:给摘要加上文件路径,位置丢失率从 29% 降到 21%;再加上"当前函数名 + 下一步意图",降到 9%。这说明位置信息必须成组出现才有用,单给路径等于没给。
3.3 状态错乱:验证结果是最容易被摘要吃掉的东西
第三类失败最隐蔽,也最危险。代理改完代码、跑了测试、测试挂了、它正准备根据日志再改一轮——这时候压缩触发。摘要里通常会写"已修改 XX 文件",但很少写"测试当前是失败状态,失败原因是 YY"。
结果就是代理恢复后,以为测试已经过了,直接进入"总结陈词"模式,跟你说"任务已完成"。你一看,测试还是红的。
这个问题的根因在于:摘要生成器倾向于记录"动作",不倾向记录"动作的结果状态"。改文件是动作,测试失败是状态。动作好写,状态难写,于是状态就被省略了。但对代理来说,"测试失败"这个状态比"改了文件"这个动作重要得多,因为它直接决定下一步该干嘛。
注意:如果你在自建代理,压缩时务必把"最近一次验证的结果"作为强制字段保留,哪怕它只是一句"测试失败,错误在 X 文件 Y 行"。这一条能砍掉相当一部分状态错乱。
4. 我试过的四种接续方案,哪些真能用
4.1 方案一:加大摘要预算——治标不治本
最直觉的做法是把摘要写长一点,多塞点信息。我试了,把摘要从 200 字扩到 800 字。结果是成功率从 37% 提到 44%,提升有限,而且带来新问题:摘要本身开始占用可观的上下文,压缩的收益被吃掉一部分。
更麻烦的是,摘要变长后,目标漂移反而更严重了。因为摘要里提到的次要事项更多了,代理被勾走的概率上升。这印证了 3.1 的结论:问题不在信息量,在结构。你往一个没有焦点的清单里塞更多条目,只会让焦点更模糊。
4.2 方案二:结构化状态块——目前最稳的一招
真正有效的是把摘要拆成"叙事部分"和"状态块"两块。叙事部分随便写,状态块用固定字段:
{ "current_goal": "把 loader.ts 的读取逻辑迁移到 index.ts", "current_file": "src/config/loader.ts", "current_symbol": "loadConfig", "last_action": "已复制 loadConfig 函数体到 index.ts", "next_intent": "删除 loader.ts 中的原函数并更新引用", "last_verification": { "command": "npm test -- config", "result": "failed", "reason": "index.ts 缺少 ConfigError 导入" } }这个状态块跟着摘要一起进上下文。实测下来,恢复成功率从 44% 直接跳到 71%。原因很简单:它把"现场"用结构化的方式保住了,代理不需要从叙事里猜焦点,直接读字段就行。
long_mem这类思路的核心其实就在这里——不是存更多文本,是存更结构化的状态。engram那个"记忆痕迹"的比喻也贴切:状态块就是那个钩子,代理看到current_file和next_intent,就能把整个现场重新拉起来。
4.3 方案三:让代理自己写交接笔记——好用但要看时机
第三个方案是让代理在压缩前主动写一段"交接笔记",明确告诉"未来的自己"该干嘛。这个思路来自人类交接班——你下班前给接班的同事留张条。
我试下来,效果取决于触发时机。如果是代理自己判断"快满了"然后写笔记,质量参差不齐,有时候它写得敷衍。如果是外部强制在压缩前插入一个"写交接笔记"的步骤,质量稳定很多,成功率能到 68% 左右,略低于结构化状态块,但胜在实现简单,不用改摘要生成器。
它的短板是:交接笔记仍然是自然语言,仍然有目标漂移的风险。我记录里有几例,代理在笔记里写了三件事,恢复后挑了最不重要的那件干。
4.4 方案四:外部记忆文件——适合长任务,不适合快节奏
最后一个方案是把状态写到仓库里的一个文件,比如.agent/memory.md,压缩后代理第一件事就是读这个文件。这个方案在超长任务里很香,因为记忆可以跨会话保留。但在快节奏的短任务里,每次读写文件都是额外开销,而且代理有时候会忘了读。
我最终的方案是方案二为主、方案四为辅:结构化状态块负责单次压缩的接续,外部记忆文件负责跨会话的长期上下文。两者配合,10 天实验后半段的恢复成功率稳定在 75% 以上。
| 方案 | 成功率 | 实现成本 | 适用场景 |
|---|---|---|---|
| 加大摘要预算 | 44% | 低 | 不推荐单独用 |
| 结构化状态块 | 71% | 中 | 单次压缩接续 |
| 交接笔记 | 68% | 低 | 快速实现 |
| 外部记忆文件 | 视情况 | 中 | 跨会话长任务 |
5. 把状态块真正跑起来:字段设计与生成时机
5.1 六个必填字段,一个都不能少
上面那个 JSON 不是随便写的,每个字段都对应一种失败模式。我把字段和它解决的问题列一下,你就明白为什么不能省:
current_goal:解决目标漂移。它强制把主线单独拎出来,和次要事项隔离。current_file+current_symbol:解决位置丢失。两个一起给,代理才能定位到具体函数。last_action:告诉代理"上一步干了啥",避免重复劳动。next_intent:这是最关键的一个。它把"下一步"显式写出来,代理恢复后不用自己推理该干嘛。last_verification:解决状态错乱。把最近一次验证的命令、结果、原因全带上。
我试过删字段,删next_intent成功率掉 15 个点,删last_verification掉 12 个点。这两个是性价比最高的,如果只能保留两个字段,就留这俩。
5.2 生成时机:别等压缩触发才想起来
状态块什么时候生成?我踩过的坑是:等压缩触发了才让代理去总结状态,这时候它已经在"准备被压缩"的状态里,写出来的东西质量差。
正确做法是滚动更新。每完成一个"有意义的最小步骤"——比如改完一个函数、跑完一次测试——就更新一次状态块。这样压缩触发时,状态块已经是最新的,直接拿来用就行,不需要临时生成。
这个思路和数据库的 WAL 日志有点像:不是等崩溃了才记录,是边做边记。代价是每步多一次小开销,但换来的是压缩时零延迟、零质量损失。
def update_state_block(agent, step_result): state = agent.current_state state["last_action"] = step_result.action state["next_intent"] = step_result.next_plan if step_result.verification: state["last_verification"] = { "command": step_result.verification.cmd, "result": step_result.verification.status, "reason": step_result.verification.reason } agent.persist_state(state)5.3 恢复时的第一动作:先读状态块,再动手
光有状态块还不够,代理恢复后得知道要先读它。我在系统提示里加了一条硬规则:压缩恢复后的第一个动作,必须是读取状态块并复述current_goal和next_intent。
这条规则看着笨,但极其有效。因为"复述"这个动作强迫代理把焦点重新加载一遍,而不是直接凭摘要的模糊印象开干。我记录里,加了这条规则后,目标漂移率从 34% 降到 11%。
提示:复述不要让它自由发挥,要求它原样引用
next_intent字段。自由发挥又会引入漂移。
6. 那些文档不会告诉你的实操细节
6.1 压缩阈值别卡太死
很多人把压缩阈值设成窗口的 95%,觉得这样最省。实测下来这是自找麻烦。因为压缩本身要消耗 token 来生成摘要和状态块,你卡到 95%,压缩过程本身就可能溢出,导致压缩失败或者摘要被截断。
我的经验值是80% 到 85%触发压缩。留出的空间刚好够生成状态块和摘要,还能给恢复后的第一轮动作留点余量。这个数字不是拍脑袋,是我对比了 70%、80%、90% 三档之后的结论:80% 档的综合成功率最高,90% 档因为压缩质量下降反而更差。
6.2 状态块要防"过期"
滚动更新有个副作用:如果代理卡在某个步骤很久,状态块可能停留在旧状态。我遇到过代理在等一个长时间命令,状态块还写着"准备运行测试",结果命令早就跑完了。
解决办法是给状态块加个时间戳和"最后更新步骤号",恢复时如果发现状态块太旧,就先做一次轻量的状态重建——重新读一下当前文件、重新跑一次快速检查。这个兜底逻辑救过我好几次。
6.3 别让摘要和状态块打架
摘要和状态块是两份东西,如果它们说的不一致,代理会懵。我记录里有几例,摘要说"正在改 A 文件",状态块说current_file是 B 文件,代理恢复后两个都试了一遍。
所以生成顺序很重要:先生成状态块,再基于状态块生成摘要。这样摘要天然和状态块一致。反过来做,摘要先写好,状态块再补,就容易对不上。
6.4 测试失败的状态要写得具体
last_verification里的reason字段,千万别写"测试失败"就完事。要写到"哪个测试、什么错误、大概在哪"。我对比过,reason写具体的情况下,代理恢复后一次修对的概率是 63%;只写"测试失败"的情况下,只有 28%。
原因还是那个:代理需要的是能直接指导下一步动作的信息。"测试失败"是状态,"ConfigError 未导入,在 index.ts 第 12 行"才是可执行的线索。
7. 10 天实验下来,我对"接得上"这件事的理解
跑完这 430 条记录,最大的收获不是某个具体方案,而是对问题性质的重新认识。上下文压缩之后的接续,本质上不是"记忆问题",是"状态管理问题"。你不需要让代理记住所有事,你需要让它在任何时刻都能回答三个问题:我在干嘛、我在哪、下一步是什么。
这三个问题,自然语言摘要答不好,结构化状态块答得好。这不是说摘要没用,摘要负责"叙事连续性",状态块负责"操作连续性",两者分工明确。我早期把所有希望寄托在摘要上,就是没分清这两件事。
long_mem、engram这些方向之所以值得关注,也是因为它们都在往"结构化状态"这个方向走,而不是单纯地"存更多文本"。存更多文本是死路,因为上下文窗口再大也有上限,而任务复杂度是没有上限的。
至于Codex这类工具在国内的落地,我个人的观察是:安装、配置这些一次性问题都好解决,真正长期折磨人的就是这种"跑着跑着接不上"的体验问题。谁能把压缩恢复做扎实,谁的工具就能真正进入日常workflow,而不是停留在demo阶段。
最后分享一个我一直在用的小技巧:每次压缩恢复后,如果代理的第一个动作对了,我会在日志里给它打个正样本标记;如果错了,打个负样本。攒够几百条之后,这些标记能反过来指导你调状态块的字段权重——哪些字段真正在起作用,数据会告诉你。我这 430 条记录就是这么一点点攒出来的,没什么捷径,但每一条都算数。