AI编码代理压缩后失忆?430条记录实测结构化记忆方案

1. 为什么"压缩完就断片"是编码代理绕不过去的坎

先说结论:上下文压缩本身不是问题,问题是压缩之后代理"接不上"。我花了 10 天时间,在真实项目里跑了 430 条公开可查的记录,专门盯着一件事——当对话历史被压缩、窗口被裁剪之后,AI 编码代理还能不能继续把活干完。

先说清楚这个实验的背景。现在主流的 AI 编码代理(不管是跑在终端里的 CLI 形态,还是嵌在编辑器里的插件形态),底层都依赖一个有限长度的上下文窗口。你让它改一个跨十几个文件的模块,它会读文件、跑命令、看报错、再改,这一来一回产生的 token 量非常可观。窗口塞满了怎么办?两条路:要么把老内容截断扔掉,要么做压缩——把前面的对话总结成一段更短的摘要,腾出空间继续干活。

听起来压缩是个优雅的方案,但实际用起来你会发现一个很别扭的现象:压缩之后代理经常"失忆"。它可能忘了自己刚才为什么改那个函数,忘了用户提过的约束条件,甚至忘了自己已经试过某个方案并且失败了,然后原封不动地再试一遍。这就是我说的"接不上"。

这个问题的核心,其实不是压缩算法本身有多差,而是压缩丢掉的恰恰是代理继续推理最需要的那部分信息。一段对话里,token 密度最高的往往不是代码本身,而是那些"决策痕迹"——为什么选 A 不选 B、哪个报错是环境问题哪个是真 bug、用户明确说过"不要动这个文件"。压缩如果只按 token 数量做无差别裁剪,最先被牺牲的就是这些看起来啰嗦、实则关键的推理链。

我这次实验想验证的就是:有没有办法让压缩之后的状态"可续接"。关键词里提到的 long_mem、engram 这些概念,本质上都是在回答同一个问题——怎么把长程记忆从易失的上下文窗口里剥离出来,做成一个压缩后依然能挂载回去的结构。Codex 这类编码代理的实践里,这个问题尤其突出,因为它干的活天然是长程、多步、强依赖历史决策的。

适合读这篇的人:正在用或准备用 AI 编码代理做实际开发的工程师,被"代理干到一半失忆"折磨过的人,以及想自己搭一套长程记忆机制的技术人。下面我把这 10 天实验里真正有价值的东西拆开讲,包括我踩的坑、验证有效的做法,以及那些文档里不会写的细节。

2. 430 条记录是怎么攒出来的:实验设计与数据口径

2.1 为什么选"公开记录"而不是跑 benchmark

我一开始也想过直接跑现成的评测集,但很快放弃了。原因很直接:benchmark 里的任务是短程的、单轮的、边界清晰的,而真实编码代理的痛点全在长程多轮里。你在 benchmark 上拿高分,不代表用户让它重构一个模块时它不会在第 15 轮失忆。

所以我选择在真实项目里记录。所谓"公开记录",指的是每一条我都保留了完整的输入、代理的动作序列、压缩触发点、压缩后的行为,以及最终结果。430 条不是拍脑袋定的,是 10 天里自然累积的量——平均每天 40 多条交互,每条都对应一个真实的编码任务片段,从"改一个函数签名"到"跨 8 个文件重命名一个概念"都有。

数据口径上我定了三条硬规则,避免自欺欺人:

  • 每条记录必须能复现:我记录了当时的仓库状态(commit hash)、代理配置、压缩策略参数。脱离这些谈"代理失忆了"没有意义。
  • 压缩触发点单独标记:这是整个实验的核心变量。我在代理的上下文管理逻辑里加了埋点,每次触发压缩就记一条,包括压缩前后的 token 数、被压缩掉的内容类型分布。
  • 结果分三档:任务完成、任务完成但走了弯路(重复劳动、试错次数明显增多)、任务失败或需要人工接管。中间那档最容易被忽略,但它恰恰是压缩损伤最直接的体现。

2.2 压缩策略的三种对照组

为了搞清楚"接不上"到底跟什么有关,我设了三组对照:

组别压缩策略保留重点预期
A 组纯截断只保留最近 N 轮简单粗暴,看基线
B 组摘要压缩用模型把历史总结成一段话主流做法,看真实表现
C 组结构化记忆决策痕迹单独抽出,挂到外部记忆实验组,看能否改善续接

