AI编码代理上下文压缩实验:430条记录与long_mem/engram记忆机制

1. 为什么“上下文压缩”是 AI 编码代理绕不开的坎

先抛一个我自己的真实感受:用 AI 编码代理写代码,最爽的是前二十分钟,最崩溃的是第四十分钟。前二十分钟它记得你所有的约定——命名风格、目录结构、上次改过的那个函数、你明确说过的“别动数据库迁移文件”。到了第四十分钟,上下文窗口塞满了,系统触发一次压缩,它就像换了个人:把你三小时前定的规则忘得一干二净,开始重新发明轮子,甚至把你刚删掉的死代码又加回来。

这不是模型变笨了,是上下文压缩这个机制在起作用。所谓上下文压缩,本质上是当对话历史超出模型能承载的 token 上限时,系统把早期内容做摘要、截断或选择性丢弃,腾出空间给新内容。对普通聊天来说这没什么,反正你也不指望它记住三天前聊的天气。但对AI 编码代理(比如 Codex 这类能读写文件、执行命令、多轮迭代的代理)来说,压缩等于把它的“工作记忆”砍掉一半,而它接下来还要基于这份残缺记忆继续改代码。

我花了 10 天时间,围绕这个痛点做了一轮实验,累计留下430 条公开记录——包括每次压缩前后的行为对比、代理“失忆”的具体表现、以及我尝试的各种补救手段。这篇文章就是把那 430 条记录里最有价值的部分抽出来,讲清楚三件事:压缩之后代理到底丢了什么、为什么常规的“再提醒一遍”不管用、以及long_mem和engram这类记忆机制在工程上该怎么接。

适合谁看?如果你正在用 Codex 或类似的编码代理做真实项目,被“它又忘了”折磨过,或者你打算自己搭一套带长期记忆的编码工作流,这篇应该能帮你少走几天弯路。下面所有结论都来自那 430 条记录,不是纸上推演。

2. 实验设计与 430 条记录是怎么攒出来的

2.1 实验的基本设定:一个真实项目,不是玩具 demo

我没有用“写个 todo list”这种玩具任务,因为玩具任务根本触发不了压缩——它太短了。我选的是一个中等规模的 TypeScript 后端项目,大约 40 个源文件、3 个数据库迁移、一套自定义的错误处理中间件。任务类型覆盖了:新增 API 端点、重构已有模块、修 bug、写测试、改配置。这些任务有个共同点——它们都依赖项目里已经存在的约定和上下文,而不是孤立的一次性代码。

代理侧我用的是 Codex 系的编码代理,通过 CLI 驱动,让它自己读文件、改文件、跑测试。每次会话我都记录:起始 token 量、压缩触发的时间点、压缩后代理的第一反应、以及它后续 5 到 10 轮的行为。10 天下来,有效记录 430 条,其中触发压缩的会话有 187 条,这 187 条是我分析的重点。

2.2 记录维度:我到底在记什么

很多人做这类实验只记“成功/失败”,那太粗了。我设计了 6 个记录维度,每条记录都填:

维度说明为什么重要
压缩触发点第几轮、累计 token 量判断压缩是硬上限触发还是策略触发
丢失内容类型约定/文件状态/历史决策/错误上下文不同内容丢失的后果完全不同
代理补救行为重新读文件/猜测/直接报错反映代理的“自救”能力
人工干预次数我被迫重新解释了几次衡量压缩的真实成本
任务是否完成完成/部分完成/失败最终结果指标
恢复手段用了什么方法让它接上积累可复用的方案

这 6 个维度里,“丢失内容类型”和“恢复手段”是最有价值的。前者告诉你压缩到底伤在哪,后者告诉你药方在哪。430 条记录里,我统计出丢失内容类型的分布大致是:项目约定类占 34%,文件当前状态类占 28%,历史决策类占 22%,错误上下文类占 16%。这个分布很关键——它说明压缩伤得最重的是“约定”,而约定恰恰是代理最不该忘的东西。

2.3 一个反直觉的发现:压缩不是“均匀遗忘”

