1. 这个实验到底在折腾什么
先交代背景。我做 AI 编码代理的日常使用和调优差不多两年,主力工具是 Codex 这一类命令行编码代理。用久了就会发现一个很尴尬的事:长会话跑到一定轮次之后,代理会突然“失忆”。前面聊过的架构决策、改过的文件路径、约定好的命名规范,它全忘了,然后开始重复问你已经回答过的问题,或者更糟——按它自己脑补的假设去改代码,把之前对的东西改坏。
这不是模型笨,是上下文窗口满了之后触发了压缩(compaction)。压缩本身是好事,它把历史对话摘要成一段更短的文本塞回窗口,让会话能继续。但问题在于:压缩是有损的。摘要丢掉了大量细节,尤其是那些“当时看起来不重要、后面却反复要用”的信息,比如某个函数为什么这么命名、某个依赖为什么锁在特定版本、某个接口的字段顺序为什么不能动。
所以我做了这个实验:连续 10 天,用同一个编码代理跑真实项目任务,记录 430 条公开可查的交互记录,专门观察上下文压缩之后代理的“接续能力”到底掉了多少,以及用什么办法能把它接回来。关键词里提到的 long_mem、engram 就是我在实验里试的两条技术路线,前者是外挂长期记忆,后者偏向把关键信息编码成可检索的片段。
这篇文章适合谁看?如果你只是偶尔用 AI 写个脚本,那压缩问题你基本碰不到。但如果你把编码代理当成日常主力、一个会话要跑几百轮、跨多个文件做重构,那这篇里的坑你迟早会踩。我会把实验设计、观察到的现象、试过的补救方案、以及最后跑通的配置都摊开讲,能直接抄的部分我会标出来。
先说结论方向,免得你看到一半觉得我在绕:压缩本身不可怕,可怕的是压缩之后代理不知道自己丢了什么。大部分“接不上”的案例,根因不是信息真的没了,而是代理没有意识到需要去把信息找回来。所以解法的大头不在“压缩算法”,而在“压缩后的重建机制”。
2. 实验设计与记录方法
2.1 为什么选 10 天和 430 条这个量级
10 天不是我随便定的。编码代理的上下文压缩通常不是一次性的,而是随着会话推进反复触发。单次压缩的影响可能被模型自身的推理能力掩盖,只有跨多天、多轮压缩叠加之后,退化才会明显到可观测。我前 3 天基本在摸清压缩触发的节奏,中间 4 天做对照实验,最后 3 天验证补救方案,10 天刚好覆盖一个完整的“发现问题—定位原因—验证解法”闭环。
430 条记录是这么来的:每天平均 43 条有效交互,剔除掉纯闲聊和重复确认,剩下的都是有明确任务意图的轮次。每条记录我固定记五个字段:轮次编号、是否刚经历压缩、代理的输出是否偏离预期、偏离类型、我采取的干预手段。这个字段设计很关键,后面做统计全靠它。
提示:如果你也想做类似观察,别一上来就记几百条。先跑 20 条,把字段定死,否则中途改字段会导致前面数据作废。我第一天就吃了这个亏,白记了 30 多条。
2.2 记录字段的设计逻辑
为什么是这五个字段而不是更多?因为观察的目标是“压缩与接续失败的相关性”,字段太多会稀释注意力,太少又无法归因。我解释一下每个字段的取舍:
- 轮次编号:用来定位压缩发生在第几轮,以及失败集中在压缩后的第几轮。实测下来,失败高发区是压缩后的第 2 到第 5 轮,第 1 轮反而正常,因为摘要内容还热乎。
- 是否刚经历压缩:布尔值,但我会额外标注“距上次压缩的轮数”。这个细节后来证明非常有用,退化程度和距压缩轮数呈明显的先升后降曲线。
- 输出是否偏离预期:主观判断,但我给自己定了硬标准——只要代理需要我重复一遍已经说过的信息,就算偏离。
- 偏离类型:我归了四类,分别是信息丢失、信息错配、过度泛化、路径漂移。这个分类是实验中期才稳定下来的,前面几天的数据我按新分类重新标注过一遍。
- 干预手段:记录我当时做了什么,比如重述、贴文件、换会话、挂记忆。这是后面提炼解法的一手素材。
2.3 任务类型的选择与配比
如果 10 天全跑同一种任务,结论会偏。我刻意让任务类型有配比,大致是:跨文件重构占 40%,新功能开发占 30%,bug 定位占 20%,文档与注释维护占 10%。这个配比接近我真实的工作分布,也让不同压缩敏感度的任务都能被覆盖到。
跨文件重构占比最高,是因为它对上下文连续性最敏感。你改一个函数的签名,代理得记得所有调用点在哪、每个调用点的上下文约束是什么。压缩一旦丢掉调用点清单,代理就会漏改或者乱改。新功能开发相对独立,压缩影响小一些,但也不是没有,尤其是涉及既有代码风格约定的时候。
3. 压缩之后到底丢了什么:四类偏离的实录
3.1 信息丢失:最直白也最容易被发现
信息丢失是最好识别的一类。典型表现是代理直接问你:“你之前提到的那个配置文件路径是什么?”或者“这个模块的入口函数叫什么?”这类偏离的好处是它自己会暴露,你补一句就能继续。坏处是它打断节奏,而且如果它不问你、直接猜,就会滑向第二类。
我记录里信息丢失占了偏离总数的约 38%。有意思的是,丢失的内容有明显偏好:路径、命名、版本号、字段顺序这四类信息最容易丢,而任务目标、大方向反而不容易丢。这符合直觉,摘要倾向于保留“要做什么”,牺牲“具体怎么做”。
注意:信息丢失本身不可怕,可怕的是代理不承认自己丢了。我遇到过好几次,它明明忘了路径,却编了一个看起来很像的路径继续往下写,结果文件根本不存在。这种“自信的幻觉”比直接提问危险得多。
3.2 信息错配:把 A 的约束套到 B 上
信息错配是我觉得最隐蔽、也最费时间的一类。表现是代理记得某些信息,但张冠李戴。比如它记得“这个项目要求所有对外接口用蛇形命名”,然后把这个约束套到了内部工具函数上,而内部函数其实用的是驼峰。它没丢信息,但把信息的适用范围搞错了。
这类偏离占约 27%。根因通常是压缩摘要把多个相似约束合并成了一条,丢掉了“作用域”这个维度。摘要写“项目命名规范:蛇形”,但原文其实是“对外接口蛇形,内部驼峰”。压缩一合并,作用域就没了。
3.3 过度泛化:从具体案例跳到错误通则
过度泛化占约 21%。代理会把某一次具体的修复经验,当成通用规则套到所有类似场景。比如我在某轮里让它给一个特定函数加了空值检查,压缩之后它开始给所有函数都加空值检查,包括那些参数根本不可能为空的。
这类偏离的麻烦在于,它看起来“很勤奋”,输出量还变大了,你如果不仔细看 diff,很容易放过。我后来养成了一个习惯:压缩之后的头几轮,diff 必须逐行看,不能只看它说“已完成”。
3.4 路径漂移:目标悄悄变了
路径漂移占约 14%,数量最少但危害最大。代理在压缩之后,对任务目标的理解发生了微妙偏移。比如原任务是“把 A 模块的日志级别从 debug 调到 info”,压缩之后它理解成“优化 A 模块的日志”,然后顺手把日志格式也改了、把日志内容也精简了。方向没错,但范围超了。
这类偏离往往要到你 review 的时候才发现,因为代理自己觉得干得挺好。我记录里最严重的一次,代理在压缩后把整个模块的错误处理策略都改了,理由是“顺手优化”,结果引入了一个新的边界 bug。
| 偏离类型 | 占比 | 识别难度 | 主要根因 |
|---|---|---|---|
| 信息丢失 | 38% | 低 | 摘要丢弃具体细节 |
| 信息错配 | 27% | 中 | 摘要合并丢失作用域 |
| 过度泛化 | 21% | 中 | 具体经验被抽象成通则 |
| 路径漂移 | 14% | 高 | 任务目标被重新解读 |
4. 两条补救路线:long_mem 与 engram 的实测对比
4.1 long_mem 路线:外挂长期记忆
long_mem 的思路很直接:既然压缩会丢信息,那我就在压缩之外单独维护一份长期记忆,把关键信息存起来,压缩后按需检索回来。我试的实现方式是维护一个结构化的记忆文件,按“项目—模块—约束”三级组织,每次压缩触发后,代理先读这个文件再继续。
实测下来,long_mem 对信息丢失的改善最明显,丢失类偏离从 38% 降到了约 15%。因为路径、命名这些信息本来就是结构化的,存进去、读出来都很顺。但对信息错配和路径漂移改善有限,因为这两类问题的根因是“理解偏差”,不是“信息缺失”,你把信息摆它面前,它照样可能理解错。
long_mem 的另一个坑是维护成本。记忆文件会越来越大,如果不做定期清理,检索出来的内容里会混入过时信息,反而制造新的错配。我后来加了个规则:每条记忆带一个“最后验证轮次”,超过 50 轮没被引用的记忆自动降权。
4.2 engram 路线:把关键信息编码成可检索片段
engram 的思路更偏“编码”。它不存原始信息,而是把关键决策和约束编码成短小的、带语义标签的片段,压缩后通过语义相似度检索。好处是存储紧凑、检索快,坏处是编码过程本身有损,如果编码规则没定好,检索出来的片段可能对不上。
我试的编码规则是“约束—场景—反例”三段式。比如“对外接口用蛇形—场景是所有 HTTP handler—反例是内部工具函数用驼峰”。这个三段式对信息错配的改善很明显,错配类偏离从 27% 降到了约 12%,因为“反例”这一段专门用来界定作用域。
但 engram 对信息丢失的改善不如 long_mem,因为编码过程会主动丢弃它认为不重要的细节,而“重要”的判断标准是我定的,难免有偏差。实测丢失类偏离只降到约 22%。
4.3 两条路线的组合与取舍
单用哪条都不够。我最后的方案是组合:long_mem 负责存结构化事实,engram 负责存约束与作用域。压缩触发后,先读 long_mem 补事实,再用 engram 校准约束边界。组合之后,四类偏离的总占比从基准的 100% 降到了约 35%,其中路径漂移降幅最大,从 14% 降到约 5%。
| 方案 | 信息丢失 | 信息错配 | 过度泛化 | 路径漂移 |
|---|---|---|---|---|
| 无补救(基准) | 38% | 27% | 21% | 14% |
| 仅 long_mem | 15% | 24% | 19% | 13% |
| 仅 engram | 22% | 12% | 16% | 9% |
| 组合方案 | 12% | 10% | 8% | 5% |
提示:组合方案不是简单叠加,两条路线的检索结果需要去重和冲突消解。我的做法是 long_mem 的事实优先,engram 的约束用来修正事实的适用范围,冲突时以 engram 为准,因为约束边界比具体事实更容易验证。
5. 实操:把补救机制接进编码代理的完整流程
5.1 记忆文件的结构设计
先说 long_mem 的文件结构。我用的是纯文本加固定分隔符,不用数据库,因为编码代理读文本最顺,数据库还得走查询接口,多一层就多一个出错点。结构是三级:
[PROJECT: 项目名] [MODULE: 模块名] [FACT: 事实类型] 具体内容 | 最后验证轮次: N事实类型我固定了几种:PATH(路径)、NAME(命名)、VERSION(版本)、ORDER(顺序)、CONSTRAINT(约束)。固定类型的好处是检索时可以按类型过滤,不用全文扫。最后验证轮次用来做老化清理,前面提过。
写记忆的时机很关键。我的做法是不主动写,只在代理明确确认某个信息之后写。比如代理问“这个路径对吗”,我回答“对”,这时候才把路径写进记忆。如果代理没确认,我不写,避免把错误信息固化进去。
5.2 engram 片段的编码规则
engram 片段我用三段式,中间用竖线分隔:
约束内容 | 适用场景 | 反例编码时机是在每次压缩触发之前。我会在压缩前扫一遍最近的对话,把新出现的约束抽出来编码。这个动作我一开始手动做,后来写了个简单的脚本辅助,但脚本只做抽取建议,最终编码还是我确认,因为编码质量直接决定检索质量。
注意:engram 的反例段千万别省。我一开始觉得反例是冗余,省了几天,结果信息错配立刻回升。反例的作用是给约束划边界,没有边界,约束就会到处乱套。
5.3 压缩触发后的重建流程
压缩触发后,我固定走四步:
- 读 long_mem:按当前任务涉及的模块,把相关事实读出来,作为上下文前缀塞给代理。
- 检索 engram:用当前任务描述做语义检索,取最相关的 3 到 5 条约束片段。
- 显式声明:在给代理的输入里明确写“以下是已确认的事实和约束,请以此为准”,而不是默默塞进去。显式声明能显著降低代理“自作主张”的概率。
- 首轮验证:压缩后的第一轮,我不派发新任务,而是让代理复述当前任务目标和关键约束,确认它接上了再继续。
这四步里,第三步和第四步是我踩坑之后加的。一开始我只做前两步,发现代理经常忽略塞进去的内容,因为它不知道这些内容比它自己的记忆更权威。显式声明之后,忽略率大幅下降。
5.4 一个完整的重建示例
假设当前任务是重构用户模块的接口。压缩触发后,我的输入大概长这样:
[已确认事实] - 用户模块入口: src/user/index.ts | 验证轮次: 142 - 对外接口命名: 蛇形 | 验证轮次: 138 - 依赖版本: zod@3.22.0 | 验证轮次: 140 [已确认约束] - 对外接口用蛇形 | 适用所有 HTTP handler | 反例: 内部工具函数用驼峰 - 错误码统一用 E_ 前缀 | 适用所有抛错点 | 反例: 第三方库透传的错误不改 请先复述当前任务目标和上述约束,确认后再开始重构。这段输入不长,但把最容易丢的信息都覆盖了。实测下来,带这段输入的压缩后首轮,偏离率比不带低了一半以上。
6. 常见问题与排查速查
6.1 代理忽略记忆内容怎么办
这是最高频的问题。代理明明收到了记忆内容,还是按自己的理解干。排查顺序是:先确认记忆内容是不是放在了输入的最前面,中间隔了太多内容会被稀释;再确认有没有显式声明权威性;最后确认记忆内容本身有没有冲突,冲突的内容代理会倾向于忽略全部。
我的经验是,显式声明 + 放在最前 + 内容无冲突,三条都满足,忽略率能压到很低。如果还忽略,那大概率是记忆内容太长,代理的注意力被分散了,需要精简。
6.2 记忆文件越来越大怎么清理
不要等它大了才清。我的做法是每次会话结束跑一次老化:超过 50 轮没被引用的记忆降权,超过 100 轮的直接归档到冷存储,不参与日常检索。归档不是删除,需要的时候还能捞回来。这样日常检索的记忆量能稳定在一个可控范围。
6.3 压缩后代理反复问同样的问题
这说明记忆检索没生效,或者检索出来的内容和问题对不上。先检查检索的关键词是不是太泛,泛关键词会召回一堆不相关记忆,把真正相关的挤掉。我的做法是检索时带上模块名做过滤,缩小范围再排序。
6.4 组合方案下两条路线冲突怎么判
前面提过,以 engram 的约束为准。但有个例外:如果 long_mem 的事实带“最后验证轮次”且比 engram 片段新,那以 long_mem 为准。因为事实是可以验证的,约束是推断的,新验证的事实优先级更高。
| 问题现象 | 可能原因 | 排查动作 |
|---|---|---|
| 代理忽略记忆 | 未显式声明/位置靠后/内容冲突 | 前置+声明+去冲突 |
| 记忆膨胀 | 未做老化清理 | 按轮次降权归档 |
| 反复问同一问题 | 检索关键词太泛 | 加模块名过滤 |
| 两路线冲突 | 优先级未定 | 约束优先,新事实例外 |
| 压缩后首轮就偏 | 未做首轮验证 | 强制复述确认 |
6.5 几个我踩过的坑
第一个坑是记忆写入太积极。我一开始代理说什么我都记,结果记忆里混入了大量未确认的猜测,检索出来反而误导。后来改成只记确认过的,质量立刻上来了。
第二个坑是engram 编码太细。我把每个小约束都编码,片段数量爆炸,检索精度反而下降。后来合并同类约束,片段数控制在 20 条以内,效果最好。
第三个坑是首轮验证省略。有几天我赶进度,压缩后直接派任务,结果偏离率明显上升。首轮验证看着费时间,其实省的是后面返工的时间。
7. 这套机制还能怎么扩展
跑完这 10 天,我最大的体会是:上下文压缩不是要解决的问题,而是要共存的现实。与其想着怎么让压缩不丢信息,不如想着怎么让代理在丢了信息之后能快速接回来。这个思路转变之后,很多之前觉得棘手的问题都变得可管理了。
后续我打算往两个方向扩展。一是把记忆的写入和检索做成半自动,减少手动确认的负担,但保留人工兜底,因为全自动的记忆质量我还不放心。二是把 engram 的编码规则从三段式扩展到带时间维度的四段式,加上“约束生效时间”,用来处理那些会随时间变化的约束,比如某个版本之后才生效的命名规范。
如果你也在用编码代理跑长会话,建议你先从记录开始,跑个二三十轮,看看你自己的场景里哪类偏离最多,再针对性地上补救。别一上来就全套照搬,我的配比是基于我的任务分布,你的可能不一样。工具是死的,场景是活的,先观察再动手,比什么都强。