☰
AI编码代理上下文压缩失忆?用init.d和《记忆碎片》式外部记忆自救
2026/10/14 22:23:02 网站建设 项目流程

上周处理那个跨平台小工具的迁移项目时,我的AI编码代理在同一个会话里跑到了接近20万token。一切看起来还算顺利,直到它突然冒出来一句:“上下文快满了,我准备做一次压缩,把早期讨论裁剪成摘要。”我点了同意,然后它就变了个人——忘了我们约定用 Java 17 而不是 Kotlin,忘了错误处理要走统一异常体系,甚至忘了刚才重构到一半的接口签名。

那一刻我脑子里蹦出一个词:脑叶切除术。上下文压缩确实让会话得以继续,但也把最宝贵的过程记忆全部切掉了。这篇文章记录我后来怎么靠两样老东西把这个问题解决掉:Unix 的 init.d 启动脚本机制,以及电影《记忆碎片》里“外部化记忆”的思路。如果你也被长会话的“失忆”折磨过,下面这套方法可以直接抄走。

1. 先看清病灶:20万token的“脑叶切除术”到底切掉了什么

1.1 上下文压缩的运作机制

很多AI编码代理在长会话里都会遇到上下文上限。当消息总量逼近限制时,工具会启动一个“压缩/归纳”流程:把最早期的一大批对话全部折叠成一段摘要,只保留最近几千字的消息原文,然后继续跑。听起来很合理,本质上就是给会话做了一次“高倍浓缩”。

我在项目里遇到的情况非常典型。会话刚开始时,我们花了大段时间讨论技术选型、目录结构、接口协议,这些信息完整地躺在上下文里。到了接近20万token的时候,代理提出要压缩。压缩完成后,那些早期讨论全部变成了一段两百字的梗概:“我们选择了Java技术栈,并讨论了若干架构问题。”模式识别没问题,但细节全没了。

这就像整理房间时把所有发票倒进碎纸机,只留下总额数字——账面上看没问题,真要对账时就傻眼了。AI代理的压缩机制天然会保留“结论”而丢弃“过程”,可工程恰恰靠过程细节吃饭。

1.2 被切掉的不是“废话”,而是“锚点”

压缩丢掉的往往是三样东西:

  • 技术栈和架构决策的具体理由。比如“我们不用公共库X,因为许可证不兼容,别浪费时间调研它了”,压缩后可能变成“评估过一些库存在兼容性风险”。
  • 已经完成的工作清单。代理会忘记“登录接口已经改完,只差集成测试”,从而再次打开代码开始“重新改造”。
  • 临时约定和踩坑记录。例如“这个模块不要动,重构会同步引爆三个下游服务”这类信息,摘要里几乎不可能出现。

我在那次迁移中印象最深的一次事故:压缩前我们刚确认完数据库迁移脚本的命名规范,压缩后代理自作主张把脚本目录重新整理了一遍,结果CI直接挂掉。它不是故意捣乱,是真的不记得我们说过什么了。这种“记忆断层”比纯粹的token不足更危险,因为代理依然表现得自信满满,只是基于错误或缺失的信息在行动。

1.3 为什么“再解释一遍”救不了

很多人遇到失忆后的代理,第一反应是重新把关键信息说一遍。这在短会话里有效,但在长会话里基本救不了。原因有三个:

第一,压缩后的摘要已经占据了新上下文的“世界观”,你再解释的内容要和一份并不完整的摘要对抗,代理经常判断成“新信息”和“旧信息”存在冲突,然后开始困惑。第二,token预算本身就很紧张,重新解释会占用大量空间,挤掉后续真正需要的工作记忆。第三,压缩会顺带削弱代理的“自我一致性”,它连自己说过的话都不记得了,自然更容易朝三暮四。

典型的症状是“反刍行为”:它反复问同一个接口的参数是什么,反复打开同一个文件做同一种修改,然后在下一次上下文刷新时又忘记。我试过在压缩后耐心从头补背景,结果半小时后它又失忆了,那次之后我决定换思路——与其被动治疗,不如提前让代理“对压缩免疫”。

