1. 这个实验到底在折腾什么
先把这个标题拆开看。上下文压缩,指的是 AI 编码代理在长会话里,当对话历史逼近模型上下文窗口上限时,系统会自动把前面的内容做摘要、裁剪或者丢弃,腾出空间继续往下跑。AI 编码代理,就是那种能自己读代码、改文件、跑命令、看报错的自动化编程助手,比如 Codex 这类 CLI 形态的工具。而long_mem和engram是两种记忆机制的代称——前者偏向长期记忆的持久化存储,后者偏向把关键信息"刻"进一个可检索的记忆痕迹里。
问题就出在这两者的交界处:压缩发生了,记忆还在,但代理"接不上"了。
我花了 10 天时间,跑了 430 条公开可查的记录,专门盯这件事。为什么值得盯?因为绝大多数人用编码代理时,前 20 分钟体验极好,一旦会话拉长到需要压缩,代理就开始犯迷糊——重复改同一个文件、忘记刚才定好的接口约定、把已经删掉的函数又加回来。这不是模型变笨了,是压缩把上下文切断了,而记忆层没有把断点接上。
这篇东西适合三类人看:一是天天用 Codex 这类 CLI 代理写代码、被长会话折磨过的开发者;二是在自己项目里做 agent 记忆层、想知道压缩后怎么补救的工程师;三是单纯好奇"AI 代理到底怎么记住事"的技术爱好者。我会把 430 条记录里反复出现的模式、我自己的复现步骤、以及踩过的坑,全部摊开讲。
先说结论方向,免得你看到一半才发现不是你要的:压缩本身不是问题,问题是压缩的粒度和记忆的检索时机不匹配。后面会展开。
2. 核心机制拆解:压缩、记忆、接续三者怎么打架
2.1 上下文压缩到底压掉了什么
很多人以为压缩就是"把老对话删掉"。实际不是。主流编码代理的压缩策略大致分三档,我在这 10 天里都遇到过:
- 滑动窗口:保留最近 N 轮,更早的直接丢。简单粗暴,但丢掉的可能是最开始定的架构决策。
- 摘要压缩:让模型把前面的对话总结成一段话,用摘要替代原文。省 token,但摘要会丢细节,尤其是具体的变量名、文件路径、函数签名。
- 分层保留:系统提示、任务目标、最近几轮保留原文,中间过程压成摘要。这是目前相对聪明的做法。
关键点在于:摘要压缩丢的恰恰是编码任务最要命的东西——精确的标识符。模型可以记住"我们决定用一个工具函数处理日期格式化",但记不住那个函数叫formatDateV2还是fmt_date。等它下次要调用时,就会瞎猜一个名字,然后报错,然后开始怀疑人生。
我在记录里统计过,压缩触发后前 3 轮内出现"引用不存在的符号"这类错误的概率,明显高于压缩前。这不是玄学,是摘要天然的信息损失。
2.2 long_mem 和 engram 各自想解决什么
long_mem的思路是:把重要信息写到外部存储(文件、向量库、键值对),需要时再检索回来。它的优势是容量几乎无限,劣势是检索时机难把握——你什么时候去查?查什么关键词?查回来的东西怎么塞回上下文?
engram的思路更接近神经科学的"记忆痕迹":把关键状态编码成一个紧凑的、可快速激活的表示,压缩发生时它不参与压缩,始终挂在上下文里。优势是"永远在线",劣势是容量有限,你得决定什么够格进 engram。
这两者在实验里的表现差异很有意思。我观察到:
| 机制 | 压缩后接续成功率(我的记录) | 主要失效场景 |
|---|---|---|
| 纯滑动窗口 | 低 | 丢失早期架构决策 |
| 摘要压缩 | 中 | 丢失精确标识符 |
| long_mem 检索 | 中高 | 检索时机错、召回噪音 |
| engram 常驻 | 高 | 容量溢出后被迫淘汰 |
| long_mem + engram 组合 | 最高 | 配置复杂、调试成本高 |
注意这个表是我 430 条记录里的经验分布,不是严格对照实验,样本也有偏,别当成论文数据看。但趋势是稳的:组合方案最稳,但最贵。
2.3 "接不上"的本质是状态断裂
我把所有失败案例归了类,发现"接不上"从来不是单一原因,而是三种断裂的叠加:
- 符号断裂:压缩后代理不知道某个函数/变量/文件的确切名字。
- 意图断裂:代理忘了"我们为什么要做这件事",于是做出和原目标矛盾的操作。
- 进度断裂:代理不知道"已经做到哪了",重复劳动或者跳过步骤。
long_mem 擅长补符号断裂(能检索到具体名字),engram 擅长补意图断裂(目标常驻),而进度断裂最麻烦——它需要一份持续更新的"任务清单",这既不是纯记忆也不是纯上下文,得单独设计。
提示:如果你只做一件事来改善接续,优先做"进度锚点"——一个始终在上下文里的、极简的任务状态列表。它的性价比远高于堆记忆容量。
3. 我的实操方案:从零搭一套能接上的记忆层
3.1 环境与工具准备
我用的是 Codex 这类 CLI 编码代理作为被测对象,配合一个自建的记忆服务。整体架构不复杂:
- 代理本体:CLI 形态,支持自定义系统提示和工具调用。
- 记忆存储:本地文件 + 一个轻量向量检索(我用的是最朴素的 TF-IDF 加关键词匹配,没上重型向量库,因为编码场景里精确匹配往往比语义匹配更管用)。
- engram 层:一个固定大小的 JSON 状态文件,每次压缩前强制刷新。
为什么不上重型向量库?实测下来,编码任务里你要找的往往是"那个叫 xxx 的函数",关键词匹配的召回精度反而更高,而且延迟低、可解释。语义检索适合"我上次讨论的那个关于并发的东西",但这类需求在编码代理里占比不高。
3.2 关键参数怎么定
这里有几个我反复调过的参数,直接给参考值:
- engram 容量:控制在 800 到 1500 token。太小装不下关键状态,太大就失去"常驻"的意义,还会挤占正常上下文。
- 摘要触发阈值:上下文用到 70% 到 75% 时开始压缩,别等到 90%。留出缓冲,压缩过程本身也要消耗 token。
- 检索 top-k:每次检索返回 3 到 5 条。返回太多会引入噪音,代理反而被带偏。
- 进度锚点刷新频率:每完成一个子任务就刷新一次,不要按轮次刷新。
这些数字不是拍脑袋来的。engram 容量我是从"一个函数签名平均 20 token、一次任务平均涉及 40 个关键符号"倒推的,40 乘 20 是 800,加上目标描述留到 1500。你可以按自己项目的符号密度调整。
3.3 压缩前的强制刷新流程
这是整套方案里最关键的一步,我把它固化成了一个流程:
- 检测到上下文用量超过阈值。
- 暂停代理,不让它继续生成。
- 触发一次"状态提取":让代理自己总结当前任务目标、已完成步骤、待办步骤、涉及的关键符号。
- 把提取结果写入 engram 文件,同时把关键符号写入 long_mem 存储。
- 执行压缩。
- 压缩后,把 engram 内容重新注入上下文头部。
- 恢复代理。
第 3 步让代理"自己总结"而不是外部脚本总结,是因为代理对自身状态的理解比外部启发式规则准。但要注意,这一步本身也可能被压缩影响,所以必须在压缩之前做。
注意:状态提取的提示词要极其明确,要求输出结构化格式(比如固定字段的 JSON),否则代理会写一大段散文,注入回去反而占地方。
4. 430 条记录里反复出现的坑
4.1 检索时机错了,等于没检索
最常见的失败:代理在需要某个符号时,没有主动去查 long_mem,而是凭记忆瞎写。为什么?因为它不知道自己不知道。压缩后它以为上下文里有的就是全部。
解决办法是给代理一个强制的"符号校验"步骤:在生成任何函数调用前,先检查该符号是否在当前上下文或 engram 里,不在就去检索。这个检查我用工具调用的形式强制注入,不依赖代理自觉。
4.2 engram 溢出后的淘汰策略
engram 满了怎么办?我试过三种淘汰策略:
- FIFO:淘汰最早的。结果把任务目标淘汰了,灾难。
- LRU:淘汰最久未用的。稍微好点,但任务目标可能一直"没被用"而被淘汰。
- 优先级:任务目标和进度锚点永不淘汰,其余按 LRU。这个最稳。
优先级策略需要你给每条 engram 记录打标签,稍微麻烦,但值得。我现在的做法是:目标类、进度类记录标记为 pinned,符号类记录标记为 normal,其余为 low。
4.3 摘要压缩的"幻觉继承"
这个坑很隐蔽。摘要压缩时,如果摘要本身写错了(比如把formatDateV2写成formatDate),这个错误会被后续所有轮次继承,而且代理会越来越确信它是对的。我在记录里见过代理连续 8 轮调用一个根本不存在的函数,就因为第一次摘要写错了名字。
对策:摘要生成后,对其中出现的所有符号做一次校验,校验不通过就重新生成摘要,或者把符号原文附在摘要后面。宁可摘要啰嗦一点,也别让它丢符号。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 代理重复改同一文件 | 进度断裂 | 检查进度锚点是否刷新 |
| 调用不存在的函数 | 符号断裂 | 检查摘要是否丢符号 |
| 做出与目标矛盾的操作 | 意图断裂 | 检查 engram 是否被淘汰 |
| 检索回来的内容不相关 | 检索时机或关键词错 | 检查触发检索的条件 |
| 压缩后立刻报错 | 注入格式不对 | 检查 engram 注入位置 |
| 代理"假装"记得 | 幻觉继承 | 校验摘要里的符号 |
这张表我贴在显示器边上,出问题先对一遍,能省不少时间。
5. 一些不那么显然的经验
5.1 压缩不是越晚越好
直觉上你会想"能拖就拖,少压缩几次"。但实测下来,在 70% 左右主动压缩,比拖到 90% 被动压缩,接续成功率高不少。原因是主动压缩时你有余裕做状态提取,被动压缩往往是系统硬触发,来不及保存状态。这就像写文档,随手存盘的人永远比等到崩溃才想起没保存的人少丢东西。
5.2 记忆层要"可读"
我坚持把 long_mem 和 engram 都存成人类可读的格式(JSON、Markdown),而不是二进制或纯向量。为什么?因为调试时你能直接打开看代理到底记住了什么。有次代理行为诡异,我打开 engram 一看,发现它把"删除临时文件"记成了"删除所有文件",差点酿成大祸。可读性救了我。
5.3 别指望一次配置到位
这套东西我调了 10 天,前 3 天基本在试错。记忆层的参数、提示词、注入位置,每个都要根据你的代理和任务类型微调。430 条记录里,真正"一次就对"的配置几乎没有。所以建议你留出调试时间,别指望抄个配置就完事。
5.4 组合方案的成本要算清楚
long_mem 加 engram 组合确实最稳,但它带来的复杂度和调试成本是实打实的。如果你的任务大多是短会话(不需要压缩),那这套东西纯属过度设计。先判断你的场景是否真的会触发压缩,再决定要不要上记忆层。我见过有人给一个每次只跑 5 分钟的任务配了全套记忆,纯属浪费。
6. 后续可以怎么扩展
这套方案目前只解决了"单会话内压缩后接续"。跨会话的记忆延续我还没深入做,但思路是现成的:把 engram 持久化到磁盘,下次会话启动时按项目路径加载对应的 engram。这样同一个项目的多次会话就能共享记忆。
另一个方向是多代理协作时的记忆同步。如果两个代理同时改一个项目,各自的 engram 会冲突。我初步的想法是搞一个共享的进度锚点,但还没实测,这里就不展开了,免得误导。
最后分享一个我踩坑踩出来的小技巧:给 engram 加一个版本号和时间戳。有次我调试时改了 engram 格式,结果旧格式的文件被新代码读取,代理直接崩了。加了版本号之后,读到不匹配的版本就自动重建,省心很多。这种小细节,文档里不会写,但实际用起来能救命。