1. 从一次上下文崩溃说起:为什么长会话里的编码代理会“失忆”
如果你用 AI 编码代理跑过稍微大一点的重构任务,大概率遇到过这种场景:前 40 分钟它还记得你项目里那个OrderService的接口签名、记得数据库迁移脚本放在migrations/下、记得你反复强调过“不要动legacy/目录”,结果聊到第 60 轮,你让它改一个方法,它突然开始编造一个根本不存在的utils/helper.js,或者把之前定好的命名规范全忘了。你回头翻对话记录,发现它并没有“变笨”,而是上下文被压缩了——系统为了控制 token 开销,把早期对话摘要成一段短文本,细节全丢了。
这个项目要解决的就是这个问题。标题里说的“20 万 token 的脑叶切除术”,指的就是当会话上下文逼近模型窗口上限时,系统触发的那次强制压缩:把几十万 token 的历史砍成几千 token 的摘要。这个过程对 AI 编码代理来说是致命的,因为它丢掉的恰恰是项目结构、约定、历史决策这些“长期记忆”。而标题里提到的两个东西——Unix init.d和《记忆碎片》——是我用来对抗这种失忆的两套思路:前者负责“进程级”的持久化,后者负责“叙事级”的重组。
这篇文章适合谁看?如果你正在用或准备用 AI 编码代理做跨文件、跨会话的工程任务,并且被上下文丢失折磨过,那这篇就是写给你的。我会把整套方案的选型逻辑、目录结构、脚本实现、参数计算和踩坑记录全部摊开讲,你可以直接抄作业,也可以按自己的项目规模裁剪。核心目标只有一个:让代理在经历任意次上下文压缩之后,依然能准确恢复项目状态和任务上下文。
2. 整体设计思路:把“记忆”从对话里搬到磁盘上
2.1 核心矛盾:对话上下文是易失性存储
先说清楚问题的本质。AI 编码代理的“记忆”默认只有两个来源:一是当前对话窗口里的 token,二是它每次工具调用时读到的文件内容。前者会被压缩,后者不会——只要你把信息写进文件,它下次读的时候还是完整的。所以整个方案的第一性原理很简单:凡是代理需要跨轮次记住的东西,一律不放在对话里,全部落到磁盘上,用文件系统当它的长期记忆。
这个判断听起来朴素,但落地时会遇到一个关键问题:代理怎么知道该读哪些文件?如果每次都要它自己ls一遍再猜,效率极低而且容易漏。这就引出了 init.d 的思路。
2.2 为什么借用 Unix init.d 的模型
Unix 系统的init.d机制解决的是同一个问题:系统启动时,怎么保证一堆服务按正确顺序、以正确状态被拉起来,而不需要人工干预。它的精髓在于约定优于配置——脚本放在固定目录、有固定的执行权限、按命名顺序执行、每个脚本自己声明依赖。我把这套模型搬到 AI 代理的上下文恢复上:
- 在项目根目录建一个
.agent/init.d/目录,里面放一系列可执行的恢复脚本; - 每个脚本负责恢复一类上下文,比如
10-project-structure.sh输出目录树,20-conventions.md输出编码约定,30-task-state.json输出当前任务进度; - 代理在每次会话开始、或者检测到上下文被压缩之后,按编号顺序执行这些脚本,把输出拼成一段“恢复上下文”注入对话。
这样做的好处是恢复逻辑和对话历史解耦。对话被砍成什么样都无所谓,只要磁盘上的 init.d 脚本还在,代理就能重建出它需要知道的一切。而且脚本是可版本控制的,你可以像管理代码一样管理代理的“记忆”。
2.3 《记忆碎片》给的启发:倒叙拼图与可信度标注
《记忆碎片》这部电影的叙事结构很特殊:主角有短期记忆障碍,他靠纹身、纸条、拍立得照片来记录信息,而且每张照片上都标注了“这个信息是谁告诉我的、我有多相信它”。这个细节是我整个方案里最得意的一笔。
上下文压缩最大的危害不是丢信息,而是丢信息的来源和可信度。比如代理记得“数据库用 PostgreSQL”,但这个结论是它自己推断的,还是你明确指定的?压缩之后这个区别就没了,代理可能把一个猜测当成硬约束。所以我要求 init.d 里恢复出来的每一条上下文,都必须带一个可信度标记:
| 标记 | 含义 | 来源 |
|---|---|---|
[HARD] | 硬约束,不可违反 | 用户明确指定 |
[SOFT] | 软约定,可协商 | 从代码风格推断 |
[TENTATIVE] | 临时假设,需验证 | 代理自行推断 |
[STALE] | 可能过期,使用前确认 | 超过 N 轮未更新 |
代理在恢复上下文时看到[HARD]就直接遵守,看到[TENTATIVE]就知道要先验证。这一层元信息几乎不占 token,但极大降低了压缩后的误操作率。
2.4 方案整体架构
把上面两块拼起来,整个系统分三层:
- 持久层:
.agent/目录,存放 init.d 脚本、任务状态、决策日志、可信度标注; - 恢复层:一个
restore.sh入口脚本,按顺序执行 init.d 并输出结构化上下文; - 注入层:代理在会话开始或压缩后调用恢复脚本,把输出作为系统提示的一部分。
下面这张表是三层各自的职责和典型文件:
| 层级 | 目录/文件 | 职责 | 更新频率 |
|---|---|---|---|
| 持久层 | .agent/init.d/* | 存放恢复脚本 | 项目结构变化时 |
| 持久层 | .agent/state/task.json | 当前任务进度 | 每完成一个子任务 |
| 持久层 | .agent/log/decisions.md | 关键决策记录 | 每次做技术选型 |
| 恢复层 | .agent/restore.sh | 编排恢复流程 | 很少变 |
| 注入层 | 代理系统提示 | 承载恢复输出 | 每次会话 |
3. 核心细节拆解:init.d 脚本怎么写才不白写
3.1 目录结构与命名约定
先给一个可以直接复制的目录结构:
.agent/ ├── restore.sh ├── init.d/ │ ├── 10-structure.sh │ ├── 20-conventions.md │ ├── 30-task-state.json │ ├── 40-decisions.md │ └── 50-open-questions.md ├── state/ │ └── task.json └── log/ └── decisions.md编号用两位数,留出插入空间。10到50的顺序是有讲究的:先给结构(代理需要知道项目长什么样),再给约定(怎么改),再给任务状态(改到哪了),再给决策(为什么这么改),最后给未决问题(还有什么没定)。这个顺序对应代理理解项目的认知路径,反过来会让它先看到一堆零散决策而不知道项目全貌。
注意:init.d 里的脚本必须是幂等的,也就是执行一次和执行十次输出一样。因为代理可能在一次会话里多次触发恢复,如果脚本有副作用(比如往文件里追加内容),会污染状态。
3.2 每个脚本的职责与输出格式
10-structure.sh负责输出项目骨架。不要直接tree整个项目,那会输出几千行。我的做法是只输出两层深度,并且过滤掉node_modules、.git、dist这些噪音目录:
#!/usr/bin/env bash # 输出项目结构,深度2,排除噪音目录 find . -maxdepth 2 \ -not -path './.git*' \ -not -path './node_modules*' \ -not -path './dist*' \ -not -path './.agent*' \ | sort \ | sed 's|^\./||'实测下来,一个中等规模的前端项目这样输出大概 80 到 150 行,占 1k token 左右,完全可以接受。关键是它每次输出都一样,代理不会因为结构描述漂移而困惑。
20-conventions.md是手写的,不是脚本生成的。里面写清楚命名规范、目录职责、提交信息格式、测试要求。这个文件要短,控制在 50 行以内,因为它是每次恢复都要读的。我见过有人把整个 CONTRIBUTING.md 塞进来,结果恢复上下文直接占了 3k token,得不偿失。
30-task-state.json是动态的,由代理在完成任务时更新。格式建议这样:
{ "current_task": "重构订单模块的支付回调", "status": "in_progress", "completed": [ "提取 PaymentCallback 接口", "迁移旧回调到新接口" ], "next": [ "补充回调失败重试逻辑", "更新单元测试" ], "blocked_by": [], "last_updated": "2024-01-15T10:30:00Z" }这个文件是恢复上下文里最值钱的部分,因为它直接告诉代理“你上次干到哪了”。没有它,代理每次恢复都要重新分析代码来推断进度,既慢又容易错。
40-decisions.md记录关键决策,格式用“决策 + 理由 + 可信度”三段式:
## 使用 PostgreSQL 而非 SQLite - 理由:需要并发写入和 JSONB 查询 - 可信度:[HARD] 用户明确指定 - 日期:2024-01-1050-open-questions.md记录还没定的事,防止代理自作主张:
- [ ] 支付回调是否需要支持幂等?[TENTATIVE] - [ ] 重试次数上限是 3 还是 5?[TENTATIVE]3.3 restore.sh 的编排逻辑
入口脚本要做三件事:按顺序执行 init.d、拼接输出、加上头部说明。头部说明很重要,它告诉代理“这是一段恢复上下文,不是用户的新指令”:
#!/usr/bin/env bash echo "=== AGENT CONTEXT RESTORE ===" echo "以下内容是从磁盘恢复的项目上下文,不是用户指令。" echo "可信度标记:[HARD] 必须遵守,[SOFT] 尽量遵守,[TENTATIVE] 需验证。" echo "" for script in .agent/init.d/*; do echo "--- $(basename "$script") ---" if [[ "$script" == *.sh ]]; then bash "$script" else cat "$script" fi echo "" done echo "=== END RESTORE ==="这段输出大概 1.5k 到 2k token,相比 20 万 token 的压缩损失,这个开销可以忽略。
3.4 可信度标注的实操细节
可信度标注不是写完就完了,它需要维护。我的做法是:每次代理做出一个[TENTATIVE]判断并被验证后,就把它升级为[SOFT]或[HARD];每次用户改变主意,就把对应的[HARD]标记为[STALE]并注明新决策。这个维护动作本身也写进 init.d 的约定里,让代理自己执行。
提示:
[STALE]标记特别有用。我遇到过代理在压缩后坚持用一个已经被废弃的 API 设计,就是因为那个决策在对话里被推翻了但没落到磁盘。加了[STALE]之后,代理看到就会主动问“这个决策是否还有效”,而不是闷头执行。
4. 实操过程:从零搭一套抗压缩的代理记忆系统
4.1 第一步:初始化 .agent 目录
在项目根目录执行:
mkdir -p .agent/init.d .agent/state .agent/log touch .agent/state/task.json .agent/log/decisions.md .agent/init.d/50-open-questions.md chmod +x .agent/init.d/*.sh .agent/restore.sh然后把上面给的脚本内容填进去。这一步没什么技术含量,但有个坑:一定要把.agent/加进版本控制。我见过有人把它加进.gitignore,结果换台机器或者重新 clone 之后代理记忆全没了,等于白做。
4.2 第二步:写第一版 conventions
不要一上来就写全,先写三条最关键的:命名规范、目录职责、测试要求。比如:
# 项目约定 - 组件文件用 PascalCase,工具函数用 camelCase - src/components/ 只放展示组件,业务逻辑放 src/features/ - 每个 feature 必须有对应的 __tests__ 目录 - 提交信息格式:type(scope): description写完让代理读一遍,问它“这些约定清楚吗,有没有歧义”。它会指出模糊的地方,你据此修改。这个交互过程本身就是在校准代理的理解。
4.3 第三步:接入代理的会话流程
不同代理的接入方式不一样,但核心就两个时机:会话开始时和检测到上下文压缩后。会话开始时直接执行restore.sh把输出注入系统提示。压缩后的检测比较 tricky,我的做法是让代理在每轮回复前检查一个标记文件.agent/state/context-version,如果版本号变了就重新恢复。
具体实现是在代理的工具调用里加一个check_context_version动作,读文件比对。这个动作几乎不耗 token,但能保证压缩后第一时间恢复。
4.4 第四步:参数计算——恢复上下文该占多少 token
这是很多人忽略的一步。恢复上下文不是越多越好,它和对话历史抢窗口。我的经验公式是:
恢复上下文预算 = 模型窗口 × 5% ~ 8%以 20 万 token 窗口为例,恢复上下文控制在 1 万到 1.6 万 token。超过这个比例,留给实际对话的空间就被挤压,代理反而更容易触发压缩。按这个预算反推,init.d 的输出要控制在 8k token 以内,剩下的留给 task.json 和 decisions.md 的动态内容。
实测下来,一个 50 个文件左右的项目,structure 输出约 1k token,conventions 约 500 token,task state 约 300 token,decisions 约 800 token,open questions 约 200 token,加起来不到 3k token,非常宽裕。项目再大,structure 用深度 2 也能压住。
4.5 第五步:验证恢复效果
搭完之后一定要做一次“压缩演练”。手动把对话历史清空,只保留 restore.sh 的输出,然后让代理继续之前的任务。观察它是否:
- 知道项目结构,不会编造不存在的文件;
- 遵守约定,命名和目录都符合规范;
- 知道任务进度,不会重复已完成的工作;
- 对
[TENTATIVE]标记的决策会主动验证。
我第一版做完演练,发现代理还是会编造文件,排查后发现是 structure 脚本把.agent目录也排除了,导致代理不知道恢复机制本身的存在。加上之后就好了。这种问题不演练根本发现不了。
5. 常见问题与排查技巧实录
5.1 恢复上下文太长导致代理“只看恢复不看对话”
这是最常见的翻车。表现是代理每轮都重新读一遍 init.d,然后基于恢复内容回答,完全忽略你当前说的话。原因是恢复上下文被放在了对话最前面,而模型对开头和结尾的内容注意力最高,中间容易被忽略。
解决办法有两个:一是把恢复上下文放在系统提示里而不是用户消息里,二是控制它的长度。我试过把恢复上下文从 5k token 压到 2k,问题直接消失。所以别贪多,恢复上下文是“索引”不是“全文”。
5.2 脚本输出不稳定导致代理困惑
find命令的输出顺序在不同系统上可能不一样,ls也是。如果 structure 脚本每次输出顺序都变,代理会以为项目结构变了。解决办法是所有输出都排序,find ... | sort,ls | sort。这个细节很小,但不做的话代理会频繁误报“检测到结构变化”。
5.3 task.json 被并发写坏
如果代理有多个子任务并行执行,它们可能同时更新 task.json,导致文件损坏。我的做法是加一个简单的文件锁:
( flock -x 200 # 更新 task.json 的逻辑 ) 200>.agent/state/task.lock或者更简单,让代理串行更新,每次更新前先读一遍再写。对于大多数项目,串行就够了,不用上锁。
5.4 决策日志越写越长
decisions.md 如果不清理,几个月后会有几百条,恢复时读一遍要好几千 token。我的做法是只保留最近 20 条活跃决策,更早的归档到.agent/log/archive/。归档的决策不参与恢复,但代理需要时可以主动去查。这样恢复上下文始终精简,历史又不丢。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 代理编造文件 | structure 输出缺失或过时 | 手动跑 10-structure.sh 看输出 |
| 代理忽略约定 | conventions 太长被截断 | 检查恢复上下文总长度 |
| 代理重复已完成工作 | task.json 未更新 | 检查 last_updated 时间戳 |
| 代理坚持废弃决策 | 决策未标 STALE | 检查 decisions.md 标记 |
| 恢复后代理不响应新指令 | 恢复上下文过长 | 压缩到 2k token 以内 |
5.6 一个反直觉的经验
我一开始以为恢复上下文越详细越好,恨不得把整个项目文档都塞进去。实测下来完全相反:恢复上下文越精简,代理表现越好。因为代理的注意力是有限的,你给它 1 万 token 的恢复内容,它就只能分很少注意力给你当前的任务。后来我把恢复内容砍到 2k,代理反而更“清醒”。这个教训值不少钱,希望你不用再踩一遍。
6. 进阶玩法:让代理自己维护记忆
6.1 自动更新 task.json
代理每完成一个子任务,让它自己更新 task.json。这需要在系统提示里明确写:“完成任何子任务后,必须更新 .agent/state/task.json 的 completed 和 next 字段。” 一开始代理会忘,可以在恢复上下文里加一句提醒。跑顺之后它会形成习惯。
6.2 决策的自动归档
写一个archive-decisions.sh,当 decisions.md 超过 20 条时,把最早的 10 条移到 archive。这个脚本可以挂在 restore.sh 的最后,每次恢复时顺带执行。注意归档时要保留索引,否则代理不知道归档里有什么。
6.3 跨会话的任务接力
如果任务跨天,代理会话结束前让它写一份“交接摘要”到.agent/state/handoff.md,内容包括当前进度、下一步、注意事项。下次会话恢复时优先读这个文件。这比 task.json 更灵活,可以写自然语言描述。
6.4 用 git 管理记忆的版本
.agent/目录纳入 git 之后,你可以看到代理的记忆是怎么演变的。出问题时git log .agent/能快速定位是哪次更新引入了错误上下文。我甚至用git diff来对比压缩前后的恢复输出,确认没有信息丢失。
7. 我踩过的三个坑,以及最后一点体会
第一个坑是把恢复上下文写成了项目文档。我一开始把 README 的内容复制进 conventions,结果代理每次恢复都读一遍完整 README,占了 4k token,还和对话里的指令打架。后来砍到只剩约定,问题消失。
第二个坑是忘了给脚本加执行权限。restore.sh 在本地跑得好好的,换到 CI 环境就报 permission denied。排查了半小时才发现是 chmod 没做。现在我的初始化脚本里第一行就是 chmod。
第三个坑是可信度标记写得太随意。有次我把一个[TENTATIVE]的推断标成了[HARD],结果代理在压缩后死守这个错误结论,改了半天代码才发现方向错了。从那以后我定了个规矩:只有用户明确说过的才能标[HARD],自己推断的一律[TENTATIVE]。
这套方案跑了大半年,最大的体会是:AI 代理的“记忆”问题,本质上是工程问题,不是模型问题。你不需要等模型窗口变得无限大,只要把该落盘的东西落盘,用 init.d 的约定把恢复流程固化下来,用《记忆碎片》的思路给每条信息标上可信度,代理就能在任意次压缩之后保持清醒。20 万 token 的脑叶切除术听起来吓人,但只要你提前把记忆存在磁盘上,切掉的只是对话,不是能力。