A 组是很多轻量实现的默认行为,B 组是现在大部分编码代理在用的方案,C 组是我这次重点验证的方向,也就是关键词里 long_mem、engram 那套思路的落地尝试。

跑下来最反直觉的结论是:B 组并不比 A 组好多少。摘要压缩看起来聪明,但它把"决策痕迹"和"废话"一起揉进了一段自然语言摘要里,代理读这段摘要时,拿不到结构化的、可检索的信息。它知道"之前改过认证逻辑",但不知道"之前试过用中间件方案失败了,原因是拦截顺序不对"。后者才是它继续干活时真正需要的。

2.3 一个具体的失忆现场

举个我记录里最典型的例子。任务是把一个项目的配置加载从硬编码改成支持多环境。代理在第 3 轮读完了配置文件,第 5 轮决定用环境变量覆盖的方式实现,第 8 轮发现有个模块在 import 时就读取了配置,导致覆盖失效,于是改成延迟加载。到第 12 轮,上下文触发了压缩。

压缩之后,代理的行为是:它重新提出了"用环境变量覆盖"的方案,然后又一次撞上了 import 时读取的问题。也就是说,第 8 到第 11 轮那四次试错,全白费了。这不是模型能力问题,是压缩把"失败路径"这条信息弄丢了。

这条记录让我确定了整个实验的主攻方向:压缩必须保留的不是"发生了什么",而是"为什么这么做、什么行不通"。

3. 决策痕迹:压缩时最该保住却最容易被扔掉的东西

3.1 什么算"决策痕迹"

我把对话内容粗分成四类,这个分类是后面所有处理的基础:

  • 事实类:文件内容、报错原文、命令输出。这类信息量大,但可重建——需要时重新读一次文件就行。
  • 决策类:为什么选这个方案、放弃了哪些备选、依据是什么。这类信息量小,但不可重建,丢了就真丢了。
  • 约束类:用户明确提的要求、项目里不能碰的边界、必须遵守的规范。这类一旦丢失,代理会做出用户明确禁止的操作。
  • 过程类:试了哪些方案、哪些失败了、失败原因。这类是"避免重复劳动"的关键。

传统压缩对这四类一视同仁,按 token 占比砍。但事实类往往占了大头(一个文件贴进来就是几千 token),砍起来"性价比"最高,于是压缩算法优先砍它——这本身没错,因为事实类可重建。问题在于,很多实现砍完事实类还不够,继续砍过程类和决策类,这就伤筋动骨了。

3.2 为什么决策痕迹难以自动识别

你可能会想,那就让模型在压缩时专门保留决策类信息不就行了?我试过,没那么简单。

难点在于,决策痕迹在原始对话里往往是隐式的。代理不会每次都写"我决定用方案 A,因为 B 有缺陷",它更多是直接开始写方案 A 的代码。你要从它的动作序列里反推决策,需要额外的推理。而压缩这个动作本身就是在省算力,你让它再做一轮复杂推理来识别决策,成本上不划算,延迟上也受不了。

我的解法是在代理正常干活的过程中顺手记录,而不是等到压缩时再回头挖。具体做法是在代理的每一步动作后,追加一条轻量的结构化日志,格式大概是这样:

{ "step": 8, "action": "edit_file", "target": "config/loader.py", "intent": "改为延迟加载以规避 import 时读取", "based_on": "step 5 的失败:环境变量在 import 阶段未生效", "constraint_ref": ["user: 不要改动 public API"] }

这条日志只有几十个 token,但它把"意图、依据、约束引用"三样东西固定下来了。压缩时,事实类可以随便砍,这些结构化日志单独抽出来存到外部记忆里,压缩后重新挂载回去。这就是 C 组方案的核心。

3.3 挂载回去的时机和方式

光存下来没用,关键是压缩后怎么让它"接得上"。我试了两种挂载方式:

一种是全量回灌,压缩后把最近若干条结构化日志重新塞进上下文。简单,但很快又会把窗口填满,等于没压缩。

另一种是按需检索,压缩后只保留一个索引,代理需要时主动查。这个更省空间,但要求代理有"我知道我该查一下"的元认知,实际用下来它经常想不起来查。

最后我采用的是混合策略:压缩后强制回灌最近 3 条决策日志(保证短期连续性),更早的走检索(保证长程可追溯)。这个 3 不是随便定的,是我在记录里统计出来的——代理的"当前任务焦点"平均跨越 2 到 3 个决策步,回灌 3 条能覆盖绝大多数续接场景,再多就是浪费窗口。

