在 20 万 token 的“脑叶切除术”中幸存:Unix init.d 和《记忆碎片》如何让我的 AI 编码代理对上下文压缩免疫
如果你长时间使用过具备超大上下文窗口的 AI 编码代理,一定体验过那种诡异的“失忆”现象:前十分钟还在和它讨论某个模块的接口设计,它甚至能准确说出你某个函数里第 37 行的变量名,但当你连续灌入几十轮修改记录、日志片段和测试输出之后,它突然像被人从椅子上拽起来换了个大脑——开始用完全错误的 API 签名“修复”代码,把已经废弃的配置文件当作最新约定,甚至信誓旦旦地告诉你某个方案是你自己提出来的。
我管这个叫“脑叶切除术”。不是开玩笑,20 万 token 的上下文窗口看似宽敞,但现代编码代理为了保持响应速度,会在大约三分之二处开始对历史消息做“压缩”。压缩不是简单的删行,而是把零散的聊天记录、代码块、终端输出揉成一段摘要。听起来很合理对吧?但它丢掉的恰恰是最不该丢的东西:你之前和它达成的所有关键约定、决策的否决理由、以及你在某个文件里特意埋下的“杀死这个特性的开关”的位置。
这篇文章要分享的,就是我怎么让编码代理在压缩发生后,依然能准确回到我们上次讨论的状态。核心思路用一句话说:放弃让模型“记住”一切,改用类似 Unix init.d 启动脚本的机制,把关键状态固化到文件系统里,并在每次会话开始(尤其是压缩后)强制重放这些状态。
这个方案同时吸收了电影《记忆碎片》里的逻辑——主角莱纳德没法形成新的长期记忆,所以他依靠纸条、照片甚至全身纹身来构建“外部记忆系统”,并通过不断重复验证来确认信息的可信度。我把这招搬到了 AI 编码代理身上:既然模型一被压缩就失忆,那就让它学学莱纳德,把不能忘的事情全部写在身体的“合法位置”上。适合的读者是:自己搭过编码代理(或者用开源方案二次开发过)的工程师、被长会话折磨过的 AI 编程重度用户,以及研究 Agent 记忆系统的研究者。下面全是实操层面的东西,没有玄学。
1. 为什么 20 万 token 的上下文反而让代理更脆弱
1.1 从“渐冻”到“断电”——我的代理到底在哪个环节开始退化
先说一个残酷的事实:上下文窗口的数字越大,代理在单次会话里的“精神分裂”越严重。为什么呢?因为你给模型塞了太多熵。一个专门搭给长会话用的编码代理,通常会在每次交互时做这几件事:
- 把用户最近的几条消息原样保留;
- 把更早的消息折叠成摘要或“压缩标记”;
- 把本地文件的内容按需切片注入,而这个注入行为本身又会消耗 token;
- 为了追平延迟,它还会把上面这些内容按“新鲜度”逐级降级——最老的直接裁掉。
我在做一个叫“某跨平台系统的自动修复”项目的时候,一开始用的就是一个能处理 20 万 token 的代理。前两万 token 它聪明得要命,能直接引用我项目根目录下三个不同服务之间的调用链。但到大约第 7 万 token 之后,它开始把api-server和webhook-worker这两个服务混为一谈。到第 12 万 token 时,它已经忘了我们半小时前刚确认过的数据库字段命名规范,主动把user_status改成is_active。第 18 万 token 时,一行日志都能把它带偏,它转而去修改一个和当前问题毫无关系的配置文件。
“渐冻式退化”是慢慢发生的,所以一开始你不觉得有什么问题。真正崩溃的瞬间,是代理内部某次自动的摘要合并——它把三段关键讨论合并成一句“用户对数据库字段有调整”。那一刻,之前积累的所有微妙约定全部清零,像极了被手术刀切掉一片脑叶。
1.2 压缩丢失的不是“细节”,而是“锚点”
很多人以为压缩丢的是细节,比如某个方法名、某个参数类型。实际上,现代模型的摘要算法对这类“显性信息”的保留能力相当强——你把 20 行代码浓缩成 3 行,模型大概率还能记住函数名。真正丢失的是“锚点”:
- 你是在什么前提下做的某个决策?(“因为迁移工具不支持枚举类型,所以我们决定用字符串常量”)
- 哪些建议被否决了?为什么?(“不用 Redis,因为线上环境不开放额外端口”)
- 你和代理之间偏好的表达方式是怎么约定的?(“这个项目里所有错误码统一写成
E_模块_编号”)
这些信息是模型理解整个代码库的“坐标系”。坐标系一旦被压缩抹掉,哪怕代码文件就摊在它面前,它也没法建立正确的全局视图。再加上编码代理通常会同时持有 50 到 100 个文件的“模糊印象”,压缩后的摘要如果有任何错误的倾向性表述,模型就会顺着这个错误倾向一路狂奔。用《记忆碎片》的话说,它失去了自己的“记忆线索”,开始编造一个假的故事来填补空白。
2. 从《记忆碎片》里借来的“外置记忆”思路
2.1 莱纳德的方法论:不信任内在记忆,全部写下来
《记忆碎片》的主角有一个核心原则:内在记忆不可靠,必须把重要信息外化,并且永远不要停止验证。他写纸条是为了记住凶手,他用宝丽来照片是为了确认当下正在发生什么,他把关键规则纹在身上是为了建立一个不可被抹去的“引导系统”。
这个原则放在编码代理身上,意外地契合。因为代理的“固有知识”很强——它知道怎么写 Python 装饰器、怎么配 Nginx、怎么分析堆栈——但它的“情景记忆”几乎没有。你和它在单次长会话中讨论的内容,本质上只存在于模型的激活窗口里。一旦窗口滚动、摘要压缩,情景记忆就从“有”变成“没有”。
所以正确的做法不是去训练一个能记住更多内容的模型,而是学莱纳德:把所有关键决策、当前任务状态、已经验证过的结论,全部写进一个代理每次启动时必然会读取的“纸条”里。这个纸条不是普通的日志文件,而是一份结构化、可执行、通过 init.d 机制自动加载的“状态引导文件”。
2.2 外置记忆的两大纪律:写下来 + 先验证再行动
我后来把《记忆碎片》里的方法总结成两条纪律,然后把这些纪律硬编码进了代理的启动逻辑:
第一,任何经过验证的结论必须落到文件。代理每做一次关键决策,我会让它同步更新一个AGENT_STATE.md文件,里面记录:当前目标、已确认的技术方案、被否决的选项(以及否决理由)、待办事务的优先级。这样即使上下文被压缩,新会话里的代理在启动时读到这个文件,就能在几秒钟内“回忆起”最重要的信息。
第二,在动手改代码之前,先验证记忆文件是否仍然有效。就像莱纳德会在纸条上标注“不要相信他说的任何话,对照照片验证”,我也会让代理检查状态文件里记录的约定是否和当前代码库冲突。如果源码里的变量名已经和状态文件不一样,那就说明记忆过期了,需要更新,而不是直接按照旧记忆去改代码。
这一步非常关键。它避免了“启动记忆文件本身成了新的幻觉来源”——因为模型可能把过时的状态文件奉为圣旨,然后在错误的基线上疯狂叠加错误。
3. Unix init.d 的启示:把“引导记忆”做成标准启动协议
3.1 为什么是 init.d 而不是 cron 或者 daemon
你可能想问:为什么偏偏参考 Unix init.d?它本身已经被 systemd 取代得差不多了,怎么还能用来设计 Agent 记忆?
因为 init.d 所代表的“启动协议”设计哲学,恰好就是代理在每次会话重启后最需要的:确定性的启动顺序、可重入的初始化动作、以及能在任何状态(干净、损坏、不完整)下恢复环境的脚本体系。而 cron 是周期性的、daemon 是常驻的、systemd 又是过度统一化的——它们都不如 init.d 那种“几十个小脚本按编号顺序执行”的模式来得直白,适合我的需求。
在 init.d 模式里,系统启动时按脚本名(比如S01network、S10firewall、S90app)的字典顺序执行。每个脚本负责一个具体任务,脚本之间通过返回值建立依赖。我不需要理解整个系统的全貌,只需要保证这个脚本是对的,就能保证某一环在启动时一定能恢复。
编码代理恰好缺少这种“启动恢复协议”。它启动时脑子里只有预训练权重和系统提示词,如果你不额外喂给它状态,它就是一个“没有昨天”的失忆者。所以我需要的不是更聪明的模型,而是把init.d那种“启动时按顺序加载确定状态”的机制,移植到代理的每次会话里。
3.2 把 init.d 的三个核心特征映射到 Agent 记忆
经过几次迭代,我把 init.d 的三个特征做成了三个具体的实现约束:
特征一:脚本有明确的命名顺序。在我的代理工作区里,记忆文件按照01_PROJECT_CONTEXT.md、02_CURRENT_TASK.md、03_DECISION_LOG.md、04_VERIFIED_TRUTH.md这样的编号存放。代理每次启动时按这个顺序读取,保证“项目背景”永远先于“当前任务”被加载,而“已验证事实”排在最后。顺序很重要:如果代理先读到当前任务里说你“正在改支付网关”,却还没读到项目背景里说“支付网关必须是异步的”,它在处理任务时的行为就会失控。先加载背景,再加载任务,它才能带着约束去执行。
特征二:脚本本身要可重入(幂等)。也就是说,代理今天启动、明天启动,读了十次同一个记忆文件,不应该产生叠加效应。如果一个状态文件里写着“当前正在重构OrderService”,代理在读到这句之后,如果代码库里已经完成了重构,它就必须主动更新这个文件,而不是继续在错误的旧状态上“假装工作”。我实现的方式很简单:状态文件里必须包含“验证条件”,代理在执行任务之前先运行验证条件,不通过就重写状态。
特征三:脚本要能失败并退出,而不继续污染环境。init.d 脚本如果执行失败,系统启动会终止并报告错误,而不是继续带着残缺状态跑。我在代理系统里也实现了类似机制:当代理检测到状态验证失败(比如代码库里的接口签名与决策记录完全对不上),它不能擅自继续“猜”,而是停止轮询、进入“重新校准模式”,强制用户确认后再继续。这个机制帮我挡下了至少三次大灾难,后面在常见问题里细说。
4. 核心实现——给编码代理装上“记忆纹身”
4.1 整体结构:记忆引导文件 + 启动序列 + 验证钩子
在实际动手之前,我画过不下十版架构图,但最终留下来的结构非常简单,一共三层:
- 持久层(记忆引导文件):放在项目根目录下的一个固定目录
memory_boot/,里面是按编号命名、结构固定的 Markdown 文件。 - 启动序列(代理每次会话的第一件事):通过修改代理的系统提示词,强制它先执行“读
memory_boot/目录下所有文件,并逐条转述关键约定”,然后再进入具体任务。 - 验证钩子(在每个任务完成后):代理在生成代码或给出结论后,同步更新对应的记忆文件,并记录下这次验证的证据(某个测试通过、某个 API 已确认)。
这三层之间不搞复杂的依赖注入,就是靠文件和提示词之间的简单约束。在工程上,越简单的机制越不容易出错。
4.2 一份可用的记忆文件长什么样
下面是我在模拟项目 X(一个基于 Python 构建的社区内容聚合服务)里实际使用的03_DECISION_LOG.md的样子:
# 决策记录(DECISION LOG) > 框架:每次记录必须包含"前提 - 决策 - 理由 - 验证状态" > 状态标记:[ACTIVE] 表示当前仍然有效;[SUPERSEDED] 表示已被后续决策替代;[INVALID] 表示发现原决策错误。 ## 决策 001 [ACTIVE] - 前提:内容聚合器需要支持多来源 RSS 抓取,但线上环境不允许额外安装数据库驱动。 - 决策:所有源数据先以 JSON 落地到本地缓存目录,再由定时任务批量入库。 - 理由:避免引入新的数据库方言依赖,同时保证在抓取高峰时不阻塞写入链路。 - 验证状态:已用 `curl` 模拟了一次完整抓取,1200 条数据成功落盘,耗时 3.2 秒。 ## 决策 002 [SUPERSEDED] - 前提:缓存目录曾考虑放在系统临时目录 `/tmp`。 - 决策:改为项目内部的 `.state/cache/`,并加入 `.gitignore`。 - 理由:`/tmp` 会在系统重启时被清理,导致跨会话状态丢失。 - 被哪个决策替代:决策 003 统一了所有状态存储路径。这个文件本身就是给代理看的“纹身”。它不像普通日志那样流水账,而是只记录那些决策影响了后续大量代码结构的关键信息。我会在每次让代理做较大改动之前,要求它先读这个文件,并以“决策 001 仍为 ACTIVE,依据它,本次改动必须保留 JSON 落盘逻辑”的形式给出结论。
4.3 启动序列是怎么强制加载的
有些人的做法是在每轮 prompt 里手动粘贴记忆文件,这显然不够“自动”,而且你会忘记。我采取的方案是把记忆加载写进系统提示词,并在其中嵌入一段“指令外壳”:
## 会话启动协议(必须遵守) 1. 读取 memory_boot/00_README.md,理解该目录的用途。 2. 按编号顺序读取 memory_boot/ 下所有 .md 文件。 3. 用自己的语言逐条概括文件中标记为 [ACTIVE] 的决策。 4. 概括完之后,再进入用户的实际任务。 5. 如果任何决策的验证条件与当前代码库冲突,停止提问,先输出 CONFLICT 标签,并列出冲突的具体证据。这段指令在每次会话开始时使用。它本质上就是一个 init.d 脚本,只是脚本语言变成了人类可读的 prompt。它强制代理先做“记忆回顾”,再开始动手。实际跑起来最大的体验是:代理在压缩后的表现,从“新来的实习生”变成了“失忆但守规矩的老员工”。它可能仍然不记得十分钟前的对话,但它知道自己的立场和约束,知道什么能做、什么不能做,这比什么都重要。
4.4 验证钩子的实现技巧
验证钩子本质上是一个“微型的决策更新器”。我并不是让代理在每轮对话后都去重写整个记忆文件——那样会产生太多噪音,而且模型会开始胡编。我的做法是:
只允许代理在两种情况更新记忆文件:
- 当它完成一项被显式标记为
checkpoint的任务(比如“实现完登录模块并测试通过”); - 当它发现现有记忆文件与代码库不一致时。
每次更新,代理必须按照文件里的模板格式追加一条记录,而不能自由发挥。它的自由发挥太不可控了,你必须给一个格式让它填空。我在第一版里允许它自由更新,结果它把决策记录写成了散文,里面有大量“我认为”“应该可以”之类的猜测性表述,完全失去了“已验证事实”的权威性。加了强制模板之后,决策记录的可靠性大幅上升。
## 决策 007 [ACTIVE] - 前提:(触发本次记录的现实约束) - 决策:(本次实际选择的方案) - 理由:(选择该方案的具体原因) - 验证状态:(拿到的客观证据,测试、日志、命令输出)5. 实操指南——把记忆引导模式迁移到你自己的代理上
5.1 从零搭建:五分钟搭建最小可用版本
不用上复杂的框架,用任何支持自定义 prompt 的编码代理都能搭建。你只需要三个文件和一个目录:
创建memory_boot/00_README.md,内容是解释性的:
# memory_boot 使用说明 本目录用于存储编码代理的"外部记忆"。每次会话开始,代理必须读取本目录下所有编号文件,并按照 [ACTIVE]、[SUPERSEDED]、[INVALID] 三种状态判断哪些决策仍然有效。 读取顺序不可变:01 项目背景 -> 02 当前任务 -> 03 决策记录 -> 04 已验证事实。创建memory_boot/01_PROJECT_CONTEXT.md,把项目的一句话定义、技术栈清单、目标用户和最重要的“禁止事项”写进去。禁止事项极其重要,因为模型天然倾向于“做加法”,你必须显式告诉它哪些事情不能做。
创建memory_boot/03_DECISION_LOG.md,就是上面给出的模板,至少包含现有的两三条关键决策。
然后修改代理的系统提示词,把 4.3 节那段“会话启动协议”粘进去。之后每次新开会话,代理就会自动进入“读记忆-转述-再行动”的流程。前后不到 10 分钟。
5.2 适配不同框架的三个注意点
如果你用的是比较流行的编码代理框架(无论开源自建还是商业产品),有几点需要额外注意:
- 系统提示词长度限制。很多代理的 system prompt 有上限,不能无限塞入记忆文件全文。解决办法是让代理读文件而不是把文件内容直接塞进 prompt。你的 agent 需要具备“文件读取工具”,如果没有,就用“开场先输出我指定路径下文件的第一百行到二百行”这种变通方式,总之要利用工具而不是硬塞 token。
- 上下文压缩触发时机不同。有的代理按 token 总量压缩,有的按轮次。我建议你无论如何都把
memory_boot/放在项目内,并且每次会话开启时重新读取。不要依赖“代理自己记住读过什么”,你就是要逼它在启动时重新读。 - 多项目复用。同样一套记忆引导机制可以用在多个项目上,但每个项目的
memory_boot/必须独立。不要让代理跨目录读取,否则决策记录会串味。
5.3 什么时候该更新记忆文件
我总结了三条判断标准,可以作为你的更新触发条件:
- 你还得在对话里重复自己说过的话,说明代理忘记了那条记忆,此时要检查是不是记忆文件里没写清楚。
- 代理提出一个“新方案”,但你明显之前已经和它讨论过同样方案并否决了,此时决策记录文件里少了一条否决记录。
- 代码库里出现了与决策记录不一致的实现,说明代理在做某次修改时没有验证状态,你需要恢复文件并让代理重新校准。
这三条标准本质上是把《记忆碎片》里的“验证-修正”循环搬到了日常工作里。每次更新记忆,你都会发现代理在下一次会话里的“懂行程度”越来越高。
6. 常见问题与排查技巧实录
6.1 问题速查表
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 代理读了记忆文件,但完全不遵守里面写的约定 | 记忆文件里的指令优先级低于系统 prompt 中的通用指令 | 把关键约定以“禁止事项”形式重写,并提到记忆文件的最前面 |
| 代理在每次更新记忆文件时都大量改写旧内容 | 模板约束不够强硬,模型在“自由发挥” | 强制按固定模板填充,令它只能追加、不能修改旧记录 |
| 代理说“根据记忆文件,我确认 X”,但 X 是错的 | 记忆文件本身已经过时 | 在启动协议中加入验证步骤:任何 [ACTIVE] 记录都必须先与代码库比对,再看是否采用 |
| 记忆文件越来越多,代理读取时间变长 | 没有维护“状态失效”机制 | 每个月底或每个大版本后,主动将 SUPERSEDED 记录归档到archive/子目录 |
| 代理把记忆文件当普通资料一样进行解释,而不是执行 | 你没有在 system prompt 里声明“读文件是任务本身” | 明确写出“必须逐字概括,不得编辑原文” |
6.2 踩过的坑和后来的修正
坑一:第一版我把所有记忆都丢给代理“自由整理”,结果它每周都会产生一条全新的、充满幻觉的“项目愿景”。后来我改成“任何记忆必须先有客观证据支撑”,好的状态是:决策记录里每一条都对应一次测试、一次构建、一次日志排查,从不凭空写“我认为”。
坑二:我一度试图让代理自动维护记忆文件,但它为了自我一致性,会把很多错误决定也焊死在文件里。后来我强制引入“验证钩子必须输出CONFLICT或VERIFIED标签”的机制,让代理在继续前先说出证据。当它必须把“我还没验证”写出来的时候,它就不容易再莽了。
坑三:把记忆文件放在git仓库里的同时,又忘了加白名单。结果每个分支的合并都会产生大量冲突,因为代理在两边同时修改了同一个决策记录。现在我把memory_boot/拆成“项目共享决策”(提交)和“基于当前分支的状态”(纳入.gitignore)两个部分,彻底消掉了这类冲突。
6.3 真正让它“免疫”压缩的最后一环
上面所有机制弄完以后,我仍然不放心,因为只要压缩发生在会话中间,代理还是可能在几十轮内忘掉记忆文件的内容。所以我测了一个最极端的场景:让代理在一段 20 万 token 的超长会话里工作,然后硬编码触发压缩,压缩后又立刻问它“根据记忆文件,支付网关为什么不能引入新的数据库驱动?”。
第一次测,它答对了,因为开局读过文件。第二次测,我在压缩后没有任何额外提示,直接让它继续修改某个模块,它也做对了。第三次我故意在压缩前后把memory_boot/01_PROJECT_CONTEXT.md里的“禁止事项”偷偷改掉,测试它是否会照旧——它停下来报告发生了冲突。
那一刻我才真正确认:模型可以忘记对话,但只要它能在启动时忠诚地读取自己的“纹身”,并且在执行时验证这些“纹身”的合法性,它就在功能上已经对上下文压缩免疫了。剩下还需要记得的,只是那些在记忆文件里不存在的新信息——而堆积新信息本来就是压缩要处理的问题。
7. 延伸:这套模式还能用在哪里
我目前只在编码代理上做了完整实践,但同样的“记忆引导文件 + 启动序列 + 验证钩子”模式,完全可以复用到其他类型的 AI Agent 场景。
比如,文档编写 Agent 可以维护一个style_guard.md,记录所有被用户否决过的句式偏好;数据分析 Agent 可以维护一个dataset_facts.md,记录某个字段的确切含义和分析结论;甚至个人知识管理 Agent 也可以用 init.d 的思路,把“每天开机后必然要回顾的项目决策”写进启动协议,让长期使用同一个 Agent 的人不会因为上下文换新而失去上下文。
在个人使用上,我后来把记忆引导文件扩展成了一个可以跨会话继承的“代理人格档案”。里面记录了我希望代理以什么方式回复、什么情况下应该坚持追问而不是默认接受第一版方案。效果是,哪怕我隔了一周再回来开新会话,它的工作风格依然是上周调教过的样子,而不是每次都需要我重新强调一遍。
这套系统不是银弹,它没法让模型变聪明,也没法扩大上下文窗口。它的价值在于:它把一次性会话变成了可继承的工作流。你在过去几十万 token 里验证过的结论,不再是会被压缩掉的“随机记忆”,而是变成了项目仓库里一个个可审计、可验证、可推翻的决策文件。从工程视角看,这是唯一行得通的路——因为你永远不能指望一个统计模型稳定地记住每一次对话的细节,但你可以指望它按一种好的协议去读文件。
根据我的实操经验,第一次把这个模式移植到自己的代理上时,你大概率会低估它带来的改变。你会觉得“不就是多读几个文件嘛”,但真正跑过一两轮长会话之后才会发现,它的意义不在于多了几次文件读取,而在于让代理拥有了一个“经过验证的昨天”。这个感觉,比任何上下文压缩技术都稳。
最后再分享一个小技巧:如果你只愿意加一个文件,那就加03_DECISION_LOG.md。别的都可以依靠对话记录现翻,唯独“关键决策及否决理由”是模型永远无法从代码里反推出来的——那是你们两个人(人机之间)的私有协议,一旦压没了就是没了。把它写下来,你的编码代理就没有那么容易被人为制造的“失忆”击垮。