如果你让 AI 编码代理在长会话里跑过真实项目大概率会遇到这一幕上下文窗口明明还有富余但你追问某个函数的具体实现时代理却开始用模糊的表述糊弄你——“这个函数之前定义过具体叫什么不重要逻辑是查得到的”。你回翻日志发现某一步系统提示上下文已压缩。你失去的不只是几轮聊天记录而是整个项目在代理脑海里的精确记忆。这个现象我叫它“脑叶切除术”。20 万 token 的窗口说切就切切完代理对项目一脸茫然跟刚开机时没什么两样。这个坑我踩了不止一次。后来我从两个完全不相干的东西里找到了灵感一是 Unix 的 init.d 启动体系二是电影《记忆碎片》里那个记不住新事的男主角。前者教会我“状态应该放在文件系统里而不是进程里”后者教会我“记忆可以外包给环境”。把这两套思路合到一起我搭了一套让编码代理对上下文压缩近乎免疫的工作流实测了几个月效果远超预期。这篇文章把这套方案从问题根源、设计思路、具体实现到踩坑记录完整讲一遍。适合所有被长上下文困扰的开发者也适合刚接触 AI 编码代理、想从一开始就避开这个坑的新手。不扯玄学全部是我真实跑过的方案。1. 先搞懂上下文压缩为什么是编码代理的“脑叶切除术”1.1 压缩的真相不是摘要是断片很多人对上下文压缩有个误解以为它是“智能摘要”——把老对话浓缩一下核心信息都还在。实际根本不是这么回事。典型的上下文管理策略是粗暴的超出窗口部分要么直接丢弃要么用一小段概括性文本替换掉原始内容。而概括只有一个目标就是“剧情梗概级别”地保留对话主旨它不会替你保留代码细节。举个我真实遇到的例子。我让代理重构数据层的某个查询接口前后聊了大概一百多轮期间确认了表结构、索引策略、缓存键的命名规则、异常处理方式。然后上下文被压缩了。我再问代理“缓存键的命名规则我们刚才不是定了吗按这个规则实现一下”它回了一句“我们之前确实讨论过缓存键的规则具体规则在之前的对话里我建议你重新发一下。”那一刻我意识到压缩后的代理根本不是在“回忆”它是在“意识到自己失忆了”。对自然语言聊天来说梗概尚可接受。但代码是精确文本一个函数名、一个参数顺序、一个魔法数字丢了整个实现就废了。压缩对编码代理来说根本不是降清晰度是直接断电。1.2 编码代理对“失忆”格外敏感的三层原因第一层是精确性依赖。代码不像聊天代码里的每个标识符都是确切的。代理依赖上下文里的精确符号来写代码、改代码。一旦压缩把符号替换成“某个函数”它就只能靠猜——猜出来的东西轻则风格不一致重则引用不存在的接口编译都过不了。第二层是跨文件索引。现代项目的代码修改几乎都是跨文件的。代理需要同时记住 A 模块的接口、B 模块的调用方式、C 模块的配置项。这些信息可能散落在几十轮对话里。压缩把这些相关性打散之后代理就失去了“全局索引”只能盯着当前打开的文件瞎转。第三层是调试链路的连续性。调 bug 的本质是在追踪一条因果链“报错→查日志→定位可疑函数→验证假设→找到根因”。这条链在上下文里是一步步建立的。压缩之后代理只记得“有个 bug 在处理”但忘了已经排查过哪些方向、排除过哪些假设于是它会把之前否定的方案再翻出来炒冷饭浪费大量 token。2. 第一味解药init.d 的重启哲学把状态写进文件系统2.1 为什么 Unix 服务可以随便崩溃还一脸淡定想明白这件事得先看 Unix 是怎么处理进程生命周期的。系统里的各种服务随时可能崩溃但运维根本不慌因为服务本身并不持有“不可再生”的状态。配置在 /etc 下数据在 /var/lib 下日志在 /var/log 下服务进程自己的内存里只有可随时重建的缓存。崩溃之后init.d 里的启动脚本把它拉起来它读配置、加载数据、回到可用状态整个流程行云流水。这个设计里有个关键原则进程内存是易失的文件系统是持久的。聪明的服务都默认自己随时会死所以频繁地把状态刷到磁盘里。这才有了幂等启动、按序启动、可重启这些特性。你见过哪个服务在崩溃前还要靠进程内存里的“聊天记录”来恢复吗没有因为那是不及格的设计。反观我们大多数人用编码代理的方式简直是把所有记忆都放在“进程内存”里。上下文窗口就是代理的 RAM贪便宜把什么都往里面塞从来没想过这个 RAM 会被系统强制清空。等到被压缩的那一刻自然就是一场灾难。2.2 给代理套一个“开机自检”协议init.d 的思路给了我最直接的启发如果代理的上下文注定会被压缩那我就在文件系统里为它建一套“引导档案”让它每次失忆之后都能像 Unix 服务一样按固定脚本恢复状态。这套档案的核心是一份 PROTOCOL.md相当于它的 /etc/init.d 启动脚本。里面写清楚重启后第一件事做什么、第二件事做什么。我在实际项目里的 PROTOCOL.md 长这样# Agent Boot Protocol 如果你发现自己的上下文不完整、对项目的记忆模糊或被告知上下文已压缩 请立即按以下步骤执行 1. 读取 .agent/STATE.yaml —— 这是你的当前状态。严格按其中的 current_task 继续。 2. 读取 .agent/DECISIONS.md 的最后 20 条 —— 这是你最近做过的重要决策与理由避免推翻已定结论。 3. 读取 .agent/TODO.md 的前 10 条 —— 这是待办清单找出当前最优先的 next_action。 4. 按 STATE.yaml 中的 files_to_load 列表逐个读取相关源文件。 5. 如果以上步骤执行完仍不确定下一步向用户询问不要猜测。这个协议最关键的地方在于第 5 条——不许猜。编码代理失忆后最大的风险不是不干活而是自信地干错活。有了明确指令让它先加载状态文件、再定向读源码就能把失忆后的不确定性压到最低。实操心得PROTOCOL.md 一旦写好就别频繁改。它相当于代理的“出厂引导”你自己每次都要保持最新但也不要让它长得超过一屏。引导步骤超过 5 条代理就会在执行时偷懒跳步。3. 第二味解药像《记忆碎片》一样把记忆外包给环境3.1 电影里的生存系统便条、纹身和仪式《记忆碎片》的男主角 Leonard 患的是顺行性遗忘症新发生的事情只能记住几分钟。正常人靠大脑记忆他靠外部环境记忆。他的系统里有三样东西纹身记录永久不变的核心事实——比如“他杀了你老婆”“别接电话”拍立得照片记录人物和地点的客观信息旁边写着备注还有一套固定的仪式和操作流程让他每次“醒来”时都能快速定位自己身处何阶段、下一步该干嘛。这个系统最妙的地方在于它完全不依赖大脑能记住多少而是环境替他记住。你看他活得虽然艰难但确实能够持续推进调查——因为信息不随记忆消失只随环境变化而丢失。只要他的笔记还在、纹身还在、流程还在他就永远可以重启调查状态。这不就是我们想要代理拥有的能力吗代理的“顺行性遗忘”就是上下文压缩。它同样不可能靠“更努力地记忆”来抵抗压缩唯一可靠的方案就是在项目环境里建立体外记忆。3.2 把电影系统翻译成代码库的“体外记忆”我把 Leonard 的三件套翻译成了编码代理的三类文件纹身对应那些绝对不许变、每次都要重新确认的项目事实。比如系统架构约束、模块边界、接口契约、命名规范。这些信息放进 .agent/FACTS.md它是最高优先级的事实源代理每次重启都要重读且不得违背。拍立得照片对应决策日志即 DECISIONS.md。每条记录包含时间、决策内容、理由、备选方案为什么被否决。照片会褪色、会被乱写但 Leonard 依然依赖它们同理决策日志也可能写得不够好但只要有记录代理在重启后至少知道自己曾经选过哪条路不会盲目推翻自己。仪式对应检查点动作。Leonard 每次做关键动作前都会拍照、写备注、确认纹身代理每次进入高风险操作前也应该强制执行“状态写入”。我把这个写进了系统提示词改接口签名前、大规模重构前、排查出根因后、切换任务文件时这四个时刻必须更新 STATE.yaml 和 DECISIONS.md。实操心得很多人只记“做了什么”不记“为什么这么做”。DECISIONS.md 里一定要写理由。理由写清楚了代理重启后才能区分“这个决策是硬约束还是临时选择”。光记结论不记理由等于拍立得上只画了个黑点谁都看不懂。4. 完整实现一套对压缩免疫的代理工作区4.1 目录结构总览我不想把方案搞得太重核心就是项目根目录下建一个.agent目录放几个纯文本文件。不需要数据库不需要特殊工具任何编码代理都能直接读写。我用的结构如下project/ .agent/ PROTOCOL.md # 开机引导协议失忆后最先读的文件 FACTS.md # 永久事实架构约束、接口契约、命名规范 STATE.yaml # 当前状态进行到哪、下一步做啥、刚碰过哪些文件 DECISIONS.md # 追加式决策日志时间 决策 理由 TODO.md # 待办清单按优先级排标注阻塞项 checkpoints/ # 阶段检查点关键节点的时间点快照不用把所有逻辑放在一个文件里职责拆开反而更容易让代理按需加载。FACTS.md 是重读次数最少的STATE.yaml 是每次重启必读的DECISIONS.md 是只读尾部的。4.2 三个核心文件的实操模板先说 STATE.yaml它必须是机器可解析的格式我选了 YAML。内容不需要多够代理定位自己就行project: user-profile-service current_stage: implement-cache-layer current_task: 完成 Redis 缓存装饰器的编写 last_file_edited: services/profile_cache.py next_action: 为装饰器补充缓存穿透保护 blocked: false files_to_load: - services/profile_cache.py - repositories/user_repo.py - config/cache_config.py active_branches: - feature/cache-layer recent_findings: 已排除数据库连接池问题延迟集中在重复查询FACTS.md 的条目最好每条一两行像是给代理的“永久纹身”## 架构约束 - 所有数据访问必须走 repository 层禁止在 service 层直查数据库 - 缓存键格式统一为 user:{id}:profile禁止自定义变体 - 对外接口的返回结构统一为 { code, data, message }DECISIONS.md 是纯追加式每条记录用一个固定模板## [2025-06-12] 缓存策略选择 Redis 本地过期标记 - 决策缓存统一走 Rediskey 由 profile_cache.py 统一管理 - 理由多实例部署下本地缓存会不一致统一走 Redis 保证一致性 - 否决方案进程内 Caffeine 缓存因多实例不一致、DB 旁路缓存因实现复杂4.3 压缩发生后代理的“冷启动”流程假设代理正在改代码系统突然提示上下文已压缩代理对当前任务只剩模糊印象。我把 PROTOCOL.md 里的冷启动流程归纳成下面这个操作序列第一步读 PROTOCOL.md 本身——这个文件短读完代理就知道自己该按流程走。第二步读 STATE.yaml——它看到 current_task 是“完成缓存装饰器”next_action 是“补充缓存穿透保护”立刻知道刚才干到哪了。第三步读 DECISIONS.md 尾部——看到缓存策略已经定了就不会再纠结“要不要换一种缓存方案”。第四步按 files_to_load 列表打开那三个源文件重新加载代码上下文。第五步回到 TODO.md认领最优先的待办项。整个过程花不了多少 token但代理从“失忆懵圈”变成“恢复工作状态”只需要一两次交互。我实测下来这个冷启动流程通常只需要一轮对话就能把代理拉回正轨而以前等代理自己摸索回状态往往要浪费几万 token。4.4 写检查点的时机宁可多写不可不写状态文件不是写一次就完了关键是要在正确的时间点刷新。我这里有一个强制规则四个时刻必须更新状态开始一项大改动前先把当前状态固化一下这样折腾坏了还能回退。排查出根因的那一刻立刻记录 findings因为根因是排查链的终点也是最容易被压缩掉的成果。切换文件或切换任务前把上一个任务的输出和结论写进 DECISIONS.md防止回头继续时断片。每完成一个可验证的小任务更新 STATE.yaml 的 next_action让代理永远知道下一步方向。注意不要让代理把状态文件当成“写日记”。每条记录必须紧凑、结构一致、有明确的行动指向。过度运维状态文件会挤占真实开发的 token 预算得不偿失。5. 实测记录与失败模式5.1 一个 20 万 token 长会话的完整观察这套方案真正让我信服是在一个真实项目的长会话里。项目是一个带用户服务、订单模块、管理后台的中型系统代码规模不大但跨文件依赖很多。会话积累到 20 万 token 时我故意让代理继续实现一个新功能果然到某个节点上下文被压缩了。压缩前代理的工作状态很清醒知道用户服务用 repository 模式、缓存键的规范、订单模块的接口签名。压缩后它第一反应果然开始犯迷糊甚至问我“订单模块的订单号生成策略你之前在哪儿提过”。我直接丢给它一句话“按 PROTOCOL.md 执行冷启动。”下一轮开始代理读完了状态文件和决策日志回头看它的回答已经能准确说出当前正在改哪个文件、下一步要做什么、为什么不能直接用缓存键的变体。整个过程只用了一轮交互的引导成本。对比没有这套方案之前同样的情况我得把之前定过的东西重新叙述一遍有时候代理还会坚持它的“幻觉”——声称某个接口之前定义过但实际上根本没定义。方案落地后“接口不存在”这类幻觉大幅减少因为代理知道先加载文件再回答问题。5.2 四类典型失败模式与排查方法用了一段时间也总结出几个常见的翻车情况整理成一张表遇到问题可以对着查症状可能原因排查方法失忆后代理完全不理协议直接开始猜测PROTOCOL.md 的入口没有被写进系统提示词代理不知道有这文件在系统提示词里固定追加“开始前先查 .agent/PROTOCOL.md若上下文不完整必须按协议执行”代理读了状态文件但依然跑偏STATE.yaml 更新滞后内容与真实代码状态不一致强化写状态时机的强制规则特别是“切换文件前必须更新”决策日志越长越没人读DECISIONS.md 几百条之后代理读尾部也找不到关键决策定期把重要且仍生效的决策上浮到 FACTS.mdDECISIONS.md 只留最近 30 条以内可读代理花太多 token 维护状态文件状态文件模板太复杂代理被迫反复改写大文件简化模板STATE.yaml 保持一次到两次交互就能写完的量级踩坑提醒这套方案不是银弹。如果代理的提示词太短、系统提示里根本没有 PROTOCOL.md 的入口代理即使看到 .agent 目录也不会主动初始化。第一件事永远是把它写进系统提示词的最前面当作开机引导的第一行。6. 重要提醒与后续扩展6.1 别把“免疫”变成新的“累赘”方案核心是“少而精”不是“大而全”。我见过有人把这类状态文件做成了一整套带数据库同步的复杂系统结果代理每轮都要花掉几千 token 去读写和同步状态留给写代码的空间反而缩水了。这套架构最适合的形态始终是几个纯文本文件总量控制在几 KB 级别让代理用至少一次交互就能完全载入。另一个容易踩的坑是“层层套娃”。比如 FACTS.md 里写“详见 DEPENDENCIES.md”DECISIONS.md 里又引用一大堆外部文件。一旦代理需要为了找一条信息连读五六个文件冷启动的 token 成本就上来了协议本身的优势就没了。保持扁平让代理打开文件的第一屏就能拿到最关键的内容。还有一点状态文件要跟代码库一起提交到版本控制里。我习惯把 .agent 目录纳入 git 管理每次检查点写入就伴随一次 commit。这样万一代理产生了错误的状态记录还能通过历史版本找回上一个正确状态。这相当于给“体外记忆”加了一层备份成本极低收益很大。6.2 这套思路还能延伸到哪些场景init.d 加《记忆碎片》这套组合的适用范围其实比编码代理宽得多。只要是需要“长时运行且随时可能断片”的系统都可以套用同样思路外部状态加引导协议加频繁检查点。一个是多代理协作。多个编码代理任务不同、会话独立完全可以共享同一个 .agent 目录通过 FACTS.md 和 DECISIONS.md 同步事实与决策避免各说各话。一个是离线批处理任务。比如让代理跑一个持续数小时的批量重构期间如果上下文耗尽或进程中断重启后凭状态文件就能接着上次的进度继续而不是从头再来。还有一个是知识沉淀。我发现这套文件化记忆的习惯本质上只是在逼迫代理“把思考显式化”。把决策理由写下来不仅是为了飞单后恢复状态更是为了让未来的自己和未来的其他人能够看懂当初为什么这么设计。这已经超出了对抗上下文压缩的范畴变成了项目长期可维护性的基础功夫。我个人在实际操作中的体会是代码代理有没有“文件系统记忆”差别不是体验上的差异而是能不能用于严肃生产工作的根本差异。会话上下文只是缓存缓存一定失效项目目录才是持久层可靠系统永远靠持久层说话。你最好从新项目的第一天就把 .agent 目录建好写下第一版 PROTOCOL.md 和空白的 STATE.yaml。这个动作一分钟就够但它决定了你的代理是“断电即失忆的玩具”还是“重启即恢复的工程工具”。最后再分享一个小技巧我每次让代理开始一项新任务前都会在下达指令时附一句“开工前先把 STATE.yaml 里 current_task 同步成这个新任务”。这个方法比任何复杂配置都管用因为你永远无法保证代理会在你不提醒的情况下主动维护好状态但一句简单的指令就能保证状态永远是最新的。记住你越是在状态文件上多花一点点时间代理在漫长会话里就越不可能出岔子。