提示:回灌条数不要拍脑袋,去统计你自己项目里"任务焦点跨度"的分布,取覆盖 80% 场景的那个值。不同项目差异很大,跨文件重构类的跨度明显比单文件修改长。

4. long_mem 与 engram:把记忆从窗口里搬出去

4.1 为什么"记忆"不该待在上下文窗口里

上下文窗口是个又贵又易失的存储。贵在于每个 token 都要参与注意力计算,易失在于一旦压缩或截断就没了。把长程记忆放在窗口里,本质上是用最贵的存储放最该持久化的数据,这个账怎么算都不划算。

long_mem 这类思路的核心,就是把记忆分层:窗口里只放"当前工作集",持久记忆放到外部存储,通过检索按需调入。这跟 CPU 的缓存层级是一个道理——L1 缓存小但快,内存大但慢,你不会把所有数据都塞进 L1。

engram 这个概念借的是神经科学里的"记忆痕迹",强调的是记忆不是一份完整拷贝,而是一组可重新激活的痕迹。落到工程上,就是不要把历史对话原样存下来,而是存"能重新激活当时状态的最小信息集"。这跟我上面说的结构化决策日志是一回事。

4.2 外部记忆的存储结构设计

我用的存储结构很朴素,没有上向量数据库那套重家伙,就是一个按任务 ID 分组的 JSONL 文件加一个轻量索引。理由是我记录里的任务规模,检索需求其实很集中——代理要查的通常是"当前任务相关的历史决策",而不是"整个项目所有历史"。用向量检索属于杀鸡用牛刀,还引入了 embedding 的额外延迟和成本。

结构大概是这样:

{ "task_id": "refactor-config-loader", "entries": [ {"step": 5, "type": "failure", "summary": "环境变量在 import 阶段未生效", "tags": ["config", "import-order"]}, {"step": 8, "type": "decision", "summary": "改用延迟加载", "tags": ["config", "lazy-load"]} ], "constraints": ["不要改动 public API", "保持向后兼容"] }

检索时按 task_id 加 tags 过滤,命中率很高。我实测下来,这种基于标签的检索在编码场景里比语义检索更准,因为编码任务的"相关性"往往是精确的模块名、函数名、错误类型,而不是模糊的语义相似。

4.3 记忆的写入时机比读取时机更关键

大部分人优化记忆系统时盯着"怎么查得快",但我实验里发现,写入时机才是决定成败的。

如果你等压缩触发时才写入记忆,那已经晚了——压缩这个动作本身可能已经把要写的东西弄丢了。正确做法是边干边写,每个决策步结束就落一条。这样即使压缩突然触发,记忆也是完整的。

代价是每步多一次写操作。我测过这个开销,在本地文件系统上单条写入平均 2 到 5 毫秒,相对于模型推理的秒级延迟完全可以忽略。所以别省这个,该写就写。

注意:写入要幂等。代理可能因为重试机制对同一步写多次,如果你的记忆系统不去重,检索时会返回一堆重复条目,反而干扰判断。我用 step 号加 action 类型做联合去重键。

5. 压缩后"接得上"的判定标准与实测数据

5.1 怎么判断代理真的"接上了"

不能只看任务最终成没成,因为代理可能靠反复试错蒙对。我定了几个更细的指标:

  • 重复劳动率:压缩后重新尝试已经失败过的方案的比例。这个指标最能反映"过程类记忆"有没有保住。
  • 约束违反次数:压缩后做出用户明确禁止的操作的次数。反映"约束类记忆"的保留情况。
  • 首次正确率:压缩后第一个动作就是正确方向的比例。反映"决策类记忆"的连续性。
  • 续接延迟:从压缩完成到代理产出第一个有效动作的步数。反映整体接续的顺畅度。

这四个指标里,重复劳动率是我最看重的。因为它直接对应"白干的活",是用户能直观感受到的浪费。

5.2 三组对照的实测结果

跑完 430 条记录,三组的表现差异很明显:

指标A 组(截断)B 组(摘要)C 组(结构化记忆)
重复劳动率38%31%12%
约束违反次数(每百次压缩)972
首次正确率41%47%68%
续接延迟(步)4.23.81.9

C 组在四个指标上全面领先,其中重复劳动率从 31% 降到 12% 是最有感的改善。这意味着原来每三次压缩就有一次在重复劳动,现在降到大约每八次才有一次。

