我见过最抽象的一幕,是AI编码代理在对话接近二十万token时,系统弹出一条上下文压缩通知,紧接着它就像被按了记忆清除键——忘了自己正在改哪个文件,忘了我们两小时前定的架构方案,甚至开始一本正经地复述十轮前已经被否决的旧想法。说这是“脑叶切除术”一点不夸张:会话还活着,但记忆被切掉了。而且这种“手术”在长任务里几乎必然发生,只是早晚问题。
为了治这个病,我先后从两个完全不相干的地方偷师:一个是Unix体系的init.d启动机制,一个是电影《记忆碎片》里的外部记忆思路。最后给代理搭了一套简单的“记忆工作流”,实测下来,在上下文压缩发生之后,代理能在一轮对话之内恢复状态,继续干活。这篇文章就把这套东西的来历、结构、模板和踩坑记录完整拆开讲。
1. 那场“脑叶切除术”:AI在20万token边界的失忆实录
1.1 上下文压缩到底在压缩什么
先说清楚问题的根源。大多数AI编码代理的上下文窗口是128k到200k token。一旦对话累计超过这个配额,系统不可能把新消息直接丢掉,于是就会对已有历史下手。
不同产品的压缩策略不太一样。有的把最早的一部分对话整体替换成一段AI生成的摘要,有的直接以滑动窗口方式砍掉中间某段消息,还有的会给每条旧消息设置一个“衰减系数”,越老的对话权重越低,模型越“看不太清”。不管哪种方式,规律都一致:最早进入上下文的那些基础约束,被优先牺牲。
最讽刺的是,最先被压缩掉的往往就是最不能丢的东西——项目背景、用户偏好、架构选型理由、验收标准。这些信息通常是在对话前几千个token里反复确认过的,结果压缩发生时一个不剩。
1.2 失忆之后的典型症状
被压缩后的代理并不会知道自己失忆了。它会表现得非常自信,非常流畅,只是逻辑接不上。我总结了三个最常见的症状。
症状A是重复提问。“这个项目是做什么的?”“这几份文件的定位分别是什么?”——这些你在第一轮就回答过的问题,它会像新同事入职一样重新问一遍。症状B是新旧方案冲突。压缩前你刚定下来用某一种设计模式重构模块C,压缩后它基于残缺记忆提出了一套方向完全相反的改法,而且从它的视角来看一切都很正常。症状C是进度回溯。明明已经把第三个模块改完了,它恢复后问的第一句话是“第一个模块是不是还没处理完”。
遇到这种场面,最难受的不是要多花时间,而是你根本不确定它下一步会不会又忘一次。只要任务够大、对话够长,这个循环就无法避免。
1.3 为什么常规的“把话说重一点”不解决根本问题
一开始我尝试过在对话里反复强调关键约束,让代理多“复习”几遍。但token是有限的,每多强调一遍就多占一部分上下文空间,反而让压缩来得更快。我也试过把任务拆成多次会话,每次重新开一个对话,但那种做法等于让代理每次从胎教开始学起,上下文衔接全靠人工手动粘贴,成本和错误率都上去了。
这件事的本质,是人脑里短期记忆和长期记忆分工的问题。人类睡一觉就会把不重要的对话沉淀掉,但重要的事能靠海马体固化到长期记忆里。而AI编码代理没有这个机制,它的一切“记忆”都活在上下文窗口这只“临时容器”里。容器一破,一段都剩不下。
所以问题变成了:能不能给代理装一个外部装置,让它不依赖上下文也能恢复状态?
2. init.d给我的第一课:恢复应该是“可重入”的,不是“重新培养”的
2.1 init.d脚本为什么可以反复执行
先讲一个我特别喜欢的系统设计细节:Unix里的init.d脚本。你在Linux上跑过service即可,无论某个服务当前是启动还是停止状态,执行一遍start脚本,结果总是服务正常运行。这种性质叫幂等,或者叫“可重入”。
实现这个效果靠的是脚本里隐藏的检查步骤:脚本开头会先问“服务是否已经在跑?进程文件是否存在?配置文件是否已就位?”如果已满足,就直接跳过安装动作;如果缺失,才执行补齐动作。不管系统从哪种状态进来,跑完一遍脚本一定会到达目标状态。
这个哲学很有意思:它不假设系统“记得”什么,也不假设操作者“知道”刚才发生了什么。恢复的正确姿势不是从头推演,而是把状态文件拉出来看一眼,按约定顺序重放一遍。
2.2 第一代方案:我写过“记忆补丁”,但用处不大
受这个启发,我最初给代理设计了所谓的“记忆补丁”——一段很长的提示词,把项目背景、当前进度、下一步计划全部写进去,每次压缩后粘贴给代理。它确实能让代理瞬间恢复,但问题也明显:补丁是静态的,上一轮完成的新进度不会自动同步进去。每次做完一步,我都得手工把补丁改一遍,改漏一次,恢复效果就大打折扣。
而且“补丁”本质上仍然是在往上下文里塞信息。塞的越多,越占token,下一次压缩反而来得更快。这属于饮鸩止渴。
2.3 第二次设计:让代理每次进入会话时自己“重启”
后来我把思路倒过来:不再试图往上下文里塞历史,而是给代理一个固定的“重启流程”。每次新会话或压缩发生之后,代理不靠记忆回答“我们在做什么”,而是主动执行一段恢复动作——读取状态文件,重建项目认知,确认任务清单,然后才开始干活。
这就像服务器一开机,init.d自动把各个服务拉起来,不需要操作员回忆上次关机前手工启动了什么。代理被压缩一次,就相当于一次“重启”,它要做的是执行同一套恢复协议,而不是努力回忆已被删除的旧消息。
这套协议因为每次都从同样的文件读取状态,所以是幂等的。压缩后跑一遍,恢复;再压缩,再跑一遍,还是恢复。我开始觉得这个方向对了。
3. 《记忆碎片》式外部记忆:把关键信息从对话里“纹”到文件上
3.1 电影里的做法:不信任记忆,信任笔记
《记忆碎片》讲的是一个短期记忆严重受损的人,靠什么来维持日常行动?他没有随手记的好习惯,而是强制自己把所有关键信息写在纸条上、照片背面,甚至直接纹在身上。他有一条核心规则:当我不信任自己的记忆时,我只信任外部记录。
这个设定几乎可以直接平移给AI编码代理。代理在每一轮对话里都是“清醒”的,但它无法可靠地把第一轮埋下的约束带到第五百轮之后。既然不可靠,就不要依赖它。我们的工作不是让代理“记住更多”,而是让代理知道“该去哪看”。
3.2 给AI代理建一个“外部海马体”
外部记录最大的优势是稳定且可被重复读取。只要文件还在,任何一次新会话都能通过读取它恢复认知。这比在对话里反复强调可靠得多。
我设计了一套三层记忆结构,用来划分哪些信息该放在哪里:
第一层是会话本身,只承载当前一小段推理过程。它的一切内容都默认不可靠,压缩后即消失。第二层是工作层,也就是memory目录下的几个核心文件。这是代理日常恢复的主要依据,要求每次会话必读、每步操作后必更。第三层是长效层,包括git历史、代码注释、项目README。当代理需要更深入地理解项目全局时,通过git log和git diff重建过去一段时间的变更脉络。
关键的区别在于:会话是暂时的,memory/是工作的,git历史是永久的。三层各有分工,代理在恢复时按顺序取用,不需要把所有东西都塞进上下文。
3.3 “不要教AI记住什么,要教它去哪里找”
这个领悟对我是颠覆性的。以前我总想着把信息喂给代理,希望它“知道得更多”。现在反过来——信息放在外部文件里,代理只需要知道路径和读取顺序。
这跟你自己工作是一样的:你不会把项目所有细节都背在脑子里,但你知道设计文档放在哪个共享目录,代码规范写在哪个Wiki页面。需要的时候去查,比硬记更可靠。AI代理也一样,给它一套“去哪个文件查什么”的规则,它就能在任何压缩后重新建立认知。
我把这句话写进了项目的protocol文件开头:“每次进入会话,先执行恢复协议。如果你发现上下文里的信息与memory目录中的记录冲突,一律以memory目录中的记录为准。”
4. 从理念到文件:一套让上下文压缩失效的记忆工作流
4.1 目录结构长什么样
理念落地之后,我在项目根目录下建了一个memory文件夹,结构很简单,四个文件:
memory/ ├── current-task.md # 当前任务:目标、范围、进度、下一步 ├── decisions.md # 决策日志:做了什么选择,为什么,影响哪些文件 ├── file-map.md # 项目地图:各文件的职责与相互依赖 └── protocol.md # 恢复协议:代理每次会话必读的执行顺序不要小看这四个文件。它们体积都很小,每个通常只有几KB,但承载的“信息密度”远高于几十轮碎碎念的对话记录。压缩无论发生几次,代理只要还能读文件,就能快速回到正轨。
4.2 三个核心文件的模板
current-task.md是我最常更新的文件,它的模板如下:
# 当前任务 ## 目标 一句话描述当前要完成的主任务。 ## 范围 - 涉及目录/文件:src/xxx.py、tests/test_xxx.py - 暂时不碰:xx模块、xx目录 ## 当前进度 - [x] 完成数据处理函数框架 - [ ] 接入新的API并处理错误边界 - [ ] 补测试用例 ## 上次可运行状态 - 分支:feature/xxx - 验证命令:python -m pytest ## 下一步 1. 实现错误重试逻辑 2. 运行测试并修复失败项 ## 风险与约定 - 不要在未经确认的情况下改变对外接口decisions.md记录的是“为什么”层面的信息:
## 2025-06-10 #17 - 决策:改用异步SQLite驱动而非同步驱动 - 原因:当前瓶颈集中在磁盘IO,异步可让并发更高 - 影响:db.py、service层的调用方式会变 - 替代方案:保留同步驱动加线程池,性能和可维护性平衡后选前者file-map.md则简单粗暴,按文件路径列出职责和依赖关系,代理读完它就能在几秒钟内重建项目版图。
4.3 protocol.md:代理的“开机自检流程”
真正起最大作用的,是protocol.md里那份“开机自检流程”。我把代理每次进入会话后的操作顺序明确写死:
# 会话恢复协议 进入会话后按以下顺序执行,无需用户重复解释背景: 1. git log --oneline -5 查看最近提交 2. 读取 memory/current-task.md 3. 读取 memory/decisions.md 中最近 3 条记录 4. 读取 memory/file-map.md 了解模块结构与依赖 5. 根据 current-task.md 中的“下一步”开始工作 每个子步骤完成后必须执行: 1. 更新 memory/current-task.md 中的进度 2. 如果产生新决策,追加一条到 memory/decisions.md 3. 若本阶段已完成,运行测试并做一次 git commit第一到四步其实就是“重启流程”,第五步才进入实际工作。代理不需要回忆任何历史,只需要照做。
4.4 可以直接抄的会话恢复提示词
有了这套结构,实际使用时我只需要在每次会话的第一条消息里发送一段固定提示词,甚至不需要解释:
请先不要回答任何业务问题。执行以下操作: 1. 读取 memory/protocol.md 2. 严格按协议中的顺序读取对应文件 3. 用不超过5句话总结你理解的当前任务状态与下一步计划 4. 等我确认后,再开始本轮工作就这么简单。上下文压缩后,只要我用这段提示词开启新会话,它就能自己完成“重启”。把这套流程形象地说——代理对上下文压缩几乎免疫了,因为它不再把记忆放在会丢失的地方。
4.5 为什么这套结构不会被压缩摧毁
这里有个很关键的点:protocol.md、current-task.md这些文件本身不受上下文窗口影响。对话被压缩只影响代理“当前看到了什么”,它仍然有读文件的能力。只要第一步让它“去读protocol.md”,它就能自己找回整个项目的状态。
所以免疫的本质不是让代理“记住”,而是让代理“不依赖记忆”。这和人类在健忘的情况下使用笔记、日历、清单干活是一个道理——把认知负担转移给外部系统。
5. 实测与踩坑:被连续压缩三轮之后,代理还能不能干活
5.1 一轮对照实验是怎么设计的
空谈理念没用,我拿本地练手的一个数据清洗工具做了实测。任务目标是把这个工具从单一脚本改成模块化结构,并把所有输入输出接口整理一遍。整个任务量足够大,对话累计很快会超过窗口上限。
第一轮测试不启用memory工作流,让代理靠对话记忆继续。当上下文即将触顶时,我手动触发了一次“模拟压缩”——就是清掉代理的对话历史,只保留最后一条指令,让它继续完成任务。
第二轮测试启用完整的memory目录和恢复协议,同样聊到接近触顶,执行同样的模拟压缩,然后再把恢复提示词发进去。
5.2 结果对比:有记忆系统和没有记忆系统的差距
对比结果非常直观:
| 维度 | 无记忆系统 | 有记忆系统 |
|---|---|---|
| 压缩后首次响应 | 重新询问项目背景与目标 | 直接输出当前进度总结 |
| 任务方案一致性 | 出现与之前相反的接口设计 | 完全延续原有方案 |
| 回到原进度的对话轮数 | 约6到8轮 | 1轮 |
| 人工干预次数 | 至少3次补充背景 | 0次 |
没有记忆系统的时候,压缩后代理不仅不记得模块划分方案,甚至主动建议把已经重构好的模块再拆一遍。我需要反复粘贴背景说明,它才逐渐“想起”之前的设计——准确说是靠新上下文重建了认知。
有记忆系统的时候,压缩后第一轮就准确说出了当前改到哪个文件、卡在哪个测试、下一步优先级是什么。整个恢复过程不超过一分钟,之后直接进入干活状态。
5.3 我踩过的四个坑
这套方案不是第一天就顺畅运行的。实际用了两三周,踩过几个坑,逐个记录一下。
坑一是记忆文件本身变臃肿。一开始我把current-task.md当成通用笔记本,什么都往里写,两周后文件直奔两千行,代理每次读一遍就要耗掉大量token,恢复反而变慢。后来我把任务粒度调小,每个任务建一个文件,完成就归档,current-task.md永远只保存当前这一轮的任务信息,行数控制在150行内。
坑二是代理只顾读文件,不更新文件。我在prompt里写了“每步完成后更新状态”,但代理有时会漏掉,尤其在连续做多个小改动时。解决办法是:把“更新current-task.md”写进协议里的强制步骤,同时在每个子任务完成后让它执行一次git commit,用git提交作为强制检查点——只要看到提交记录缺失,就知道它漏了更新状态。
坑三是记忆文件与代码现实脱节。有时候代理改完了代码,但忘了同步更新current-task.md,导致下一次恢复读到的是旧状态。我的处理规则是在protocol.md里写明:如果memory文件与git历史冲突,一律以git历史为准,并要求代理把冲突点标注出来让我处理。这个仲裁规则很重要,否则文件反而会变成误导源。
坑四是多代理并行时互相覆盖。我试过开两个代理分别处理不同模块,它们都会去更新同一个current-task.md,结果就是后写覆盖先写。后来改成:每个代理有独立的task文件,公共的decisions.md只能追加不允许改写,彻底避免互相踩踏。
5.4 更轻量的入门变体:直接用git当记忆系统
如果你觉得建一整套memory目录工作量太大,还有一个更轻量的起步方式:把git本身当成记忆系统。做法是每次会话结束时强制提交一次,压缩后让代理先看git log,再执行git diff HEAD,从最近的提交记录恢复上下文。
这个变体需要的文件只有一个,就是你项目里的.git目录——只要养成了“会话结束必提交”的习惯。实测下来它对中小型项目非常管用,而且几乎零维护成本。我的完整memory工作流,其实就是从这一招开始,逐步叠加演化出来的。
以我个人经验来说,真正让这套方案成立的,不是某一份文件格式多完美,而是“每次会话从协议开始、每步操作落盘到文件、每个决策留痕到日志”这三个习惯。格式化模板反而没那么重要——你完全可以按自己项目的习惯改造。
如果你也被20万token边界上的“脑叶切除术”坑过,我建议先从最小方案开始:一个current-task.md,一段恢复提示词,一次会话结束前的git提交。先跑两次确认能接受这个节奏,再逐步加入decisions和file-map。这套东西初期投入很少,但换来的回报是,你和AI代理终于可以聊一个足够大的项目而不用时刻担心它中途“失忆”。