2. init.d 方案:把“启动即加载”当作救生索

2.1 向老Unix学“确定性启动”

Unix的系统初始化一直有一套非常朴素但可靠的设计:开机时,内核加载完后,init进程按runlevel依次执行/etc/rc.d/init.d/里的脚本,每个服务都有自己的启动脚本,启动顺序固定、参数固定、行为可预期,完全不依赖“上次关机前系统记住了什么”。

这套机制给我的启发非常直接:既然代理的长期记忆不可靠,那就别把关键信息放在“它的记忆”里,而是把所有初始化逻辑外置成文件,新会话一打开就自动加载。记忆会失效,脚本不会。

当然,这里的重点不是真的去写System V脚本,而是借鉴它的设计哲学:把所有“启动一个可用工作环境”所需的信息,固定成可重复执行的顺序,而不是依赖模型在长对话里记住。

2.2 项目即系统:三个固定启动文件

我按init.d的思路,在项目根目录建了三个固定文件,模拟“启动脚本”:

  • AGENT.md:项目身份和基本规则。写清楚项目是做什么的、技术栈是什么、必须遵守的约定有哪些。
  • CONVENTIONS.md:工程规范。错误处理方式、命名规则、目录职责、禁用依赖、命令执行方式。
  • STATE.md:当前工作状态。正在做什么、做到哪一步、下一步是什么、最近三个决策。

下面是当时用的AGENT.md简化版:

# AGENT.md 项目:跨平台小工具迁移 技术栈:Java 17 + Spring Boot 3 + SQLite 构建命令:./gradlew build 测试命令:./gradlew test 核心约定: - 服务层抛业务异常,控制器统一处理 - 数据库变更必须附带迁移脚本 - 禁止引入新的第三方HTTP客户端 当前工作状态见 STATE.md 完成里程碑后更新 worklog/

这份文件不追求大而全,只承载“代理一开始就必须知道、且无法通过猜代码快速确认”的信息。它承担的角色就是init.d里的启动脚本:每次会话开始,按顺序执行,加载固定配置。

2.3 启动加载序列的设计

受init.d的runlevel排序启发,我给每次新会话设计了一个四步启动序列:

  1. 先读AGENT.md,建立项目身份和技术前提。
  2. 再读CONVENTIONS.md,明确工程规范和红线。
  3. 然后读STATE.md,知道当前进度。
  4. 最后翻最近三天的worklog/,了解最近的动作和结果。

这个顺序是有讲究的:先身份、再规则、然后状态、最后日志。如果顺序反过来,代理容易陷入大量细节里,反而抓不住重点。就像Unix启动必须先挂载根文件系统、再启动网络、然后启动服务——依赖顺序搞反了,整个系统起不来。

我用了固定句式把这些指令写进会话的第一条消息里:

每次会话开始时,先读取 AGENT.md,再读取 CONVENTIONS.md, 然后读取 STATE.md,最后按时间倒序阅读 worklog/ 里最近三天的记录。 之后开始处理 STATE.md 中标记的“下一步动作”。

这套序列跑顺之后,代理即使刚被压缩清空了记忆,也能在两到三分钟内“重启”到之前的认知水平。它不需要回忆,只需要加载。

2.4 压缩后的“重启”康复流程

当代理又一次提出要压缩时,我不再慌张。压缩前我会主动做一轮快照更新,把STATE.md刷新到当前进度。压缩后开一个新会话,把启动序列执行一遍,然后直接让它从STATE.md的“下一步动作”继续。

实测下来有一个明显的体验变化:压缩前代理经常会在中途跑偏,因为它的“记忆”是隐式的、逐渐衰减的;而改用init.d式加载后,相当于每次工作都从同一个基准线出发,即使中间被切掉,也切不掉基准线本身。我把这个流程固定在项目的README里,所有新会话都走同一套启动逻辑,不再依赖“代理自己记得什么”。