我原本以为压缩就是按时间顺序砍掉最早的内容。实测下来完全不是。压缩策略更像是按“信息密度”和“近期引用频率”加权丢弃。什么意思?一段早期内容如果后来被反复引用(比如某个核心类型定义),它反而可能被保留;而一段中期内容如果一直没被引用(比如你随口说的一句“这个字段先留着”),它会被优先丢掉。

这个发现直接改变了我的应对思路。既然压缩是加权的,那我就可以主动提高关键信息的“引用频率”,让它在压缩时被判定为高价值。后面第 4 节讲的 long_mem 和 engram 接入,核心逻辑就是围绕这一点展开的。

3. 压缩之后代理到底“断”在哪:四类典型断裂

3.1 约定断裂:它开始用另一套风格写代码

这是最普遍、也最烦人的一类。我在会话开头明确说过:这个项目用Result<T, E>类型做错误处理,不用 try/catch 抛异常。前 30 轮它老老实实遵守。压缩之后,第 31 轮它直接给我写了个throw new Error(...)。

我去翻记录,发现压缩摘要里把“错误处理约定”这条整个丢了,只保留了“项目使用 TypeScript”这种泛泛信息。代理不是故意违反,是它真的不知道有这条约定。更麻烦的是,它违反之后如果你不立刻纠正,它会基于自己的错误输出继续往下建,越错越远。430 条记录里,约定断裂导致的返工平均要花 3 到 5 轮才能拉回来。

3.2 状态断裂:它以为文件还是旧版本

第二类是文件状态丢失。代理在早期读过某个文件,知道里面有个parseConfig函数。压缩后它要改这个函数,但它记忆里的版本是压缩前的。如果这中间文件被改过(比如它自己上一轮改的),它就会基于过时认知操作,轻则改错位置,重则覆盖掉正确代码。

我记录过一个典型案例:代理在压缩后试图给一个已经不存在的函数加参数,因为它记得那个函数还在。它甚至没重新读文件就直接改了,结果当然是编译失败。这类断裂的危险在于它很隐蔽——代理表现得很有信心,你不仔细看 diff 根本发现不了。

3.3 决策断裂:它推翻了之前商定好的方案

第三类是历史决策丢失。比如我们前面讨论过“这个功能先不做缓存,等 Q3 再说”,压缩后代理可能又提议加缓存,或者更糟——它直接按加缓存的思路写了代码。这类断裂的根源是:决策往往以对话形式存在,而不是以代码形式存在。代码里有痕迹的决策容易被保留,纯对话里的决策最容易被压缩丢掉。

3.4 错误断裂:它重复踩同一个坑

第四类是错误上下文丢失。前面某轮它犯过一个错,我纠正了,它改了。压缩后它又犯了同样的错。因为“这个坑踩过”这个信息在压缩摘要里没体现。430 条记录里,同一个错误在压缩后被重复犯的概率大约是 41%——接近一半。这个数字挺吓人的,意味着你纠正过的错误有近一半会卷土重来。

提示:这四类断裂里,约定断裂和决策断裂最值得优先防御,因为它们导致的返工成本最高,而且最难通过“重新读文件”自动恢复。

4. long_mem 与 engram:两种记忆机制的工程接入

4.1 为什么“再提醒一遍”是无效的

很多人第一反应是:压缩了我就再提醒一遍呗。我试过,效果很差。原因有两个。第一,你提醒的内容本身也会进入上下文,加速下一轮压缩,形成“提醒—压缩—再提醒”的死循环。第二,你提醒的是“点”,但代理需要的是“面”——你提醒了错误处理约定,它可能又忘了命名约定,你永远补不完。

所以正确的思路不是“对抗压缩”,而是把关键信息从易失的对话上下文里搬出来,放到一个压缩碰不到的地方。这就是 long_mem 和 engram 这类机制的价值。它们本质上是给代理配了一个“外部记忆”,压缩只压缩对话,不压缩这个外部记忆。

4.2 long_mem 的接入方式:把约定和决策结构化存起来

