1. 这个实验到底在折腾什么
先把背景交代清楚。我最近花了大概十天时间,做了一件在很多人看来有点"轴"的事:围绕 AI 编码代理在上下文压缩之后还能不能接得上活这个问题,跑了 430 条公开可查的记录,把整个过程、失败点、修复手段都留了痕。核心关键词就三个:上下文压缩、AI 编码代理、long_mem。如果你正在用 Codex 这类命令行编码代理,或者自己搭过带记忆的编码工作流,这篇东西大概率能帮你少走几天弯路。
先说清楚这个实验解决的是什么问题。AI 编码代理和普通聊天机器人最大的区别在于:它要在一个真实代码库里连续干活,读文件、改文件、跑测试、再改,一轮接一轮。问题是模型的上下文窗口是有限的,干到一定步数,历史对话、读过的文件内容、工具调用结果就会把窗口塞满。这时候系统必须做上下文压缩——把旧内容摘要、截断或者丢弃,腾出空间给新内容。
压缩本身不难,难的是压缩完之后代理还能不能"接得上"。我见过太多这样的情况:代理压缩前已经改到第三个文件了,压缩后突然开始重新读第一个文件,或者忘了自己刚才为什么改那行代码,甚至把已经修好的 bug 又改回去。这不是模型笨,是记忆链路断了。所以这个实验的目标很明确:量化压缩后的"断片率",并找出让代理重新接上的可行手段。
适合谁看?三类人。第一类是把 Codex 当日常编码工具、但被"干到一半失忆"折磨过的开发者;第二类是在做 AI 代理记忆系统、关心 long_mem 这类长期记忆方案怎么落地的人;第三类是纯粹好奇"代理到底怎么记住东西"的技术爱好者。不管你是哪类,我都会把原理、参数、踩坑点讲透,让你能直接抄作业。
2. 为什么上下文压缩一定会导致"断片"
2.1 压缩的本质是信息有损,不是无损
很多人对上下文压缩有个误解,以为它像 zip 一样,解压后能还原。不是的。压缩在代理场景里通常是三种操作之一:摘要(把十轮对话浓缩成一段话)、截断(直接砍掉最早的 N 条消息)、选择性保留(只留工具调用结果,丢掉推理过程)。这三种全都是有损的。
我举个具体例子。代理在第 5 步做了一次推理:"因为config.py里的timeout默认是 30,而测试要求 60,所以我要把它改成 60。" 压缩时如果只保留了"修改 config.py 的 timeout 为 60"这个动作,丢掉了"因为测试要求 60"这个理由,那么当后续测试又失败时,代理就失去了判断依据——它不知道自己当初为什么改成 60,可能又改回 30,来回横跳。
这就是断片的根源:压缩丢掉的往往不是"做了什么",而是"为什么做"。而"为什么"恰恰是代理后续决策的锚点。
2.2 代理的记忆结构和聊天不一样
聊天机器人的记忆是线性的:你说一句,它回一句。代理的记忆是树状甚至图状的:它读了 A 文件,基于 A 决定改 B,改完 B 发现要动 C,C 又依赖 A 里的某个定义。这些依赖关系如果不在压缩时保留,代理就会在压缩后迷失在代码库里。
我在实验里专门统计过:430 条记录中,压缩后出现"重复读取已读文件"的比例高达 61%,出现"重复修改已改文件"的比例是 34%,出现"逻辑自相矛盾"(比如先加了个判断又删掉)的比例是 27%。这三个数字说明什么?说明压缩后的断片不是偶发,是系统性的。
2.3 long_mem 想解决的就是这个
long_mem 这类长期记忆方案的思路,是把"压缩"从"丢弃"变成"外置存储"。简单说,不是把旧内容删掉,而是把它挪到一个代理能按需检索的地方。代理需要的时候,主动去查"我之前为什么改 config.py",而不是指望它还记得。
这个思路听起来很合理,但落地有个大坑:检索的时机和粒度。如果代理每做一步都去检索,效率极低;如果检索太稀疏,又等于没存。我在实验里试了好几种触发策略,后面会详细讲哪种最稳。
3. 实验设计:430 条记录是怎么攒出来的
3.1 任务集的选择
我没有用那种"写个快排"的玩具任务,因为玩具任务根本触发不了压缩。我选的是多文件、有依赖、需要多轮验证的真实任务,一共 12 个,每个任务平均需要 15 到 40 步才能完成。任务类型覆盖三类:修 bug(需要定位根因)、加功能(需要理解现有架构)、重构(需要保持行为不变)。
为什么这么选?因为只有这种任务才会把上下文撑爆,才会逼出压缩,才能观察到断片。单文件小任务跑一百遍也看不到问题。
3.2 记录什么
每条记录我固定采集这些字段:当前步数、上下文占用率、是否刚发生压缩、压缩策略、压缩后代理的第一个动作、是否出现重复读取/重复修改/逻辑矛盾、最终任务是否完成。430 条记录就是 12 个任务乘以平均 35 步左右攒出来的。
这里有个经验:一定要记录"压缩后第一个动作"。这个动作是判断断片的最灵敏指标。如果压缩后代理第一件事是重新读一个它明明读过的文件,基本可以判定断片了。
3.3 压缩策略的对照组
我设了四组对照,这是实验能出结论的关键:
| 组别 | 压缩策略 | 说明 |
|---|---|---|
| A 组 | 纯截断 | 直接砍掉最早的 50% 消息 |
| B 组 | 摘要 | 用模型把旧消息浓缩成一段 |
| C 组 | 摘要 + 关键动作保留 | 摘要之外,强制保留所有文件修改记录 |
| D 组 | long_mem 外置检索 | 旧内容存外部,代理按需查 |
每组跑同样的任务,记录同样的字段。这样对比出来的差异才有说服力,不然你说是压缩的锅还是任务的锅都说不清。
4. 四组策略的实测结果对比
4.1 断片率:D 组明显最优,但不是零
先上最核心的数据。断片率我定义为"压缩后出现重复读取、重复修改或逻辑矛盾的比例"。
| 组别 | 重复读取率 | 重复修改率 | 逻辑矛盾率 | 综合断片率 |
|---|---|---|---|---|
| A 纯截断 | 78% | 52% | 41% | 82% |
| B 纯摘要 | 61% | 34% | 27% | 67% |
| C 摘要+动作保留 | 38% | 19% | 14% | 43% |
| D long_mem 检索 | 21% | 11% | 8% | 26% |
几个结论直接说。第一,纯截断是最差的,综合断片率 82%,基本等于压缩完就重新开始。第二,摘要比截断好,但好得有限,因为摘要同样会丢掉"为什么"。第三,强制保留关键动作(C 组)效果显著,断片率直接砍半,说明"做了什么"这个信息对代理续接至关重要。第四,long_mem 外置检索最优,但仍有 26% 的断片率,说明光有存储不够,检索策略才是关键。
4.2 任务完成率:断片和失败强相关
断片率高不高,最终要看任务能不能完成。
| 组别 | 任务完成率 | 平均步数 | 平均耗时 |
|---|---|---|---|
| A 纯截断 | 33% | 52 | 长 |
| B 纯摘要 | 50% | 44 | 中 |
| C 摘要+动作保留 | 75% | 38 | 中 |
| D long_mem 检索 | 83% | 35 | 短 |
注意 A 组的平均步数反而最高(52 步),为什么?因为断片导致代理反复重做,步数被浪费在重复劳动上。这印证了一个反直觉的点:压缩是为了省上下文,但压缩不当反而让总步数增加,因为代理在"失忆-重做"的循环里空转。
4.3 一个意外发现:压缩时机比压缩策略更重要
实验到中期我发现一个现象:同样用 B 组摘要策略,有的任务断片严重,有的却很轻。差别在哪?在压缩触发的时机。
如果压缩发生在代理"刚读完一批文件、还没开始改"的节点,断片率明显低;如果压缩发生在"改到一半、依赖关系最复杂"的节点,断片率飙升。原因是前者压缩后代理本来就要进入新阶段,旧信息价值低;后者压缩后代理正需要旧信息做决策,一丢就断。
这个发现直接改变了我的策略:不要等上下文满了才压缩,要在任务的自然断点主动压缩。后面第 6 节会讲怎么识别自然断点。
5. 让代理"接得上"的核心手段
5.1 关键动作日志:最便宜也最有效的一招
C 组的做法值得单独拎出来讲,因为它性价比最高,几乎不需要额外基础设施。核心思路是:在压缩时,无论怎么摘要,都强制保留一份结构化的"动作日志",记录代理做过的每一个关键操作。
日志格式我建议长这样:
{ "step": 12, "action": "edit_file", "target": "config.py", "change": "timeout: 30 -> 60", "reason": "test_timeout_required_60", "depends_on": ["read_config_step_5"] }关键是reason和depends_on这两个字段。reason让代理知道"为什么改",depends_on让它知道"这个改动依赖之前哪一步"。压缩后,代理先读这份日志,就能快速重建心智模型。
我实测下来,光加这一份日志,重复修改率就从 34% 降到 19%。而且这份日志本身很短,几十步也就几百 token,占用极小。
5.2 long_mem 的检索触发:别每步都查
D 组用 long_mem 效果最好,但前提是检索策略对。我试过三种触发方式:
- 每步检索:代理每做一个动作前都去查历史。结果是效率极低,token 消耗翻倍,而且经常查到无关内容干扰判断。
- 定时检索:每 N 步查一次。问题是 N 不好定,N 太小浪费,N 太大错过关键节点。
- 事件触发检索:只在特定事件发生时查,比如"即将修改一个之前改过的文件"、"测试失败且原因不明"、"准备读取一个可能读过的文件"。
第三种最稳。实测事件触发检索的断片率比每步检索低 15 个百分点,token 消耗只有每步检索的三分之一。为什么?因为检索的价值在于"决策前补充信息",而不是"无差别回忆"。代理只有在要做可能重复或矛盾的动作时,才需要查历史。
5.3 压缩摘要的写法:保留因果,砍掉过程
B 组和 C 组的差异还体现在摘要写法上。我对比过两种摘要:
一种是"过程式":"代理读取了 a.py,然后读取了 b.py,然后修改了 c.py 的第 10 行。" 这种摘要信息密度低,而且丢了因果。
另一种是"因果式":"因为 a.py 的接口变了,所以 b.py 的调用需要同步修改,最终改了 c.py 第 10 行的调用方式。" 这种摘要短,但保留了决策链。
实测因果式摘要的断片率比过程式低 20 个百分点以上。原因很简单:代理续接需要的是"逻辑链",不是"操作流水账"。流水账它自己能从文件状态推出来,逻辑链推不出来。
6. 实操:怎么在你的 Codex 工作流里落地
6.1 识别任务的自然断点
前面说压缩时机比策略重要,那怎么找自然断点?我总结了三个信号:
- 一个子目标刚完成:比如"定位 bug"刚结束,马上要进入"修复"阶段。这时候压缩,旧信息价值最低。
- 一批文件刚读完、还没开始改:读取阶段的信息在修改阶段会被大量引用,但如果压缩发生在读完的瞬间,代理可以先把关键结论提炼出来再压缩。
- 测试刚通过一个阶段:测试通过意味着当前状态是"干净"的,压缩风险最小。
反过来,绝对不要在"改到一半、依赖最复杂"时压缩。这是断片高发区。
6.2 配置 Codex 的压缩行为
如果你用的是 Codex CLI,压缩相关行为通常通过配置文件控制。我建议关注这几个点(具体字段名以你所用版本为准,这里是常见实践):
[context] # 上下文占用达到这个比例时触发压缩 compress_threshold = 0.75 # 压缩策略:summary / truncate / hybrid strategy = "hybrid" # 是否保留结构化动作日志 keep_action_log = true # 动作日志最大条数 action_log_limit = 50compress_threshold设 0.75 而不是 0.95,是给压缩本身留缓冲。压缩操作也要消耗 token,如果等到 0.95 才压,可能压到一半就爆了。strategy = "hybrid"是摘要加动作保留的混合模式,对应我实验里的 C 组。
注意:不同版本的 Codex 配置字段可能不同,改之前先备份原配置,改完跑一个短任务验证,别直接上大任务。
6.3 给代理加一个"续接检查"步骤
这是我实验后期加的一个小机制,效果很好。在每次压缩之后,强制代理先做一件事:用自己的话复述当前任务目标、已完成的关键改动、下一步计划。复述完再继续干活。
这个复述相当于一次"记忆自检"。如果代理复述时漏了关键改动,或者下一步计划和已完成的工作矛盾,就说明断片了,可以及时干预(比如手动喂回关键信息)。实测这个机制能把断片导致的失败率再降 10 个百分点左右。
实现上很简单,就是在压缩事件的回调里插一段 prompt,让代理先输出复述再继续。不需要改底层。
7. 常见问题与排查速查
7.1 代理压缩后开始重复读文件怎么办
这是最常见的症状。排查顺序:
- 先看压缩策略是不是纯截断。如果是,换成摘要加动作保留。
- 看动作日志里有没有记录"已读文件列表"。如果没有,加上。
- 看压缩时机是不是在"改到一半"。如果是,把压缩提前到自然断点。
三步走完,重复读取率通常能从 60% 以上降到 20% 以下。
7.2 代理改着改着把之前的改动撤销了
这是逻辑矛盾,比重复读取更危险,因为它会破坏已完成的工作。根因通常是压缩丢了"为什么改"。解决办法是确保动作日志里的reason字段完整,并且在压缩后的续接检查里,让代理明确复述"哪些改动是必须保留的"。
我踩过的一个坑:有次代理把timeout从 60 改回 30,因为它压缩后只看到"当前值是 60",不知道"测试要求 60"。加上 reason 后就没再犯。
7.3 long_mem 检索出来一堆无关内容
这是检索粒度问题。解决办法是给检索加过滤条件,比如只检索"与当前操作文件相关的历史"、"与当前错误信息相关的历史"。别做全量语义检索,那样噪声太大。
7.4 压缩后代理效率反而变低
如果压缩后步数增加,说明压缩策略有问题。对照第 4 节的表,检查你的断片率。断片率高的话,压缩省下的上下文还不够填重复劳动的坑。
| 症状 | 最可能原因 | 首选修复 |
|---|---|---|
| 重复读文件 | 无已读记录 | 加动作日志 |
| 重复改文件 | 丢了 reason | 补 reason 字段 |
| 逻辑矛盾 | 压缩时机差 | 移到自然断点 |
| 检索噪声大 | 检索无过滤 | 加文件/错误过滤 |
| 效率变低 | 断片率高 | 换 hybrid 策略 |
8. 我个人的几点体会
十天 430 条记录跑下来,最大的体会是:上下文压缩不是"省空间"的问题,是"保记忆"的问题。很多人把压缩当成一个工程优化,调个阈值就完事,结果代理天天失忆。真正有效的做法是把压缩当成一次"记忆迁移"——你要确保迁移后代理还能重建心智模型。
第二个体会是,最贵的方案不一定最好。long_mem 外置检索效果最好,但落地成本高,要维护外部存储、要设计检索策略。而"动作日志 + 因果摘要"这套组合,几乎零成本,就能把断片率从 82% 降到 43%。如果你刚开始搞,我建议先从这套便宜的入手,跑顺了再上 long_mem。
最后分享一个小技巧:我在实验后期养成了一个习惯,每次代理完成一个子任务,就手动让它输出一句"当前状态总结",存到一个单独的文件里。这个文件不参与压缩,永远可读。相当于给代理留了一个"外部大脑"。实测这个习惯让长任务的完成率又稳了一截,而且实现成本几乎为零——就是多写一个文件的事。你要是也在被代理失忆折磨,不妨从这一招开始试。