3. 《记忆碎片》方案:不靠内部记忆,靠外部便签

3.1 用“纹身和纸条”对抗失忆

电影《记忆碎片》里的主角患有短期记忆丧失,他给自己设计了一套外部记忆系统:把关键信息纹在身上,把线索写成纸条,用宝丽来照片记录现场。每条外部信息都充当一个“记忆锚点”,让他即使失去短期记忆也能持续追踪目标。

AI编码代理在长会话压缩后的状态,跟电影主角非常像:内部记忆被清除,但外部文件系统还在。代理的短期记忆在token窗口里,长期记忆在硬盘上。想让它对压缩免疫,就得把关键信息从token窗口搬到硬盘上去。

3.2 三条必须写的外部记忆

在AGENT.md和CONVENTIONS.md之外,我还维护了三类可写的外部记忆:

  • STATE.md——状态快照。只记录“当前”这一刻的信息,不追溯历史。包含:正在做的任务、已完成的关键步骤、下一步动作、当前所在分支、最近三个决策结论。
  • DECISIONS.md——决策日志。记录重要选择“为什么这么做”。比如“数据库脚本用时间戳前缀命名,是为了多环境按序执行”。这类信息在压缩后最容易丢,但恰恰是避免代理重复调研、重复踩坑的关键。
  • worklog/——工作日志。按日期追加,每次里程碑结束就写一条。格式固定为“做了什么、结果如何、下一步是什么”。

这三类文件的分工很明确:状态快管“现在”,决策日志管“为什么”,工作日志管“过程”。代理压缩后如果只读一个文件,读STATE.md;如果还想理解来龙去脉,读DECISIONS.md和worklog/。

3.3 回写纪律比模板更重要

设计再好的文件,如果代理从来不更新,也是废纸。我花了很大力气才理解:回写是一种纪律,不是一种格式。

我规定两条硬性回写时机:

  • 每次完成一个里程碑(比如跑通一个接口、改完一个模块),立即更新STATE.md,并在worklog/追加一行日志。
  • 每次上下文压缩之前,先停止手头工作,强制刷新STATE.md的“下一步动作”。

我把这条要求写进了启动指令里:

每当你完成一个可验证的里程碑,或者上下文即将压缩时, 必须更新 STATE.md 的“当前进度”和“下一步动作”, 并在 worklog/ 下追加一条日志,格式: 日期 | 做了什么 | 结果 | 下一步

一开始会觉得频繁更新文件很打断节奏,但踩过几次失忆的坑后就会发现,多花三十秒回写,省下的是几个小时重新定位和重新决策的时间。

3.4 压缩后的康复清单

压缩发生后,我把恢复流程做成了一份固定清单,代理只需要按步骤执行:

  1. 读取AGENT.md和CONVENTIONS.md,恢复工作环境认知。
  2. 读取STATE.md,确认当前进度和下一步。
  3. 读取DECISIONS.md中最近的五条记录,避免重新做决策。
  4. 读取worklog/最近三条日志,对齐最近动作。
  5. 执行git status和git log --oneline -5,确认代码仓库的真实状态。
  6. 将以上信息压缩成一段“工作简报”复述给我确认。

完成这套清单后,代理通常能无缝接回之前的工作。即使它完全不记得对话历史,外部文件已经把“该知道的”都告诉它了,剩下的只需要执行。

4. 落地配置:一整套可直接抄走的工程实践

4.1 文件目录怎么摆

我建议的文件布局长这样:

project_root/ ├── AGENT.md ├── CONVENTIONS.md ├── STATE.md ├── DECISIONS.md ├── worklog/ │ ├── 2026-03-01.md │ └── 2026-03-02.md └── .gitignore

这些文件全部纳入git版本管理,历史记录天然成为第二层保险。万一某个状态文件被误改,还能通过git diff找回来。实际使用中,我把worklog/设计成纯追加模式,不改历史,只加新文件,避免并发写入互相覆盖。

