1. 为什么"压缩之后接不上"是个真问题
先把场景说清楚。现在用 AI 编码代理写代码的人越来越多,一个典型的工作流是这样的:你给它一个任务,它开始读文件、跑命令、改代码、看报错、再改,来回几十轮。这个过程中,对话历史会迅速膨胀——读进来的文件内容、命令输出、diff、报错栈,全是 token。等到上下文窗口快满了,系统就得做上下文压缩(context compaction):把前面几十轮的原始记录揉成一段摘要,丢掉细节,腾出空间继续干活。
问题就出在这儿。压缩本身不难,难的是压缩完之后代理还能不能"接得上"。我见过太多这样的情况:压缩前它已经定位到src/parser.ts第 142 行有个边界条件写错了,压缩后摘要里只剩一句"正在修复解析器的问题",然后它转头去读了一个完全不相干的文件,或者把已经改好的代码又改回去一遍。这不是模型笨,是压缩把"工作状态"和"任务状态"一起压没了。
这个实验的标题里给了两个关键数字:10 天、430 条公开记录。这说明它不是拍脑袋写的经验帖,而是有人真的跑了一段时间、把过程记录下来了。关键词里的long_mem、engram指向的是"长期记忆"这条技术路线,Codex则是当前最常被拿来讨论的编码代理之一。所以这篇要聊的核心就一件事:上下文被压缩之后,代理靠什么把断掉的线接回来。
适合谁看?如果你只是偶尔用 AI 补全几行代码,这篇对你意义不大。但如果你在搭 agent 工作流、在调编码代理的长任务表现、或者被"压缩后失忆"折磨过,那接下来的内容应该能对上你的痛点。我会把"为什么会断""断在哪""怎么接"这三件事拆开讲,中间穿插那个 430 条记录实验里能推断出的规律,以及我自己踩过的坑。
先说结论方向,免得你看到一半才反应过来:压缩后的接续能力,本质上不取决于摘要写得多好,而取决于你有没有在压缩之外单独维护一份结构化的"任务状态"。摘要是有损的,任务状态是精确的,两者分工不同。把这两件事混为一谈,是绝大多数"压缩后接不上"的根因。
2. 压缩到底丢了什么:三类信息的损耗差异
要解决问题,先得知道丢了什么。我把一次编码会话里的信息粗分成三类,它们在压缩中的损耗程度完全不一样。
2.1 事实性信息:文件路径、函数名、行号
这类信息最容易被摘要"糊掉"。原始记录里是src/parser.ts:142,摘要里很可能变成"解析器模块的某个位置"。对人类来说这无所谓,反正可以重新搜;但对代理来说,它下一步动作高度依赖精确坐标。它不知道确切位置,就只能重新 grep、重新读文件,一来一回又是几千 token,压缩省下来的空间瞬间被吃回去。
我在自己的实验里观察到一个规律:压缩后第一轮动作的"重复率"是最能说明问题的指标。如果代理压缩后第一件事是去读一个它压缩前已经读过的文件,基本可以判定这次压缩是失败的。430 条记录里,凡是出现这种"回头读"的,后续任务完成质量普遍偏低。
2.2 决策性信息:为什么这么做、排除了哪些方案
这类信息损耗最隐蔽,也最致命。压缩前代理可能已经试过方案 A 失败了,转而用方案 B。摘要里如果只写"正在实现功能",那代理压缩后很可能又去试方案 A。这就是典型的"决策失忆"。
决策信息的另一个麻烦是它往往不在文本里。代理"排除方案 A"这个结论,可能是从一次失败的测试输出里推断出来的,而那段输出在压缩时被当成"冗余日志"丢掉了。等它再看到类似的报错,它不会记得"这个错我见过,是方案 A 的锅",只会当成新问题重新排查。
2.3 意图性信息:用户到底想要什么
这类信息在长会话里会被反复稀释。用户最初说"帮我把这个接口改成支持分页",中间聊了十几轮细节,压缩后摘要可能只剩"修改接口"。代理于是开始自由发挥,加了一堆用户没要的东西。意图漂移是长任务里最常见的翻车方式,而且它往往在压缩之后才暴露出来。
下面这张表是我根据实验记录整理的,三类信息在不同压缩策略下的保留情况:
| 信息类型 | 纯摘要压缩 | 摘要+结构化状态 | 摘要+状态+关键原文锚点 |
|---|---|---|---|
| 事实性(路径/行号) | 大量丢失 | 基本保留 | 完整保留 |
| 决策性(方案取舍) | 几乎全丢 | 部分保留 | 较好保留 |
| 意图性(用户目标) | 易漂移 | 稳定 | 稳定 |
| token 开销 | 最低 | 中等 | 较高 |
这张表想说明的是:没有免费的午餐。你想让代理压缩后接得上,就得在压缩之外多花一点 token 去维护状态。关键是花得值不值——我的经验是,维护一份几百 token 的结构化状态,能省下压缩后几千 token 的重复探索,这笔账非常划算。
3. long_mem 与 engram:两条记忆路线的分工
关键词里出现的long_mem和engram,其实代表了两种不同的记忆思路,很多人把它们混着用,效果反而不好。我按自己的理解拆一下。
3.1 long_mem:跨会话的长期记忆
long_mem这类机制解决的是"这次会话和上次会话之间"的连续性。比如你昨天让代理重构了一个模块,今天接着让它加功能,它应该记得昨天的重构结论。这类记忆的特点是生命周期长、更新频率低、以事实和偏好为主。
它适合存什么?项目约定("这个仓库用 tabs 不用 spaces")、架构决策("状态管理统一走 store,不直接改组件")、用户偏好("报错信息要中文")。这些内容一旦写入,短期内不会变,压缩与否都不该影响它们。
它不适合存什么?当前任务的临时进度。把"正在改第 3 个文件"这种信息塞进 long_mem,只会污染长期记忆,下次会话读到一堆过期状态,反而添乱。
3.2 engram:当前任务的"记忆痕迹"
engram这个词本意是记忆痕迹,放在这个语境里,我理解它指的是当前任务内部的结构化状态快照。它和 long_mem 最大的区别是生命周期:engram 随任务生、随任务灭,任务结束就该清掉。
它存的是那种"压缩会丢、但丢了就接不上"的东西:当前改到哪个文件、已经验证过哪些假设、下一步计划是什么、有哪些已知的坑。这份东西不需要很长,但必须精确。
我自己的做法是给 engram 定一个固定 schema,大概长这样:
{ "task_goal": "给 /users 接口加分页,默认每页 20", "current_file": "src/routes/users.ts", "done": ["加了 page/limit 参数解析", "改了 SQL 查询加 LIMIT"], "verified": ["单页查询返回正确", "边界 page=0 已处理"], "next": ["补 total 字段", "加参数校验"], "known_issues": ["ORM 的 count 查询在空表时返回 null,要兜底"], "excluded": ["不用 offset 分页,数据量大时性能差"] }这份状态大概 150 到 250 token,压缩时原样保留、不参与摘要。实测下来,只要这份东西在,代理压缩后第一轮动作的准确率会明显提升。原因很简单:它不需要"回忆",它直接"读"到了当前状态。
3.3 两者怎么配合
分工原则一句话:long_mem 管"这个项目一直是这样",engram 管"这个任务现在到哪了"。压缩的时候,long_mem 不动(它本来就不在对话历史里),engram 原样保留,只有中间的原始对话被摘要替换。这样代理压缩后拿到的是:长期约定 + 精确任务状态 + 有损的过程摘要。三层信息各司其职,接续就稳了。
注意:很多人图省事,把 engram 也塞进摘要里让模型自己总结。这是大忌。模型总结状态时一定会丢精度,而状态恰恰是最不能丢精度的部分。状态必须由代码维护,不能交给模型"回忆"。
4. 430 条记录里反复出现的三种断线模式
那个实验最有价值的地方,是它把"接不上"这件事变成了可观察的现象。我把记录里反复出现的模式归成三类,你可以对照自己的日志看看中了几条。
4.1 回头读:压缩后重新探索已知区域
这是最高频的一种。表现是压缩后代理立刻去读一个压缩前已经读透的文件。根因通常是事实性信息丢失——摘要没保留精确路径,代理只能重新找。
排查方法很直接:在日志里标记每次压缩的时间点,看压缩后前 3 个动作里有没有"读已读文件"。如果有,说明你的摘要对事实性信息保留不足。修法也简单,在摘要模板里强制要求保留"当前正在编辑的文件路径"和"最近一次修改的位置"。
4.2 方案回退:重新尝试已被排除的路径
这种比回头读更隐蔽,因为它看起来"很正常"——代理在认真工作,只是方向错了。表现是它开始尝试一个压缩前已经验证失败的做法。根因是决策性信息丢失。
我在记录里看到过一个典型例子:代理压缩前已经确认某个第三方库的版本不兼容,改用内置方案了;压缩后它又开始研究怎么装那个库。这种回退浪费的不只是 token,还有时间,而且用户如果不盯着,很可能就让它一路错下去。
修法是把"已排除方案"作为 engram 的必填字段。每次代理明确排除一个方案,就写进去。压缩时这个字段原样保留。
4.3 意图漂移:做着做着跑偏了
这种最难发现,因为它不表现为"卡住",而表现为"过度发挥"。代理压缩后开始加用户没要求的功能,或者把简单需求做复杂。根因是意图性信息在长对话里被稀释。
防漂移的关键是把原始意图原文钉住。不是摘要,是原文。用户最初那句话,一字不改地放进 engram 的task_goal字段。摘要可以变,原文不能变。这样代理每次压缩后都能看到"用户原话是什么",漂移概率大幅下降。
下面这张表把三种模式和对应的修法对齐一下:
| 断线模式 | 典型表现 | 根因 | 修法 |
|---|---|---|---|
| 回头读 | 压缩后重读已读文件 | 事实性信息丢失 | 摘要强制保留路径与位置 |
| 方案回退 | 重试已排除方案 | 决策性信息丢失 | engram 记录 excluded 字段 |
| 意图漂移 | 加需求、做复杂 | 意图被稀释 | engram 钉住用户原话 |
5. 把接续做稳的实操配置
前面讲的是原理和现象,这一节讲怎么落地。我按"最小可用"到"进阶"的顺序给配置,你可以按自己的场景挑。
5.1 压缩触发点的选择
别等到上下文快满了才压缩。我的经验是在 70% 到 80% 用量时主动压缩,而不是被动等系统触发。原因有两个:一是主动压缩时你还能控制保留哪些内容,被动触发往往是最粗暴的截断;二是留出余量给压缩后的接续动作,否则刚压缩完又满了,等于白压。
具体阈值看模型窗口大小。窗口越大,触发点可以越靠后,因为你有更多空间做精细压缩。但无论如何,别贴着上限跑。
5.2 摘要模板的字段设计
摘要不能是自由发挥的一段话,得有固定字段。我用的模板大概是这样:
[任务摘要] 目标:<用户原话,一字不改> 当前文件:<精确路径> 最近修改:<文件:行号,改了什么> 已完成:<列表,每条带验证状态> 待办:<列表> 已知问题:<列表> 已排除方案:<列表,带排除原因>关键是每个字段都要求精确,不允许"某个文件""大概改好了"这种模糊表述。模板里明确写"路径必须完整""行号必须给出",模型才会照做。
5.3 engram 的更新时机
engram 不是压缩时才生成的,而是每次代理完成一个可验证的动作后就更新。比如它改完一个文件、跑通一个测试、排除一个方案,就立刻写进 engram。这样压缩时 engram 已经是最新的,直接拿来用就行。
这个"边做边记"的习惯,是整套方案里最反直觉但最重要的一环。很多人想着"等压缩时再总结状态",但压缩时模型已经在忙着压缩了,再让它总结状态,质量一定打折。状态维护应该是常态化的,不是压缩时的临时任务。
5.4 压缩后的第一轮动作约束
压缩完成后,别让代理自由发挥。给它一个明确的"接续指令":先读 engram,确认当前状态,然后从next字段的第一项开始。这一步能极大降低回头读和方案回退的概率。
我实测下来,加了这一步之后,压缩后第一轮动作的准确率提升非常明显。原理也简单:它不需要"猜"该干什么,engram 直接告诉它了。
提示:接续指令里可以加一句"如果 engram 与摘要冲突,以 engram 为准"。因为摘要有损,engram 精确,冲突时信精确的。
6. 几个容易踩的坑和我的处理方式
最后聊几个实操里真会遇到的坑,都是我自己或身边人踩过的。
坑一:engram 越写越长。一开始觉得多记点没坏处,结果 engram 膨胀到几千 token,压缩省的空间又被它吃回去了。后来我定了硬约束:engram 不超过 300 token,超了就砍done里的历史项,只留最近几条。历史完成项对"接续"其实没多大用,真正有用的是next和known_issues。
坑二:把 engram 当日志用。有人把每一步操作都写进 engram,变成流水账。这违背了它的定位——它是状态快照,不是操作日志。状态是"现在在哪",日志是"怎么走到这的"。压缩后代理需要的是前者。日志该丢就丢,别舍不得。
坑三:long_mem 和 engram 混在一个存储里。我早期图省事放一起,结果清理任务状态时误删了长期约定,代理突然开始用 spaces 缩进,排查半天才发现是记忆被清了。后来严格分开:long_mem 一个存储、按项目隔离、只增不删(除非明确废弃);engram 一个存储、按任务隔离、任务结束即清。
坑四:以为换了更大的窗口就不需要这套东西。窗口再大也有满的时候,而且窗口越大,压缩时的信息量越大,粗暴压缩的损失反而越惨。这套状态维护的思路,和窗口大小无关,是长任务的基本功。
坑五:忽略"验证状态"。engram 里done和verified要分开。改完不等于验证过。压缩后代理如果分不清哪些是"改完但没测"的,很可能在错误的基础上继续叠加。把验证状态标清楚,它能少走很多弯路。
这套东西我用了大概两三个月,最大的感受是:上下文压缩本身不是问题,"压缩后没有精确状态可依"才是问题。摘要负责压缩过程、保留大意,engram 负责钉住状态、保证接续,long_mem 负责跨会话的长期一致性。三者分工明确,长任务才不会在压缩点翻车。那个 430 条记录的实验,本质上也是在验证同一件事——接续能力是可以工程化解决的,前提是你别把希望全押在"摘要写得好"上。