我最近把项目的 AGENTS.md 认真重写了一遍,效果立竿见影——AI 提的代码里,绕过封装直接调内部实现的提交肉眼可见地变少了。这文件不是什么新概念,就是给 AI 编码助手看的“项目说明书”,但我发现大多数人要么没建,要么写成了流水账,真正能把架构约束、封装边界讲清楚的不多。这篇聊的就是我踩过几轮之后总结出的四种必须写进去的项目事实,以及为什么要这么写。
1. AI 写代码为什么会“见路不走”:先看看它绕封装的三张惯用面孔
先说个真实场景。我们团队在维护一个老系统,为了把订单模块和支付模块解耦,特意在中间加了一层门面类,统一对外暴露能力。结果 AI 提交的一次改动里,直接在订单模块 import 了支付模块的某个内部单例,绕过了门面,硬生生把两个月前刚洗干净的架构又搅浑了。代码能编译、测试能过,但评审的人一眼就看出不对劲:这一层封装白做了。
这绝对不是个例。我观察到的 AI 写代码“绕远路”行为基本有三种惯用面孔。
第一种是就近调用。模型在生成代码时,看到的上下文就是当前文件附近的符号表、目录树、已有的 import 片段。它没有“全局架构图”,谁离得近、谁名字看起来像要用的,它就调谁。你明明留了门面类在 service 层,但工具类里恰好有一个功能相似的函数,它就直接用了。
第二种是跨层偷偷 import。比如项目约定 controller -> service -> repository 三层,repository 只能被 service 用。AI 在改 controller 的逻辑时,发现直接 import repository 里的某个方法能省两步,就真的这么写了。它不会觉得这是违规——在它眼里,只要能解决问题就是合理路径。抽象层的存在是因为“人”知道依赖方向,模型只看代码文本时,这个方向感是缺失的。
第三种更有迷惑性,叫抄实现而不是走接口。有些封装是接口加内部实现分离的,AI 发现接口方法不能满足当前需求,它不会去扩展接口,而是直接把内部实现逻辑复制一份到新代码里改吧改吧。表面上没破坏原模块,实际上等于把核心逻辑散得到处都是,后续维护时改了一处漏一处。
为什么 AI 会反复出现这些问题?我的结论是:它不是在故意找茬,而是真的不知道这些规则。你脑子里有一套“什么能碰、什么不能碰、碰之前要找谁”的潜规则,这套规则从来没写下来过,代码里也看不出来。模型再聪明,也没办法从一行一行的代码里推断出“这个函数虽然看起来通用,但设计上只允许模块 A 调用”。
所以解决问题的方向就很清楚了:把那些只存在于人类脑子的项目事实,显式地写下来,放到 AI 每次开工前都会看的地方。这就是 AGENTS.md 的意义所在。
当然,也有人会说,那我每次在 prompt 里强调一遍不就行了吗?后面我会讲为什么这个做法不靠谱,以及为什么放在一个固定文件里、跟随代码库走,才是长期稳定的解法。
2. AGENTS.md 在哪一层起作用:项目事实从“人脑私有知识”变成“AI 开机自检清单”
先解释一下 AGENTS.md 是怎么工作的。现在主流的 AI 编码助手在开始处理一个仓库之前,会自动扫描根目录或者特定目录下的说明文件,把这个文件的内容塞进初始上下文。也就是说,只要这个文件存在,AI 在动手之前就看到它了。这跟你临时在对话框里贴一段话不一样,它是结构化的、每次都加载的、跟着代码库走的“开机自检清单”。
我习惯把它理解成给 AI 看的“入职手册”。新人进组第一天,你会给他讲:我们项目分成哪几层,上层能不能调下层,哪些工具函数是内部的不要动,改数据库前要找 DBA 确认。这些你不会写进代码注释里,但每个合格的组员都必须知道。AGENTS.md 就是干这个的,只不过读者变了,从人变成了模型。
这个定位非常重要。因为大多数项目里的 README、贡献指南、架构文档,默认读者是“人”,写的时候充满上下文,比如“这个模块是历史遗留的,后面郭哥会重构,大家先别动”。人能看懂,因为人会脑补省略的内容。但模型不会脑补,它只会把这些话当成背景信息扫一眼就过去了。
AGENTS.md 里的每一条,都应该指向一个具体动作:禁止做什么、必须做什么、做之前要问谁。它不是在描述项目,而是在约束行为。我见过很多团队把这个文件写成了迷你版 README,开头是“这是一个基于 Spring Boot 的电商项目”,然后列一堆技术栈,AI 看完毫无感觉,照样乱 import。问题就出在描述性信息太多,约束性信息太少。
另一个关键点是,AGENTS.md 最好直接放在仓库根目录。因为它跟随代码走,人人都能 review,每次改动都会留痕。AI 读到的永远是当前分支上的最新版本,不会出现你本地改了一版、CI 上还是老规则的情况。这一点比放在个人笔记里、放在某个平台的私有设置里都可靠。
我在实际使用中还发现一个现象:AGENTS.md 对 AI 的约束力,取决于文件的开篇。模型对上下文头部的关注度显著高于中后部。所以如果你有很多规则要写,最重要的架构约束、封装边界一定要放在最前面。后面那些次要的代码风格、命名规范可以往后面放,哪怕被截断了,损失也不会太大。
不过这里要泼一盆冷水:AGENTS.md 不是银弹。它是一层护栏,不是自动驾驶。模型在长会话、多文件的复杂任务里,依然可能把后面的约束“忘掉”,或者被大量中间代码把注意力稀释掉。后面我会具体讲怎么缓解这个问题。现在我们先把重心放回最核心的部分:到底把哪四种项目事实写进去,才能最大程度避免绕封装、破坏架构。
3. 把哪四种项目事实写进去:每条都要能挡住一次真实事故
这四种事实是我在一次又一次 code review 里被坑出来的,不是从哪本方法论里抄来的。每一条都对应一类具体的“AI 犯错模式”。你可以直接对照自己的项目,看哪几条缺失最严重。
3.1 事实一:架构分层与依赖方向
这条解决的是“跨层偷偷 import”和“就近调用”的问题。大部分项目都有分层,但绝大多数项目没有把分层的依赖规则写下来。AI 看不到规则,自然就按照“哪个近用哪个”的逻辑走。你需要在 AGENTS.md 里写清楚:项目分哪几层、每层的职责边界是什么、依赖方向是朝哪儿的、禁止反向依赖、同层之间能不能互相调用。
不要写成抽象的原则,比如“高内聚低耦合”这种话模型听了跟没听一样。要写成可判断的真命题:controller 不得直接使用 repository;任何一层不得 import infrastructure 里的实现细节;service 之间可以互相调用,但必须通过接口,不能直接 new 对方实现。你甚至可以画一个字符版的依赖方向图,简单暴力:
## 架构依赖方向 - 分层关系:controller -> service -> repository -> infrastructure - 依赖只能自上而下,禁止反向依赖。 - controller 禁止直接使用 repository,必须走 service。 - 任何层禁止 import infrastructure 内部的实现类。 - 如果业务逻辑需要跨层访问,说明 service 层缺入口,先补齐 service 层方法。最后一条尤其重要。它给了 AI 一个“正确的绕路方式”。模型在遇到障碍时,如果只有“此路不通”,它可能会尝试其他歪路;但如果文件明确给了“你应该走哪条路”,它按正确路径走的概率会大很多。这跟给人类新手指路是一个道理,光说“这里不能走”不够,还要告诉他“从旁边那扇门进去”。
写这条的时候,我还建议把当前项目里**已有的、经常被绕过的“中间层”**单独点名。比如你们的 gateway、门面、外观类、统一认证入口,这些模块天生就是用来被“走一遍”的。AI 最容易觉得它们多余,于是自动跳过。你单独写一句“调用外部系统必须经过 ApiGateway,禁止直接构造客户端”,它能挡掉好多次事故。
3.2 事实二:封装边界与公开接口
这条解决的是“抄实现而不是走接口”和“调用内部单例”的问题。分层管的是模块之间怎么走,封装边界管的是“什么算公开、什么算内部”。很多项目对外的公共 API 是刻意收敛过的,但代码层面没有任何标记,外部模块照样能 import 内部符号。AI 判断“能不能用”的标准只有一个:这个符号在不在代码里。你没法让它凭空知道“这个函数虽然写在 utils 里,但设计上只给订单模块用”。
所以你得把“封装事实”写出来。可以直接在 AGENTS.md 里划定公开区域与内部区域:
## 封装边界 - 对外公开 API 仅限 `src/public/index.ts` 导出的符号。 - 任何带 `internal` 标记或位于 `internal/` 目录的模块,禁止被其他模块 import。 - 如果外部模块需要某个内部能力,先给维护者提需求,在公开 API 中新增方法,禁止直接到内部实现里取。 - 工具函数允许跨模块使用,但只准调用 `src/utils/common.ts` 中列出的方法,其他 utils 均为模块私有。这里的核心是“把内部实现半透明化”。给 AI 划出一条明确的分界线:这一圈之外你随便用,这一圈之内你需要提前打招呼。模型遵循显式边界的能力比我们想象中强,前提是边界必须清晰,不能说“内部实现尽量不要用”——“尽量”这个词在约束语境下基本等于没说。它会把你的话当成建议,而不是规则。
我在项目里还加了一条很管用的约定:如果 AI(或人类)发现某个内部方法确实需要被外部使用,正确动作是把方法提升为公开能力,并写上注释;而不是在调用处直接绕过。这样一来,需要绕过封装的情况就直接转化成了对公开 API 的补充,架构演进的方向反而是明确可控的。因为 AGENTS.md 跟随仓库提交记录走,哪一次新增了公开方法、为什么新增,全部有迹可循。
3.3 事实三:全局约定与风格惯例
这一条看起来没有前两条“硬”,但实际对代码评审时间的影响非常大。AI 写代码时,对于命名、错误处理、日志输出这些没有强制约束的东西,会从训练数据里挑一个“概率最高”的写法。问题是,你的项目已经形成了自己的潜规则,只是没有人写下来。
比如你们的项目约定所有错误都必须抛业务异常,统一由全局 handler 处理,禁止在业务代码里 catch 之后吞掉。但 AI 生成的代码可能是另一个思路:catch 一下,打条日志,返回 null。单看这段代码没什么大问题,但放在整个系统里就是事故——调用方拿不到任何失败信号,还以为执行成功了,数据悄悄丢了。
这就是我为什么建议把全局约定也当成“项目事实”写进 AGENTS.md。命名规范、错误处理路径、日志格式、事务边界、并发模型、枚举与常量的存放位置……这些都要写,尤其是那些已经形成“两年多没人破坏过”的默契规则。
写的时候有一个技巧:把每个约定写成可以被机器检查的形式。不要说“日志要规范”,要说“禁止使用 console.log,统一使用logger,结构化字段必须包含 route 和 costMs”。不要说“事务要处理好”,要说“所有跨表写操作必须经过@Transactional或仓储层事务模板,禁止在 service 裸写多条 update”。这样 AI 在生成代码的时候,等于有一张“提交前检查清单”,它自己就能过滤掉一批不合规的写法。
另外,这一节里我强烈建议把“新代码的默认行为”写清楚,而不是只写“禁止行为”。例如:
## 编码约定 - 新增业务枚举统一放在 `src/constants/biz.ts`,禁止散落在各模块。 - 错误统一抛 `BizException`,禁止 catch 后 return null/undefined。 - 日志禁用 print/console,统一 `logger`,必带参数 `route`。 - 新增对外接口需要先写单元测试,禁止先用集成测试跑通就算完。你可能会觉得这种条目太多了写不过来。我的经验是:不用一上来就写全,把平时 review 里被 AI 逼疯的那几类问题先列进去,后面遇到新的再补。AGENTS.md 是活文档,不是入职手册,一次写全是做不到的。
3.4 事实四:历史约束与已知禁区
这是最少有人写、但价值最高的一类。很多架构约束不是从设计原则推导出来的,而是历史事故留下的教训。比如某个模块曾经因为并发调用问题被迫加锁,你不知道的话,就会在代码里加缓存,导致死锁;某个目录是自动生成代码,改了之后一刷新全没了;某两个模块之间有循环依赖,上一次重构花了三周才解开。这些“伤疤”在代码里几乎看不出来,只有踩过坑的人知道。
AI 刚进项目,就是那个“没踩过坑的人”。它看到的代码都是合法的、存在的,它无法意识到某个模块为什么长得这么别扭。把历史约束和已知禁区写进 AGENTS.md,本质上就是让 AI 站在“团队集体经验”的肩膀上,而不是每次都从零开始摸索。
我建议专门留一个小节,起名叫“禁区与雷区”。
## 禁区与雷区 - `src/generated/` 下的文件由代码生成器产出,禁止手改,需要修改就改模板再重新生成。 - `src/legacy/connector/` 是历史债务模块,只允许读取,禁止新增依赖和修改内部逻辑。 - 支付回调模块 `PaymentCallbackHandler` 有并发保护,重量级操作必须走队列,禁止在 handler 内直接做远程调用。 - 当前 `order-cache` 模块正在重构,涉及缓存 key 变更必须先找负责人确认。每一条背后都是一次事故或者一个“不能碰”的理由。你不必把这些理由全部写进去,但至少要让模型知道结果。如果空间允许,加一句简短的原因是很有帮助的——“禁止手改,因为生成器会覆盖”——这句话能让模型理解规则背后的动机,而不是机械地遵守莫名其妙的限制。
这里有个细节要注意:不要把禁区写成“惩罚语气”。像“上次谁谁改了这里出了问题,谁也不准再碰”这种话,对模型没有任何说服力,它只会觉得这是项目里的一句废话。更有效的写法是陈述事实:这里是什么状态、为什么不能碰、如果确实需要动应该找谁。模型面对中性事实的遵循度,远高于面对情绪化警告。
这四种事实,我把它们分了类,但在实际项目里它们是交叉起作用的。一个 AI 生成的代码,可能同时违反分层规则、绕过封装边界、破坏编码约定,还踩进历史禁区。写清楚这四类,基本就能覆盖百分之八十的架构破坏型改动。
4. 写、改、维护 AGENTS.md 的方法论:让约束真正被执行而不是摆设
很多人写 AGENTS.md 时最容易犯的错,是把它写成了“愿望清单”。什么“确保代码质量”“注意性能问题”“保持代码可读性”,全是人类看了觉得有道理、模型看了没法执行的话。想让 AI 真正遵守,写法上要遵循几个原则。
第一,用“必须 / 禁止 / 如果……那么……”句式,不用“尽量 / 建议 / 最好”。前者是规则,后者是参考。模型对待规则和参考的执行力度完全不是一个量级。你在约束语境里写“尽量使用缓存”,它在大多数情况下都会忽略;你写“禁止在接口内做重计算,结果必须缓存”,它就会认真对待。
第二,分优先级。我习惯把所有条目按 MUST / MUST NOT / ASK 三种盖戳。MUST 是必须遵守的硬规则, MUST NOT 是绝对不能做的,ASK 是需要找负责人确认才允许动的。ASK 条目不宜多,每一条都要对应一个明确的人,比如“涉及支付接口签名变更,先找维护者确认”。如果你写“涉及支付接口请谨慎修改”,这不算 ASK,因为没有明确动作路径——找谁确认、确认什么、怎么样算确认完成。模型没法执行。
第三,控制篇幅。AGENTS.md 是被塞进模型上下文里的,每多一行都会占用注意力预算。文件太长了,前面重要的规则会被后面不重要的内容稀释,甚至可能被上下文截断。我的实践是根目录 AGENTS.md 控制在 30 到 80 行,只写核心约束。那些详细的模块说明、接口设计文档放到 docs/agents/ 子目录,然后在 AGENTS.md 里给个链接。模型需要看的时候会自己去找,不会主动把几万字全读进来。
第四,跟着代码评审一起维护。AGENTS.md 修改要像代码一样走评审,不能一个人偷偷改。我在团队里定了一条规则:每次 code review 时如果发现 AI 又绕了一次封装,reviewer 有义务顺手在 AGENTS.md 里补一条对应规则。这个动作的妙处在于,它把“踩坑”直接转化成了“团队资产”。下一次无论谁来用 AI 写代码,这些教训都已经自动生效了。
我也见过团队把 AGENTS.md 维护成了“吐槽本”,里面写着“AI 总是乱改这个文件,真烦人”“别再调用这个接口了”。这种情绪化的表达可以解压,但对模型没用。把情绪去掉,留下事实和动作:哪个模块、什么行为、必须怎么做,就够了。
还有一个容易被忽视的点:AGENTS.md 里出现的模块名、文件名、函数名,必须和代码库现状完全一致。模型对符号名的匹配是精确的,你不写全名只写“结算模块”,它可能对不上号。所以每写完一条,我建议实际操作一次验证一下:假装自己是 AI,照着这条规则去代码库里找对应位置,能不能找到。找不到就说明写得太模糊,需要补全路径。
最后,给一个可以直接改的微型模板。这个模板不适合直接生产使用,但可以当架子:
# AGENTS.md ## 一句话项目定位 这是一个 XX 类型的服务,核心业务链路是 A -> B -> C。 ## 架构依赖方向(MUST) - 分层:controller -> service -> repository -> infra。 - 禁止跨层调用;禁止同层互跳。 ## 封装边界(MUST) - 外部模块只能使用公开的 index 导出符号。 - 带 internal 标记的内部符号禁止被外部 import。 ## 编码约束(MUST / MUST NOT) - 错误抛 BizException,禁止吞 catch。 - 禁用 console.log,使用 logger。 ## ASK 区域(改前先问人) - 支付、订单、用户三个核心模块的接口签名变更。 - `src/legacy/` 目录下的任何修改。这个架子不需要一次写满,随着项目演进慢慢填。你每填一条,都等于给未来的自己省了一次 code review 里解释架构的时间。
5. 落地后怎么验证效果:一个月实测的感受与仍然要人工盯的边界
AGENTS.md 写完之后,不要急着觉得万事大吉。我建议先做一轮“验收测试”,用一组探针任务来检验它到底有没有挡住该挡的东西。具体操作是,挑 6 到 8 个日常需求,包含以下几种典型陷阱:给某个模块加新功能、复用别处一个功能相近的内部函数、调整某个现有方法的可见性、在旧目录里加新文件。每道题正常做,然后看 AI 是不是又走了歪路。
我当时用的是几个经典探针:
- “在订单模块里加一个查询库存的方法”——看它会不会直接 import 库存模块的内部函数。
- “帮我把 utils 里的时间格式化方法用在接口层”——看它会不会跨层抄实现。
- “给支付回调增加一个限流”——看它会不会绕过队列直接做远程调用。
- “把这个类的某个 protected 方法改成 public”——看它是否擅自扩大公开面。
第一轮测下来,没写 AGENTS.md 之前,六个任务里至少三个会出现不同程度的违规。写了之后,违规数一般会降到一到两个,有些硬约束甚至能做到零违规。依赖边界和封装边界的改善是最明显的,因为这两类约束写得最死、最容易被模型理解。而那些偏编程风格的规则,改善没有想象中那么大,还是会有漏网之鱼。
这时候就要面对现实:AGENTS.md 不是一个写完就一劳永逸的东西。它最大的弱点出现在长会话、大改动里。一个 task 如果涉及到二三十个文件,中途不断有新的代码上下文塞进来,模型注意力会被长上下文稀释,开头读到的 AGENTS.md 内容可能不知不觉被“挤”到后面去了。到了改第 15 个文件时,它可能又按照自己的直觉来。
针对这个现象,我试过几个缓解办法。一个是在任务描述里主动点名要重点守住的规则,比如“本次改动涉及支付模块,请严格遵守 AGENTS.md 中 ASK 区域的要求”。这相当于手动把关键约束再往前提到注意力区。另一个办法是让 AI 在动手前先输出“本次改动计划及将遵循的约束清单”,逼它把规则和任务建立显式关联。实测下来,这两个操作都能明显降低长任务中的违规率。
还有一种情况是模型会把规则理解得过于僵硬。比如写了“禁止修改 legacy 目录”,结果 AI 连“在 legacy 目录新增一个只读配置”都不敢做了。这时候需要在规则后面补一句例外条件:“只允许新增配置文件,禁止修改现有逻辑”。模型理解例外条件的能力还是不错的,关键是你要把“例外”提前写出来,不能等它自己去推断。
另外一个很容易踩的坑是规则过时。项目在演进,架构在调整,AGENTS.md 如果不跟着更新,就会出现“规则写着不能碰 A 模块,但 A 模块两周前已经被重构好了”的情况。模型严格按旧规则走,反而制造了不必要的障碍。所以我养成一个习惯:每当架构有变动时,第一时间同步更新 AGENTS.md,绝不让它滞后超过一个迭代周期。
最后讲一下我对 AGENTS.md 的定位和投入产出感受。它不是把架构师的工作委托给 AI,而是把架构师脑子里的规则“外置”成团队资产,让每一次 AI 生成的代码都站在这些规则之上。我算过一笔账:维护 AGENTS.md 每周大概花 10 到 15 分钟,但省下的 code review 时间远超这个数。以前要逐条解释“为什么不能这么调”“上次因为这个出了事故”,现在指着文件说“规则第三条”就够了。
说到底,AI 写代码的能力已经很够用,真正拉开差距的是你怎么给它提供上下文。AGENTS.md 就是目前性价比最高的一种上下文工程手段。如果你还没试过,建议下周就建一个,先写两条你觉得最痛的限制,跑几轮任务感受一下。等看到 AI 主动在提交说明里引用你写的规则时,你会回来继续把它填满的。