YAOTU INSIGHTS

上下文压缩后AI编码代理如何快速恢复状态:long_mem与engram实践

上下文压缩后AI编码代理如何快速恢复状态:long_mem与engram实践
1. 这个实验到底在折腾什么先把背景说清楚。AI 编码代理coding agent这类工具比如 Codex 这类命令行形态的助手本质上是把「读代码、改代码、跑命令、看结果」这一整套动作串成一个循环。它靠的是把当前会话的上下文——包括你的指令、它读过的文件、它执行过的命令输出——全部塞进模型的输入窗口里让模型基于这些信息决定下一步。问题就出在这个「塞」字上。一个稍微像样的重构任务代理可能要读十几个文件、跑几十条命令上下文很快就撑爆了模型的窗口上限。这时候系统会触发上下文压缩context compaction把前面一大段历史浓缩成一段摘要丢掉原始细节腾出空间继续干活。压缩本身没问题问题是压缩之后代理经常「接不上」。我见过太多这种情况压缩前它明明已经定位到src/core/parser.ts第 142 行有个边界条件没处理压缩后它转头又去重新 grep 一遍整个项目甚至改错了文件。这不是模型笨是压缩把它的「工作记忆」给洗掉了。这个实验就是冲着这个痛点去的。作者用了 10 天时间跑了 430 条公开记录想搞清楚一件事上下文压缩之后怎么让 AI 编码代理快速恢复状态、接着往下干而不是从头再来一遍。适合谁看如果你在用 Codex 这类 CLI 编码代理或者自己在搭基于大模型的编码工作流又或者你只是好奇「长记忆」在工程上到底怎么落地这篇都值得往下读。核心关键词就三个上下文压缩、AI 编码代理、long_mem。engram 是实验里用到的一个记忆结构思路后面会展开讲。2. 为什么压缩之后会「断片」原理拆解2.1 上下文窗口的物理限制与压缩的必然性大模型的上下文窗口是有硬上限的。哪怕现在动辄 128K、200K token 的窗口放到真实的编码任务里也扛不住。原因很简单一个中等规模的 TypeScript 项目光node_modules之外的源码就可能几十万行代理每读一个文件就是几千 token 进去每跑一次npm test失败输出又是几百上千 token。一个任务跑下来累积的原始上下文轻松突破窗口上限。所以压缩不是「可选优化」是「必须动作」。主流做法是当 token 用量达到窗口的某个阈值常见是 70%~85%时触发一次摘要让模型把之前的对话历史浓缩成一段结构化文本保留「目标、已完成、待办、关键文件路径、关键结论」丢掉具体的代码片段和命令原始输出。这里有个关键取舍压缩率越高省的空间越多但丢的信息也越多。压得太狠代理就彻底失忆压得太松没跑几步又得再压一次陷入「压缩-膨胀-再压缩」的死循环。实验里 430 条记录很大一部分就是在测这个平衡点。2.2 「断片」的三种典型表现我把实验记录里代理压缩后的失败模式归了三类这个分类对后面设计恢复策略很关键重复劳动型压缩后代理忘了自己已经读过哪些文件重新把同样的文件再读一遍。浪费 token 是小事要命的是它可能读到一半又触发压缩永远在「读文件」阶段打转。目标漂移型压缩摘要写得太笼统比如只写了「修复解析器的 bug」但没写清楚是哪个函数、什么条件下触发。代理恢复后开始自由发挥改了一堆不相关的地方。状态错乱型代理忘了自己已经改过某个文件基于「未修改」的假设又改了一遍结果把之前的正确修改覆盖掉。这种最危险因为它会静默地破坏代码。注意这三种失败模式里状态错乱型的排查成本最高。因为它不报错代码还能跑只是行为悄悄变了。实验里专门有一组记录是在追踪这种「静默破坏」。2.3 为什么单纯「加大窗口」解决不了问题有人会说那就用窗口更大的模型不就行了。实测下来这条路走不通原因有两个。第一成本和延迟。窗口越大每次推理的输入 token 越多费用和响应时间都是线性甚至超线性上涨。编码代理是高频交互场景一次任务可能几十轮对话窗口翻倍意味着成本翻好几倍。第二也是更本质的长上下文里模型的有效注意力是衰减的。就算你把 200K token 全塞进去模型对中间部分的记忆和利用效率也会明显下降这就是常说的「lost in the middle」现象。所以哪怕窗口够大压缩精准恢复依然是更优解。这个实验的价值恰恰在于它不追求「不压缩」而是追求「压缩后能接上」。3. long_mem 与 engram记忆结构怎么设计3.1 long_mem 的核心思路把「工作记忆」外置long_mem 这个名字听着玄其实思路很朴素既然模型自己的上下文靠不住那就把关键状态存到模型外面需要的时候再喂回去。打个比方。你让一个实习生帮你改代码他记性不好干着干着就忘了前面干了啥。你有两个选择一是让他把所有东西都记脑子里加大窗口二是给他一个笔记本让他随时记、随时翻long_mem。显然后者更靠谱因为笔记本的容量几乎无限而且翻笔记本比「回忆」更精确。具体到实现long_mem 通常包含几个部分任务状态表当前目标、子任务列表、每个子任务的状态待办/进行中/完成。文件操作日志读过哪些文件、改过哪些文件、每个文件的关键改动点。关键结论缓存代理得出的重要判断比如「这个 bug 的根因在 X 函数」。失败记录试过但没成功的方案避免重复踩坑。这些内容以结构化格式通常是 JSON 或 Markdown 表格存在磁盘上压缩发生时摘要里只需要写一句「详细状态见 long_mem 存储」代理恢复时再按需读取。3.2 engram 的启发记忆痕迹的「强度」分级engram 这个词借用了神经科学里的概念指记忆在大脑里留下的「痕迹」。实验里借用这个思路给每条记忆加了一个强度权重而不是所有信息一视同仁。为什么这个设计重要因为压缩时如果对所有信息平等对待很容易把「关键但简短」的信息比如一个精确的行号和「冗长但不重要」的信息比如一段成功的测试输出一起压掉。加了强度分级后压缩策略可以变成记忆类型强度压缩策略任务目标与约束高原文保留不压缩关键文件路径与行号高结构化保留已完成的子任务结论中摘要保留命令原始输出低只保留成功/失败状态中间推理过程低丢弃这张表是整个实验里我觉得最实用的产出之一。它把「压缩」从一刀切变成了分级处理实测能显著降低断片率。3.3 为什么不用向量检索那一套你可能会想记忆检索不是有现成的向量数据库方案吗为什么实验里没走这条路。我的理解是场景不匹配。向量检索擅长的是「模糊语义匹配」比如你问「之前那个关于解析的问题」它能召回相关片段。但编码代理需要的是精确状态恢复它要知道「parser.ts第 142 行改没改过」这是个确定性问题不是相似度问题。用向量检索去回答确定性问题召回不准反而添乱。所以实验选择了结构化存储 显式读取的路子。代理在恢复时不是「搜一下相关记忆」而是「按固定 schema 读一遍状态表」。牺牲了一点灵活性换来了确定性。这个取舍在编码场景里是对的。4. 实操10 天实验是怎么跑起来的4.1 实验环境与工具链搭建先说环境。实验用的是 Codex 这类 CLI 编码代理作为执行主体跑在一个中等规模的 TypeScript 项目上。工具链大致是代理本体命令行形态的编码助手支持自定义系统提示和工具调用。记忆存储本地文件系统用 JSON 存状态表Markdown 存操作日志。压缩触发监控 token 用量达到窗口 75% 时触发。记录系统每条交互都落盘包括压缩前后的完整上下文这是 430 条记录的来源。搭建时有个坑要提醒压缩触发阈值不要设太高。我一开始设到 90%结果经常在压缩过程中又超限导致压缩本身失败。75% 是个比较稳的值留出足够 buffer 给摘要生成。4.2 记忆写入的时机与格式记忆不是随时写而是卡在几个关键节点写每完成一个子任务更新任务状态表把该子任务标记为完成记录关键结论。每次文件修改后追加一条操作日志格式是时间戳 | 文件路径 | 操作类型 | 改动摘要。每次压缩前强制刷新一次完整状态确保磁盘上的记忆是最新的。每次失败后记录失败方案和失败原因避免重复尝试。格式上我强烈建议用固定 schema 的 JSON不要用自由文本。自由文本在恢复时解析成本高而且容易歧义。比如任务状态表可以长这样{ goal: 修复 parser 在嵌套括号场景下的解析错误, subtasks: [ {id: 1, desc: 定位报错函数, status: done, conclusion: 根因在 parseExpr, line 142}, {id: 2, desc: 修复边界条件, status: in_progress, files_touched: [src/core/parser.ts]}, {id: 3, desc: 补充单元测试, status: todo} ], failed_attempts: [ {approach: 直接改 line 140 的判断条件, reason: 破坏了另一条分支} ] }这种结构代理读起来几乎零成本恢复速度快。4.3 压缩摘要的模板设计压缩摘要不能随便让模型自由发挥得给模板。实验里用的模板大致是四段式当前目标一句话必须包含具体的文件或函数名。已完成列表每条带结论。进行中当前正在做什么卡在哪。下一步明确的动作最好精确到文件和函数。提示模板里「下一步」这一项最容易被忽略但它恰恰是恢复后最需要的。没有它代理恢复后要重新规划又浪费一轮。4.4 恢复流程压缩后代理怎么「接上」恢复流程是整个实验的核心分三步读状态表代理恢复后第一件事不是看代码而是读 long_mem 里的任务状态表重建「我在哪、我要去哪」。校验文件状态对状态表里标记为「已修改」的文件快速 diff 一下当前磁盘状态确认没有被外部改动覆盖。续接下一步直接执行状态表里的「下一步」不再重新规划。这三步里第二步是很多人会漏掉的。实测下来加上文件状态校验后状态错乱型的失败率下降了大概六成。因为编码过程中你可能手动改过文件或者有别的进程动过不校验就会基于错误假设继续干。5. 430 条记录里踩出来的坑5.1 常见问题速查表把实验里高频出现的问题整理成表方便对照排查现象可能原因排查动作解决方向压缩后重复读同一文件操作日志没记录读取动作检查日志是否只记了修改读取也写入日志改错文件状态表文件路径不精确核对路径是否含目录路径写到完整相对路径目标漂移摘要目标写得太笼统看摘要是否含函数名强制模板含具体标识压缩频繁触发阈值设太高或摘要太长统计两次压缩间隔降阈值精简摘要恢复后不动下一步缺失检查摘要四段是否齐全补全下一步字段5.2 三个我踩过的真实坑坑一日志写入太频繁反而拖慢代理。一开始我让代理每读一个文件就写一次日志结果磁盘 IO 成了瓶颈而且日志文件膨胀得飞快。后来改成批量写入每完成一个子任务或每 5 次操作才落盘一次性能明显改善。教训是记忆写入要平衡实时性和开销不是越勤越好。坑二摘要里的「已完成」写成了流水账。早期摘要把每一步操作都列出来结果摘要本身就有上千 token压缩等于没压。后来改成只保留「结论级」信息比如「已定位根因」而不是「读了 A、B、C 三个文件后定位到根因」。摘要要的是结论不是过程。坑三恢复时读记忆的顺序错了。我一开始让代理先读操作日志再读状态表结果它被一堆历史细节带偏反而忘了当前目标。正确顺序是先读目标再读状态最后按需查日志。这个顺序调整后恢复准确率提升很明显。5.3 独家避坑技巧分享几个文档里不会写、但实测有效的技巧给记忆加版本号每次压缩前给状态表打个版本戳恢复时校验版本防止读到过期记忆。失败记录单独存不要和成功记录混在一起否则代理容易在恢复时被失败方案干扰。摘要里保留一个「锚点文件」指定一个当前最相关的文件路径代理恢复后第一件事就是打开它快速进入状态。定期清理低强度记忆engram 强度为「低」的记录超过一定轮次就归档避免记忆库无限膨胀。6. 这套方案能扩展到哪实验虽然只跑了 10 天、430 条记录但它的思路可以往外延。比如多代理协作场景每个代理有自己的 long_mem通过共享状态表来同步进度就能避免「两个代理改同一个文件」的冲突。再比如跨会话的长期项目把 long_mem 持久化到项目仓库里下次开新会话时直接加载代理就能「记得」上次干到哪。我个人在实际操作中的体会是上下文压缩本身不是问题问题是压缩时没有把「状态」和「信息」分开对待。信息可以丢状态必须留。long_mem 和 engram 的价值就是把状态从易失的上下文里抽出来变成可持久、可校验、可恢复的外部结构。这个思路一旦建立剩下的就是工程细节的打磨了。最后再分享一个小技巧如果你现在就在用编码代理不妨先手动做一件事——每次它压缩前让它用固定模板写一段状态摘要存到文件里。哪怕不做完整的 long_mem 系统光是这个习惯就能让断片率降一大截。