先说个我亲眼见过的场景:团队让 AI 给用户模块加一个导出功能,结果它直接改了实体类,还在 Controller 里 new 了一个仓储对象,绕过了 Service 层。代码能跑,但架构已经坏了一半。这类问题太常见了——AI 不是不聪明,是没人告诉它你们项目的规矩在哪。后来我们开始写 AGENTS.md,把四种项目事实整整齐齐地放进去,AI 写的代码肉眼可见地收敛了,绕过封装和破坏架构的情况少了七成。
这篇文章就聊一件事:怎么用 AGENTS.md 把项目里的关键约束喂给 AI,让它在动手之前先看懂你们的架构。内容适合正在用 AI 编程助手、又总被它"乱写"坑到的开发者,也适合想在团队里建立一套 AI 编码规范的技术负责人。我会把四种事实分别拆开讲清楚,附可直接抄的模板,再聊几个我踩过的坑。
1. 为什么 AGENTS.md 能按住 AI 的手:上下文工程的基本盘
1.1 AI 在不了解项目时,是怎么"乱来"的
你把 AI 编码助手想象成一个第一天入职的实习生,它对你们项目的认知约等于零。你丢给它一个文件路径,说"改这里",它就只看到这一小块代码,然后基于它见过的几百万个开源项目来猜全局规则。这就是问题的根源。
我归纳过三种典型翻车现场:
- 现场一:AI 想当然。一个三层架构的项目,AI 直接在表现层调持久化接口,因为在它训练过的公共代码库里,有人就是这么写的。它不知道你们项目的依赖方向是单向的。
- 现场二:AI 重复造轮子。项目里明明有一个封装好的基础组件,AI 没看见,自己又写了一份功能几乎一样的,代码量翻倍,维护点翻倍。
- 现场三:AI 抄近路。写测试时直接访问私有状态,或者为了修 bug 给实体类加了一个 public setter,让状态变更散落得到处都是。
这些问题的共同点不是模型能力不行,而是上下文里缺少"项目事实"。
1.2 AGENTS.md 的定位:把隐性知识变成显式文本
人脑里有很多工程知识是不落盘的:控制器不能直接碰数据库、订单状态不能从已支付跳回待支付、新加依赖前要问一声……这些知识以前靠口口相传,新人来了靠问,老手走了就丢失。现在 AI 来了,你不能指望它"悟",只能把它当成一个阅读速度极快的实习生,你得把规矩写成文档放在它进门的必经之路上。
AGENTS.md 就是这条路。它是目前主流 AI 编程助手默认会读取的项目说明文件,放在仓库根目录,AI 处理代码前会先把它读进上下文。相比普通的 README,AGENTS.md 不是给人看的项目介绍,而是给 AI 看的"行为守则"。
这个文件写得好不好,直接决定 AI 是在你画的跑道里跑,还是在隔壁草地上乱窜。
1.3 放对位置、写对格式,AI 才会真读
AGENTS.md 的文件名不要随意改,就用这个标准名。位置放仓库根目录,优先级最高。如果项目很大,可以在关键子目录里再放局部 AGENTS.md,覆盖更细的规则。根目录管全局红线,子目录管局部细节。
格式上,Markdown 就够。注意几点:
- 每条规则尽量是"可检查的断言式语句",比如"控制器层禁止直接调用仓储接口",而不是"注意分层架构"这种模糊表达。
- 用"必须/禁止"表示硬约束,用"推荐/尽可能"表示软偏好,AI 会据此权衡优先级。
- 文件别写太长,根目录的 AGENTS.md 最好控制在 200 行以内,超出的细节放到 docs 目录里,再在 AGENTS.md 里指路让 AI 去读。
我见过团队把整个架构文档粘进去,结果 AI 读完就忘了任务,反而影响代码生成质量。上下文是稀缺资源,要精不要多。
2. 第一种事实:架构边界与模块地图
2.1 先让 AI 知道"世界长什么样"
AI 写代码时,要决定文件放在哪个目录、import 应该指向哪里、新类该继承谁,这些决策的前提是知道项目的整体结构。你不告诉它,它就靠猜。
最有效的办法就是给一棵目录树。注意不是随便贴个 find 的输出,而是带注释的结构树,标注清楚每一层的职责和边界。比如:
src/ ├── interfaces/ # HTTP 入口与路由,只做参数校验和响应转换 │ └── controllers/ ├── application/ # 用例编排,事务边界在这一层 │ └── services/ ├── domain/ # 核心业务模型,不依赖任何框架细节 │ ├── entities/ │ └── repositories/ # 只放接口,不放实现 └── infrastructure/ # 数据库、消息队列等外部实现 └── repositories/这段目录树本身就在传递规则:AI 看到 domain/repositories 下只有接口,生成实现时就会自觉放到 infrastructure 层;看到 controllers 的注释说的只是参数校验和响应转换,就不会往 Controller 里塞业务逻辑。
2.2 分层规则:写清依赖方向,禁止反向
目录树解决了"哪里有什么",还要解决"谁能依赖谁"。依赖方向是架构的生命线,AI 不懂这个就会到处乱 import。
我当时在 AGENTS.md 里写的是:
## 架构边界 - 项目为分层架构:interfaces → application → domain → infrastructure - 依赖只能从外层指向内层,禁止反向依赖,禁止同层模块之间直接进行实现级调用 - infrastructure 不得反向依赖 application;domain 不依赖任何其他层 - 跨模块通信只能走模块公开门面(Facade),禁止直接引用对方内部类这里面有个容易忽略的点:"禁止同层模块之间直接进行实现级调用"。同层互调往往看起来人畜无害,两个 Service 互相 new 一下很顺手,但模块间耦合就是这么产生的。AI 尤其喜欢这么干,因为它在局部上下文里看不到你们两个模块之间应该通过事件或接口解耦。
2.3 架构规则的写法要能"被检查"
架构规则写完之后,得能被人和工具检查。我说一个自己试出来的经验:规则落成文本后,最好在评审的时候真的去对照检查。比如你写了"禁止 Controller 直接调用 Repository",那 AI 生成的 PR 里如果出现了这种调用,review 的时候要明确打回。这样做还能给团队一个信号:AGENTS.md 不是摆设,是硬规矩。
再补充一个细节:目录树要跟着代码现实走。文件移动了目录没更新,AI 就会拿着过期的地图走错路,还不如不写。所以每次重构涉及目录变化时,AGENTS.md 必须同步更新。
3. 第二种事实:封装边界与访问红线
3.1 封装是 AI 最容易破坏的地方
如果说架构边界是宏观规则,封装边界就是微观规则。AI 面对一个类时,它分不清哪些字段是内部状态、哪些方法是给别人用的。它看到一个save()方法,就会直接调用,哪怕这个方法应该只被仓储层使用。
我印象很深的一次:让 AI 修一个订单金额计算的 bug,它直接在实体类里加了一个setTotalPrice()方法,然后各处调用,把原来封装好的金额计算逻辑全绕过了。代码编译通过,但完全是另一套设计。原因很简单,AGENTS.md 里只写了分层,没写"状态变更必须走行为方法"。
3.2 公开 API 与内部实现:在 AGENTS.md 里画清楚
封装边界的写法核心是区分"别人能用"和"只有自己能用"。我建议在 AGENTS.md 里单独开一节约束这个:
## 封装与访问规则 - 每个模块的能力通过公开门面类暴露,其他模块只允许引用门面 - domain 中的实体类,禁止对外暴露 public setter;状态变更必须调用领域行为方法 - 数据访问统一走仓储接口,业务层禁止直接创建数据库会话或执行原生查询 - 现有工具类优先复用,禁止为了局部需求重新实现同类功能 - 新增 public 方法前先想:这个方法真的应该公开吗?内部逻辑优先下沉为包级私有"现有工具类优先复用"这条尤其重要。AI 训练数据里充斥着各种工具类的写法,它很容易再写一个新的,和项目里已有的功能重叠。你在 AGENTS.md 里点名这个禁忌,AI 在生成时会先去找已有实现。
3.3 用措辞区分"硬约束"和"软偏好"
写规则的时候,我强烈建议措辞分级。跟人沟通你可以说"尽量"、"最好",跟 AI 沟通要更严格:硬约束用"禁止/必须",软偏好用"推荐/尽可能"。这个区分不是形式主义,它会影响 AI 在规则冲突时的决策权重。
比如"禁止对外暴露 public setter"就是硬约束,AI 在犹豫要不要加 setter 时会直接放弃;而"推荐优先复用工具类"是软偏好,如果它确实找不到合适的工具类,新写一个也是允许的。分级明确后,AI 不会因为完全没法灵活处理而频繁卡住,也不会因为全是软约束而把你的规矩当耳旁风。
我还遇到过一种情况:规则写得过于严格,每一条都是"必须",结果 AI 变得畏手畏脚,改了十行代码要停下来问三次"这样可以吗"。所以一根红线加一包建议,比一百根红线效果好得多。
4. 第三种事实:数据流与状态流转规则
4.1 AI 为什么总爱在业务层直接写 SQL
这是 AI 编程最让人头疼的问题之一。打开一个 Service 文件,需要查个数据,AI 顺手就在方法里拼了个查询,因为它觉得这样最直接。它并不知道项目里数据入口唯一定义在仓储层,也不知道这么做会把业务逻辑和数据访问搅在一起。
要解决这个问题,得把"数据流向"这件事写进 AGENTS.md。AI 需要知道:数据从哪里进来,经过哪一层,最后落到哪里;写操作应该由谁发起,读操作应该走哪条路。以下是我在模板里放的写法:
## 数据流规则 - 所有持久化操作必须通过仓储接口,业务层禁止直接访问数据库客户端或执行自定义 SQL - 创建/更新操作的入口统一为应用服务方法,控制器只做参数接收和结果返回 - 聚合根之间的数据协调,优先通过领域事件解耦,禁止一个服务直接修改另一个聚合的内部数据读起来平平无奇,但对 AI 来说这就是航线图。它再想直接写 SQL 时,会先看到这条约束,然后去找合适的仓储接口。
4.2 把状态机写清楚,别让 AI 发明状态
业务系统里很多 bug 是非法状态流转造成的。AI 不知道订单只能从待支付到已支付,它可能会在代码里写"已发货订单可以直接改为待支付",因为从局部代码看它以为这样能满足业务变化。
AGENTS.md 里直接列出合法状态流转即可。以订单为例:
## 状态流转 - 订单状态机:PENDING → PAID → SHIPPED → COMPLETED - 允许的逆向流程:仅 PENDING ← PAID(取消订单场景),其余禁止逆向 - 状态变更必须调用 Order 领域的 confirm/cancel/ship/complete 等方法,禁止直接改 status 字段这种文本列表比画流程图更实用,AI 解析起来没有歧义。你要做的就是把团队脑子里那张状态图落成文字,越明确越好。
4.3 缓存一致性、事件链路这类"隐性事实"也要写
除了状态机,还有一些隐性数据流规则,比如缓存一致性。AI 在处理"更新数据"任务时,可能只会更新数据库,忘了还有缓存需要失效。如果你们项目里有写后失效缓存的约定,就要写进去:
## 数据一致性 - 所有写操作必须先更新数据库,再删除缓存;禁止先删缓存再写库 - 发布事件必须在一个事务成功提交后进行;事务失败,事件不允许发出这样 AI 生成代码时就会保留事务边界和事件发布位置,而不是拍脑袋乱放。说到底,AGENTS.md 写的不是技术教程,是你们项目里的"地心引力"——哪些力必须顺着走,哪些方向一碰就出事。
5. 第四种事实:变更准则与工程约定
5.1 不只是约束代码,还要约束 AI 的"行为方式"
前面三类事实管的是"代码往哪放""哪些不能碰""数据往哪流",第四类管的是"AI 该按什么流程做事"。举个例子:AI 改一个核心模块时,它应该先读哪个设计文档;测试写不写;提交信息按什么格式;能不能自行新增依赖。这些不写在架构文档里的东西,恰恰决定了一次代码变更的质量。
我在项目里常用的写法是这样:
## 变更准则 - 修改 domain 核心实体前,必须先阅读 docs/design/domain-model.md - 任何新功能必须附带单元测试,测试文件与被测文件同目录,命名为 xxx.test.ts - 提交信息格式为 type(scope): subject,AI 生成提交信息时按此规范 - 所有新增依赖必须经团队确认,禁止引入已有功能的替代库 - 接口签名变更必须同步修改调用方和相关文档这里有个关键词:"必须先阅读"。它给 AI 设定了一个动作顺序——动手之前先做某件事。实测下来,这个模式比单纯说"注意设计文档里的规则"有效得多,因为它把"读文档"变成了生成流程中的一步。
5.2 负面清单:AI 更需要知道"不许干什么"
我越来越觉得,给 AI 列"禁止清单"比列"应该清单"更实用。人的注意力容易被"不要做 X"抓住,AI 也一样。负面清单可以直接阻断那些最损害架构的默认倾向。
我整理过一份高频负面清单,精简后如下:
- 禁止在 Controller 中直接调用仓储或领域方法。
- 禁止为已有封装引入平行的新实现。
- 禁止修改生成文件、锁文件、构建产物目录中的任何内容。
- 禁止用魔法数字代替已有常量;新增常量必须靠近使用处定义。
- 禁止为了测试便利,把私有方法改为 public。
负面清单不要写太长,挑最疼的五到八条就好。每条都要有明确的行为指向,而不是"禁止写烂代码"这种没法执行的废话。
5.3 让规则有优先级,冲突时 AI 知道听谁的
不同规则之间一定会有冲突。比如"新功能必须附带单元测试"和"禁止为了测试便利把私有方法改 public",如果测试真的需要访问私有状态怎么办?这就是规则优先级的问题。
我会在 AGENTS.md 开头写一段总则:本项目级 AGENTS.md 的优先级高于子目录级;硬约束之间的冲突,以保护封装和数据一致性为先。这么做不是为了理论上的完备,而是真的能帮 AI 做取舍。没写优先级之前,AI 遇到冲突会随机选一个,写了之后它至少会按你的价值观来决策。
6. 落地实操:一份可以直接抄的 AGENTS.md 模板
6.1 模板正文(按四种事实组织)
下面这份模板是我在实际项目中打磨过的版本,你可以根据团队技术栈改改就用。结构上就是前面讲的四类事实,再加一个总则。
# AGENTS.md ## 总则(优先级最高) - 本文件规则优先级高于子目录内 AGENTS.md;如规则冲突,以本文件为准。 - 所有代码变更必须遵循既有架构与封装边界,不得以"快速实现"为由破坏设计。 ## 架构边界 - 项目分为 interfaces / application / domain / infrastructure 四层。 - 依赖方向:interfaces → application → domain → infrastructure,禁止反向。 - 跨模块通信通过模块门面接口,禁止直接引用其他模块内部类。 ## 封装与访问规则 - 实体类禁止公开 setter,状态变更走领域行为方法。 - 数据访问只能通过仓储接口,业务层禁止执行原生查询。 - 现有工具类优先复用,禁止重复实现已有能力。 - 新增 public 方法前,先评估该方法是否应公开,内部逻辑优先下沉为包级私有。 ## 数据流规则 - 写操作统一入口为应用服务,控制器只做参数校验和响应封装。 - 订单状态机:PENDING → PAID → SHIPPED → COMPLETED;仅支持 PENDING ← PAID 逆向流转。 - 写操作先更新数据库,再删除缓存;事务提交完成后才能发布领域事件。 ## 变更准则 - 修改 domain 核心实体前,先阅读 docs/design/domain-model.md。 - 新功能必须附带测试文件,命名与被测文件对应。 - 新增外部依赖必须经团队确认。 - 禁止修改构建产物和生成文件。 - 提交信息格式统一为 type(scope): subject。 ## 领域术语表(部分) - 订单(Order):聚合根,状态由领域方法管理。 - 库存(Stock):独立聚合,变更通过领域事件通知订单模块。这个模板看起来不复杂,但每一条背后都是踩过坑之后的取舍。如果你项目比较小,可以砍掉一些;如果项目更大,子目录里的局部 AGENTS.md 再补充细节,根目录文件保持精简。
6.2 配套措施:光写文件还不够
AGENTS.md 不是写完就生效,我见过太多团队"写了放那,AI 照样乱来",原因往往不是文件没用,而是配套没跟上。
首先,要把 AGENTS.md 纳入代码评审的必读清单。每次让 AI 改完代码,审查 PR 时对照这四条规则过一遍:边界合不合规?封装有没有被绕过?数据流有没有走歪?测试和提交信息符不符合约定?只要有一次违规被打回,AI 在后续生成时就会更重视这个文件。
其次,把 AGENTS.md 加入"修改触发条件"。当 PR 涉及目录结构变化、依赖变化、核心领域模型变化时,必须同步更新 AGENTS.md。简单说:谁改了架构,谁负责改地图。
第三,让 AI 生成的代码在写完后自己做一次"规则自查"。你可以在提示词里加一句:完成代码后,对照 AGENTS.md 逐条检查你这次变更是否违规。这一步会让 AI 的输出稳定很多,因为它在提交前多了个反思环节。
6.3 工具链配合:让 AGENTS.md 真正被读到
不同 AI 编程工具读取 AGENTS.md 的优先级有差别,但根目录的 AGENTS.md 基本是共识。如果发现 AI 的行为完全没有被规则影响,先查文件名和位置对不对,再看工具设置里是否有关闭项目记忆的选项。
我建议把 AGENTS.md 放在版本库里,和代码一起走。这样每一次规则变更都留下记录,出了问题时能回溯到底是哪条规则缺位。另外,可以在团队里约定:AGENTS.md 的修改需要至少两人评审,避免某个人把个人偏好写进去,变成噪音。
7. 常见问题与排查实录
7.1 写了 AGENTS.md,AI 还是乱来?
先别急着怀疑文件没用,按这个顺序排查:
- 文件名是不是 AGENTS.md?很多工具默认读这个拼写,写成 AGENT.md 单数或者 agents.md 小写,可能就跳过。
- 文件是不是放根目录?虽然子目录也能放,但根目录的读取优先级最稳。
- 规则写得太抽象?"注意分层"这种话 AI 抓不到,得是"Controller 禁止调用 Repository"这种断言。
- 工具没加载?部分编辑环境的 AI 插件需要在设置里打开项目文档读取,确认一下开关状态。
最隐蔽的问题是规则写在 AGENTS.md 里,但你让 AI 处理代码时,提示词里没提这个文件。有些工具只把 AGENTS.md 放在首轮上下文,如果对话太长被截断,后续轮次 AI 可能会"失忆"。这时在关键提示词里主动提一句"请先阅读根目录 AGENTS.md"能有效兜底。
7.2 规则之间冲突,AI 听谁的?
冲突几乎是必然会发生的。比如你要它"禁止公开 setter",又要它"新功能必须附带测试",测试需要改状态时,AI 就不知道怎么办了。
我解决的办法是在总则里写清优先级:保护封装和数据一致性优先于测试便利;核心架构边界优先于局部实现调整。有了这个总则,AI 遇到冲突时至少不会随机选方向。另外记得,硬约束之间的冲突要靠删除/合并规则来消除,不能指望 AI 自己去调和。
我遇到过最典型的案例:让 AI 完成一个跨模块功能,它发现两个模块之间没有公开发口,于是直接把内部类改成 public 了。它没做错什么,是规则没告诉它"跨模块通信只能走门面,门面接口不够就去申请扩展门面"。所以你写规则时,不只是禁掉坏动作,还要给一个合规的替代动作,AI 才能做出正确选择。
7.3 规则过时和不一致,怎么防?
AGENTS.md 是活文档,最容易死掉的状态就是"代码已经进化了三轮,规则还停在半年前"。AI 按旧规则生成代码,人评审时也用旧规则对照,结果新代码用的其实是新结构,两边互相打架。
我的做法是把 AGENTS.md 当成一个特殊 PR 类型来管理。普通功能分支不允许改它;架构调整分支必须改它。这样每次规则变更都有出处、有评审。目录一挪,文档同步;依赖一换,术语表同步。坚持半年,AGENTS.md 就会成为团队里最靠谱的地图。
7.4 第四个坑:AGENTS.md 被写得越来越长
规则会自然膨胀,每发现一个 AI 乱来的案例就加一条,最后文件变成了一本三百行的大部头。后果是 AI 的上下文被大量规则占满,反而忽略了真正核心的红线。
克制一点。根目录文件保持 150 到 200 行以内,放的应该是全局性、长期有效的规则。那些只在某个模块内有效、只针对某个历史问题的临时规则,放到子目录 AGENTS.md 或者写进具体任务的提示词里。判断标准就一条:删掉这条规则后,全局代码质量会不会肉眼可见地下降?不会,就不写。
AGENTS.md 不是写一次就结束的东西。我个人的体会是,它更像一份需要持续维护的团队契约,人和 AI 都要定期读一遍。你在规则里画清晰的线,AI 反而更自由,因为它不用再猜哪些能做、哪些不能做。最后分享一个小技巧:每次 AI 生成完代码,让它对照 AGENTS.md 逐条自检一遍再提交,这个动作看起来多此一举,但省掉的人工 review 时间远比你想象的要多。