4.2 提示词注入:把启动脚本写进系统层

外部文件只有被代理真正读到才有意义。我建议把“启动序列”写进项目说明文档,或者代理工具支持的项目指令区域。

我用的启动指令模板如下:

你是本项目的长期编码助手。 每次会话开始,按顺序执行: 1. 读取 AGENT.md,了解项目背景与技术栈。 2. 读取 CONVENTIONS.md,熟悉工程规范。 3. 读取 STATE.md,掌握当前进度与下一步。 4. 读取 worklog/ 最近三天的日志。 5. 开始工作前,先用一段话复述当前目标、当前状态、下一步动作。 注意:每次完成任务里程碑或上下文即将压缩时,更新 STATE.md 并追加 worklog/。 禁止跳过以上步骤直接处理代码。

这条指令的核心思路是“强制加载+复述确认”。强制加载保证信息进入上下文;复述确认保证代理真的消化了文件内容,而不是扫了一眼就当作没看见。我在多个项目里试过,加上“复述”这一步后,代理忘记约定的概率至少下降一半。

4.3 自动化钩子:用脚本守住纪律

人的纪律会松懈,代理也一样。我加了两条自动化钩子来兜底:

  • git pre-commit钩子:检查STATE.md修改时间,如果超过两小时未更新且本次提交涉及代码变更,则拒绝提交并提示“先更新STATE.md”。
  • 定时提醒脚本:每小时检查worklog/是否有当日记录,没有就追加一条占位日志,提醒下次会话从这里续写。

钩子脚本不复杂,核心逻辑就是检查文件的修改时间。这里给一个简化版参考:

#!/usr/bin/env bash if [ $(find STATE.md -mmin +120) ]; then echo "STATE.md 已超过两小时未更新,请刷新进度后再提交。" exit 1 fi

这套自动化的价值不在于“管住人”,而在于让“外部化记忆”这件事变得不可跳过。时间久了,它会成为一种肌肉记忆。

4.4 验证方法:模拟压缩

把一套机制搭好之后,一定要主动测一次,否则真出事时你根本不知道文件写没写够。我的做法是“模拟压缩演练”:

  1. 在一个进行到一半的项目上,先刷新STATE.md和worklog/。
  2. 清空当前会话,完全不用旧对话。
  3. 只把启动指令和四个文件投喂给全新会话。
  4. 观察它能否在五分钟内接上工作,说出“我在做什么”“下一步是什么”。

如果新会话复述得七零八落,说明外部化记忆还不够详细,需要补文件。如果它直接能接着干活,恭喜,你的代理已经对“脑叶切除术”免疫了。我现在几乎每个长项目开始前都会做一次这种演练,成本很低,收益极高。

5. 常见坑与排查实录

5.1 外部记忆文件膨胀成天书

用了一段时间后,STATE.md越写越长,从十几行膨胀到两三百行,代理每次加载都花大量token去读历史包袱,反而抓不住重点。

我的解法是“状态文件只留当前态”。历史内容全部移入worklog/或归档目录,STATE.md只保留“当前进度、下一步动作、最近三条决策”,一旦超过一屏就该压缩。代理读得快,理解也准。

5.2 代理“看到了但不执行”

有段时间我确实把启动指令写进去了,但代理压缩后经常读完了AGENT.md,转头还是按默认习惯干活。后来我发现问题出在“没有强制复述”上——文件进没进上下文是一回事,模型遵不遵守是另一回事。

解决方法是把“开始工作前用一段话复述当前目标、当前状态、下一步动作”写进启动指令,并且要求复述中必须包含“我在哪个分支、正在改哪个文件”这类具体信息。复述这个动作相当于给代理打了一剂“确定性锚定”,效果立竿见影。

5.3 并发写入互相覆盖

不同会话同时跑同一个项目时,多个代理会同时改STATE.md,后写的把先写的盖掉,信息直接丢失。

