前几天晚上开新会话,让 Claude Code 给一个接口加个可选参数。很小的活。
它动手之前先问我:要不要顺便把这块重构成 service 层。
我三个月前就否决过这个方案。而且我确信我写下来了,就在 AGENTS.md 里。问题是我写的那一行,这个会话根本读不到——它被埋在三百多行文档的第四十几行,前面还有十几条不相关的规则在跟它抢注意力。
我盯着那句"要不要顺便"愣了半分钟,然后打开自己的 AGENTS.md 从头翻。
找到了。就一行:
- service 层拆分方案已否决
没了。为什么否决、谁提的、当时什么背景,一个字没有。那行字长得就像一条莫名其妙的戒律。换我是 agent,我也得再问一遍。
这个文件是怎么长成这样的
最开始只有六行,全是行为类的:不要改无关代码、优先最简单方案、不要静默做架构决策、小改动别跑全量测试、没让提交就别提交。
后来出一次事故加一行。
- 不要改无关代码
- 优先最简单的方案
- 不要静默做架构决策
- 保留能跑通的逻辑和提示词
- 小改动不要跑全量测试
- 未经要求不要 commit / push / deploy / delete
- 数据库连接池参数不要动,历史原因
- 图片处理一律走队列,不要同步
- service 层拆分已否决
- 前端不要引入新的状态管理库
- ...(此处省略二十几条)
每加一行的时候我都知道原因。两个月之后原因就没了,只剩一行看起来毫无道理的规矩。再后来 agent 看到这些规矩也不好意思问,就绕开走——绕开的方式往往是发明第三种做法。
真正的毛病不在内容,在时机
AGENTS.md 最要命的一点是它每次会话都无条件进 context,不管这次任务相不相关。
画外音:这是那种"看起来省事、实际每次都在收费"的设计。
一个文件塞得越满,里面有效的规则就被稀释得越稀。我以前的思路是"写多一点总比少写好,反正它读得到",结果恰好反过来——写太多,重点被冲掉,等于没写。
而且我在做的其实是把四类完全不同的东西往一个筐里扔:
- 代理在这个项目里该怎么干活(行为)
- 项目现在长什么样(现状)
- 当初为什么这么做(决策与理由)
- 什么试过、炸了(失败)
区别不在分类学,在于变化频率和读取时机完全不同。行为规则我一年改两次,但每次会话都要;现状每周在动,大部分任务需要;决策只在碰到相关模块时才需要;失败只在踩到那片地时才需要。全按"每次都进"的成本喂进去,等于用全额票价买一张只坐一站的车。
上个月在 dev.to 上刷到有人把这事拆成四个 Markdown 文件——行为、现状、决策、失败各一份,AGENTS.md 只留"该怎么干"和"去哪儿找"。我第一反应是这也太啰嗦了,四个文件谁维护。
看完之后我闭嘴了。因为我那个三百行文件里最值钱的东西,恰恰是分不清这四类才丢掉的。
边界比内容难
自己动手之后才发现,难的不是写什么,是每一条新信息都想住进错误的文件。
一次调试踩的坑,写进 ERRORS.md 是对的;但写的时候手一滑,就想把它总结成一条"以后不要这么做"的规则塞进 AGENTS.md——因为规则听起来更省事。代价是那条坑的现场、报错、试错过程全丢了,只剩一句结论。下次有人(包括 agent)想反驳这条结论,没有任何证据可以参照。
所以现在 AGENTS.md 里只留两类:一是干什么、不干什么,二是"要了解现状去看 OVERVIEW.md、要动历史决策去看 MEMORY.md"。它从"项目大脑"降级成了"路由器"。降级那天我心里还挺不舒服的,觉得是不是偷懒了。用了一周之后,重复解释的次数反而少了。
OVERVIEW 不是开发日记
这条我觉得比文件拆分本身更重要。
现状文档只描述当前真实状态。数据库换了就改那行,不要追加一句"我们以前用的是另一个"。
<!-- 就这样写 -->
主库:Postgres,通过 Prisma 访问,schema 在 /prisma/schema.prisma
<!-- 别这样 -->
主库:Postgres
(注:2025 年 3 月之前用的是 MongoDB,后来因为 XX 换掉了,
具体过程见会议记录……)
理由很直接:agent 读这页是为了搞清楚"现在跑的是什么",不是来读考古层的。每一行历史都在稀释当前状态的信噪比,而且追加式更新有个隐蔽的后果——半年后这份文档会同时包含三个年代的真相,agent 分不清哪行还有效。想知道以前用什么,git log 和 MEMORY.md 里都有。
代码和文档,谁说了算
这是整个系统里最容易被当成一句漂亮话带过去的地方。
我见过的做法分两派:一派把文档当圣旨,代码和文档不一致就是代码写错了;另一派反过来,觉得文档早晚腐烂,不如直接读代码。两派都错。
我现在的口径是:代码是"实际发生什么"的权威,文档是"原本打算怎样"的权威,两者不一致本身就是一个待调查的信号,不是一处待修的 bug。
这个区分救过我一次。有份文档写着"上传完成后立刻写元数据",代码里是先入库、再异步补元数据。我第一反应是有人图省事偷懒,差点让 agent 按文档"修"回去。查下去才发现,元数据服务在高峰期会超时,同步写会把整个上传链路拖死——这是当初临时改的,改完没记理由,文档留在原地没动。
那份文档不是描述现状的,它描述的是一个已经被现实否决的意图。如果我或者 agent 信了文档,把代码"修"回那个顺序,等于把当初的保护悄悄拆掉。这种坑不报错,只在上线之后慢慢显形。
所以现在遇到不一致,我的动作是先不下判断——先把它当成一个问题查:为什么这里会分叉?代码改过什么?文档是什么时候停更的?
坑还不止于此。agent 有个很自然的倾向:它倾向于相信先读到的那份。先读到文档就按文档改代码,先读到代码就按代码改文档,两种都很快,两种都在猜。我后来在给它看的说明里明确写了一句话:发现不一致,先标记、先问,不要自己选一边站。
不写什么,比写什么重要
维护门槛我定得很粗,但每条都能一秒判断:
- MEMORY.md:如果一个有能力的同事后来可能因为不知道原因,而合理地把这个决定改回去,就记。
- ERRORS.md:如果这个坑让我或者别人花半天以上重新发现一遍,就记。
- OVERVIEW.md:这行字已经和代码对不上了,就改。
反过来,新增一个设置页、修个拼写、一次普通依赖升级——不记。这些是变更日志,变更日志有 git。
我一开始忍不住什么都记,ERRORS.md 一个月涨到五百行,后来连我自己都不查了。一个没人查的知识库等于不存在。这话听起来很废话,但真正动手的时候,克制比勤奋难得多。
还有一件我没想明白的
评论区有人提了个我觉得很准的担忧:一个针对某个依赖 bug 的临时 workaround,可能在三轮版本升级之后变成没人敢动的"永久迷信"。它不是过期了,是没人记得当初为什么写。
我现在的做法是不设自动过期——自动过期听起来很美,但过期之后那行字还在,谁来判断它真的失效了?我改成给绑定了依赖的条目加一行"什么时候该重新检查":
## Node 版本锁在 18
原因:某个原生依赖在 20 上会 segfault
Recheck when: 该依赖升到 2.x 之后
通用教训不加这一行。至于这个做法是不是真的好用,我现在还真不敢下结论。可能三个月后我又来写一篇说自己想错了。
所以我的规矩就剩一条:往 AGENTS.md 里写东西之前,先问自己一句——这是"该怎么干",还是"为什么这么干"。
答"为什么"的,往后挪,挪到它该在的那份文件里去。
至于那个又问我"要不要顺便"的 agent,这周表现还行。它问完之后自己去翻了 MEMORY.md,回来跟我说:看到记录了,不改。😅
本期缴税:一次"我明明写过"换来一次"写在了它不会看的地方"。