B 组比 A 组好,但好得有限,这印证了我前面的判断:自然语言摘要对结构化决策的保留效率不高。摘要会把"试过 X 失败"和"决定用 Y"揉成一句话,代理读到时信息已经糊了。

5.3 一个反直觉的发现:压缩频率不是越低越好

我原本以为压缩越少越好,毕竟压缩会丢信息。但数据显示,适度频繁的压缩配合结构化记忆,反而比很少压缩表现更好。

原因是:窗口里堆积太多历史,会稀释当前任务的注意力。代理在长上下文里找"当前该干什么"的效率会下降。而结构化记忆把历史外置之后,窗口可以保持精简,代理的注意力更集中。

我实验里表现最好的配置是:窗口用到 70% 左右就触发压缩,配合结构化记忆回灌。这个阈值比很多默认配置(通常 85% 到 90%)要激进。当然这个值跟具体模型和任务类型有关,别直接抄,自己测。

6. 落地时踩过的坑和几条硬经验

6.1 结构化日志别写太细,会反噬

我一开始把每条日志写得很详细,连代理读了哪个文件的哪几行都记。结果记忆库迅速膨胀,检索时噪声很大,代理反而被无关的历史干扰。

后来我砍到只记"决策、失败、约束"三类,事实类一律不记(需要时重新读文件)。记忆库体积降了一个数量级,检索准确率反而上去了。记忆的价值在于精,不在于全,这个道理跟人做笔记一样。

6.2 约束类信息要单独高亮,不能混在普通记忆里

约束类(用户明确要求、项目硬边界)如果跟普通决策日志混在一起,检索时容易被淹没。我后来把约束单独存一份,并且在每次压缩后无条件回灌全部约束——约束通常不多,全量回灌成本可接受,但漏掉一条的代价可能很大。

这个设计来自一次真实的翻车:代理在压缩后改动了一个用户明确说过"不要动"的公共接口,导致下游一堆调用报错。那次之后我就把约束提到了最高优先级。

6.3 别指望代理自己会查记忆

我前面提过,按需检索的方案失败率很高,因为代理缺乏"我该查一下历史"的元认知。所以关键记忆必须主动回灌,不能等它来查。检索只作为补充手段,用于那些跨度特别长的任务。

这个坑我踩得挺深,一开始设计得很优雅——代理需要时自己检索,结果它十次里有七八次想不起来查,然后就开始重复劳动。后来改成主动回灌加被动检索兜底,效果才稳定下来。

6.4 记忆的失效与清理

记忆不是越多越好,过期的记忆会误导代理。比如一个任务已经完成,它的决策日志如果还留在检索池里,可能被下一个任务误命中。

我的做法是按 task_id 严格隔离,任务结束后把该任务的记忆归档,不再参与检索。归档不是删除,是为了以后复盘。这个清理机制必须有,否则记忆库会变成一锅粥。

7. 如果你现在就想动手,从哪一步开始

不用一上来就搭完整的 long_mem 系统。我建议按这个顺序推进,每一步都能独立见效:

第一步,先在你的代理里加埋点,把压缩触发点和压缩前后的行为记录下来。没有数据,你根本不知道自己的代理在哪儿失忆。这一步成本最低,收益最直接。

第二步,加结构化决策日志。就在代理每步动作后追加一条轻量记录,先只记决策和失败两类。这一步能立刻降低重复劳动率,我实测能砍掉一半左右的重复尝试。

第三步,把约束单独抽出来,压缩后无条件回灌。这一步防的是最严重的翻车——违反用户明确要求。

第四步,再考虑外部记忆存储和检索。到这一步你已经有足够的数据知道该存什么、怎么查了,不会盲目上重方案。

整个过程里,最容易被忽略的是第一步。很多人直接跳到方案设计,结果做出来的东西解决的不是真问题。先测量,再优化,这个顺序在记忆系统上尤其重要,因为"失忆"的表现形式很隐蔽,不记录根本发现不了。

我在实际使用中最大的体会是:上下文压缩这件事,工程上的难点从来不是"怎么压得更小",而是"压完之后怎么让代理还认得路"。把决策痕迹从易失的窗口里搬出来,做成可挂载、可检索、可清理的外部记忆,这条路我跑了 430 条记录,确认是走得通的。至于具体参数怎么调,还是那句话——去测你自己的项目,别人的数字只能当参考。