前阵子帮一个团队做 AI 辅助开发的改造,碰到的典型场景让我印象很深:AI 生成的代码,从 Controller 直接跳过 Service 层,一把梭调 Repository,面完 code review 一排人都愣了——明明提示词里写了“要分层”,结果它还是绕过了封装。后来我发现,问题不是提示词写得不够狠,而是 AI 根本不知道这个项目有哪些架构约定。你脑补它“应该知道”的东西,它其实一个都不知道。这就是我今天想聊的“AI 代码上下文工程”:把项目里那些决定代码走向的事实,显式写进 AGENTS.md,让 AI 在生成代码之前就先看到规则,而不是靠它猜。这篇文章会拆解四种最值得写进 AGENTS.md 的项目事实,配上可落地的写法、正反例和踩坑经验,适合所有在团队里引入 AI 编程、又不想让代码库慢慢烂掉的同学。
1. 先搞清楚:AI 为什么会绕过封装、破坏架构
想解决一个问题,先得理解它的根因。AI 写代码时绕过封装、破坏架构,在我看来不是“它坏”,而是“它瞎”。大部分情况下,AI 编码工具拿到的是一个局部上下文:你打开某个文件,它看到的是这个文件附近几百行,最多再帮你拉一下被引用的几个文件。它没有能力、也没有耐心去全局扫描一遍整个项目的架构设计。
1.1 AI 本质上是“上下文敏感”的补全器
你可以把 AI 当成一个能力很强、但视野很窄的新人。它确实知道怎么写 Repository、怎么写 DTO 映射,但如果它只看到了某个 Controller 文件,它不会知道这个项目里 Controller 必须经过 ApplicationService 才能碰 Repository,更不会知道你项目里那条“谁都不许直接调用老改造模块内部方法”的红线。
我曾经让 AI 重构一段查询逻辑,它顺手就 import 了一个只应该在数据层内部使用的工具类。代码能跑、测试能过,但从架构角度看就是裸奔。为什么会这样?因为它看到的上下文里,那个工具类就在附近,自然觉得“可以用”。人之所以不会犯这种错,是因为你脑子里有整张架构地图;AI 没有,它的世界就是那几百个 token。
这个认知挺重要:AI 的错误不是随机失误,而是上下文缺失的必然结果。等于说,只要你不把约束信息放进它的视野,它就一定会凭局部信息做出“最顺手的决定”,而这些决定往往就是绕过封装、破坏架构。
1.2 为什么“提示词叮嘱”和“事后 review”都不够
很多团队会说:我们在系统提示词里写了“请遵守分层架构,不要越层调用”。我试过,短期有效,但不可靠。原因有三个。
第一,系统提示词是全局的,没法针对单个项目、单个模块做差异化约束。你同时维护三个项目,一个项目禁用某个框架特性,另一个项目要求所有接口走统一返回结构,这些细碎事实不可能全塞进系统提示词里。
第二,提示词是“期望”,不是“事实”。你告诉 AI“应该合理分层”,但没说“这个项目有哪些层、每一层能调用什么、不能调用什么”。它没有事实依据,只能自由发挥。
第三,code review 是事后拦截。AI 已经产出了坏代码,你再去改,不如让它一开始就别这么写。我曾经统计过一组数据:引入 AI 代码后,团队 review 中约四成评论是在纠正“架构越界”类问题,比如减少了封装、直接操作内部状态、绕过统一出口。这些问题的修复成本高,而且容易引起二次 bug。
linter 也救不了。静态检查只能抓“语法级错误”和部分“API 级误用”,但它不知道“这个模块内部方法不允许外部调用”这种业务语义。你没法用一条 lint 规则表达“XXX 是孤岛模块,谁都不许碰”。所以,唯一靠谱的路径,就是把架构约束前置到 AI 的上下文里,而且最好是它每次开始干活时都会自动看到的地方——AGENTS.md 就是这个位置。
1.3 AGENTS.md:一份写给 AI 看的项目事实清单
AGENTS.md 这个名字现在越来越常见了。说白了,它是一份放在项目根目录的 Markdown 文件,作用类似 README,但读者不是人,而是 AI 编码代理。主流的 AI 编程工具会在会话开始时、或者读取文件时自动加载它,把它作为项目的“背景知识”。
为什么它比提示词管用?因为它跟项目绑定,而且是“事实性描述”,不是“期望性要求”。你写的不是“你要优雅”,而是“本项目的调用规则如下:Controller → ApplicationService → Repository,禁止反向依赖”。AI 看到的是项目规则,而不是通用道德训话,其遵循概率会显著提高。
一句话总结这个思路:上下文工程不是玄学,就是把 AI 缺少的、影响代码走向的项目事实,放到它一定看得到的位置。下面就来拆解我推荐的四种事实。
2. 四种必须写进 AGENTS.md 的项目事实
我按“写进文件后效果最明显”排了个序:架构边界、封装规则、代码定式、演进约束。这四种不是随便挑的,而是覆盖了 AI 生成代码时最容易踩坑的四个层面。
2.1 事实一:架构分层与依赖方向,画清楚能往哪走
第一种事实是关于“布局”的。你的项目有几个层、模块之间谁依赖谁、数据流往哪个方向走。AI 不知道这些,所以它看到哪个方法方便就调哪个。
举个我实际处理过的例子。某项目 X 是个订单系统,分了 controller、application、domain、infrastructure 四层,依赖方向是单向的:controller 依赖 application,application 依赖 domain,infrastructure 在最底层。AI 写了一个新接口,直接从 controller 里 new 了一个 repository 去查数据库。从功能上讲完全没问题,但它把三层调用链直接压成一层。
后来在 AGENTS.md 里加了这么一段:
## 分层规则 - 禁止跨层调用:controller 只能调用 application 层接口。 - application 层是业务用例入口,禁止在 controller 中写业务逻辑。 - repository 只允许被 application 层或 domain 服务调用。 - 基础设施层(数据库、缓存、外部 API)的访问必须收敛到 infrastructure 层。这段内容不多,但它把“能往哪走”画清楚了。之后 AI 生成的代码,九成左右会先走到 application 层。为什么会这样?因为它现在有了“路径选择”的依据,就不会再凭局部上下文做就近调用。
写这种规则时有个技巧:不要只写“应该分层”,要写出具体层的名字和允许的调用关系。AI 对抽象概念敏感度低,对具体名称敏感度高。你可以把它理解成一个导航:光说“请走大道”没用,你要把路名标出来,它才会按路走。
2.2 事实二:封装边界与访问规则,标清楚谁不能碰谁
第二种事实更细一层:具体到某个类、某个模块、某个函数,哪些是公开入口,哪些是内部实现,谁不允许被谁调用。
很多项目里都有类似的“禁入区”。比如某个模块做了重构,新老 API 并存,老的一批方法只是为了兼容历史调用方而保留,新代码不允许再直接用。又比如某个工具类,看起来人畜无害,实际含有大量内部约定,外部直接调用会绕过校验流程。
我见过最痛的一个例子:某团队有一个“支付渠道适配层”,内部暴露了一个submitToChannel()方法,这个方法必须经过外层统一验签、幂等等逻辑。AI 生成的新代码为了省事,直接调了内部方法,绕过了整个校验链。这种 bug 比语法错误危险得多,因为它只在特定场景下炸。
在 AGENTS.md 里,这类规则要写成“禁入清单”:
## 封装禁区 - `payment/internal/*` 下的类禁止被外部模块 import,统一走 `PaymentGateway` 门面。 - `legacy-client` 仅用于兼容历史服务方,新代码禁止使用,替代方案见 `compat/README.md`。 - `utils/date_utils.py` 里的函数仅供内部测试使用,业务代码统一使用 `time_service`。“禁入清单”比“可用清单”更有效。为什么?因为 AI 面对的选择空间太大,你告诉它“可以走 A、B、C 三个入口”,它大概率会选最近的那个;但如果你明确说“D、E、F 是禁区”,它会直观地形成“这个不能碰”的约束,搜索空间被按住了。
写这类规则时,我建议路径精确到目录或文件级别,模糊表述(如“不要调用内部方法”)AI 没法映射到具体代码,效果会差一大截。
2.3 事实三:代码定式与一致性约定,给 AI 一套标准答案
第三种事实是关于“套路”的。每个成熟项目里都有约定俗成的写法:异常怎么包装、日志怎么打、DTO 怎么转换、事务怎么开、接口返回值用什么结构。这些定式存在,是为了让代码库保持一致,降低后续阅读和维护成本。但 AI 不知道这些定式,它会按照自己训练语料里的“通用写法”去生成,风格立刻跟你的项目割裂。
举一个很常见的例子:某项目所有接口返回统一结构Result<T>,包含 code、message、data 三个字段。AI 生成的新接口,直接返回了一个裸对象。代码能跑,但跟前端联调时直接出问题,因为前端默认所有接口都是Result结构。
在 AGENTS.md 里,这就是一条非常好的定式规则:
## 编码定式 - 所有对外接口必须返回 `Result<T>`,禁止直接返回裸实体类。 - 业务异常统一抛出 `BizException`,由全局异常处理器捕获,禁止在 controller 内 try-catch 并自行组装错误响应。 - 日志统一使用 `LoggerFactory`,禁止直接 `print` 或 `console.log`。 - 新增数据库查询必须通过 `XxxMapper`,禁止在 application 层拼接 SQL 字符串。写定式规则要注意:目标是“给定式”,不是“给百科”。你在告诉 AI 这个项目里已经确定的选择,让它照做即可。这样 AI 的输出会稳定融入现有代码风格,等于你不需要每次都在任务描述里重复这些约定,自动就少了很多一致性返工。
另外一个经验是:定式规则最好配上“什么时候用”的条件。比如“当需要新增对外接口时,必须返回 Result ”,比单纯写“接口返回 Result ”更清晰。AI 对带条件的指令理解得更准确,因为它在选择路径时能直接完成一次判定。
2.4 事实四:历史决策与演进红线,告诉 AI 哪些地方不能动
第四种事实最容易被忽略,但价值往往最高:项目里有些设计是刻意做出来的,背后有历史原因,外人看了觉得多余,但动了就会出事。
比如某项目里有个字段叫status_1,看起来命名很怪,其实是为了兼容早期数据,迁移时没来得及改名。AI 看到这个字段,很可能“好心”给你改成status,然后连带改了一堆 SQL 和映射——一旦上线,老数据全挂。
又比如有一个兼容层,专门把旧格式的报文转成新格式,逻辑看着很绕。AI 在重构时极大概率会判断“这个可以做简化”,直接把兼容逻辑删掉。看起来代码变简洁了,但旧客户端的请求就解析不了了。
这类约束,核心是要把“为什么”写清楚。AI 不需要理解全部背景,但至少要知道“这里有坑,不能动”。写法可以这样:
## 演进约束 - `order.status_1` 字段兼容历史数据,禁止改名或删除,迁移完成前须保留。 - `compat/` 目录中的报文转换逻辑服务于旧客户端,禁止简化或移除,变更必须经过兼容性评审。 - `billing` 模块当前正处于重构期,新代码禁止继续向 `billing` 添加逻辑,请迁移到 `billing-v2`。 - 已知技术债:`sync-jobs` 的锁机制依赖数据库行锁,禁止自行替换为分布式锁组件,除非完成压测论证。看到没有,这类事实本质上是一份“有注释的代码禁区”。它比普通注释强的地方在于:注释跟着代码走,AI 不一定逐行看;而 AGENTS.md 里的规则是全局性的,AI 不管在哪个文件,都能看到这份约束。
我的一个判断是:一个项目里如果这类演进约束超过十条,说明技术债和兼容场景已经不少了,写进 AGENTS.md 的收益会尤其大。它相当于是把团队脑中那些“老人才知道的教训”沉淀成结构化的机器可读信息,避免每次都由某位资深同事在 review 时口头纠正。
3. 实操:写一份 AI 真正会遵守的 AGENTS.md
前面讲的是“写什么”,这一节讲“怎么写”。我见过很多人兴致勃勃写了 AGENTS.md,结果发现 AI 根本不读,或者读了也当没看见。多半是写法出了问题。
3.1 文件放哪、命名和基本结构
AGENTS.md 一般放在项目根目录,跟 README.md 同级。命名就是AGENTS.md,别自行改名成AI_CONTEXT.md之类的,否则很多工具不会自动识别。如果项目里有多模块结构,可以在子模块目录再放一份局部的 AGENTS.md,规则范围更聚焦。
结构上我的习惯是保持 300 行以内,超过这个量级 AI 的“注意力密度”会下降。文件开头放一段简短的项目概述,然后是规则区。一旦规则超过 15 条,建议按类别分组,不然 AI 在生成代码时可能漏掉后面的部分。
还有一个我个人比较坚持的点:AGENTS.md 不承担“给人看”的职责。README 是给人介绍项目怎么跑、怎么用的,AGENTS.md 是给 AI 下规则的。这两个文件不要混用。否则你为了照顾人类读者的体验写一大堆背景故事,AI 的上下文被无关信息撑爆,关键规则反而没了权重。
3.2 用“面向 AI”的语言写约束
写 AGENTS.md 不是写散文,要用准命令式的句子。我建议一个规则就是一个祈使句,主语明确、动作明确、对象明确。对比一下就知道了:
- 不推荐写法:“项目中应当注意对服务层的调用,避免直接从控制器访问数据访问对象。”
- 推荐写法:“禁止 controller 直接调用 repository,必须经 application 层。”
推荐写法用了三个要素:违规动作(直接调用)、禁区对象(repository)、正确路径(application 层)。AI 在生成代码时,能直接把它翻译成一个调用链判断,而不是还得解析一下“应当注意”到底是什么意思。
我还有一个习惯:给每条规则编号。规则编号的价值在于,你可以在 review 时直接引用,比如给 AI 的反馈写“违反规则 A2,请修正”,它一下子就能定位到对应条款。这也为后面的构建“可执行闭环”打基础。
规则里正反例并置也很有效。给 AI 一个“坏例子(不要这样)+ 好例子(要这样)”的对比,比单独描述好得多。AI 对示例的敏感度高得惊人,一对并置例子可能顶得上十行抽象描述。
3.3 写冲突性规则时要留出优先级
一个容易被忽略的点是:规则之间可能冲突。比如“禁止重复造轮子,优先使用已有工具类”,和“禁止直接调用 utils 内部函数”,这两条同时存在时,AI 会困惑。它找到一个工具类,能直接解决问题,但又不能确定这个类是不是“内部函数”。
所以我在 AGENTS.md 里会设置优先级规则:
## 规则优先级 1. 封装禁区规则优先于复用规则,拿不准时先问人或走门面类。 2. 演进约束(禁止改动)优先于性能优化建议。 3. 模块内局部规则优先于全局规则。优先级机制给 AI 一个决策框架:当两条规则冲突时,选哪条。这看起来像个小细节,实际效果很明显——至少减少了那种“AI 在合规边缘反复试探”的回复。
3.4 让规则具备“可检查性”
写规则时可以思考一个问题:这条规则,能不能由一个机器人检查?如果一个人类 reviewer 都没法判断某段代码是否违反规则,那 AI 也不可能遵守。
我比较推荐的一种写法是“If-Then 规则”:
- 当你要对外暴露一个新接口时,必须走 controller 且返回 Result<T>。 - 当你要修改 `legacy-client` 时,必须先阅读 `docs/legacy-compat.md`。 - 当你要给现有表加字段时,必须同步更新迁移脚本和 mapper 映射。这种写法把条件前置,AI 可以在生成的每一步判断“我现在是不是在做这件事”,然后自动套用对应的约束。相比那句话“请保持架构整洁”,可执行性完全是两个层次。
我还会在 AGENTS.md 的最后加一个“提交前自检清单”,相当于给 AI 一个完整流程内的最后防线:
## 提交前自检 - [ ] 是否绕过任何分层或封装禁区? - [ ] 是否使用了项目已有的统一入口和定式? - [ ] 是否改动任何演进约束中列出的文件或字段? - [ ] 新增接口是否走 Result<T> 统一包装?这个清单不一定每条都触发,但它把最重要的规则浓缩成一个检查动作,对 AI 的“收尾行为”约束很有效。
3.5 版本管理与评审:AGENTS.md 也要被 review
AGENTS.md 不是写一次就完事的。它跟代码规范一样,会随着项目演进不断更新。我的建议是:每次新增规则,都要走一次“规则评审”,至少得有人问一句“这条规则真的必要吗?它会不会误伤正常需求?”
规则过多会有反效果:AI 的上下文被大量限制占据,创造性被压死,甚至可能出现它为了绕过规则而生成反直觉代码的情况。所以我不建议把 AGENTS.md 当成垃圾桶,什么细节都往里塞,只放那些“违反了会产生明显架构问题”的事实。
我自己会设定一个缓冲机制:先写进 AGENTS.md,跑两周观察 AI 行为,如果规则没触发过任何一次纠正,就删掉或者合并到别的条款里。规则的有效性以行为变化为准,而不是以“写了”为准。
4. 实测效果与常见问题排查
4.1 写之前 vs 写之后,一个直观对比
拿前面提过的那个订单系统 X 来说,在同一批任务下对比来看:没写 AGENTS.md 之前,AI 生成的代码里大约有三成会直接跨层调用,需要人工 review 时反复纠正;写完 AGENTS.md 之后,这个比例直接降到个位数,而且纠正过程明显变快——review 时只要说“违反了 A1 规则”,AI 立刻能定位并修正。
另一个更隐蔽的变化是:AI 生成的代码风格更“像团队写的”。因为它看到了定式规则,生成的接口返回值包装、异常处理方式、日志风格都与存量代码保持了一致。团队的同学不用在一个个“由 AI 写出来的,但不像我们风格”的代码块上浪费时间。
当然我也要说清楚:AGENTS.md 不是银弹。它提升的是“下限”,就是让 AI 少犯结构性错误,而不是让 AI 突然变成一个架构大师。它不能帮你写出天才级别的设计,但能帮你挡掉那些会让架构逐渐腐烂的劣质改动。
4.2 踩过的坑:为什么有时候 AI 视而不见
实操中我遇到过几次“AI 完全不遵守 AGENTS.md”的情况,排查下来无外乎几类原因。
第一,规则太长太散,被 AI 当噪音忽略了。AGENTS.md 写了上千行,连仓储层怎么建表这种细节都塞进去了,关键规则反而淹没在文本里。解决方案是精简,只保留“违反会产生大问题”的约束,并把最重要的规则放在文件前半部分。
第二,规则表述太抽象,AI 无法映射到代码。比如“请保持好的架构风格”,AI 不知道什么叫“好的架构风格”。得把规则量化到具体类名、目录路径、方法名级别。
第三,规则和任务指令冲突。有时候你在提示词里说“尽快实现功能”,而 AGENTS.md 要求“必须走统一审批链”,AI 更关注即时任务而非长期规则,就会选择性忽略后者。这种情况要么在任务描述里刻意引导 AI“先读 AGENTS.md 并遵守”,要么在代码评审时明确指出违反了哪条规则。
第四,工具没自动加载。有可能你用的 AI 工具并没有在会话中读 AGENTS.md,或者只读根部文件而没读子目录的规则。先确认工具行为再谈规则质量。
4.3 排查技巧:怎么判断 AI 到底有没有读
如果怀疑 AI 没读 AGENTS.md,最简单的办法是让它在生成代码前先复述规则。你可以在任务里加一句“在开始前,重述 AGENTS.md 中与本次修改相关的三条规则”。它能准确复述,说明上下文里有这份文件;如果复述得牛头不对马嘴,多半是没加载,或者文件格式有问题。
还有一个低成本的验证方式:设计一个故意“踩禁区”的测试任务。比如要求 AI“用最简单的方式查一下订单列表”,看它是否直接从 controller 调 repository。如果它仍然这么干,说明规则没有进入它的决策上下文。这是最直接的探针。
等稳定之后,我建议定期做一次“AGENTS.md 效果盘点”,把过去两周 AI 生成的代码 review 记录翻出来,统计绕过封装、破坏架构的案例占比有没有下降。数据不会骗人,如果没下降,就回去改规则写法,而不是继续加规则。
5. 进阶玩法:把 AGENTS.md 接入工具链形成闭环
最后聊点进阶思路。AGENTS.md 本身是一份静态文件,但如果把它跟团队的工具链串起来,能发挥更大的作用。
5.1 让 review 标准跟 AGENTS.md 规则一一对应
我在团队里做过一件事:把 code review 的检查项跟 AGENTS.md 的规则编号对应起来。AGENTS.md 里写“规则 A1 禁止跨层调用”,review 模板里就有一项“是否违反 A1”。这样 AI 生成代码后,review 不再依赖某个人脑子里记着规则,而是按清单逐条核验。规则有编号,反馈就能精准定位,AI 修正时也不再“给你重写一遍”,而是针对性的局部修改。
更进一步,可以在 review 场景中直接引用:当 AI 生成了一段绕过封装区的代码时,review 评论里写“违反 B2 封装禁区规则,请改用 PaymentGateway 入口”。因为 AI 能看到 AGENTS.md 里的规则原文,这种反馈会产生即时的学习效果。几次下来,同类错误明显减少。
5.2 把规则映射到自动化检查
AGENTS.md 是给 AI 看的,但如果你把它结构化成可解析的内容,很多规则可以直接映射为自动化检查。比如“禁止直接 import 某些目录”可以转成一条 import 检查规则;“接口必须返回 Result ”可以通过扫描接口定义的脚本快速核验。
当然不是所有规则都能自动化,架构语义层面的约束很难完全转成静态检查。但哪怕只能覆盖五成,也是实打实的收益。自动化检查接住的是 AI 规则的“下限”,AGENTS.md 管住的是生成阶段的“概率”。两者叠加,架构被破坏的概率会进一步下降。
5.3 当作活文档,持续投资
最后想强调一点:AGENTS.md 的价值与维护投入成正比。它跟 README 不一样,不是补丁型文档,它是团队的“架构契约库”。每当项目里出现一个复杂的架构决策、一个新人容易踩的坑、一段有历史包袱的兼容代码,都值得考虑要不要沉淀进 AGENTS.md。
我个人体会最深的一点是:AI 编程普及之后,“项目的隐性知识能不能显式化”会变成代码库健康度的分水岭。以前这些隐性知识存在资深同事脑子里,靠人传人;现在把它写进 AGENTS.md,等于直接把团队经验变成了 AI 的默认行为。这个投入,越早做越划算。
如果你目前还没开始,不用等一个完美的架构梳理完成才开始。先挑一种事实写进去,比如最痛的“封装禁区”,用一两天观察 AI 行为变化,然后迭代。上下文工程本来就是积累出来的,不是一次写出来的。