
最近这一个月我几乎在每一轮代码审查里都能看到同一种病AI 写的代码功能是通的但它出现的位置全错了。要么在 Controller 里直接 new 了一个仓储实现类要么为了读一个状态字段硬生生绕过整个业务服务层要么把老模块里两百行逻辑复制出来塞进新接口顺手就把单体架构里唯一允许的依赖方向给逆转了。功能跑通架构烂掉这在 AI 辅助编程的团队里几乎成了常态。后来我认真做了一次小的工程改造把四类项目事实固化进项目根目录的 AGENTS.md 文件里让主流 AI 编程助手在每次会话开始时自动读到这套约束。改造之后AI 生成的代码绕过封装、破坏架构的比例明显下降代码审查里从“结构性问题”变成了“业务细节问题”。这篇文章就把这套方法、完整写法和踩过的坑一次性讲清楚适合正在用 AI 写代码但又苦于架构被慢慢腐蚀的团队和个人。1. AI 为什么会绕过封装——先把病灶看清楚1.1 三张最典型的“破坏现场”先说三个我在真实项目里反复见过的场景你可以对照一下自己的代码库。第一绕过服务层直接访问仓储。业务上需要一个“未完成订单数量”现有 OrderService 没有提供这个查询方法。AI 的修法是什么它没有去 OrderService 里新增一个方法而是直接在 Controller 的 action 里写orderRepository.CountUnfinished()。功能确实跑通了但 Controller 依赖了 Repository服务层的查询口径被架空后续如果有人要加缓存、加权限过滤会发现自己根本找不到统一入口。第二复制粘贴老逻辑。某个结算逻辑在旧模块里已经有了AI 生成的代码为了“尽快完成任务”直接把旧实现复制过来改了改。功能没问题但代码库多了一份几乎一样的实现。过两周业务口径调整改了一处忘了一处线上算出来的钱就不一致了。第三跨层反向依赖。AI 需要在基础设施层做一个小小的业务判断它发现直接引用业务模块的某个枚举最省事于是在 infra 层里 import 了业务层。依赖方向原地反转架构图上画好的单向箭头变成了双向缠绕模块复用性和测试隔离性同时归零。这三个场景的共同点是什么不是 AI 不会写代码而是 AI 没有“项目世界观”。1.2 根因局部视野和最短路径是 AI 的天性语言模型生成代码依赖的是喂进上下文窗口的资料。真实项目的代码量动辄几十万行AI 只能拿到当前会话被检索、被压缩后的片段。换句话说AI 是在局部视野里做决策的它看不到全局架构图不知道模块边界在哪甚至不知道某个类在项目里是什么定位它只知道“眼前这个文件要改、附近这些文件可以调用”。在局部视野下最短路径优先是必然选择。从 Controller 直接访问 Repository比去 Service 加一个方法再调用要少改一个文件路径更短。复制旧代码比抽象出公共接口再替换调用方更贴近文本相似度难度更低。AI 不是在“故意破坏架构”它只是在一个信息不全的局部环境里选择了局部最优解。可惜局部最优解叠加起来就是架构腐化的全局灾难。1.3 为什么团队文档救不了你可能觉得我们有架构文档啊让 AI 读不就行了我试过效果很差。架构决策记录、技术设计文档、团队 Wiki动辄几万字AI 不可能在每次生成代码前通读一遍。就算把文档塞进上下文信息也埋在叙述性文字里不会对生成行为构成约束。更别提在聊天框里每次重申规则——费 token 不说换了会话就忘毫无一致性可言。AGENTS.md 的价值恰恰在这里它体积小几百行就能覆盖关键约束它在会话启动时会被 AI 自动读取不占额外的交互成本它用命令式的“允许/禁止/豁免”说话比任何散文式文档都更容易被模型当作指令遵守。简单说它把约束前置到了 AI 动手之前而不是等代码写完、重构完再亡羊补牢。2. AGENTS.md 的定位给 AI 看的“项目事实说明书”2.1 它和 README 有什么分工很多人第一次接触 AGENTS.md 会问这跟 README 有什么区别区别很大。README 给人看讲的是怎么启动、怎么部署、怎么贡献代码。AGENTS.md 给 AI 看讲的是“这个项目有哪些边界不能碰、什么时候应该改哪个文件、遇到什么情况必须先走哪条路”。一个描述现状一个定义规则。两者可以互相引用但不能互相替代。我在项目里的做法是README 保持面向人类的标准结构AGENTS.md 则彻底面向 AI全部写判断条件。比如“当你要修改订单状态时必须先调用状态机接口”这种话人看会觉得啰嗦AI 看了却能形成行为约束。一个最小可用的 AGENTS.md结构大致是# AGENTS.md ## 项目概览 两三句话说明这是什么系统、技术栈是什么帮助 AI 快速定位 ## 架构红线 依赖方向、禁止的跨层引用、必须遵守的分层规则 ## 封装契约 每个模块的对外入口、禁止实例化的内部类、禁止直接访问的字段 ## 业务链路所有权 核心业务链路分别归属哪些模块新增代码应该落在哪个目录 ## 工程惯例 错误处理、日志、事务、幂等、命名规则等项目特有约定别小看这个结构。它实际上是把“项目的世界观”压缩成了一个低 Token 消耗的常驻指令包。2.2 所谓“项目事实”具体是哪四种标题里说的“四种项目事实”我用一句话分别概括架构边界与依赖方向——谁可以依赖谁谁绝对不可以依赖谁。公共 API 与封装契约——哪里是墙、哪里是门什么能碰、什么不能碰。业务链路的所有权与数据流——这笔业务到底归哪个模块管数据必须经过哪道关卡。隐形工程惯例——项目特有的命名、错误处理、事务、日志、幂等等潜规则。这四种事实分别对应四类架构破坏项目事实不写时 AI 常见的破坏行为架构边界与依赖方向跨层引用、依赖反转、模块间乱 import公共 API 与封装契约new 内部实现类、访问私有字段、绕过对外入口业务链路所有权与数据流业务逻辑散落到无关目录、状态直接赋值、绕过统一查询入口隐形工程惯例异常处理风格混乱、日志不规范、幂等缺失、重复造轮子2.3 写之前先摆正心态你在写“约束”不是在写“百科”刚开始写 AGENTS.md 的人最容易犯的错是把它写成项目简介——项目用什么框架、有哪些模块、目录结构是什么。这些信息有用但远远不够。直接决定 AI 行为质量的是“红线”和“例外处理”部分是越像黑名单越好的内容。我自己的体会每一句描述都必须满足“AI 看完后面对一个新任务能直接判断能不能做、怎么做”的标准。如果一句话只是表达愿望比如“保持代码整洁”AI 不知道该拿它怎么办如果表达的是规则比如“Controller 中禁止调用 Mapper”AI 就能在生成过程中把它当成硬约束。这是两种完全不同的效力。3. 第一种事实架构边界与依赖方向写成“红绿灯”而不是“形容词”3.1 依赖规则必须写到“禁止 import”的颗粒度很多项目的架构文档喜欢写“保持高内聚低耦合”“遵循分层架构”这种话 AI 看了等于没看因为它是形容词不是规则。真正有效的写法是把依赖关系画成有向边然后明确写出哪条边是绿灯、哪条边是红灯。我项目中 AGENTS.md 的这一段后来稳定成了这个模板## 架构红线 - 依赖方向controller - service - repository - infrastructure - 绿灯controller 可以调用同级 controller 之外任意 service - 绿灯service 可以调用本模块 repository 和其他模块的对外 Service - 红灯禁止 controller 直接 import repository、mapper、db、infra 的类 - 红灯禁止 service 直接调用其他模块的 repository - 红灯禁止 infra 层反向 import 业务模块包括枚举和常量 - 如果一定要跨层调用必须先在本层封装接口再通过依赖注入调用禁止 new为什么“禁止 new”这三个字特别重要因为 AI 在没有约束时最高频的跨层手段就是直接new一个实现类。一旦写明禁止 new、必须走依赖注入AI 就会去找 Spring 容器、去找构造注入的目标类型行为立刻会规范很多。3.2 为什么依赖规则能阻止架构破坏AI 生成代码时会预测“下一个 token 最可能是什么”。给它一段“不得 import Mapper”的上下文它生成import语句时就会把目标候选集中到允许的范围内。这比事后靠代码审查发现问题要便宜得多——代码一旦生成了让人再改一遍的成本是十倍百倍。另一个关键点依赖规则要同时给出“正确的替代路径”。只说“不能直接调 Repository”AI 可能会困惑于该怎么实现最后它干脆不写这个功能或者绕一个更奇怪的弯。正确做法是告诉它“如果 Controller 需要数据先调用对应 Service 的方法如果 Service 没有就新增一个方法”。这就是我在本章标题里说的“红绿灯写法”——红灯之后必须紧跟绿灯否则规则就是死路。3.3 没写依赖约束时的典型事故有一次我故意没把依赖规则加进 AGENTS.md想验证一下差异。结果 AI 在设计一个导出 Excel 的功能时直接在 Controller 里拼了 SQL 查询条件去查数据库因为它发现最快的方式是“直接调用一个现成的查询构造器”。功能确实在十分钟内跑通了但那个 Controller 从此无法单元测试数据库访问散落在表现层后面加权限校验时差点把导出的数据漏成安全事件。加了依赖规则之后同样一个需求AI 生成的代码自动分了三层Controller 只接收请求参数Service 里组装查询条件Repository 只负责数据访问。整体代码量多了二十行但每个文件的职责清清楚楚。这个对比让我彻底相信AGENTS.md 里那几句明确到“禁止 import 谁”的话比十次代码审查里唠叨十遍都管用。4. 第二种事实公共 API 与封装契约明确哪里是墙、哪里是门4.1 封装信息三件套对外入口、内部实现、禁止访问AI 判断“哪些类可以调用”时很依赖它在语料里见过的“相似代码”模式。如果项目里有人曾经直接 new 过一个 Repository 实现类AI 检索到的相似片段就会诱导它做同样的事。所以 AGENTS.md 里必须主动声明每个模块的对外入口、内部实现和禁止访问项。我的写法是按“域”来组织## 封装契约 - 订单域对外入口OrderService、OrderQueryService 是仅有的两个公共入口 - 禁止在 Service 外层实例化OrderRepositoryImpl、OrderMapperImpl、OrderEntity - 禁止从域外修改OrderEntity 的 _items 集合要增删商品必须调用 OrderEntity.ApplyItemChange() - 创建订单唯一入口OrderFactory.Create()禁止 new OrderEntity 再手动 set - 豁免历史数据迁移脚本可以例外但必须标注 MigrationOnly 并走独立目录这套声明的本质是给 AI 画出一张“墙和门”的地图。地图上没有标注的地方AI 就不会乱闯。4.2 防止“开窗破门”AI 是怎么找到绕过路径的很多开发者低估了一件事AI 非常擅长从测试代码里学习非法用法。项目中如果有人写过new OrderRepositoryImpl()的单元测试AI 在生成生产代码时就可能模仿这个模式。因此 AGENTS.md 不仅要约束生产代码还要声明“测试代码也不得绕过契约”除非是专门的集成测试目录。另一种绕过路径是链式访问。AI 发现实体类有一个 public getter 返回可变的 List它就直接order.getItems().add(x)完全绕过了领域方法。对这种坑AGENTS.md 里要明确写- 禁止通过 getter 返回的内部集合直接修改数据必须调用领域方法这类规则单独一条可能不起眼但它是“封装不被击穿”的最后一颗钉子。没有这条AI 会把所有实体都当成纯数据容器来用领域模型慢慢地就退化成了贫血模型。4.3 当不得不破坏封装时给 AI 一条“合规的例外通道”完全禁止破坏封装是不现实的总有迁移脚本、性能优化、临时修复需要开一个口子。与其让 AI 偷偷摸摸绕不如在 AGENTS.md 里明确给出一条例外通道。我在项目里是这么写的- 如果确实需要破坏封装必须同时满足三个条件 1. 在代码中注释声明“安全例外” 2. 说明为什么公共入口不可用如无法满足性能要求 3. 关联对应的 issue 编号或技术方案链接 - 不满足以上条件的破坏性访问一律视为事故这条规则的效果出乎意料的好。它没有阻断所有例外操作但让每一次例外都变得透明、可追溯、可审查。AI 有了这条“合规通道”之后反而很少再走野路子了——因为走野路子会被复查标记为事故而合规例外是允许的。5. 第三种事实业务链路的所有权与数据流锁死“这笔生意归谁管”5.1 没有归属声明的代码库AI 会把逻辑写在任何地方第三种事实要解决的是“定位问题”。AI 接到一个需求“修改退款审批超时后的处理逻辑”它去哪个目录找代码如果 AGENTS.md 里没写业务链路的归属AI 大概率会在当前打开的文件附近梭巡找不到就在最接近的关键词目录里新建文件。结果就是退款逻辑分裂在三个模块里每个都自称正统。归属声明可以精确到“哪个链路、哪个入口、哪个实现文件”## 业务链路所有权 - 下单链路OrderService.PlaceOrder - order/application/OrderCreator - order/domain/OrderAggregate - 支付链路PaymentService.HandlePaidCallback - payment/application/PaymentProcessor - 退款链路RefundService.StartRefund - refund/application/RefundOrchestrator - 颠簸提醒要修改上述链路时先精确定位到对应链路的 application 层禁止在 controller 或 infra 目录新增业务逻辑AI 是“文本向量检索”驱动的。一旦它知道“退款链路”对应的核心关键词和文件路径是RefundService和refund/application/RefundOrchestrator它就会优先去这些文件里找断点而不是东翻西找。5.2 数据流规则谁可以读、谁可以写、必须经过谁除了业务代码放哪数据流向是更隐蔽的雷区。AI 默认会认为“所有实体字段都可以直接访问赋值”但真实项目里订单状态、金额字段、库存数量都有严格的变更关卡。我在 AGENTS.md 里画出了数据流规则简单直接## 数据流红线 - 订单状态只能通过 OrderStateMachine.TransitionTo() 变更禁止直接给 OrderEntity.Status 赋值 - 金额字段只能通过 OrderAggregate.Recalculate() 重新计算禁止在外部单独 set 金额 - 写入数据库必须经过 repository 层的 Save() 方法禁止在 service 里直接执行 SQL - 查询所有读操作必须先经过对应模块的 QueryService把“状态变更”类比成现金盘点流程就能理解仓库里现金怎么摆、怎么记录、怎么核对都是有规矩的不是谁想碰就能碰。AI 不懂这些潜规则AGENTS.md 就是在开工之前把这些规矩一条条写在墙上。5.3 状态机与“死线”不可逆操作的入口要锁死业务系统里最不可原谅的违规是状态乱跳。AI 如果只看到一个实体类里有Status属性它不知道这个属性背后还有状态机的合法性校验。“先支付再发货、先发货再完成”的顺序在代码里没有被强制表达时AI 生成的逻辑随时可能跳过校验。所以在 AGENTS.md 里要把状态机规则写成一个不可逾越的死线- 订单状态机的合法转移路径Pending - Paid - Shipped - Completed - 禁止代码中出现 Pending 直接跳到 Completed 的赋值行为 - 所有转移必须携带操作人与原因写入状态日志这几行字在 AGENTS.md 里成本极低但它能让 AI 在生成“标记订单完成”这样的代码时主动去寻找状态机接口而不是随手order.Status Completed。我实际观察到的行为变化非常明显——不是每次都对但违规率至少降了一个量级。6. 第四种事实隐形工程惯例把潜规则变成显式规则6.1 AI 默认使用“互联网主流写法”而你的项目不是主流第三种事实管的是“业务放在哪”第四种事实管的是“代码写成什么样”。AI 的训练语料来自海量开源项目它生成代码时天然倾向于“大多数项目最常见的那种写法”。但你的项目大概率有自己的约定比如业务异常必须带错误码比如所有对外接口必须做幂等比如日志里禁止打印整个用户对象。这些约定如果只存在于老员工的脑子里AI 是永远学不会的。我的做法是专门开辟一个“工程惯例”章节把所有 code review 中反复出现的要求沉淀下来## 工程惯例 - 错误处理业务异常必须抛 BizException带唯一错误码禁止返回 null、禁止抛裸 RuntimeException - 日志关键业务动作打 info出参入参要脱敏禁止直接 log 用户对象 - 幂等所有对外写接口必须先检查幂等键幂等表由工具类统一维护 - 审计订单、支付、退款等关键操作必须写 AuditEvent禁止只改业务表不写审计 - 重复代码动手前先全局搜索现有实现禁止建立与已有工具类功能重复的新工具方法一条经验这里的规则不要写成“要做正确的事”而要写成“动手前先检查什么、禁止用什么、必须用什么”。AI 理解这种条件式指令的能力比理解抽象原则强得多。6.2 把“负例”写进去比只写正向规范更有用第四种事实里我强烈建议加一个“负面清单”小节专门记录这个项目里 AI 反复犯过的错。比如## 负面清单Code Review 高频问题 - 不要在 Controller 里写超过 5 行的业务逻辑涉及用例编排一律下沉 Service - 不要复制其他模块的实现如果发现功能重复先抽象公共方法 - 不要为了测试方便把私有方法改成 public需要测试走行为验证 - 不要跳过事务注解多个写操作必须包在同一个事务边界内写负面清单的逻辑很简单AI 踩坑是概率事件你把这些坑提前写出来等于在它生成代码的路径上立了警示牌。而且每次 code review 发现新问题顺手在清单里加一行成本极低收益却会持续累积。6.3 规则之间的冲突怎么处理AGENTS.md 写多了之后规则之间可能打架。比如“必须写审计日志”和“保持 Controller 精简”同时出现时AI 不知道哪个优先。我给所有规则加了优先级编号并在文件头部声明## 规则优先级 1. 架构红线 与 状态机死线 最高任何情况下不能违反 2. 封装契约 与 数据流红线 次之需要例外时必须走豁免通道 3. 工程惯例 为第三优先级允许在有充分理由时偏离但要在代码注释中声明这个优先级解决了两个问题一是 AI 判断冲突时有了决策依据二是人在写规则时也会更谨慎不会把小事和红线混为一谈。没有优先级之前AI 偶尔会为了遵守某条低优先级措辞去违反更高优先级的约束加了优先级之后这种情况基本消失了。7. 提高 AI 遵守率的三个实操经验7.1 用命令式语气并在规则旁写出“违反后的后果”我调整过很多版 AGENTS.md最大的经验是语气决定了遵守率。用“建议”“最好”“可以”写出来的规则AI 经常会当空气用“禁止”“必须”“唯一入口”写出来的规则遵守率能差出一倍以上。可能的原因是命令式短语在模型语义空间中激活的指令跟随信号更强。同时每条重要规则旁边最好带上“违反后会发生什么”。比如- 禁止直接给 Order.Status 赋值违反后会导致状态机校验被跳过、审计异常、线上订单数据不可信AI 在做决策时如果预测到后续会被 review 打回、会导致线上事故它选择合规路径的概率会显著提高。你不必把它当人看只需要理解它做的是概率预测——给负面结果更高的概率权重行为自然就变了。7.2 和自动化检查联动让规则有“牙齿”AGENTS.md 只是软约束真正让它硬起来的是自动化检查。我在项目里做了两件事。第一把 AGENTS.md 里最核心的红线在静态检查规则里实现了一遍比如用依赖分析工具检查 Controller 是否 import 了 Mapper 层。第二让检查工具报错时直接引用 AGENTS.md 对应的章节号让错误信息既给人类看、也给 AI 看。这个联动的闭环价值在于AI 如果违反规则自动化检查会把它“顶回去”它会看到报错里写着“违反 AGENTS.md 第 3.1 条”。下一次生成类似代码时这个被纠偏的上下文会强化它对规则的重现。AI 是有上下文记忆的纠错一次的效果比在文档里写十遍都管用。7.3 版本化和迭代节奏每次发现架构问题先改 AGENTS.md 再改代码AGENTS.md 不是一次性工程它是需要持续迭代的约束系统。我现在给自己定的流程是每次代码审查中发现结构性问题不管代码是谁写的先动手修改 AGENTS.md把新问题固化成规则然后再让 AI 修复代码。这个顺序很重要——先改约束再改代码AI 在修复时会主动参考新规则修完一次之后就不会再犯同类问题。迭代节奏上不建议第一天就把所有内容写满。规则太多AI 反而会稀释注意力。我的建议是分三步走阶段写什么目标第一周架构红线 封装契约先把绕层和破封装止住第二周业务链路所有权 数据流红线让业务逻辑归位、状态变更合规稳定期工程惯例 负面清单统一写法、消解技术债每个阶段大概两到三周等你发现 AI 已经能稳定避开上一阶段的所有坑再追加下一阶段的内容。别贪多AGENTS.md 一万行没人会读包括 AI。最后再分享一个我个人的体会这套方法用下来最明显的变化不是 AI 写的代码变“完美”了而是代码审查里让人头疼的“结构性争吵”消失了。以前 review 是在争“这代码放错了层”“这封装被绕过”现在这些都能在生成阶段被 AGENTS.md 拦掉一大半剩下的只是业务逻辑本身的对错。建议你先从“架构边界”和“封装契约”这两块写起跑通一条链路之后再逐步把业务所有权和工程惯例补上。等到 AI 开始在新任务里主动引用 AGENTS.md 里的规则来问你“是否走豁免通道”的时候你会发现它终于不像一个空降的实习生而像一个熟悉项目规矩的老同事了。