如何构建一份让项目可持续维护的 AGENTS.md
你的仓库里现在住着一位新同事。
他读过全网的代码,聊什么都头头是道;他从不请假,一天能改三十个文件;他对你写的每一行注释都怀有真诚的敬意——前提是他能看懂。看不懂的时候,他会非常客气地把它们删了。
这就是今天的开发现实:AGENTS.md 不是写给未来人类同事的 README,而是写给这位实习生的入职须知。而实习生的悲剧永远是同一个——他不是不听话,他是太听话了,认真地把好事做坏。
我最近读了一份写得相当好的 AGENTS.md(285 行,来自一个叫 NextCoWork 的 Electron 项目),它几乎回答了「怎么给 agent 立规矩」这个问题的全部。这篇文章拆开讲讲,凭什么它好。
一、别讲理念,讲症状
文档开头有一句话,基本就是整份文件的方法论:规则按「违反了会出什么事」来写。
对比一下这两种写法:
❌ 记得在 useEffect 的 cleanup 里清理事件监听器。
✅
on()的返回值必须进useEffect的 cleanup。漏一个,HMR 每次热更叠一层监听器,表现为一次状态更新触发 N 次重渲染,只在 dev 出现,极难定位。
左边是理念,右边是症状。理念读完就忘,症状会留在脑子里——因为它描述的是一段具体的、糟心的、可以想象的排查经历。
写每条规则的时候,问自己一个问题:违反它的那天,屏幕上会出现什么?把那个画面写进规则里。「且零报错」这种话尤其值钱,因为它预告了 agent 最害怕的场景:一切看起来都是对的。
二、给规则配上坐标和数字
这份文档里到处都是 services/sessions.ts:18、App.tsx:139 这样的地址。这不是炫技,是给 agent 一条可以自己走过去的路——他不用猜「范例大概长什么样」,直接打开看。
数字同理:「components/ui 已有 19 个原子控件」「渲染层 any 出现 0 次」「26 个 tsx 超过 400 行,最大 2291 行」。数字干两件事:一是可验证,agent 写完能自己核对有没有越线;二是可守线,「0 次」是一条清清楚楚、越过去就知道自己越过去了的线。
「尽量避免 any」则什么线都不是。
三、主动交代烂账,否则他会替你「顺手修好」
文档里有一节叫「已知违规」,把几个越界 import、四处裸写的频道字符串一一点名,后面跟着一句关键的话:
碰到那块代码时顺手修,不要专门开一轮重构。
还有一节技术债清单,细到「有个空的重复目录,见到可删」。
为什么这个动作重要?因为 agent 看到代码不符合文档时,默认行为就是顺手统一一下风格。你不写明「这账我知道,先记着」,他就替你开一轮你从没批准过的重构,然后在回复的末尾用一句话带过。烂账清单的本质不是自曝其短,是取消他自作主张的授权。
四、立法,要冲着他的真实弱点去
这份文档最有味道的部分,几乎全是冲着 LLM 的习惯性失误写的:
「不做防御式 UI——画出来的每个控件都是一次会失败的承诺。」「别为了消掉 React 警告挂 index key。」「改既有文件一律用精确替换,整文件写回去等于把你读取之后别人做的所有改动删掉。」「一条你看不出用途的注释,默认它比你知道得多。」
这些规则有个共同点:人不常犯,agent 常犯。人会犯的错,ESLint 管得住,code review 管得住;agent 会犯的错——过度重构、覆写并发改动、删除看不懂的注释、用防御式 UI 弥补后端的无能——只有这份文档管得住。
写 AGENTS.md 之前,先回忆上一个 agent 把你哪个仓库改崩过。那份回忆就是目录。
五、写理由,别只写结论
判空写显式比较:
x !== null/x !== undefined/flag === true。理由是这个代码库里''、0、false全是有意义的值。
结论只约束文档写到的场景,理由会自己走到文档没写到的场景里去。下次他写一个你当初没想到的判断,那句「0 是有意义的」会自己生效——这是理由相对结论唯一的、但致命的优势。
同一个逻辑,文档里还有一条更狠的:看起来多余的代码,必须注释为什么不多余。空判、去抖、多出来的一次 await,全是最容易被后来者当垃圾清掉的东西,清掉之后的 bug 往往要几周才复现。不写理由,你的代码就活在「随时被善意简化」的风险里。
六、规则会打架,所以要有终审法官
文档的最后一行是这样的:
最后一条,优先级高于上面全部:只做被要求的改动,且破坏面最小。
规则一多必然冲突——「架构优先于先跑起来」对上「最小 diff」,听谁的?不给裁决顺序,agent 会自己挑一个,而且每次挑得不一样。一句话就够,但必须有。这也顺带定义了这份文档的价值观:怀疑一切「顺手」。
七、把踩过的坑,写成坑的位置
「新增 locale 要同时改:SUPPORTED_LOCALES、两份 catalog、设置项校验、语言选择器,然后跑 typecheck + test。」这种联动清单,代码里完全看不出来,只能靠文档传承。
比清单更值钱的是验证命令本身的陷阱:
绝不手敲
tsc -p tsconfig.web.json(不带--noEmit):产物会落在源码旁边并盖住源码,dev/build/vitest 从此加载旧版本且零报错。
这种句子不是「最佳实践」,是事故报告。一次事故换一行文档,这才是 AGENTS.md 里含金量最高的资产。你踩过的每一个坑,都值得写成坑的坐标。
八、文档会过期,让代码注释接手
最后这点最容易被忽略:AGENTS.md 会烂。这份文档自己就承认「多处注释指向已不存在的 docs/」。
所以它做了个聪明的设计——要求新代码带需求注释:这块代码为了满足什么需求而存在,不满足会出什么症状。再配上一条「别人的注释是事故现场,不读懂不准删」。于是 AGENTS.md 是宪法,代码注释是判例法:agent 走到哪个文件,哪个文件就有当地规则,随代码生、随代码灭,不指望有人回来维护那份中央文件。
带行号、带计数的段落也一样:改代码的那天,记得回文档同步。文档里每个 :18 都是一根引线。
写完之后,自查五问
- 每条规则背后有症状吗?
- agent 能顺着
file:line自己核对吗? - 仓库的烂账交代了吗?
- 规则冲突的时候,谁赢?
- 半年后这段还成立吗?谁负责回来改它?
五个问题里但凡有一个答不上来,那一条就还不能进文件。AGENTS.md 不是宣言,是一份持续更新的事故汇编——谁把它写得像宣言,谁的仓库就会用一种非常文明的方式慢慢烂掉,全程零报错。
案例来源:/Users/token/Desktop/code/NextCoWork · AGENTS.md(285 行)· src/renderer/src/**
要落成 .md 文件的话说一声。
浙公网安备 33010602011771号