1. 这个实验到底在折腾什么
先把这个标题拆开看。上下文压缩,指的是 AI 编码代理在长会话里,因为上下文窗口有限,不得不把历史对话、代码片段、工具调用记录做摘要或截断,腾出空间给新内容。AI 编码代理,就是像 Codex 这类能自己读文件、改代码、跑命令的智能体。问题就出在这两者的交界处:压缩之后,代理还记得自己刚才在干什么吗?还接得上之前的思路吗?
我花了 10 天时间,用一套公开可查的记录方式,跑了 430 条实验数据,专门盯这个问题。核心想验证的东西很朴素——当上下文被压缩掉一部分之后,代理能不能靠某种外部记忆机制,把断掉的线索重新接回来。这里涉及两个关键词:long_mem(长期记忆)和engram(记忆痕迹,借用了神经科学里"记忆印迹"的概念)。前者是存储层,后者是检索和激活层。
这个内容适合谁看?如果你正在用 Codex 这类编码代理做实际项目,遇到过"聊到一半它突然忘了前面说啥"的情况,或者你自己在搭带记忆的代理系统,那这篇就是写给你的。不需要你是模型训练专家,但最好对代理的基本工作方式有点概念。我会把每一步为什么这么做讲清楚,参数怎么定、坑在哪、怎么排查,都摊开说。
430 条记录不是随便凑的数。每条记录对应一次"压缩-恢复"的完整循环,包含压缩前的状态、压缩策略、恢复尝试、以及最终代理是否成功接上任务。10 天是因为要覆盖不同类型的任务:单文件重构、跨文件依赖修改、多轮调试、以及长链路的功能开发。任务类型不同,压缩带来的伤害程度完全不一样,这一点后面会细说。
2. 为什么上下文压缩会"断片"
2.1 压缩的本质是信息有损
很多人以为上下文压缩就是把旧对话删掉,其实没那么简单。主流做法有三种:滑动窗口截断、摘要压缩、以及关键信息抽取。滑动窗口最粗暴,直接把最早的对话扔掉;摘要压缩是用模型把历史浓缩成一段话;关键信息抽取则是挑出文件路径、函数名、变量名这些"硬信息"保留。
问题在于,编码任务的信息密度极高。一句"把parse_config的返回值改成 tuple"背后,隐含了函数当前签名、调用它的所有位置、返回值的消费方式。摘要压缩很容易把这些细节揉成一句"修改了配置解析逻辑",代理再看到这句话,根本不知道要改哪个函数、改成什么样。
我实测下来,滑动窗口在短任务里几乎无感,但一旦任务链路超过 15 轮工具调用,断片率飙升。摘要压缩稍好,但摘要本身的质量波动很大,同一个任务跑两次,摘要可能一次保留了关键函数名,一次没有。
2.2 断片的具体表现
断片不是"代理完全懵了",而是更隐蔽的几种情况。第一种是目标漂移:代理还记得要改代码,但忘了最初的需求是"兼容旧配置格式",结果改出一个只支持新格式的版本。第二种是重复劳动:它忘了自己已经改过某个文件,又改一遍,把之前的修改覆盖掉。第三种最坑,叫虚假记忆:代理根据残缺的摘要,脑补出一个不存在的函数签名,然后照着这个假签名去改代码,编译直接报错。
这三种里,虚假记忆最难排查,因为代理表现得非常自信,你不仔细看 diff 根本发现不了。我在第 3 天就踩了这个坑,一个跨 4 个文件的改动,代理在压缩后重新"回忆"出一个错误的接口定义,导致后面 6 轮工具调用全在错误的基础上打转。
2.3 为什么单纯加大窗口不解决问题
有人会说,那就别压缩,把窗口开大不就行了。理论上可以,但实际不行。一是成本,上下文越长,每次调用的 token 消耗越大,长会话下成本是指数级上升的。二是效果,业界普遍观察到一个现象:上下文太长时,模型对中间部分的注意力会下降,也就是所谓的"lost in the middle"。你把 10 万 token 塞进去,模型真正用上的可能还是头尾那部分。
所以压缩不是可选项,是必选项。真正要解决的是:压缩之后,怎么让代理快速、准确地找回它需要的那部分信息。这就是 long_mem 和 engram 要干的事。
3. long_mem 和 engram 的分工设计
3.1 为什么要把存储和检索分开
我一开始想的是搞一个大而全的记忆模块,既存又查。跑了大概 50 条记录之后发现不行。存储和检索的优化目标是冲突的:存储希望信息尽量全、尽量原始,检索希望信息尽量精、尽量快。混在一起,要么存得太多查得慢,要么为了查得快把信息砍得太狠。
所以拆成两层。long_mem负责存,把每次压缩前的关键状态完整落盘,包括文件快照、工具调用序列、以及代理当时的"意图描述"。engram负责查,根据当前上下文,从 long_mem 里激活最相关的几条记忆,注入回代理的上下文。
这个设计借鉴了认知科学里的记忆模型:长期记忆存储量大但提取慢,工作记忆容量小但反应快,两者靠"激活"机制联动。engram 就是那个激活器。
3.2 long_mem 存什么、怎么存
long_mem 的存储单元我定义为一条"记忆条目",每条包含四个字段:
| 字段 | 内容 | 作用 |
|---|---|---|
| 意图 | 代理当时想完成什么 | 用于语义匹配 |
| 状态 | 涉及的文件路径与关键代码片段 | 用于精确恢复 |
| 动作 | 已执行的工具调用序列 | 避免重复劳动 |
| 时间戳 | 记录生成时间 | 处理新旧记忆冲突 |
意图字段是核心。它不是简单记录"修改了文件",而是让代理在压缩前用一句话总结"我接下来要做什么"。这句话是后面检索的锚点。实测发现,意图描述越具体,恢复成功率越高。比如"修复登录接口的空指针"就比"改 bug"强太多。
状态字段我做了取舍,不存整个文件,只存被修改的函数或代码块,加上文件路径。这样单条记忆的体积可控,几百条记忆也不会撑爆存储。动作字段记录工具调用的摘要,比如"已读取 auth.py、已修改 validate 函数",用来防止代理重复执行。
3.3 engram 怎么激活正确的记忆
engram 的激活逻辑是整个实验里调参最多的部分。最朴素的方案是关键词匹配,把当前上下文里的词和记忆条目的意图做匹配。但编码场景里,同一个概念可能有多种表达,关键词匹配召回率很低。
我最后用的是"语义相似度 + 硬信息命中"的混合策略。语义相似度用嵌入向量算,负责召回语义相关的记忆;硬信息命中则是检查当前上下文里出现的文件路径、函数名,是否在某条记忆的状态字段里出现过。两者加权求和,取 top-k 注入。
权重这块我试了好几组,最后定的是语义 0.6、硬信息 0.4。为什么硬信息权重不低?因为编码任务里,文件路径和函数名是最强的线索,一个文件名的命中,往往比一段语义相似更能说明问题。这个比例不是理论最优,是我在 430 条记录里试出来的经验值,你可以根据自己的任务类型微调。
4. 430 条记录是怎么跑出来的
4.1 实验设置与任务分类
实验环境是一台本地开发机,代理用的是 Codex 的 CLI 形态,通过标准输入输出交互。任务库我准备了 40 个真实项目任务,分成四类,每类 10 个,每个任务重复跑多次以覆盖不同的压缩触发点,最终累计 430 条有效记录。
四类任务是:
- 单文件重构:在一个文件内改函数结构,压缩伤害最小,作为基线。
- 跨文件依赖修改:改一个接口,牵连多个调用方,考验记忆的关联能力。
- 多轮调试:反复试错,历史里有很多失败尝试,考验代理会不会重蹈覆辙。
- 长链路功能开发:从需求到实现跨几十轮,考验长期目标的一致性。
每条记录我记录了压缩触发时的轮次、压缩策略、恢复后代理是否走对方向、以及最终任务是否完成。判定"走对方向"的标准是:恢复后的前 3 轮工具调用,是否与压缩前的意图一致。
4.2 压缩策略的对照
我对比了三种压缩策略在有无记忆机制下的表现。这里的数字是任务完成率,不是精确的统计结论,是我这批任务上的实测值:
| 压缩策略 | 无记忆 | 仅 long_mem | long_mem + engram |
|---|---|---|---|
| 滑动窗口 | 41% | 58% | 79% |
| 摘要压缩 | 53% | 67% | 86% |
| 关键信息抽取 | 62% | 74% | 88% |
几个观察值得说。第一,engram 带来的提升在滑动窗口下最明显,因为滑动窗口丢的信息最多,记忆机制补回来的空间最大。第二,关键信息抽取本身已经不错,加上 engram 后提升有限,说明当压缩策略本身保留了硬信息时,记忆的边际价值下降。第三,无论哪种策略,光有 long_mem 不够,必须配上 engram 的激活,否则存了也白存。
4.3 一个典型的恢复过程
拿一个跨文件依赖修改的任务举例。任务是给一个数据处理管道加一个字段校验,涉及 5 个文件。跑到第 18 轮时触发了压缩,此时代理已经改完了 3 个文件,正在改第 4 个。
压缩后,engram 从 long_mem 里激活了 3 条记忆:一条是"给 record 类加 validate 方法"的意图,一条是"已修改 parser.py 和 model.py"的状态,一条是"待修改 exporter.py"的动作。这三条注入后,代理在第 19 轮直接打开了 exporter.py,没有重新去读已经改过的文件,也没有忘记要加校验这个目标。
对比之下,关掉 engram 只留 long_mem 的那次,代理在第 19 轮重新读了 parser.py,浪费了 2 轮,而且一度想改一个已经改好的函数。这就是激活机制的价值——它不只是"记得",而是"在对的时候想起对的事"。
5. 实操中踩过的坑和排查方法
5.1 记忆污染:旧记忆干扰新任务
跑到第 5 天遇到一个烦人的问题。同一个项目里连续跑多个任务时,前一个任务的记忆会被后一个任务激活。比如刚做完"给用户模块加日志",接着做"给订单模块加日志",engram 把用户模块的记忆也召回了,代理一度想去改用户模块的文件。
根因是记忆条目没有做任务隔离。我的解法是给每条记忆打上任务 ID,engram 激活时先按任务 ID 过滤,只召回当前任务的记忆。如果当前任务没有匹配的记忆,才放宽到全局召回,但权重打七折。这个改动之后,跨任务干扰基本消失。
提示:如果你的代理是长期运行、跨多个任务复用的,任务隔离一定要做,否则记忆越多,干扰越严重。
5.2 意图描述写得太虚
前面提过意图字段的重要性,但实际操作中,代理自己生成的意图描述经常很虚。比如它会写"继续之前的修改",这种描述在检索时毫无区分度,召回一堆不相关的记忆。
我的应对是给意图描述加约束模板,强制包含"动作 + 对象 + 目的"三要素。比如"修改 validate 函数,使其支持空值校验"。模板不是硬编码字符串,而是在提示里给代理几个正例,让它照着格式写。加了模板后,意图描述的平均信息量明显提升,检索准确率跟着涨。
5.3 记忆条目膨胀导致检索变慢
跑到 200 多条记录时,检索开始变慢,单次激活要几百毫秒。排查发现是 long_mem 里堆积了大量低价值记忆,比如"读取了某个文件"这种没有后续动作的条目。
解法是加一个记忆淘汰机制。每条记忆有个"被激活次数"计数,长期不被激活的记忆降权,低于阈值就归档。另外,纯读取类、没有产生修改的动作,不单独存记忆,合并到相邻的意图条目里。这两招下来,活跃记忆条目控制在 100 条以内,检索回到几十毫秒。
5.4 常见问题速查
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 代理重复修改同一文件 | 动作字段缺失或未激活 | 检查 long_mem 是否记录了已执行动作 |
| 代理改错函数签名 | 状态字段存了过期快照 | 确认记忆时间戳与当前文件版本 |
| 恢复后目标跑偏 | 意图描述太模糊 | 检查意图是否含动作+对象+目的 |
| 检索召回不相关记忆 | 任务隔离缺失 | 确认记忆是否带任务 ID |
| 检索变慢 | 记忆条目过多 | 检查淘汰机制是否生效 |
6. 关于 Codex 这类代理的接入细节
6.1 记忆模块怎么挂上去
Codex 这类 CLI 代理,交互是回合制的:你给它输入,它输出工具调用或文本。记忆模块要挂在这两个点上——压缩触发时写入,每轮开始前检索注入。
写入点比较好办,在检测到上下文接近窗口上限、准备压缩之前,拦截一次,让代理生成意图描述,连同当前状态一起存进 long_mem。检索注入则是在每轮把用户输入或工具返回喂给代理之前,先跑一次 engram,把召回的记忆拼在提示的前部。
这里有个细节:注入的记忆不能太长,否则又挤占上下文。我限制每次最多注入 3 条,每条不超过 200 token。超出的做截断,优先保留意图和状态字段。
6.2 和代理原生记忆的区别
有些代理自带某种形式的记忆,比如把历史对话存到文件里,下次启动时读回来。这跟 long_mem 的区别在于粒度。原生记忆通常是整段对话的堆砌,检索靠全文搜索,噪音大。long_mem 是结构化的,每条记忆有明确的意图、状态、动作,检索靠语义加硬信息,精准得多。
实测对比过,用原生记忆恢复的任务完成率在 60% 出头,换成 long_mem + engram 后到 85% 以上。差距主要来自检索精度,原生记忆经常召回一堆无关的历史,反而干扰代理判断。
6.3 参数配置参考
下面是我最终稳定下来的一套配置,供参考。注意这些值是在我的任务集上调出来的,你换任务类型可能需要重新试:
long_mem: max_active_entries: 100 entry_ttl_days: 7 snapshot_scope: function_level engram: semantic_weight: 0.6 hard_info_weight: 0.4 top_k: 3 max_inject_tokens: 600 task_isolation: truesnapshot_scope设成 function_level 而不是 file_level,是为了控制单条记忆体积。entry_ttl_days是记忆的存活天数,超过就归档,避免无限膨胀。top_k设 3 是权衡,设 5 召回更全但注入变长,设 1 又容易漏。
7. 这套方案适合什么、不适合什么
7.1 适合的场景
跨文件、多轮、有明确目标的编码任务是这套方案的主场。尤其是那种"改一个接口牵连一片"的重构,压缩后最容易断片,记忆机制的价值最大。另外,长时间运行的代理会话,比如你让它跑一整个下午的开发任务,中途必然多次压缩,这时候 long_mem 就是它的"工作日志"。
多轮调试也很受益。调试过程中有大量失败尝试,这些如果不记下来,代理压缩后会重复踩坑。把失败尝试也存成记忆,标注"此路不通",能显著减少无效轮次。
7.2 不适合的场景
短任务、单文件、几轮就结束的,上这套机制是杀鸡用牛刀。记忆的写入和检索本身有开销,任务太短的话,开销占比过高,反而拖慢。我实测 5 轮以内的任务,加记忆机制后总耗时增加约 15%,收益几乎为零。
另外,如果你的任务本身没有明确目标,就是让代理随便探索,那记忆机制也帮不上忙,因为它依赖"意图"这个锚点。没有意图,检索就失去了方向。
7.3 后续可以怎么扩展
一个方向是把 engram 的激活从"被动召回"改成"主动预测"。现在是每轮开始前检索一次,未来可以根据代理当前的动作,预测它下一步需要什么记忆,提前注入。另一个方向是记忆的跨会话复用,把长期有效的项目知识(比如代码规范、架构约定)沉淀成持久记忆,新会话启动时自动加载。
我个人在实际操作中的体会是,记忆机制的价值不在于"记得多",而在于"在对的时候想起对的事"。430 条记录跑下来,最深的感受是:压缩不可避免,断片也不可怕,可怕的是断了之后接不回来。long_mem 和 engram 这套组合,本质上就是给代理装了一个"接得上"的机制。参数可以调,策略可以换,但这个思路——存储与检索分离、意图作为锚点、硬信息加权——是我试过最稳的一条路。