我的做法是给每个会话分配独立的工作分支,并规定会话期间只更新自己分支下的worklog-session-<id>.md,最后合并当天记录时人工确认STATE.md。如果你的代理支持多会话并行,这一步千万不能省。

5.4 陈旧状态仍然被信任

STATE.md里的内容一旦没及时更新,代理却依然把它当“最新真相”用,就会按照错误状态行动。最典型的是“以为测试通过,实际上代码已经改了”。

我现在给STATE.md加了一个“最后验证时间”字段,代理读到它时如果发现超过一定时间,必须先跑一遍git status和对应测试再继续。时间戳字段让代理自己意识到“这可能过期了”,比任何提醒都有效。

5.5 问题排查速查表

症状可能原因解决方式
代理既不读文件也不按约定执行启动指令没进系统层把启动序列写进项目说明/系统级提示,并加复述确认
读了STATE.md但还是跑偏文件太乱、重点不突出精简状态文件,只保留当前态和下一步
压缩后代理重复已完成的步骤STATE.md没有“已完成清单”增加“已完成”字段,压缩后作为禁止触碰列表
多个会话互相覆盖状态缺少会话隔离按会话划分worklog文件,人工合并
代理自信地基于旧信息行动状态文件过期未被察觉加时间戳字段,过期强制校验git状态

6. 边界与扩展:这套方案适合谁,还能怎么玩

6.1 适合与不适合的场景

这套“init.d + 记忆碎片”方案,最适合的是那些需要跨多个会话、连续数天推进的长周期任务,比如大型迁移、多模块重构、依赖升级、代码评审后的大批量修改。这类任务的共同特征是:上下文很容易超过窗口上限,且过程中的决策一旦丢失,代价极高。

但如果你只是用代理做一次性问答、快速原型、头脑风暴,这套方法反而会拖慢节奏。给探索性任务强行套上启动脚本和状态文件,就像写个“Hello World”还要走三层审批——纯粹是浪费。我的建议是分开对待:探索性的会话保持轻量,生产性的长期任务再启用完整方案。

6.2 多代理协作的进阶玩法

更进一步,可以让多个代理共享同一份外部记忆库。比如:一个代理负责写代码,一个代理负责审查,一个代理负责更新文档,它们共同读写AGENT.md、STATE.md和DECISIONS.md。每个代理启动时都从同一份“公共便签板”加载信息,做完自己的部分后再把结果写回去。

这相当于把“上下文压缩免疫”升级成“团队级记忆共享”。不过这个玩法对文件规范要求更高,字段命名的混乱会直接导致代理之间互相误解。我目前只在DECISIONS.md上做过小范围试验,效果不错,但还没完全铺开。

6.3 这套思路的核心启发

说到底,AI编码代理和人都差不多:短期记忆有限,上下文窗口再大也有尽头。与其想尽办法扩大窗口,不如接受“记忆会丢”这个现实,把所有关键信息提前放到不会丢的地方。Unix init.d 提供了“启动顺序”的参考,记忆碎片提供了“外部化记忆”的思路,把它们组合起来,就成了一套不依赖任何特定模型、任何特定工具的长任务工作流。

文件系统是人类几十年来最稳定的记忆介质,也是最可靠的记忆备份,它天然适合做AI代理的“体外大脑”。这套方法不需要什么高端配置,四个Markdown文件加一个启动指令,就能让你的代理在20万token的压缩风暴中稳稳幸存。


我在实际使用中最大的体会是:这套方案的真正价值不在于“防止压缩”,而在于“让压缩变得不再可怕”。以前一听到“上下文即将压缩”就紧张,担心代理失忆、担心进度丢失;现在反而把压缩当成一次自然重启,提前拍完快照,开新会话接着跑就行。

最后再分享一个小技巧:你可以故意做一次“模拟压缩”——把当前对话清空,只丢给代理AGENT.md和STATE.md,看它能不能在一分钟内接上活。如果接不上,说明你的外部化记忆还不够好;如果接上了,恭喜你,你的代理已经对脑叶切除术免疫了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询