最近用 Codex 写代码的时候,我逐渐发现一个问题:
AI 不是不会写代码。
它的问题是,太会写了。
你让它改一个接口字段,它开始写兼容逻辑:
ts
const title = data.title || data.name || data.label || data.displayName || ''
你让它修一个按钮文案,它顺手重构了半个组件。
你让它补一个 loading 状态,它顺便抽了 hook、拆了 utils、加了类型、优化了结构,最后还很贴心地告诉你:"这样更具可维护性。"
这种感觉很熟悉。
就像你请朋友来家里帮忙换个灯泡,结果他看了看天花板,说:"你这个房子的采光系统有点问题,我给你重新设计一下电路。"
谢谢,但我只是想让灯亮。
于是,我开始给 Codex 准备一套开发规则,也就是 codex-dev-norms 这类 skill。它的目标不是让 AI 变笨,而是让 AI 在写代码之前先明白一件事:
不是所有能写的代码,都应该写。
一、背景:AI 写代码的常见"热心事故"
AI 开发里有一个非常典型的问题:它经常会"补过头"。
人类工程师写代码时,多少会带着项目上下文、团队约定、业务边界,以及一点点来自线上事故的心理阴影。
AI 不一样。
它擅长从已有模式中推断下一步,也擅长把"不确定"补成"看起来合理"。这在很多场景里很有用,但在业务代码里,它也可能制造一些非常熟悉的事故现场。
1. 过度兼容
比如接口文档里明明写的是 title,AI 可能觉得后端也许会返回 name,历史数据也许会有 label,保险一点再兼容 displayName。
于是代码变成:
ts
const title = item.title || item.name || item.label || item.displayName
看起来很稳。
实际上很危险。
因为它把一个明确的接口契约,变成了一场字段猜谜游戏。今天兼容四个字段,明天再来一个 text,后天补一个 caption,最后谁也不知道真实数据到底应该长什么样。
代码开始像在相亲:谁来都能接一下。
2. 过度重构
你只想让它修一个 bug,它却觉得附近代码不够优雅。
"这个函数可以抽一下。"
"这个组件可以拆一下。"
"这个命名可以统一一下。"
"这个结构可以更通用一点。"
如果是在学习项目里,这可能还挺开心。但在真实业务项目里,这种"顺手优化"经常会变成一次范围失控。
一个小改动突然牵扯多个文件,原本只需要验证一个按钮,现在要确认半个页面有没有回归。
AI 倒是很积极,Code Review 的人开始沉默。
3. 过度抽象
AI 看到重复代码,很容易触发封装欲。
两个地方都有一个状态判断?抽。
两个组件 class 有点像?抽。
两个类型字段差不多?合。
但工程里的重复并不总是坏事。有些重复只是长得像,业务含义并不一样;有些代码现在重复,未来变化方向可能完全不同。
强行抽象之后,项目可能从"有一点重复"升级成"没人敢动"。
4. 忽略编码和中文环境
中文项目还有一个经典问题:编码。
尤其在 Windows 环境里,如果读写文件时不确认 UTF-8、GBK、ANSI,轻则中文变乱码,重则你打开文件那一刻,仿佛看见项目在用另一种文明和你交流。
还有些 AI 会把中文写成一串 Unicode 转义字符。
这当然也许能运行。
但人类读起来会很想下班。
5. 缺少"改动边界感"
说到底,很多问题不是 AI 不懂代码,而是不知道边界。
它不知道哪些文件不能动。
它不知道哪些字段不能猜。
它不知道哪些重复可以保留。
它不知道这个项目现在需要的是"修复",不是"翻新"。
于是我们需要一套规则,让 AI 在动手前先收到明确提醒:
做需要做的事,不要做看起来也不错的事。
二、有哪些解决办法
要解决 AI 过度发挥,常见办法有几种。
1. 靠人工 Code Review
这是最传统也最可靠的方式。
AI 写完,人类审。
问题是,AI 写得太快了。
它一分钟可以生成一大片代码,而人类要逐行确认:这个兼容有没有必要、这个抽象有没有收益、这个字段是不是瞎猜、这个文件是不是不该动。
久而久之,Code Review 会从"质量保障"变成"AI 善后现场"。
2. 靠 Prompt 反复提醒
比如每次都告诉它:
text
只做最小改动。
不要重构。
不要猜接口字段。
不要修改无关文件。
这当然有用。
但问题是,每次都要说。你忘说一次,AI 就可能立刻恢复出厂设置,重新变成那个看到代码就想顺手优化的热心同事。
3. 靠项目级规则文件
比如 AGENTS.md、CLAUDE.md 或者 Codex skill。
这种方式的好处是:把团队约定固化下来。
不是每次靠人提醒,而是让 Codex 在工作时就能读到项目边界。哪些事可以做,哪些事不能做;什么时候需要查接口,什么时候必须保持最小改动;什么时候该加测试,什么时候不要为了覆盖率写废测试。
它相当于给 AI 配了一份"入职手册"。
只不过这份手册的重点不是欢迎加入团队,而是:
欢迎加入团队,先别乱动。
4. 靠技能化拆分规则
更进一步,可以把规则拆成多个 reference。
比如 API 有 API 的规则,前端有前端的规则,测试有测试的规则,Git 工作区有 Git 工作区的规则。
这样 Codex 不需要每次背完整本规章制度,而是根据任务读取相关部分。
修接口,就看 API 规则。
改组件,就看前端规则。
做 review,就看 review 规则。
这种方式比较接近真实工程协作:不是把 AI 当成一个万能文本生成器,而是把它当成一个需要遵守项目规范的开发者。
三、这个 skill 具体解决了哪些问题
codex-dev-norms 这套 skill 的核心价值,是把 AI 常见的失控点拆成一个个明确边界。
它不是一份"代码洁癖清单",更像是一份"防止 AI 过度热心说明书"。
下面按 reference 大致看一下它管了哪些事。
1. code-change-boundary:先管住手,别乱改
这个 reference 主要强调最小改动原则。
也就是:任务要求什么,就改什么;不要因为"更干净""更通用""更优雅""更符合最佳实践"扩大范围。
这条非常关键。
因为 AI 很容易把"解决问题"理解成"顺便把附近看不顺眼的地方都整理一下"。但真实项目里,很多时候我们需要的是可控的小变更,而不是一次兴致勃勃的局部装修。
这条规则会提醒 Codex:
你是来修这个 bug 的,不是来给项目重新投胎的。
2. global-frontend:中文、编码、前端全局边界
这个 reference 管的是全局前端开发规则,尤其是编码和中文字符处理。
它要求在读写文件前确认编码,不要默认使用可能出问题的读取方式;中文要直接写中文,不要写 Unicode escape;如果发现乱码或混合编码,要先处理编码问题。
这对中文项目特别重要。
毕竟中文乱码不是普通 bug,它更像一种仪式。一旦触发,项目里所有汉字都会开始怀疑自己到底来自哪里。
除了编码,它也强调不要改无关代码、不要格式化无关内容、不要乱改结构、命名、props、CSS class 等。
一句话:前端可以改,但不能趁机"大扫除"。
3. api-integration:别猜接口,按定义来
这个 reference 专门管 API 接入。
它的核心态度很明确:API 字段必须严格来自真实定义。
不要猜字段名。
不要猜响应结构。
不要加别名字段。
不要写 a || b || c 这种自动兼容。
如果字段不清楚,就去确认接口定义。
这正好命中 AI 的常见毛病:太喜欢替后端"考虑周全"。
但业务代码里,接口契约不是开放式作文。后端返回什么,前端就按什么接。否则所谓兼容会慢慢变成技术债,最后谁也不知道哪个字段才是真的。
4. backend-business-logic:业务逻辑别到处流浪
这个 reference 关注后端和业务逻辑边界。
它强调业务规则应该待在真正属于它的位置,不要散落在 controller、service、job、mapper、serializer 各处。
这也是 AI 容易犯的错。
哪里需要判断,就在哪里补一段。
哪里缺个转换,就在哪里加一个 helper。
短期看问题解决了,长期看业务规则开始到处流浪。以后要改一个状态判断,可能要在五个地方找它的分身。
这条规则是在提醒 AI:业务逻辑不是贴纸,不能哪里空就贴哪里。
5. frontend-implementation:小组件别写成小宇宙
这个 reference 关注前端实现。
它强调可读性优先,不要为了优化引入复杂度;组件应该小而聚焦;是否抽 hook、utils,要看真实复杂度,而不是主观觉得"这样更优雅"。
这对 AI 很有必要。
因为 AI 很擅长把简单事情做得很完整。
一个本来只在当前组件里用一次的逻辑,它可能会抽成 hook;一个简单 formatter,它可能会塞进 utils;一个局部状态,它可能会设计成可复用方案。
看起来很专业。
但专业不等于复杂。
尤其前端代码,很多时候"放在这里一眼看懂",比"抽出去但你要点进去看"更好。
6. state-management-boundary:状态该归谁,就归谁
这个 reference 管状态边界。
它区分了组件状态、hook 状态、store 状态、URL query 状态和 server state。
这能解决一个很常见的问题:AI 喜欢把状态放到"看起来更通用"的地方。
本来只属于一个组件的展开状态,被放进 store。
本来应该体现在 URL 上的筛选条件,被藏在内部 state。
本来属于服务端缓存的数据,被复制成多份本地状态。
最后状态之间互相看不顺眼,页面刷新、返回、切 tab 都可能出现奇怪行为。
这条规则就是告诉 Codex:状态不是越全局越高级,放对地方才高级。
7. side-effects-and-subscriptions:副作用要有生命周期
这个 reference 管事件监听、订阅、定时器、watcher 等副作用。
它强调注册要清楚,清理要明确,生命周期要看得见。
AI 写副作用代码时,很容易只关心"让它生效",忘了"让它结束"。
比如加了 resize listener,但没有清理。
比如开了 timer,但卸载时不处理。
比如 watcher 和事件订阅重复触发同一段逻辑。
这种问题短期不一定明显,但跑久了就会变成难查的行为异常。
这条规则相当于提醒 AI:不是所有副作用都能靠缘分收场。
8. constants-and-magic-values:别让魔法值满地跑
这个 reference 管常量和魔法值。
比如业务状态、事件名、缓存 key、storage key、路由、API path、正则、默认值、分页数等。
这些值如果只有局部意义,可以留在局部。但如果代表共享业务含义,就应该有稳定来源。
AI 有时会随手写字符串:
ts
status === 'success'
单看没问题。
但如果项目里到处都是 'success'、'SUCCESS'、'done'、'finished',以后改业务状态时就会很热闹。
这条规则的意义是:有业务含义的值,不要让它在项目里自由奔跑。
9. type-convergence:类型相同,不代表概念相同
这个 reference 管类型收敛。
它会提醒 Codex:如果多个类型字段相同,并且业务含义、生命周期也相同,可以考虑统一;但如果只是结构长得像,不要强行合并。
这很重要。
因为 AI 很容易看到两个 interface 字段一样,就觉得可以抽一个公共类型。
但前端表单类型、接口响应类型、数据库实体类型,可能字段一模一样,语义却完全不同。
强行合并之后,一个地方变动,另一个地方被迫跟着变,最后类型系统从保护伞变成连坐制度。
10. convergence-and-abstraction:不是所有重复都该抽象
这个 reference 管代码收敛和抽象边界。
它承认一个现实:允许合理的局部重复。
如果逻辑只出现一次,或者业务含义不同,或者未来变化方向不同,或者抽象会让代码更难懂,那就不该为了"消灭重复"而抽象。
这条规则非常反 AI 本能。
AI 看到重复,常常想收敛;看到相似,常常想封装。
但抽象不是奖励机制,不是写得越多越高级。好的抽象应该降低理解成本,而不是把简单代码变成参数迷宫。
11. ui-style-convergence:UI 风格可以统一,但别硬统一
这个 reference 管 UI 样式和变体收敛。
比如相同按钮、输入框、卡片、徽标、表格样式,重复的 class list,暗色模式规则,响应式布局规则,都可以考虑收敛到基础组件、设计 token 或 variant。
但它不是鼓励 AI 看到 class 重复就马上抽组件。
UI 里有些重复来自设计系统,有些重复来自局部业务场景。前者值得收敛,后者未必。
所以这条规则的价值是:让 AI 既能识别真正的设计重复,也别把每个长得像的东西都拉去认亲。
12. engineering-config-convergence:工程配置别各玩各的
这个 reference 管工程配置收敛。
比如 TypeScript 配置、lint 配置、测试配置、构建配置、formatter、package scripts、依赖版本、环境变量 schema、发布流程等。
这些地方一旦重复且不一致,就很容易出现"我这里能跑,你那里不行"的经典剧情。
但同样,它也不是让 AI 随便改配置。
工程配置影响范围通常很大,所以只有当任务确实涉及配置收敛时,才应该动它。
换句话说:配置文件不是许愿池,不能路过就扔一个优化进去。
13. dependency-management:不要为小事加新包
这个 reference 管依赖管理。
它强调不要为了简单功能引入新依赖,不要引入职责重复的第二个库,不要在窄任务里升级无关依赖,也不要随便改 lockfile。
AI 有时会很喜欢推荐库。
格式化时间?加个库。
深拷贝?加个库。
简单校验?加个库。
但依赖不是免费的。它带来体积、维护、安全、升级和团队认知成本。
这条规则是在提醒 Codex:能写三行解决的问题,不要先召唤整个 npm。
14. docs-and-comments:注释解释为什么,不解释废话
这个 reference 管文档和注释。
它鼓励解释业务目的、分支原因、边界场景、状态流转、兼容约束、性能取舍和临时方案移除条件。
但它反对显而易见的注释,也反对没有 owner 或移除条件的 TODO。
AI 特别容易写这种注释:
ts
// 设置标题
setTitle(title)
这不是注释,这是把代码又朗读了一遍。
好的注释应该告诉人类:为什么这里要这么做,而不是告诉人类:这行代码长什么样。
15. testing:测试行为,不测试自我感动
这个 reference 管测试。
它强调不要给薄包装写无意义测试,不要重复相同输入输出的测试,不要测试 mock 本身,不要为了覆盖率数字写测试。
有价值的测试应该覆盖正常路径、边界值、空值、非法值、权限、状态转换、错误处理、回归场景等。
这点也很适合约束 AI。
AI 可以很快写出一堆测试,但数量不代表质量。如果测试只是证明 mock 会返回 mock,那它更像是在给自己鼓掌。
16. error-handling-and-logging:错误不要被悄悄吞掉
这个 reference 管错误处理和日志。
它强调不要无声吞错,能明确失败就不要隐藏错误状态;处理错误要在真正能恢复的边界;转换或重新抛出错误时要保留有用上下文。
日志也一样。
要记录能帮助诊断真实问题的信息,不要给正常流程加一堆噪音,也不要把临时 debug 留进生产代码。
这条规则的目的很朴素:出问题时,人类至少要知道问题在哪里,而不是只能看着控制台思考人生。
17. naming-and-files:命名稳定,文件别乱拆
这个 reference 管命名和文件组织。
它强调一个概念应该有一个稳定名字,不同名字应该意味着不同含义;优先使用领域术语,不要随手起 helper、manager、common、base、util 这种模糊名字。
它也提醒不要为了文件变小就拆文件,不要为了方便把无关职责塞进一个文件。
AI 很容易"整理结构",但结构整理本身就是改动。
如果任务不要求,就不要顺手改命名、挪文件、拆模块。否则看起来只是组织调整,实际上可能影响导入路径、公共 API 和团队认知。
18. code-review:Review 先找风险,不先夸优雅
这个 reference 管代码审查。
它要求 review 优先关注错误行为、回归、边界条件、契约不匹配、缺失验证、不安全副作用、测试缺失等。
这能避免 AI review 变成空泛评价:
"整体结构清晰。"
"代码可读性较好。"
"建议进一步优化。"
这些话不是完全没用,但如果没有指出真实风险,就像开会时说"我们要持续改进"一样,听起来正确,落地困难。
好的 review 应该先把可能出事故的地方指出来。
19. final-response:交付时说人话
这个 reference 管最终回复。
它要求 Codex 在完成任务后说清楚:改了什么,验证了什么,还有什么不确定,下一步是什么。
这也很重要。
因为 AI 很容易把最终回复写成过程汇报:
"我首先分析了项目结构,然后检查了相关文件,接着进行了修改......"
用户真正关心的是结果。
所以这条规则会要求它开门见山:事情是否完成,完成到什么程度,有没有验证,哪里还需要注意。
四、它真正解决的不是代码问题,而是边界问题
看完这些 reference,会发现 codex-dev-norms 真正解决的不是某一种具体 bug。
它解决的是 AI 开发里的边界问题。
AI 很强,但它不知道你的项目里哪些东西不能碰。
它不知道这个接口字段必须严格按定义来。
它不知道这个重复现在不该抽象。
它不知道这个状态只属于当前组件。
它不知道这个中文文件不能随便用默认编码读写。
它也不知道你今天只是想修一个按钮,而不是顺便参加一次架构改造。
所以这套 skill 的意义,就是把这些隐含经验变成显式规则。
让 Codex 从"能写代码",变成"能按项目边界写代码"。
五、让 AI 少一点自我发挥,多一点工程克制
AI 写代码越来越强,这是事实。
但在真实项目里,强不等于可以随便发挥。
很多时候,我们需要的不是一个永远有新想法的 AI,而是一个知道什么时候该停手的 AI。
codex-dev-norms 这类 skill,就是给 Codex 准备的一套开发家规。
它不会让 AI 失去能力。
恰恰相反,它让 AI 的能力更可控、更稳定、更适合进入真实工程协作。
以前 Codex 像一个精力旺盛的搭子:你说一句,它能写一屏。
现在有了这套规则,它还是能写一屏。
但终于知道,哪半屏不该写。