给 AI 写代码约束这件事,我踩过一个很典型的坑:一开始收集了 40 多条规则,MUST、SHOULD、NEVER、ASK 四级分得清清楚楚,看着挺完备。结果实际用起来完全不生效------要么它什么都不问就自己决定了,要么什么都改不动。
问题不在规则数量,也不在分级不够细。在于那 40 多条规则虽然分了四个标签,但每个标签对应什么行为,从来没定义清楚。
后来我把这套约束重构了一遍,规则反而删到只剩真正有约束力的部分。中间想通了一件事,今天完整写出来------它不只适用于写代码约束,也适用于任何需要「让 AI 稳定执行某套规范」的场景。
一、先看一个真实的坑
朋友扔给我三份文件,说「按这个生成一个东西」。
一份 .md,一份 .txt,另一份也是 .txt。
我读完 .md 就准备开工------内容很完整,从元信息到工作流到规则分级,结构清晰。但读到最后一节「附录:追加规则模板」,代码块是空的。
模板里该有编号、级别、触发条件、例外、违反时的处理------一个字段都没有。
这份文档是残缺的。 但用户没提,我如果照着半截内容生成,后面「追加规则模板」这一节就会是空的,用户拿到手才会发现。
我停下来,把三份文件又看了一遍,发现另外两份 .txt 讲的就是这一段:一份是完整的追加流程,另一份是修改、废弃、降级规则的操作规范。三份文件合起来才是一份完整文档。
这是我给 AI 定的第一条约束:能读项目就先读,读全了再动手。 而这件事能发生,恰恰是因为「先读项目」被写成了一条明确要求,而不是一句「请仔细阅读代码」。
后面 90% 的坑,都来自同一类问题:规则写在了,但没说清怎么算做到了。
二、四级分级:把「标签」变成「行为」
我原来那份规则里已经有 MUST / SHOULD / NEVER / ASK 四级标签。看起来很规范对吧?但标签本身不解决问题。
AI 拿到「MUST:新增依赖前必须问用户」,它不知道:
- 算不算「新增依赖」?装一个 devDependency 算吗?
- 问的时候要说明什么?只问「可以用吗」,还是要给出替代方案?
- 我已经装上了,可以先斩后奏吗?
标签只是分类,不是行为。行为没定义清楚,规则就只是一句愿望。
我把四个标签重新定义成了下面这样,每一级都明确规定「什么时候必须停下」「什么时候可以自动通过」:
| 级别 | 含义 | 行为 |
|---|---|---|
| NEVER | 硬禁止 | 不许写。确实必须写时,先说明原因并请用户确认 |
| ASK | 打断式 | 停下来问用户,拿到答复再动手。不允许自行豁免 |
| MUST | 强制 | 遵守;无法遵守时必须显式标注违反哪条、原因、风险 |
| WARN | 风格建议 | 提醒一句即可,不拦截,不要求答复,不标注 |
同一组规则,重新分级之后,行为完全变了。举个具体的例子:
原规则:「超500 行必须警告,给出当前行数、超标原因、不拆分风险、是否要拆分方案。」
这一条写在 MUST 里。但它是硬禁止吗?显然不是------它不阻止你写。允许吗?也不完全,因为它的意思是「要停下来问」。MUST 的定义是「违反可以但必须标注」,那它到底算不算违反?
改成 ASK 之后就清楚了:**停下来问,用户说「不拆」就继续,但要在自检里标注「已豁免 500 行规则」。**规则有了确定的执行路径。
分级只有一条判定线
四级看起来复杂,但真正做判断的时候,只需要问一个问题:
这条规则会不会改变外部行为、接口、数据、依赖、文件结构?
- 会 → ASK,停下来问
- 不会,只影响内部写法、命名、注释、结构 → WARN,提醒一句就往下走
「函数不超过 50 行」不动任何外部接口,是 WARN,提醒一下,写成 60 行也不影响功能。
「删除文件、改公共接口」一定会影响别的调用方,是 ASK,必须问。
这条线的好处是:新增规则时不需要再讨论它该归哪一类,问一句就出来了。 我现在加规则,两分钟内定完级别------以前要来回掰扯半天。
三、决定规则级别的,不是重要性,是判据
这是整个方法论里我最想分享的一条。
刚开始我以为「这条规则重不重要」决定了它该放哪一级。写了几轮发现不对。
真正的判断标准是另一个问题:这条规则的判据,是不是一个客观事实?
我原来的规则里有这么一条:「避免过度抽象」。
这条重要吗?非常重要。但它是 MUST 还是 WARN?
后来发现,它没法客观判定。「过度」到什么程度算过度?两个方案,一个抽象得狠一点但灵活,一个不抽象但简单,评审时能吵一下午。这种规则一旦写成 MUST,AI 就会开始自我豁免------因为它自己也没法判断是否违反。
于是我把它降级成 WARN,换成了一句可判定的话:
只有一个使用者的公共目录,属于提前抽象。
现在它能判定:数一下这个目录被几个地方引用。只有一个,就是提前抽象。
这就是判据的价值------它把一条主观建议,变成了 AI 能自己检查的事实。
我现在的判断流程是两步:
- 判据是客观事实吗?→ 是,进 MUST
- 不是,但会改变外部行为吗?→ 是,进 ASK
- 都不是 → WARN
排第一的是判据,不是重要性。 因为客观判据意味着 AI 能自己验证并给出证据,而不需要人类在旁边盯着。主观规则就算写进 MUST,也只是让它在不确定时反复自我说服。
同理,我把一批模糊表述都换成了可判定的版本:
| 原来(模糊) | 现在(可判定) |
|---|---|
| 分层要清晰,依赖要单向 | 依赖图出现反向边或环 → 违规 |
| 第三方库要隔离 | 业务代码中出现第三方类型 → 违规 |
| 保持数据一致性 | 同一份字典出现第二处硬编码 → 违规 |
| 组件应该有良好封装 | 调用方需要理解内部有多少状态才能用 → 门面漏了 |
| 避免重复的通信实现 | 出现第二个通信实例 → 违规 |
改完之后,这些规则才真正能被执行。
四、重构后发现的最大空白
规则重构到40 多条时,我发现一件事:所有规则里,没有一条在管 import 方向。
有禁止吞异常的,有禁止硬编码密钥的,有管命名和注释的,有提醒超 500 行的。但没有任何一条在说「谁可以依赖谁」。
而这恰恰是最容易让代码库腐化、又最容易被判定的维度。它不需要主观判断------依赖图画出来,有没有环,有没有反向边,一目了然。
于是我补了 10 条结构类规则。下面是其中最有价值的几条,每条都带判据:
1. 分层单向,依赖只指向更稳定的一侧
视图 → 编排 → 逻辑 → 数据访问 → 通信底座
禁止反向依赖,禁止跨层跳跃。
判据:依赖图出现反向边或环。
2. 同级不互引,共享能力下沉
同级的业务单元之间互不认识,需要共享的一律下沉到公共层。
判据:同级之间出现引用。
3. 第三方库只出现在适配层
对外只暴露自己的稳定抽象。
判据:业务代码中出现第三方类型。
4. 全应用只有一个通信出口
鉴权、租户标识、加解密、重试、错误提示、日志,全部集中。
判据:出现第二个通信实例。
5.「类型 → 实现」走注册表,不走 if-else
新增一个类型 = 新增一条注册记录,不修改分发处。
判据:分发处出现不断增长的 if-else 或 switch。
6. 就近原则 + 规则二
作用域越小越靠内。只有当第二个使用者出现时,才把能力向上提一级。
判据:只有一个使用者的公共目录。
7. 单一数据源
同一份字典、枚举、常量,全应用只有一处定义、一处缓存。
判据:出现第二处硬编码。
8. 表现层薄壳化
表现层只做组装,业务逻辑下沉,纯函数层不依赖框架、可独立测试。
判据:表现层文件里出现多步业务判断。
9. 新增业务单元不应改动框架代码
路由、页面、组件、文案、配置等注册类横切点,走「约定路径 + 自动扫描」。
判据:新增一个模块的变更集触及了外壳目录。
10. 同一段结构多处复制时,抽为「配置 + 解释器」
调用方给配置对象,引擎负责渲染与生命周期。配置承载行为规格(条件、联动、禁用、校验),不只是静态数据。
判据:同一段结构在多处被复制。
这 10 条的共同点是:每一条都有「怎么算违规」的明确说法。 我可以自己检查,也可以让 AI 自己检查并给出证据。
这也是我挑它们的标准------不是为了显得完整,而是因为它们能落地。那些同样正确但无法判定的原则(高内聚低耦合、开闭原则、单一职责),我留在心里,不写进规则库。
五、三个维护上的坑
规则本身写完不是结束。真正麻烦的是维护,这三个问题我都撞过。
坑一:删规则会留下编号空洞
我删掉了一条 WARN 规则(W5),后面的 W6、W7 还在,编号从 4直接跳到 6。
看起来没什么大不了。但我的规则文档里写着一条约定:「废弃规则保留编号,不重用。」
这条约定一旦有编号空洞就失效了------因为下次想「补一条新规则」,填 5 还是填 23?文档没说清,就会有人随便填。
现在我删规则会重排后续所有编号。虽然麻烦,但规则库的编号必须连续,否则它就不是一份可引用的文档。
坑二:规则上限只写数字,等于没写
我原来写着「MUST ≤ 15 条,WARN ≤ 20 条」。
结果某天发现 WARN 已经21 条了。
上限写着 20,实际 21,而且没人觉得有问题。 因为数字在那儿摆着,看着就是「大概遵守了」。
我后来把上限改成了 MUST ≤ 20、WARN ≤ 25,但更重要的是加了理由:
MUST 的判据都是可观察的客观事实,不占用注意力;WARN 是风格层,过多会稀释 MUST 的权重。
加了理由之后情况变了------因为理由说明了为什么超了会出问题,规则数量就自然被当成一个需要权衡的东西,而不是一个可以随手填的数字。
任何约束都该有理由。 只写结论的约束,会在被违反的时候静默失效。
坑三:别把框架绑定的内容写成通用规则
有一份《目录与设计最佳实践》,开头写着「本文正文不依赖具体语言与框架,可直接跨项目复用」。
我信了。直到逐条看下去,发现目录树是这样的:
bash
src/
├── api/
├── components/
├── hooks/ # 或者叫 composables/
├── store/modules/
├── config/axios/
└── modules/
hooks/ 还是 composables/、types.ts 还是 type.ts、.vue 还是 .tsx------这些全是前端框架特定的写法,跨到 Python 项目里毫无意义。
所以我只从里面取了 16 条设计实践里真正通用的部分,目录树、词汇表、量化线里的「前端 500 行」,全部不要。
文档自称通用不等于真的通用。 判断标准是看它的示例代码里有没有具体技术栈的痕迹。
顺带说个有意思的:那份文档里「单文件超 500 行要提醒」这条,我一开始想留。但转念一想,500 行这个阈值本来就是从前端实践里来的,对一个几千行的 Python 单文件可能完全合理。所以这条在我自己的规则集里,变成了「给出行数、超标原因、风险,让用户决定」------不给固定阈值,只要求必须说明。
六、体系演进:一句话交代
我最初的版本比现在复杂得多,设计了「三层防线」:
- 最外层:硬拦截,危险操作和绕过测试的行为在执行前直接拒绝
- 中间层:门禁脚本,改完代码自动跑一遍,有违规退出码非 0
- 最内层:规则层,也就是上面写的那些
跑了一段时间,我把整个脚本和拦截层都删了,只留规则。
原因有两个,都很实际:
硬拦截会误伤。 我们的拦截规则里有一条是「不允许绕过测试」,但 pytest -k xxx 这种缩小测试范围的行为是合法的本地调试,也会被打成违规。而误伤一次,AI 的反应不是「哦我理解错了」,是「那我换个写法绕过它」------规则一旦被绕过,就真的失效了。宁可放宽也不要误伤。
门禁脚本只能事后发现可模式化的违规。 空catch、硬编码密钥、拼接注入这些能查出来,但「命名没表达业务」「注释太多」这类不行。更重要的是,脚本管不了交互------「该不该提级到公共层」「这个接口要不要改」这种问题,只能当场问。
真正需要约束 AI 的地方在生成过程中,而过程里的约束只能靠规则分层,靠的是「什么时候必须停下来问」。
所以现在这套东西的核心其实特别简单:
该问的必须问,不该问的别啰嗦,判据清楚的才配写成硬规则。
七、如果你也要写一套规则:从这一步开始
不需要从零开始。先做三件事:
第一步:把你已经写下的规则全部过一遍,逐条问「这条的判据是不是客观事实」
是 → 进 MUST。不是 → 往下看。
第二步:再问「它会不会改变外部行为、接口、数据、依赖、文件结构」
会 → ASK,定义清楚问什么、拿到答复后怎么做。不会 → WARN,定义清楚「提醒一句就往下走,别停下来等确认」。
第三步:给每一级都写清「行为」
这是最容易被跳过、也最关键的一步。只有标签没有行为,等于没分级。
四个可以直接抄的定义:
NEVER 不许写。确实必须写时,先说明原因并请用户确认
ASK 停下来问用户,拿到答复再动手。不允许自行豁免
MUST 遵守;无法遵守时必须标注违反哪条、原因、风险
WARN 提醒一句即可,不拦截,不要求答复,不标注
最后送一条我踩过才发现的:
如果一条规则你判断不了它有没有被违反,那它大概率不该是 MUST。 它应该降级成 WARN,或者------更好的做法------改写成一句可判定的样子。
「命名要有意义」不可判定,所以改成「禁止 a、b、tmp、data1」。
「避免过度抽象」不可判定,所以改成「只有一个使用者的公共目录属于提前抽象」。
改完之后,你的规则库会从一堆愿望,变成一份 AI 能自己执行、自己检查、自己给出证据的东西。
规则本身比较枯燥,真正有用的就是上面这套判断方法:先看判据是否客观,再看是否影响外部行为,最后给每一级定义清楚行为。
这套顺序用得对了,规则写多少条其实没那么重要。