long_mem 我的用法是把它当成一个结构化的项目记忆库。不是存原始对话,而是存提炼后的条目,每条包含:类型(约定/决策/状态/错误)、内容、生效范围、最后验证时间。接入的关键是三个动作:

  1. 写入时机:每当我在对话里确立一条约定或做一个决策,立刻写入 long_mem,而不是等压缩后补救。
  2. 读取时机:代理每轮开始前,先查 long_mem 里与当前任务相关的条目,注入到上下文最前面。
  3. 失效机制:条目带“最后验证时间”,如果某条约定对应的代码已经改了,标记失效,避免代理拿旧约定办事。

实测下来,接入 long_mem 后,约定断裂从原来的高频降到了很低。代价是每轮多注入几百 token,但相比返工成本,这笔账非常划算。

4.3 engram 的接入方式:让“引用频率”替你保命

engram 我用得更巧一点。前面说过压缩是按引用频率加权的,那 engram 的作用就是主动制造引用。我把关键信息(比如核心类型定义、关键函数签名)做成 engram 条目,然后让代理在每轮推理时都显式引用一次。这样这些信息在压缩时会被判定为高价值,不容易被丢。

具体做法是:在系统提示里加一条规则——“涉及类型或核心函数时,先引用 engram 中的定义再操作”。这条规则让代理养成了“先查再写”的习惯,等于每轮都在给关键信息“续命”。430 条记录里,用了 engram 的会话,核心类型相关的状态断裂几乎消失了。

4.4 两者怎么配合:一个负责“记住”,一个负责“不忘”

long_mem 和 engram 不是二选一,是分工。long_mem 负责记住那些对话里说不清、但必须遵守的东西(约定、决策),engram 负责让那些必须一直活着的信息不被压缩掉(类型、签名、关键状态)。我的配置是:long_mem 存语义层,engram 存引用层,代理每轮先读 long_mem 对齐约定,再查 engram 对齐接口,然后才开始干活。

机制存什么解决哪类断裂接入成本
long_mem约定、决策、错误记录约定断裂、决策断裂、错误断裂中,需要设计条目结构
engram类型、签名、关键状态状态断裂低,主要是加引用规则
两者配合语义+引用双层四类断裂全覆盖中高,但收益最大

5. 实操:把记忆机制接进 Codex 工作流

5.1 环境准备与基础配置

先说环境。我用的是 Codex CLI 形态,跑在本地。安装和登录这块,网上教程很多,核心就几步:拿到安装包、配置好模型端点、确认 CLI 能正常拉起会话。这里有个坑要提醒——模型端点的配置一定要在会话开始前确认好,因为一旦会话中途端点出问题,压缩和记忆注入都会乱套,排查起来非常痛苦。

配置层面,我建议把 long_mem 和 engram 的读写做成两个独立的脚本或工具函数,而不是塞进代理的提示词里。原因很简单:提示词里的东西会被压缩,工具调用不会。代理通过调用工具来读写记忆,这个动作本身是稳定的。

# 示意:记忆读写工具的调用形态 mem_write --type convention --content "错误处理统一用 Result<T,E>" --scope "src/**" mem_read --scope "src/api/**" --limit 10 engram_ref --symbol "Result" --context "error handling"

5.2 会话启动时的记忆注入顺序

会话一开始,别急着让代理干活。先做三步注入:

  1. 读 long_mem 里所有“生效中”的约定和决策,按 scope 过滤出与本次任务相关的,注入到上下文最前面。
  2. 读 engram 里本次任务会碰到的核心符号,注入到约定之后。
  3. 让代理复述一遍它读到的约定,确认它真的“看见”了。

第三步很关键。我踩过的坑是:注入了但代理没“消化”,它只是把内容放在上下文里,推理时没真正用。让它复述一遍,等于强制它做一次注意力对齐。430 条记录里,做了复述的会话,前 10 轮的约定遵守率明显更高。

5.3 压缩触发后的恢复流程

就算有记忆机制,压缩还是会发生。关键是压缩后怎么快速恢复。我的流程是:

  1. 检测到压缩发生后,立刻暂停代理的自主操作。
  2. 重新执行一次 5.2 的注入流程。
  3. 让代理对比“压缩前它认为的文件状态”和“实际文件状态”,列出差异。
  4. 确认差异后再放它继续。

