资讯动态

用init.d和《记忆碎片》思路,解决AI编码代理长上下文失忆问题

发布时间:2026/10/13 16:09:41 来源:尧图企业网站定制
1. 从“脑叶切除术”说起长上下文代理为什么越用越傻做过AI编码代理的人大概都有过这种体验一开始它思路清晰能记住你半小时前说过的架构约定能准确引用三天前那次重构里改过的函数签名。但聊到某个临界点之后它突然就像被做了一场“脑叶切除术”——你明明在同一个会话里它却开始重复问你已经回答过的问题忘记了你定下的命名规范甚至把自己刚写过的代码又重写一遍还改出了新bug。这个临界点通常就是上下文窗口被压缩的那一刻。20万token听起来很大但一个真实的编码会话里工具调用返回、文件内容、报错日志、历史对话全都在往里塞撑满只是时间问题。一旦触发压缩代理的“人格连续性”就断了。我最近在折腾一个模拟项目X的编码代理时就反复撞上这堵墙。后来我用了两个看起来完全不搭边的东西来解决它一个是Unix时代的老古董init.d另一个是诺兰的电影《记忆碎片》。这篇文章就把这套思路完整拆开讲包括它为什么有效、具体怎么落地、以及我在实操中踩过的坑。先给结论核心思路是把代理的“记忆”从易失的上下文窗口里搬出来变成文件系统上一份可持久、可检索、可重建的状态。上下文压缩可以清空对话但清不掉磁盘上的文件。代理每次“醒来”靠的不是回忆而是像《记忆碎片》主角那样读自己留下的笔记重新拼出“我是谁、我在干什么”。这套东西适合谁适合所有在做长会话AI代理、被上下文压缩折磨过的开发者。不需要你有多深的AI背景但需要你对进程管理、文件系统这些基础概念不陌生。下面我按设计思路、核心机制、实操落地、问题排查四个部分展开尽量把每个“为什么”都讲透。2. 整体设计思路把记忆从上下文搬到磁盘2.1 上下文压缩到底杀死了什么要理解这套方案得先搞清楚上下文压缩的破坏性到底在哪。很多人以为压缩只是“丢了一些旧消息”其实远不止。代理的连续性依赖三类信息事实性记忆项目结构、命名约定、已完成的改动、程序性记忆当前任务处于哪一步、下一步该做什么、情境性记忆为什么当初做了某个决策、否决了哪些方案。压缩算法通常按“重要性”或“时间近远”来裁剪但问题是它无法区分“这条消息是闲聊”还是“这条消息定义了整个项目的错误处理规范”。一旦后者被裁掉代理后续所有代码都会偏离规范而且它自己毫无察觉。这就像《记忆碎片》里的主角如果纹身被擦掉一块他接下来的行动就会基于错误的前提。所以我的设计目标很明确让代理的关键状态不依赖上下文窗口的存活。上下文可以随便压缩压缩完代理还能从外部把状态读回来。2.2 为什么选init.d而不是别的你可能会问持久化状态为什么不用数据库、不用向量存储、不用现成的记忆框架我试过一些最后回到init.d原因有三个。第一启动语义天然匹配。init.d的本质是“系统启动时按顺序执行一组脚本”这跟代理“每次会话开始时重建状态”的需求几乎一模一样。代理每次被唤醒就像系统重启跑一遍初始化脚本把该加载的加载进来。第二零依赖、可审计。一个init.d脚本就是普通shell脚本放在文件系统上你能直接cat出来看它干了什么。相比之下某些记忆框架把状态藏在二进制索引里出问题时你根本不知道它记住了什么、忘了什么。调试代理最怕黑盒。第三顺序即优先级。init.d的脚本按编号顺序执行比如S10、S20这个顺序本身就是一种优先级声明。我可以把“加载项目规范”放在S10“恢复任务进度”放在S20“校验状态一致性”放在S30逻辑一目了然。注意这里说的init.d是借用它的设计模式不是要你真的去改操作系统的启动脚本。我是在代理的工作目录下建了一个agent.init.d/目录用同样的编号约定来组织初始化脚本。2.3 《记忆碎片》给的核心启发电影里主角的处境和长会话代理惊人地相似短期记忆不断丢失只能靠外部记录维持连续性。他做了三件事我几乎原样搬到了代理设计里。第一即时记录。想到什么立刻纹在身上或拍成照片绝不依赖脑子记。对应到代理就是任何关键决策、任何状态变更立刻写进文件而不是指望它留在上下文里。第二分级标注。“事实”用纹身永久“待办”用纸条临时“人物”用宝丽来照片带注释。对应到代理就是区分哪些状态是永久的项目规范、哪些是会话级的当前任务、哪些是易变的临时变量。第三启动时重建。主角每次醒来第一件事是环顾四周、读身上的纹身、看手里的照片重新建立对处境的认知。对应到代理就是每次会话开始跑一遍初始化脚本把状态读回来。这三条构成了整套方案的骨架。下面进入具体机制。3. 核心机制拆解状态文件、初始化脚本与重建协议3.1 状态文件的分层设计我把代理的状态分成三层分别对应不同的持久化策略和生命周期。第一层是项目级状态生命周期最长跨会话甚至跨天存在。内容包括项目结构说明、命名规范、架构决策记录、已知的坑。这些放在agent.state/project/目录下用Markdown文件存储因为Markdown对人可读、对模型也友好。文件名带语义比如naming-conventions.md、architecture-decisions.md。第二层是任务级状态生命周期是当前任务从开始到完成。内容包括任务目标、当前进度、已完成的子步骤、下一步计划、遇到的阻塞。放在agent.state/task/下用一个current-task.md文件维护每次状态变更就更新它。这个文件是代理“醒来”后最先读的东西。第三层是会话级状态生命周期就是当前这一次会话。内容包括本次会话的临时上下文、刚读过的文件摘要、待验证的假设。放在agent.state/session/下会话结束可以清理也可以保留供下次参考。分层的意义在于压缩上下文时第一层和第二层完全不受影响因为它们本来就不在上下文里第三层即使丢了重建成本也低。这样代理的“核心人格”就锚定在了磁盘上。3.2 init.d脚本的组织与执行顺序在代理工作目录下建agent.init.d/里面放编号脚本。我的实际编号方案是这样的S00-mount-state.sh检查状态目录是否存在不存在就创建骨架。S10-load-project.sh读取项目级状态输出到标准输出供代理加载。S20-load-task.sh读取任务级状态同样输出。S30-validate.sh校验状态一致性比如任务文件里引用的文件是否还存在。S40-emit-context.sh把前面加载的内容拼成一段“启动上下文”注入到代理的初始提示里。每个脚本都是幂等的跑多少次结果都一样。这点很重要因为代理可能在一个会话里被多次“唤醒”比如压缩后重新注入初始化脚本必须能安全重跑。脚本用bash写输出用简单的分隔符标记段落方便代理解析。比如S10的输出长这样echo PROJECT STATE BEGIN cat agent.state/project/naming-conventions.md echo PROJECT STATE END 代理的启动提示里会有一段指令告诉它“以下是你的持久状态请以此为准不要依赖对话历史”。这样即使上下文被压缩得只剩最后几条消息代理也能从这段注入内容里恢复认知。3.3 重建协议代理醒来后的第一件事光有脚本还不够代理得知道“醒来后该干什么”。我在系统提示里写了一段固定的重建协议大意是先读agent.state/task/current-task.md确认当前任务是什么、进行到哪。再读项目级状态里的规范文件确认约束条件。对比两者如果发现任务状态引用了已不存在的文件或已变更的规范标记冲突并暂停先解决冲突再继续。把重建结果用一句话总结出来写在会话开头作为后续行动的锚点。这个协议的关键是第3步的冲突检测。我踩过的坑是任务文件里写着“下一步修改utils/parser.js”但那个文件在上次会话里已经被重命名了。代理如果不检查就会去改一个不存在的文件然后报错然后困惑。加上冲突检测后它会先报告“任务引用的文件不存在可能已重命名请确认”这就把问题暴露在了正确的层面。实操心得重建协议不要写得太长超过五步代理就容易漏。我试过写十步的版本结果代理经常只执行前三四步就开始干活了。精简到四步以内执行率明显提高。4. 实操落地从零搭一套抗压缩的代理状态系统4.1 目录结构与初始化先建目录骨架。我在项目根目录下执行mkdir -p agent.state/project agent.state/task agent.state/session agent.init.d touch agent.state/task/current-task.md然后往current-task.md里写初始内容至少包含任务目标、当前进度、下一步。格式不用太讲究但建议用固定的小标题方便脚本解析## 任务目标 实现模拟项目X的配置加载模块 ## 当前进度 - 已完成配置schema定义 - 已完成默认值合并逻辑 ## 下一步 - 实现环境变量覆盖 - 写单元测试 ## 阻塞 无接着写agent.init.d/S00-mount-state.sh确保目录存在。这个脚本简单但必要因为代理可能在一个干净环境里启动状态目录不一定在。4.2 编写加载脚本与注入逻辑S10-load-project.sh的内容#!/bin/bash STATE_DIRagent.state/project if [ -d $STATE_DIR ]; then echo PROJECT STATE BEGIN for f in $STATE_DIR/*.md; do [ -e $f ] || continue echo --- $(basename $f) --- cat $f echo done echo PROJECT STATE END fiS20-load-task.sh类似只是目录换成agent.state/task且只读current-task.md。S30-validate.sh做一致性检查。我的版本会提取任务文件里出现的文件路径逐个检查是否存在#!/bin/bash TASK_FILEagent.state/task/current-task.md grep -oE [a-zA-Z0-9_/.-]\.(js|ts|py|md) $TASK_FILE | sort -u | while read -r f; do if [ ! -e $f ]; then echo WARN: referenced file missing: $f fi doneS40-emit-context.sh把前面所有输出拼起来加上一段固定前缀#!/bin/bash echo 以下是你代理的持久状态。请以此为准不要依赖对话历史。 bash agent.init.d/S10-load-project.sh bash agent.init.d/S20-load-task.sh echo VALIDATION bash agent.init.d/S30-validate.sh代理启动时先跑S40把输出注入到初始提示里。这一步可以用一个包装脚本自动完成也可以在代理框架的钩子里做。4.3 状态更新什么时候写、写什么状态文件不是写完就不管了代理在运行过程中要持续更新它。我的规则是任何“如果丢了会导致后续行动错误”的信息立刻写进状态文件。具体触发点包括完成一个子任务、做出一个架构决策、发现一个约束条件、遇到一个阻塞、改变任务目标。每次更新只改current-task.md里对应的段落不要重写整个文件避免引入格式错误。写的时候有个技巧用“事实理由”的格式。比如不要只写“使用camelCase命名”而要写“使用camelCase命名理由与现有代码库保持一致避免混用”。理由部分在重建时特别有用因为代理能理解为什么从而在边缘情况下做出正确判断。注意状态文件不要写得太细。我一开始把每个函数签名都记进去结果文件膨胀到几千行加载慢代理也抓不住重点。后来改成只记“决策”和“约束”细节让代理自己去读代码。状态文件是索引不是副本。4.4 与代理框架的对接不同的代理框架对接方式不同但核心都是“在会话开始时注入状态在关键节点触发状态更新”。我用的是一个支持钩子的框架在on_session_start钩子里跑S40把输出塞进系统提示在on_tool_result钩子里检查是否触发了状态更新条件。如果你的框架没有钩子退而求其次的办法是在系统提示里写死一段指令让代理每次开始工作前先自己跑一遍初始化脚本通过执行shell工具并把结果读进来。这个办法不如钩子可靠但能用。对接时有个坑注入的状态不能太长。我试过把整个项目状态全注入结果占了上下文一大块反而加速了压缩。后来改成只注入任务级状态和项目级状态的摘要完整内容让代理按需读取。摘要用脚本自动生成比如只取每个文件的前几行。5. 常见问题与排查技巧实录5.1 代理不读状态文件怎么办这是最常见的问题。代理启动后明明注入了状态它还是按自己的“记忆”干活。原因通常是注入内容被淹没在了系统提示的其他部分里或者代理的指令遵循能力不够强。我的解决办法有两个。一是把状态注入放在系统提示的最前面并且用醒目的分隔符包起来比如 CRITICAL STATE 。二是加一句强指令“在开始任何操作前先用一句话复述你的当前任务和目标。”这句复述强制代理处理状态内容而不是忽略它。实测下来加了复述指令后代理读取状态的比例从大概一半提升到了九成以上。代价是每次会话开头多花几十个token完全值得。5.2 状态文件与实际情况不一致代理更新状态有延迟或者更新时写错了就会导致状态文件与代码实际情况不符。比如状态里写着“已完成登录模块”但代码里其实只写了一半。排查办法是定期跑一致性校验。我除了S30的静态检查还会在任务关键节点让代理做一次“状态审计”读状态文件对照代码报告差异。差异分两类处理状态落后于代码就更新状态代码落后于状态就补代码。实操心得状态审计不要频繁做每个大阶段做一次就行。做太勤会打断代理的工作流而且代理自己审计自己容易“自圆其说”发现不了问题。我一般是在任务切换或长时间会话后做一次。5.3 初始化脚本执行失败脚本失败的原因五花八门路径不对、权限不够、依赖的命令不存在。因为脚本是在代理环境里跑的环境可能和你的开发机不一样。我的做法是让脚本“失败也要输出”。每个脚本开头加set e关键步骤用|| echo WARN: ...兜底确保即使出错也能输出一段可读的信息而不是静默失败。代理看到WARN会知道状态可能不完整从而更谨慎。另外脚本里不要用太花哨的命令。我一开始用了jq解析JSON状态结果代理环境里没装jq整个初始化就挂了。后来改成纯bash和基础文本工具兼容性好很多。5.4 压缩后代理“人格分裂”有时候压缩发生在会话中途代理前半段按状态文件干活后半段突然开始按压缩后的残缺记忆干活行为不一致。这个问题比较棘手因为压缩是框架层的行为你控制不了。我的缓解办法是在系统提示里反复强调“状态文件是唯一真相来源”并且在代理每次调用工具前用钩子检查它最近有没有读过状态文件如果超过一定轮次没读就提醒它重读。这个提醒机制有点像《记忆碎片》里主角定时看纹身的习惯。频率不用太高我设的是每十轮工具调用提醒一次效果不错。5.5 常见问题速查表问题现象可能原因排查动作解决方向代理忽略状态文件注入位置靠后或指令不强检查系统提示结构前置注入复述指令状态与代码不符更新延迟或写错跑状态审计按差异方向修正脚本执行失败环境差异或依赖缺失看脚本输出用基础命令失败兜底压缩后行为不一致框架压缩不可控观察压缩触发点定期提醒重读状态状态文件膨胀记录过细看文件行数只记决策和约束6. 一些延伸想法和实际体会这套方案跑了一段时间最大的感受是代理的可靠性不取决于模型多强而取决于它的状态管理多稳。同一个模型加上状态持久化之后长会话里的表现提升非常明显尤其是跨天任务基本不会出现“忘了昨天干了啥”的情况。延伸出去这套思路还能用在几个地方。一是多代理协作每个代理有自己的状态目录通过共享的项目级状态协调避免互相覆盖。二是任务交接一个代理干到一半把状态文件交给另一个代理后者跑一遍初始化就能接手。三是审计和回放状态文件本身就是一份操作日志出问题可以回溯。当然这套东西不是银弹。它解决的是“记忆连续性”问题不解决“推理能力”问题。代理该算错的还是会算错只是算错之后你能从状态文件里看出它是在哪个前提上算错的排查起来快很多。最后分享一个小技巧状态文件的文件名和目录名尽量用英文内容可以用中文。因为脚本处理文件名时英文更稳而内容用中文对模型的理解更自然。这个混搭是我试了几种组合后觉得最顺手的。如果你也在被上下文压缩折磨建议先从任务级状态文件开始就一个current-task.md配合一个加载脚本成本很低但效果立竿见影。跑顺了再往上加项目级状态和校验逻辑。别一上来就搭全套容易在细节里迷路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