1. 这个实验到底在折腾什么
先把背景交代清楚。AI 编码代理(AI Coding Agent)这类工具,比如 Codex 这类命令行形态的编码助手,本质上是把大模型塞进一个能读写文件、执行命令、跑测试的循环里。它跟你聊天不一样,它要在一个会话里连续做几十甚至上百步操作:读文件、改代码、跑测试、看报错、再改。问题就出在这里——模型的上下文窗口是有限的。
你让它干一个稍微大点的活,比如重构一个模块、修一个跨文件的 bug,它读进来的文件内容、命令输出、历史对话会迅速把上下文塞满。塞满之后怎么办?绝大多数工具的做法是上下文压缩(context compaction):把前面的历史总结成一段短摘要,丢掉原始细节,腾出空间继续干活。
这个思路听起来合理,但实际用起来你会发现一个很别扭的现象:压缩之后,代理好像"失忆"了。它忘了自己刚才改了哪个文件、为什么这么改、之前试过什么方案失败了。于是它开始重复劳动,或者做出跟前面决策矛盾的操作。这个实验就是冲着这个问题去的——上下文压缩之后,AI 编码代理怎么接得上?
实验周期 10 天,累计 430 条公开记录。这个量级不算大,但足够看出一些规律。我把它拆成几个层面来讲:为什么会断片、断片的具体表现、有哪些接续策略、每种策略的实测效果,以及我自己在复现过程中踩到的坑。
适合谁看?如果你正在用或者准备用 Codex 这类编码代理做实际项目,尤其是那种单次任务超过二三十步的活,这篇内容对你有直接参考价值。如果你只是拿它写写小函数,可能感受不深,但了解一下机制也没坏处。
关键词里出现了 long_mem 和 engram,这两个是实验里涉及的长记忆机制方向。long_mem 顾名思义是长时记忆,engram 这个词借用了神经科学里"记忆痕迹"的概念,指的是把关键信息以某种持久化形式存下来,供后续会话或压缩后的会话调用。这两个是解决"接不上"问题的核心抓手,后面会展开。
2. 上下文压缩为什么会造成"断片"
2.1 压缩的本质是有损摘要
要理解断片,先得理解压缩干了什么。假设代理已经跑了 40 步,上下文里堆了这些东西:
- 用户最初的指令("把这个模块的同步 IO 改成异步")
- 它读过的 8 个文件的完整内容
- 它执行的 15 条命令及其输出
- 它做过的 6 次文件修改
- 中间几次失败的尝试和报错
压缩的时候,工具会调用模型把这些内容总结成一段几百字的摘要。摘要里通常保留"目标是什么""大致做了什么""当前状态如何",但会丢掉大量细节:具体改了哪一行、某个报错的确切信息、某个文件里那个关键的变量名。
这就像你让一个同事接手你干到一半的活,你只给他留了张便利贴:"在改登录模块,改成异步了,还没测完。"他拿到这张纸条,能接着干吗?勉强能,但大概率会重新读一遍代码,甚至重新踩一遍你已经踩过的坑。
2.2 断片的三种典型表现
430 条记录里,压缩后的异常行为大致能归成三类,我按出现频率排:
| 表现类型 | 具体现象 | 占比(实验记录粗估) |
|---|---|---|
| 重复劳动 | 重新读取已读过的文件、重新执行已跑过的命令 | 最高 |
| 决策矛盾 | 推翻自己之前的修改,或采用与之前冲突的方案 | 中等 |
| 目标漂移 | 逐渐偏离原始任务,开始做无关的优化 | 较低但危害大 |
重复劳动最好理解,也最容易被忽略——因为它不报错,只是慢。代理压缩后忘了自己读过某个文件,又读一遍,token 和时间都浪费了。决策矛盾更麻烦,比如它之前决定用方案 A 因为方案 B 有兼容性问题,压缩后忘了,又切回方案 B,结果引入 bug。目标漂移最隐蔽,代理干着干着开始"顺手"重构别的代码,最后交付的东西跟你要的偏了。
2.3 为什么单纯加大窗口不是答案
有人会说,那就用大窗口模型,别压缩不就行了。实测下来这条路走不通,原因有两个。
第一,窗口再大也有上限,而且成本和延迟随上下文长度非线性增长。一个任务跑到 100 步以上,再大的窗口也会满。第二,就算窗口够大,长上下文本身会带来"注意力稀释"——模型在超长上下文里对中间部分的关注度下降,这跟压缩造成的丢失是两码事,但结果类似,都是"记不住关键细节"。
所以压缩是绕不开的,问题不是"要不要压缩",而是"压缩之后怎么把关键信息接回来"。这就是 long_mem 和 engram 这类机制要解决的事。
3. 接续策略的四种思路与实测对比
实验里试了不止四种,但能形成清晰对比、有复现价值的主要是下面这四类。我按"实现成本从低到高"排。
3.1 策略一:结构化摘要模板
最朴素的做法,是约束压缩时生成的摘要格式。不让模型自由发挥写一段话,而是强制它按固定字段填:
任务目标: ... 已完成: ... 当前文件状态: ... 关键决策及原因: ... 待办: ... 已知坑: ...这个模板的关键在于"关键决策及原因"和"已知坑"两栏。普通摘要往往只记"做了什么",不记"为什么"和"什么不行"。而代理断片最致命的恰恰是丢了这两类信息。
实测效果:重复劳动明显减少,因为"已完成"栏让它知道自己读过哪些文件。但决策矛盾只改善了一部分,因为摘要终究是摘要,字段填得再全,细节还是会丢。这个策略胜在零额外依赖,改个 prompt 就能上,适合作为基线。
3.2 策略二:外部记忆文件(long_mem 思路)
这是 long_mem 方向的核心做法:不把记忆塞进上下文,而是写到外部文件里,需要时再读回来。
具体操作是让代理在干活过程中维护一个MEMORY.md或者.agent/memory.json,每完成一个关键步骤就追加一条记录:改了什么文件、为什么改、验证结果如何。压缩发生时,上下文里的历史被丢掉,但外部文件还在。压缩后代理被指示先读这个记忆文件,把状态接回来。
这个思路的好处是记忆不受上下文窗口限制,理论上可以无限长。坏处是它依赖代理"自觉"去写和读,如果它忘了写,或者压缩后忘了读,机制就失效了。实验里为了降低这种依赖,把"写记忆"和"读记忆"做成了工具调用,在流程里强制插入,而不是靠模型自觉。
3.3 策略三:engram 式关键片段锚定
engram 这个思路更精细一点。它不记录全部历史,而是识别出"记忆痕迹"——那些对后续决策有决定性影响的片段,把它们原样保留,而不是摘要。
哪些算关键片段?实验里总结了几类:
- 用户原始指令的原文(不能摘要,一摘要就变味)
- 失败的尝试及其报错原文(防止重蹈覆辙)
- 接口签名、数据结构定义这类"契约"信息
- 当前正在编辑的文件的完整内容
这些片段被"锚定"在上下文里,压缩时跳过它们。剩下的可压缩部分再摘要。这样上下文里始终保留着一小撮高价值原文,接续时就有据可依。
实测下来这个策略对"决策矛盾"的改善最明显,因为失败原因和契约信息都是原文保留的。代价是它占用的上下文比纯摘要多,压缩频率会略高。
3.4 策略四:分层记忆 + 检索召回
最复杂的一种,把记忆分成三层:
- 工作层:当前正在处理的文件、最近的命令输出,全量保留
- 会话层:本次会话的结构化摘要,压缩时生成
- 长期层:跨会话的项目知识,持久化存储,按需检索
压缩后,代理先从会话层恢复本次任务状态,再根据当前操作从长期层检索相关历史。检索用简单的关键词匹配或向量相似度都行,实验里用的是关键词加文件路径匹配,够用。
这个策略效果最好,但实现成本也最高,需要一套检索逻辑和存储管理。适合把它做成团队内部工具的场景。
3.5 四种策略横向对比
| 策略 | 实现成本 | 重复劳动改善 | 决策矛盾改善 | 目标漂移改善 | 适用场景 |
|---|---|---|---|---|---|
| 结构化摘要模板 | 极低 | 明显 | 部分 | 弱 | 快速上手、个人使用 |
| 外部记忆文件 | 低 | 明显 | 明显 | 中等 | 单会话长任务 |
| engram 锚定 | 中 | 明显 | 最明显 | 中等 | 决策密集任务 |
| 分层记忆+检索 | 高 | 明显 | 明显 | 明显 | 跨会话、团队协作 |
实验里最终采用的是"结构化摘要 + engram 锚定"的组合,因为它在成本和效果之间平衡得最好。外部记忆文件作为补充,分层检索只在跨会话场景才启用。
4. 实操:把接续机制搭起来
这一节讲具体怎么落地。我按实验里的配置复现了一遍,把关键步骤和参数记下来。
4.1 环境与工具准备
实验用的是 Codex 这类命令行编码代理。安装和配置这块,网上教程很多,但有几个点容易卡住,我列一下。
安装完成后,配置文件通常在用户目录下的隐藏文件夹里。配置项里有个常见的报错是codex is ignoring 1 unrecognized configuration setting,意思是配置文件里有个它不认识的字段被忽略了。这不影响运行,但说明你的配置项名字写错了或者版本不匹配,建议对照当前版本的文档核对字段名。
另一个高频问题是模型不支持,报错类似the 'gpt-5.6-sol' model is not supported when using codex with a...。这通常是模型名写错,或者该模型没有开放给当前接口。解决办法是换成配置里明确支持的模型名,别自己臆造。
提示:配置改动后建议先用一个最小任务跑通,比如让它读一个文件并总结,确认代理能正常启动和调用工具,再去跑长任务。长任务跑到一半发现配置有问题,排查成本高得多。
4.2 结构化摘要模板的落地
在代理的系统提示或项目级指令文件里,加入压缩摘要的格式约束。核心是让它在生成摘要时按固定字段输出。我用的模板大致是这样:
## 任务状态摘要 - 原始目标: <用户最初要求的原文,不要改写> - 已完成步骤: <逐条列出,含文件名> - 当前编辑文件: <路径 + 当前状态> - 关键决策: <决策内容 + 原因> - 失败尝试: <尝试内容 + 报错原文> - 待办事项: <下一步计划>注意"原始目标"这一栏要求保留原文。这是 engram 思路的体现——用户指令一旦被摘要改写,很容易丢失约束条件,比如"不要引入新依赖"这种要求,摘要时经常被漏掉。
4.3 engram 锚定片段的识别规则
哪些内容必须原样保留、不参与压缩,需要一套明确规则,否则模型自己判断会不稳定。实验里用的规则:
- 用户消息的原文,全部保留
- 最近一次失败的完整报错,保留
- 当前编辑文件的完整内容,保留
- 接口定义、类型声明、配置文件的关键段落,保留
- 其余历史,可压缩
这套规则可以直接写进代理的指令里。实测下来,规则越具体,模型执行越稳定。模糊的"保留重要信息"这种说法基本没用,模型对"重要"的判断跟你不一致。
4.4 外部记忆文件的读写时机
如果采用外部记忆文件,关键是确定什么时候写、什么时候读。实验里的做法:
- 写:每完成一个"原子步骤"(一次文件修改 + 一次验证)后追加一条
- 读:每次压缩发生后,强制读一次
- 清理:任务结束时归档,不删除,供跨会话检索
记忆文件的格式用 JSON Lines 比较方便,一行一条,追加写入不用重写整个文件:
{"ts": "day3-1420", "action": "edit", "file": "src/auth.js", "reason": "改同步为异步", "result": "测试通过"} {"ts": "day3-1435", "action": "fail", "cmd": "npm test", "error": "timeout in auth.spec.js:42"}这种格式的好处是机器好解析,人也好读。压缩后代理读这个文件,几行就能把状态接回来。
4.5 参数与阈值的选择
压缩什么时候触发,这个阈值要调。设得太早,压缩频繁,接续开销大;设得太晚,上下文快满了才压缩,容易触发失败。
实验里用的经验值是:当上下文使用率达到70% 到 75%时触发压缩。留出 25% 左右的空间给压缩后的接续操作和后续几步。这个值不是死的,任务越复杂、单步输出越大,越应该提前压缩。
另外,engram 锚定片段的总长度要设上限,否则锚定太多等于没压缩。实验里控制在上下文窗口的20% 以内。超了就按优先级砍,优先级从高到低:用户原文 > 当前文件 > 失败报错 > 契约信息。
5. 常见问题与排查实录
这部分是实验记录里最有价值的部分,都是实际跑出来的问题。
5.1 压缩后代理"假装记得"
一个很隐蔽的现象:压缩后你问代理"你刚才改了什么",它能答上来,但答的是摘要里的内容,不是真实细节。比如摘要写"修改了认证逻辑",它就说"我修改了认证逻辑",但你追问"改的哪个函数",它就编一个。这种"假装记得"比明确说"我忘了"更危险,因为它会基于错误记忆继续操作。
排查方法:压缩后让它复述当前编辑文件的具体内容,或者让它指出某个关键函数的位置。如果答得含糊或者跟实际不符,说明接续没成功,需要手动干预,把关键信息重新喂给它。
5.2 记忆文件写入失败或重复
外部记忆文件偶尔会出现写入失败,尤其是在代理同时操作多个文件的时候。还有一种情况是重复写入,同一步骤记了两遍,导致读回来时状态混乱。
解决办法是给记忆写入加一个简单的去重:写入前检查最后一条记录,如果 action 和 file 都相同且时间接近,就跳过。这个逻辑可以放在工具层做,不用依赖模型判断。
5.3 锚定片段挤占过多空间
engram 锚定用久了,锚定片段会越积越多,因为"当前文件"会变,旧文件如果没及时释放,就一直占着。实验里踩过这个坑,跑到后面上下文里全是历史文件内容,压缩等于没压。
规则是:锚定片段要跟着"当前编辑文件"走,切换文件时释放旧的。失败报错也是,同一个错误解决后就释放,不用一直留着。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 处理方式 |
|---|---|---|
| 压缩后重复读同一文件 | 摘要未记录已读文件 | 检查摘要模板"已完成"栏 |
| 决策前后矛盾 | 关键决策原因丢失 | 启用 engram 锚定决策记录 |
| 代理答非所问、编造细节 | 接续失败,基于摘要臆测 | 手动重喂关键信息 |
| 记忆文件读回状态混乱 | 重复写入或写入失败 | 加去重逻辑,校验写入 |
| 压缩后上下文仍很满 | 锚定片段未释放 | 检查锚定释放规则 |
| 配置报 unrecognized setting | 字段名错误或版本不符 | 对照版本文档核对 |
| 模型不支持报错 | 模型名错误或未开放 | 换用配置支持的模型名 |
5.5 几条踩坑心得
第一,别指望模型自觉。所有"应该写记忆""应该读记忆"的环节,都要做成流程里的强制步骤,靠提示词约束的可靠性远不如靠工具调用约束。
第二,摘要模板的字段宁少勿多。字段太多,模型填的时候会敷衍,反而丢信息。实验里从最初的 9 个字段砍到 6 个,效果反而更好。
第三,压缩阈值要按任务类型调。纯代码修改类任务可以晚点压,因为输出相对规整;涉及大量命令输出和报错的任务要早点压,因为这类内容占空间快。
第四,测试接续机制本身要用长任务。短任务根本触发不了压缩,你测不出问题。实验里专门构造了一个需要 50 步以上的重构任务来验证。
6. 这套机制还能怎么扩展
实验结束后我一直在想,这套接续机制的价值不止于单个代理会话。几个可以往下走的方向:
一是跨会话记忆。现在记忆文件是会话级的,任务结束就归档了。如果把它做成项目级的长期记忆,下次开新会话时先加载,代理就能"记得"这个项目之前发生过什么。这对长期维护同一个代码库的场景很有用。
二是多代理共享记忆。如果同时跑多个代理处理不同模块,让它们共享一份记忆文件,就能避免互相踩脚。比如代理 A 改了接口,代理 B 通过记忆文件知道这个变更,就不会按旧接口写代码。
三是记忆的自动清理与压缩。记忆文件长期积累也会变大,需要一套归档和摘要机制,把久远的、不再相关的记录压缩掉,只留关键决策和契约信息。
engram 锚定的规则也可以更智能。现在是人工定规则,未来可以根据"这条信息后续被引用了几次"来动态判断重要性,被引用多的自动升级为锚定片段。
我个人在实际复现中的体会是,这套东西的核心不在于技术多复杂,而在于把"记忆"这件事从模型的隐式行为变成显式的、可检查的流程。模型记不记得住,你控制不了;但记忆文件写没写、锚定片段留没留,你能控制。把不可控的东西变成可控的,这才是接续机制真正的价值所在。