第 3 步是我从记录里总结出来的救命步骤。压缩后代理最大的问题是“基于过时认知操作”,强制它做一次状态对比,能拦下大部分状态断裂。这个流程听起来麻烦,但比事后返工便宜太多。

5.4 参数选择:注入多少、多久验证一次

注入量不是越多越好。我实测下来,long_mem 注入控制在 500 到 800 token 比较合适,engram 控制在 300 到 500 token。太少覆盖不全,太多加速压缩。验证频率上,我建议每 10 轮做一次“约定抽查”——随机问代理一条约定,看它记不记得。不记得就说明该重新注入了。

注意:不要把所有历史决策都塞进 long_mem。只存“仍然生效”的。失效的决策留在里面会污染代理判断,比不存还糟。

6. 常见问题与排查技巧实录

6.1 代理“假装记得”怎么办

这是最阴险的问题。你问它“还记得错误处理约定吗”,它说“记得,用 Result”,但下一轮它又写 throw。这说明它只是在复述,没有真正把约定用于推理。我的解法是:不要问“记不记得”,而是给它一个具体场景让它判断——“这个函数返回错误时,按约定应该怎么写?”用具体场景逼它调用记忆,而不是复述记忆。

6.2 记忆条目冲突怎么处理

有时候 long_mem 里会有两条冲突的约定,比如早期说“用 A 方案”,后期改成“用 B 方案”。如果两条都生效,代理会懵。我的做法是给条目加“优先级”和“替代关系”,新条目明确标记“替代旧条目 X”。读取时只返回没被替代的。这个机制必须在写入时就处理好,事后补救很麻烦。

6.3 压缩后代理反复读同一个文件

这是状态断裂的典型表现。代理不确定文件状态,就反复读,浪费大量 token,还加速下一轮压缩。根因是它不信任自己的记忆。解法是在 engram 里记录“文件最后已知状态哈希”,代理读文件后对比哈希,一致就不再重复读。这个小技巧能省下可观的 token。

6.4 常见问题速查表

现象可能原因排查动作解决手段
代理违反约定约定断裂查 long_mem 是否注入重新注入并让它复述
改错文件位置状态断裂对比文件实际状态强制状态对比流程
重复犯同一错误错误断裂查错误记录是否在库把错误写入 long_mem
反复读同一文件状态不信任查 engram 状态哈希加哈希对比机制
推翻已定方案决策断裂查决策条目是否生效补写决策并标优先级

6.5 几个我踩过的坑

第一个坑:记忆注入位置太靠后。我一开始把 long_mem 内容注入在上下文中段,结果压缩时它和普通对话一起被处理了。后来改到最前面,存活率明显提升。第二个坑:engram 引用太频繁。每轮都引用所有符号,token 爆炸。后来改成只引用当前任务相关的,效果好很多。第三个坑:忘了清理失效条目。攒了两周的失效决策,代理开始拿旧决策办事,排查了半天才发现是记忆库脏了。

7. 这套方案还能怎么扩展

10 天 430 条记录跑下来,我最大的体会是:上下文压缩不是要消灭的敌人,是要共处的现实。你不可能让代理永远不压缩,但你可以让它在压缩后依然接得上。long_mem 和 engram 这套组合,本质上是把“记忆”从易失的对话里剥离出来,变成可控、可查、可验证的外部资产。

后续我打算往两个方向扩展。一是把记忆库做成跨会话共享的,这样不同会话之间也能继承约定和决策,不用每次重新交代。二是给记忆条目加“自动失效检测”,当代码变更导致某条约定不再适用时,自动标记失效,减少人工维护。这两个方向如果跑通,代理的“长期一致性”应该能再上一个台阶。

如果你也在用编码代理做真实项目,我的建议是:别等被压缩坑了才想办法。从第一个会话开始,就把约定和决策结构化存起来。前期多花的那点功夫,会在第十次返工的时候连本带利还给你。