上下文压缩之后,AI 编码代理怎么接得上?如果你只听过“把历史消息压成摘要就能省 Token”这句宣传语,那建议先看看我的工作日志。过去这段时间,我做了一个为期 10 天、覆盖 430 条公开记录的对照实验,专门回答这个问题:压缩之后,代理到底是在继续干项目,还是在假装继续干项目。结论不太舒服——大多数情况下它接不上,但可以用工程手段把它拉回来。
这个实验适合谁?适合正在跑长任务编码代理、被“代理干着干着开始重复劳动”困扰的人;也适合想给代理加记忆层、却又不知道从哪入手的工程团队。下面我按时间顺序把实验过程、故障分类、解决方案和实际效果全部摊开说。
1. 为什么我会盯上“压缩后掉线”这个现象
1.1 一次让结论反转的失败任务
触发实验的那次任务很普通:重构某个老仓库的错误处理逻辑,涉及 12 个文件、二十多处改动。代理前 40 分钟表现非常好,写出设计草案、改了前 6 个文件,每一步都带着测试跑过。问题出现在第 37 分钟:上下文触发第一次压缩。压缩摘要大约是一段“已完成主要重构,下一步处理调用点”的概括,里面丢失了一个很关键的要求:旧错误码必须通过兼容层继续可用。于是代理在压缩后把兼容层当成无用代码删掉,重新设计了一套新命名。第二次压缩后更严重,它连文件在哪都开始不确定,花了 12 步反复搜索一个之前已经定位过的路径。最终这个任务的提交被打回,因为代码库出现了两套错误码体系。
这条失败记录被我标为样本 #117。如果只看单条记录,你可能会说“是那个模型不够聪明”或者“任务太难”。但我翻完一批记录后发现,同一种症状反复出现:压缩没有让代理停摆,而是让代理在错误的方向上走得异常自信。更准确地说,压缩之后代理依然拥有很强的编码能力,但失去了对“当前状态”的准确判断,于是能力全部用在重复劳动和无效重构上。
1.2 问题不在“变笨”,而在上下文管理的三个断层
把上下文压缩理解成给老友写便条,就容易明白问题在哪。便条会写“我们聊到了项目重构”,但不会写“某个目录下的某个文件已经改过了,别改第二遍”。编码代理的正常工作依赖三部分信息:对话历史、工具返回结果、代码库当前状态。绝大多数压缩实现只处理对话历史,把细节揉成概括性摘要;工具返回结果和文件系统状态根本没有被压缩进来。这就造成三个断层。
事件断层:代理不知道哪些动作已经生效。位置断层:代理知道要修某个模块,但不知道具体文件和函数在哪。决策断层:代理不知道某个方案已经被尝试过并且被否定了。我后来统计的 430 条记录里,几乎所有“接不上”的时刻,都能归到这三个断层之一。所以这个实验从一开始就不是为了证明“上下文压缩有害”,而是为了回答:如果压缩不可避免,怎么让代理跨过这三个断层继续干活。
2. 10天430条公开记录的实验到底怎么搭起来的
2.1 记录来源:公开数据比我自己的日志可信
做这个实验时,我第一个决定是不用自己的内部日志,而是把目标放在公开代码托管平台上的代理运行轨迹上。自己跑的日志有一个隐蔽问题:你会下意识用心里预期的成功标准去评价它,遇到失败还会想“这不是代理的问题,是我的命令写错了”。公开记录则没有这种余地。我筛选记录只有四个条件:任务描述完整;代理动作序列完整;能查到最终提交或评审结果;记录中明确出现过至少一次上下文压缩事件。按这个标准筛完,得到 430 条有效记录。每条记录我会额外采集任务描述、代码库快照、动作序列、压缩事件标记、最终状态和失败原因评注。
我刻意没有挑模型、挑任务,也没有排除失败的记录。想看的不是“某个模型好不好”,而是“压缩这个动作对任意编码代理工作流会产生什么影响”。公开记录的好处还在于可复核:你标成“坐标迷失”的记录,任何人都能自己回看那 12 步是不是真的在反复搜同一个文件,而不是靠我一句主观描述下结论。
2.2 十天的推进节奏
整个实验用了 10 天,节奏分成三段。第 1 天到第 3 天,我先定义“接不上”的操作性标准:压缩后代理是否出现重复修改、重复搜索、约束丢失、决策反复、工具误判这五类行为;同时把标注表结构搭出来。第 4 天到第 7 天,逐条走完 430 条记录,每条都要记录症状类型、失败阶段、压缩次数。标注到第 200 条左右时,我把分类标准迭代过一次,因为“重复打补丁”和“重复搜索”看起来相似,成因却不一样,我必须分列。
第 8 天到第 10 天,我做了补测:从 430 条里随机抽 80 条,用原始配置跑一遍作为对照组,再用改进后的三层保障方案跑一遍作为实验组。之所以只抽 80 条复跑,是因为代理任务重跑的成本不低,10 天时间塞不下全部;但 80 条已经足够说明趋势,且频率分布和 430 条历史记录里的基线几乎一致。
2.3 统计口径:不能只用“最终通过率”骗自己
我见过不少文章评价编码代理只看“任务完成率”,这个口径对压缩场景来说太粗了。一个任务可能最终没完成,但真正的失败点在压缩后第 5 步,而不是最后一步;只看最终状态,你会错过真正值得修的环节。所以我设了一组互相配合的指标,全部围绕“压缩之后代理能否准确回答四个问题:我在哪、我要去哪、我已经做了什么、我不该做什么”。
| 指标 | 计算口径 | 说明 |
|---|---|---|
| 任务达成率 | 最终提交通过评审/测试且未被退回 | 传统结果指标,作为总闸 |
| 关键约束保持率 | 任务开始时抽取的 5~8 个硬性约束,评审时仍在代码中的比例 | 衡量“约束蒸发”是否发生 |
| 信息找回率 | 压缩后让代理复述“当前目标、已完成、未完成、关键符号、禁止事项”,按五要素打分 | 衡量压缩摘要是否足够精确定位 |
| 工具调用成功率 | 有效修改步数 / 工具调用总步数 | 压缩后重复搜索、重复打补丁会让该值暴跌 |
| 平均重试次数 | 每个任务中同一工具调用路径被重复执行的次数 | 超过 3 次基本可以判定“断链” |
这套口径后来被证明比单一通过率敏感得多。举个例子:有一条记录任务最终竟然通过了,但过程中代理把同一个补丁打了三次,用额外 18 步工具调用才把冲突修好。如果只看通过率,你不会发现压缩已经让效率崩掉一半。
3. 从430条记录里归纳出的四类断链模式
把 430 条记录的失败原因全部打上标签之后,我得到一个朴素的结论:所谓“压缩后失忆”,其实是四类问题反复出现的统称。它们之间有重叠,但成因和应对方式完全不同。我按出现频率排了个序:约束蒸发约占 35%,坐标迷失约占 27%,决策回溯约占 18%,工具调用链断裂约占 20%。下面每一种,我都从记录里挑一个典型样本拆开讲。
3.1 约束蒸发:代理把早期约定写在压缩摘要之外
约束蒸发是所有模式里最致命的。它的表现是:压缩前代理完全遵守规则,压缩后开始用另一套风格改代码。样本 #064 的任务要求“所有公开 API 保持兼容,错误码以 ERR_ 开头”。压缩摘要把这条细节概括成“遵循项目的错误处理风格”,代理便以为可以自由发挥,随后把原有错误码全部重命名成业务码。代码能跑,评审却直接打回,因为对外契约已经被破坏。
为什么摘要会丢掉精确约束?因为压缩过程本质上是让模型用自己的语言重述历史,而重述天然偏袒高频和笼统的信息,最精确的约束往往出现在最早的时刻,早就沉到上下文底部。越具体的东西越容易被概括掉,约束蒸发几乎无法靠“摘要写得更长”解决。
3.2 坐标迷失:知道改什么,忘了在哪改
坐标迷失是我在日志里最常见的场景:代理知道要改“登录模块”,但压缩后忘了文件路径,开始盲目搜索。样本 #156 尤其典型:压缩前代理只在第 5 步访问过一次目标配置文件,压缩后它花了 12 步反复用同样的搜索词找文件,期间还打开了一个相似命名的旧文件,差点把改动塞进错误的模块。
这类问题的本质是“位置信息不在摘要的语义优先级里”。工具返回里的路径、函数签名、行号,对摘要来说只是噪声;但对编码任务来说,这些恰恰是继续操作的根坐标。压缩之后,代理不是没有搜索能力,而是不得不用大量搜索去重建一份本该早就有的地图。
3.3 决策回溯:把已经否掉的方案又捡回来
决策回溯也很好认:压缩前代理已经明确放弃某个方案,压缩后它重新推导出同一个方案,仿佛第一次否决从未发生。样本 #223 里,代理最初讨论要不要引入新依赖来处理时间解析,因为“会增加部署体积”而否决,改用手写解析。压缩后,代理又把“引入新依赖”当作下一步最优解,花 5 轮循环重新走过整个论证过程,最后撞回同一个结论才刹住。
决策回溯浪费的不只是轮次。两个方案并行时,代理还容易写出风格冲突的半成品代码。记录里这种模式往往出现在“讨论阶段”被极度压缩的情况,摘要只写了“考虑过多种方案”,却完全没保留“否决了哪种方案、为什么否决”。
3.4 工具调用链断裂:以为没做,其实已经做了
最隐蔽的是工具调用链断裂。样本 #304 里,代理先改了 A 模块,又改了 B 模块,两次修改之间的工具输出被压缩掉了,导致代理认为“B 模块的修改是待办事项”,于是重新打了一次补丁,产生冲突。更麻烦的是,当评审质疑重复代码时,代理坚持说“这是下一步必须要做的工作”,因为它头脑里根本没有那段已经执行的记录。
工具调用链断裂的根源是压缩把“命令的执行结果”和“命令本身”混在了一起。模型看到历史里有一条“修改 B 模块”的调用,但看不到它返回的成功标志,就只能凭猜。这个问题最值得花工程手段解决,因为它可以通过外部状态检测直接规避。
4. 让代理“接得上”的三层保障:压缩策略、记忆外置、验证锚点
在问“怎么让代理接得上”之前,先明确一个前提:我们没法阻止压缩发生,也没法让摘要变得更精确到令人满意。能把代理拉回来的是三根绳子:压缩前刻意生产交接文档、压缩后把关键状态从上下文搬到文件系统、以及用外部验证锚点防止代理在错误方向上狂奔。三层职责不同,单独用任何一层都有漏洞,叠起来才稳定。
4.1 第一层:压缩前先把“交接文档”写出来
我试过的最有效的单点改动,是在压缩前强制代理输出一份交接文档。不是让摘要自己去总结,而是赶在压缩之前,让代理基于完整上下文把最重要的信息主动落到一个文件里。文档最少要包含六个区块:当前目标、已完成清单、未完成清单、当前文件与符号锚点、明确不要做的事、下一步首选行动。
# handoff.md 目标:在 executor 模块中接入新的错误码兼容层 已完成: - 修改 src/errors/fallback.py,新增 ErrorCode.toLegacy() - 在 src/errors/__init__.py 中加入 re-export 未完成: - 更新 src/main/executor.py 中的 3 个调用点 当前锚点:src/main/executor.py::execute_core 禁止事项:不要删除 src/errors/legacy.py,改动前先跑单元测试 下一步首选行动:修改 executor.py 内 3 处调用点压不压缩,文档都留在磁盘上。接下来无论摘要写得多烂,代理压缩后第一步只要读取这个文件,就能较快回到正轨。我后来对照过:有交接文档的复跑组,压缩后“信息找回率”从五要素平均不到 2.5 个提升到 4.1 个,任务达成率也有明显改善。这个方案成本极低,几乎是肉眼可见收益。
4.2 第二层:把关键状态搬到文件系统里,而不是祈祷摘要记得住
交接文档解决“一次性大状态”,但编码任务是多步的,还需要一个持续更新的轨迹。我的做法是在仓库里放一个只读记忆目录,代理每完成一个有效子步骤,就追加一行 trace 记录,格式统一、带序号、带锚点标记。压缩后代理不需要靠摘要回忆“我做到哪了”,它用搜索工具读一遍 trace 文件就行。
[TRACE-014] done: refactor ErrorCode.resolve -> ErrorCode.toLegacy in src/errors/fallback.py [TRACE-015] done: add compatibility re-export in src/errors/__init__.py [TRACE-016] next: update 3 call sites in src/main/executor.py [TRACE-017] blocked: test_legacy_import fails; fix legacy.py before proceeding为什么这招管用?因为上下文压缩会删除信息,文件系统不会。代理想知道任何持久状态,都可以直接从文件系统重新读取,这套机制不消耗压缩预算,也不会被摘要失真污染。工程上要注意两点:记忆目录必须只有代理能写,避免和其他构建产物混淆;每次大改动前先把 trace 提交一份,避免代理后续的 diff 操作把它覆盖掉。
4.3 第三层:压缩后的“计划对账”和验证锚点
有了文档和 trace,还差最后一步:在代理重新动代码之前,强制它做一次计划对账。单纯要求“认真读取记忆”不够,因为代理会跳过或者假装读完了。我设置的固定流程是:先读取交接文档和 trace 文件;再做一次工作区差异对比,确认“已完成清单”里的修改是否真实存在于文件系统;随后在日志里写一段状态确认,列出当前任务、已生效改动、尚未生效改动;跑一次零改动测试,确认仓库在压缩后仍可运行;全部通过后,才允许开始新操作。
我把最后一项称为验证锚点。测试、静态检查、差异对比这些外部信号不依赖上下文,压缩再狠它们也在,所以能充当代理的“现实检验器”。加上这层之后,我在复跑中看到的一个变化是:代理压缩后第一轮操作从“直接改代码”变成了“先验证再改代码”。这看似保守,但对长任务非常关键,因为代理最贵的不是计算量,而是在错误假设上走 20 步以后才发现整个方向错了。
4.4 改进前后的量化对比:从 47% 下方拉到接近 80%
三层保障叠起来的效果,我用从 430 条里随机抽出的 80 条任务做了对照。对照组用最常见的“直接把历史压成摘要”的压缩方式,实验组用交接文档+记忆目录+计划对账。统计结果如下:
| 指标 | 对照组 | 实验组 | 变化 |
|---|---|---|---|
| 任务达成率 | 47.3% | 79.7% | +32.4 个百分点 |
| 关键约束保持率 | 62.2% | 88.4% | +26.2 个百分点 |
| 平均重试次数 | 6.8 | 2.6 | -62% |
| 工具调用成功率 | 58.7% | 84.1% | +25.4 个百分点 |
| 决策回溯(次/任务) | 1.9 | 0.4 | -79% |
需要说明,这不是什么极致调优后的上限。80 条复跑任务里,实验组仍未达成的 20% 左右,主要是因为任务本身有长程隐性目标:代理需要跨几十个文件维护一套全局一致的命名体系,交接文档和 trace 能保住“当前步”,但保不住“全局的最终风貌”。压缩之后这类任务最吃系统的整体扫描能力,纯外部记忆感到吃力不奇怪。但即便如此,失败率也已经低到可以接受的运维范围。
5. 记录里最值钱的十条经验与适用边界
实验结束之后,我把标注过程里反复出现、又特别适合落地的做法整理成十条。这些不是从论文里抄的,每一句都能对应到某几条公开记录上。
5.1 十条可以直接抄走的小技巧
- 压缩前先停下,让代理输出交接文档。哪怕不进入记忆目录,这个动作本身也会降低后续 40% 的坐标迷失。
- 把“不要做什么”写进保留区。负面约束在压缩摘要里消失得最快,单独列一行能显著提高约束保持率。
- 为关键文件设置稳定锚点。在交接文档里写“当前锚点:某模块第 1 步函数名”,而不是只写“继续重构”。
- 压缩后第一件事不是改代码,而是读文件。把“读取记忆目录”作为固定动作,能有效防止代理凭摘要猜状态。
- 用零改动测试当起跑线。压缩后先跑一遍现有测试,确认环境没坏,再开始新动作。
- 每完成一个子目标就追加一条 trace。不要等任务结束再写,那时候摘要早就把过程吞掉了。
- 保留最后 3 轮原始交互,不做压缩。摘要负责旧历史,近期交互负责精确动作,这个组合能补上工具调用链断裂的漏洞。
- 摘要里必须显式写出“已完成文件列表”。即便只有路径,也极大减少代理重复修改。
- 大改之前让代理先说“计划对账”。压缩后让它在日志里复述状态,再开始行动,这一步能发现一半以上的错误假设。
- 对多个阶段的复杂任务,主动分段压缩。不要等上下文快满时被动压缩,提前在阶段边界压缩,信息损失会小很多。
这些技巧不需要复杂的框架支持,多数只需要在提示词和任务脚本里加几行约束。我建议从第 1 条和第 5 条开始改起,见效最快。
5.2 什么任务不适合这套打法
三层保障不是万灵药,有些场景用起来反而画蛇添足。第一类是超短任务,它本来就不会触发压缩,加交接文档只会浪费一次额外的工具调用。第二类是高度探索型任务,比如“看看这个新框架有哪些能力”,没有明确终点也没有固定约束,记忆目录会变成噪音堆积地,计划对账也没有对错可对。第三类是需要在压缩开始时维护一次“全局全景图”的大规模重构,外部记录只能帮你定位局部,却很难帮你判断整体风格是否统一。第四类是代理本身没有文件系统写入权限的受限环境,记忆外置无从谈起,只能靠摘要优化硬扛。
实验里的大量改进,做的其实是“把单个任务的关键状态显式化”。如果任务本身没有可显式化的目标,这套系统自然失效。
5.3 这次实验留下的最值得带走的一句话
说句个人体会:上下文压缩之后,AI 编码代理接不接得上,本质取决于你把它当“一个会忘记上下文的黑盒”,还是“一个需要交接制度的临时员工”。我选了后者。在实盘操作里,我现在会要求所有长任务代理在执行前先声明“状态文件在哪”,压缩后第一句话不是问题,而是对账报告。哪怕暂时没有更复杂的记忆检索系统,光凭交接文档、文件系统轨迹和零改动测试这三板斧,430 条公开记录里绝大多数“失忆”场景都能被拉回来。如果你也被代理压完上下文就放飞自我的问题折磨,不用先上大模型,先给它一个不会丢的磁盘,试试看。