AI 编程的奥卡姆剃刀原则
Posted on in Journal
| Abstract | AI 编程的奥卡姆剃刀原则 |
|---|---|
| Authors | Walter Fan |
| Category | Journal |
| Version | v1.0 |
| Updated | 2026-11-01 |
| License | CC-BY-NC-ND 4.0 |
大纲
展开看看
- AI 的啰嗦不是毛病,是默认设定:多写一行对它成本为零,对你是正的
- 剃刀砍的是实体,不是字数:函数、配置项、抽象层、依赖、服务,五种实体五个价钱
- 后端服务自带一把剃刀:引擎用 C 编译,策略用脚本解释,这条分界线是前人替你划好的
- 改动成本阶梯:XML 改错掉一通电话,C 模块改错掉整台机器
- 怎么在 AI 编程里真正用上这把刀:先要方案再要代码、提示里先划层、做减法 review
- 三段可以直接抄走的 Prompt:方案阶段数实体、代码阶段做减法、规则文件里常驻约束
- 剃刀也会砍过头:重点在“如无必要”的“必要”,不在“勿增”
给后端服务加一个小需求:按主叫号码的前缀,走不同的出局线路。交给 AI,十几秒后它交回来一个完整工程——一个新的 C 模块、一套配置 schema、模块注册入口、单元测试骨架,外加一份 README。看着挺专业,挑不出毛病。
而这件事,在现有的路由脚本里加三行就完事了。
AI 写代码不怕麻烦,这是它最大的优点。它永远愿意多补一个边界条件、多写一段错误处理、多加一层参数校验——这些是人类最懒得写的部分。问题在于,它不怕麻烦,是因为麻烦不归它承受。半夜两点告警响的时候,它在睡觉。
所以得有人替它拿剃刀。奥卡姆剃刀那句话流传了七百年:如无必要,勿增实体。落到 AI 编程上,关键在“实体”两个字——你要砍的不是代码行数,是三年后还得有人维护的那堆东西。
AI 的啰嗦不是毛病,是默认设定
这跟模型好坏没多大关系,换一个照样啰嗦。原因是结构性的。
多写的边际成本,对它是零。 你多敲一行要三十秒,它多生成一行要三十毫秒。两边这笔账的算法不一样,做法自然不一样。
需求边界不清时,覆盖所有分支是它的最优策略。 你说“按前缀路由”,它不知道你有几条线路、要不要兜底、号码不合法算谁的。它猜不准,就把能想到的都实现一遍——反正多写不扣分,漏写要挨骂。
它优化的目标跟你不是一个。 AI 优化的是“这次回答看起来对不对、全不全”;你优化的是“这套系统两年后还能不能改”。这两个目标在一次对话里高度重合,放到一两年的尺度上经常相反。一个带注释、带测试、带文档的新模块,在单次回答里是满分答案;在你的代码库里是一笔新增的长期负债。
所以这事不是模型的道德问题,是分工问题。它负责不怕麻烦,你负责判断哪些麻烦值得。 这把剃刀只能你来拿。
剃刀砍的是实体,不是字数
这条原则流传最广的拉丁表述是 Entia non sunt multiplicanda praeter necessitatem——不应在必要之外增加实体(这句话是后人替奥卡姆总结的,他本人的著作里并没有这个原句)。注意它说的是实体(entia),不是文字。
工程里的“实体”,就是今后要有人维护的东西。这五类实体,新增时看着差不多,价钱差出一个数量级:
| 实体 | 新增成本 | 长期成本 | 谁来还这笔债 |
|---|---|---|---|
| 一个函数 | 几分钟 | 低,影响范围就在这个文件 | 下次读这段代码的人 |
| 一个配置项 | 几分钟 | 中,要写文档、给默认值、永远向后兼容 | 每个改配置的运维 |
| 一层抽象(接口、基类、插件机制) | 半天 | 高,所有人改任何东西前都得先理解它 | 每个新来的人 |
| 一个第三方依赖 | 一行 | 高,CVE、版本升级、许可证、供应链 | 安全和值班的人 |
| 一个新模块 / 进程 / 服务 | 一两天 | 最高,部署、监控、告警、容量、灰度、回滚全都要配齐 | 整个团队,一直 |
代码行是最便宜的实体,服务是最贵的,中间差着好几个数量级。
而在 AI 那边,这五样几乎等价——都是“输出一些文本”。生成一个函数和生成一个新服务,对它来说就是 token 多少的区别。它感受不到这张表。
所以你看 AI 给的方案,别先看代码质量,先数实体:这个方案让我多出几样今后要照顾的东西?
后端服务自带一把剃刀,别把它扔了
FreeSWITCH、OpenResty、OpenSIPS 这几个东西看起来八竿子打不着——一个是软交换,一个是 Web 网关,一个是 SIP 代理——但它们的骨架长得一模一样:
一个用 C 编译出来的常驻核心,外面裹着一层可以热更新的脚本 / 配置层。
- FreeSWITCH:C 核心 + dialplan XML(用 XML 描述“这通电话该怎么处理”)+ mod_lua(在通话流程里插 Lua 脚本)
- OpenResty:nginx 核心 + Lua(在请求处理的各个阶段插 Lua 代码)
- OpenSIPS:C 核心 + 它自己的路由脚本(一套专门描述 SIP 消息怎么走的 DSL)
打个比方:引擎是编译好的,策略是解释执行的。 引擎管“怎么把一通电话接起来、怎么把一个请求转出去”,这部分不常变、性能敏感;策略管“这通电话该走哪条线”,这部分天天变、跟业务绑死。
这条分界线不是偶然,是这些项目的作者早就替你划好的。变化快的东西放脚本层,稳定的东西放 C 层。 你每次让 AI 往 C 层加东西,都是在拆人家的设计。
这条线值不值得守,看出事时的代价就知道了:
| 层 | 典型改动 | 怎么生效 | 怎么回滚 | 改错了炸多大 |
|---|---|---|---|---|
| 配置 / XML | dialplan 里加一个 condition、路由表加一条记录 | reloadxml 之类的热加载,秒级 |
改回去再 reload | 一通电话走错路;进程不受影响 |
| 脚本 | Lua 脚本、OpenSIPS 路由脚本 | 热加载或 reload,秒到分钟 | git revert + reload |
走到这段脚本的请求报错;其他会话照常 |
| C 模块 | 自己写的 mod_xxx |
编译、发包、重启进程 | 回滚镜像、再重启一次 | 一个段错误把整个进程带走,所有在线会话一起掉 |
| 改核心 / fork 上游 | patch 主干代码 | 重编译整个服务 | 几乎回不去,分支从此永久分叉 | 全部,而且你从此要自己维护跟上游的 merge |
一句话:脚本层的错误是局部的、可回滚的、半夜能救的;C 层的错误是全局的、要发版的、半夜救不了的。
所以对 AI 的默认约束应该是:先问它能不能在脚本层做完。 它张口就来的“我们写一个新模块”,十有八九能用五十行 Lua 换掉。宁可多写几行 XML、多写一个 Lua 函数、多写几条路由规则——哪怕脚本层稍微笨一点、重复一点,也比凭空多出一个要编译、要发版、能把进程搞崩的 C 模块划算。
什么时候该写到 C 层去? 剃刀不是“永远选最简单的”,是“没有理由就选最简单的”。这几种情况理由是成立的:
- 每个媒体包都要过一遍的路径——编解码、抖动缓冲、SRTP 加解密。每 10 或 20 毫秒调用一次的热路径,脚本层扛不住。
- 需要访问脚本层根本拿不到的内部状态——没有 API 暴露出来,只能在 C 里动。
- 要跟一个只有 C 接口的第三方库打交道。
- 脚本层已经写成了一团乱麻。注意这一条的结论不是“继续堆脚本”,而是分层边界当初就划错了,该重划,不是该加班。
说得出上面任意一条,就写 C 模块;说不出,就按便宜的那层做。
怎么在 AI 编程里真正用上这把刀
剃刀的道理谁都懂,难的是在你跟 AI 一来一回的过程中真的用上。
-
先要方案,再要代码。 让它给两三个方案,并且每个方案都明确列出“新增了哪些实体”:新文件、新配置项、新依赖、新进程。这张清单一摆出来,很多方案自己就淘汰了——它自己写出“新增一个常驻进程”的时候,你和它都会犹豫一下。
-
在提示里先划层。 “只改 dialplan,不要新增文件”“在已有的路由脚本里改,不许加模块”“不引入任何新依赖”。层级约束说死,它就不会往外扩。这类约束应该写进项目的规则文件(
AGENTS.md、CLAUDE.md、.cursorrules之类),而不是每次对话重复一遍——重复的约束迟早有一次会忘。 -
先问一句“不加东西行不行”。 现有机制能不能覆盖?配置项能不能复用?已有的 hook 点够不够?这一问经常直接省掉整个方案。顺序很重要:先问能不能不加,再问加什么。
-
做减法 review,不做加法 review。 拿到 AI 的代码,别只检查“对不对”,要逐项问“删掉会怎样”。删了不出事的,就是废话。AI 最爱生成的三类废话:复述函数名的注释、永远进不去的防御分支、只有一个实现的接口。
-
一次 diff 只做一件事。 AI 很乐意“顺手”帮你改格式、改命名、补日志。顺手改的东西,review 时是噪音,出事时是干扰项——你盯着那三行真改动,旁边铺着两百行重排。
-
让它为每一个新增实体给出理由。 挨个问“这个为什么必要”。它通常会很爽快地承认“这个确实可以去掉”。它当初加上去,不是因为需要,是因为没人拦。
把“新增实体”变成一件需要申请的事:默认拒绝,给得出理由才批准。 对函数可以宽松,对服务必须严苛——价钱不一样,审批标准就不该一样。
三段可以直接抄走的 Prompt
上面那六条,落到实处就是三段话。抄走改改就能用。
一、动手之前:让它给方案,并且数清实体
在写任何代码之前,先给我方案,不要给实现。
需求:<写在这里>
现有代码 / 架构:<贴上相关文件、模块或一段架构说明>
要求:
1. 给 2-3 个方案,按“新增实体的总成本”从低到高排列。
2. 每个方案都要附一张实体清单,逐项写明:
- 新增或修改了哪些文件
- 新增了哪些配置项(含默认值、对老配置的兼容影响)
- 新增了哪些抽象层(接口、基类、插件机制)
- 新增了哪些第三方依赖
- 新增了哪些进程 / 服务 / 部署单元
没有就写“无”,不许省略这一项。
3. 必须包含一个“最小改动方案”:只用现有机制,不新增任何实体。
如果确实做不到,明确指出是哪一个现有机制不够用、缺在哪里。
4. 每个新增实体后面写一句话理由。理由只接受这四种:
性能热路径 / 现有机制表达不了 / 外部契约强制要求 / 安全隔离需要。
这四条都说不出,就把这个实体从方案里去掉。
5. 最后给你的推荐,并用一句话说清:为什么不选更便宜的那个方案。
先别写代码,等我选。
二、拿到代码之后:做减法 review
对下面的代码做“减法 review”。不要评价写得好不好,只回答哪些东西可以删。
<贴代码或 diff>
逐项检查这些:
- 只有一个实现的接口、基类、工厂、策略类
- 只被调用一次、纯转发的中间层函数
- 永远进不去的防御分支、不可能为真的判断
- 复述函数名或参数名的注释
- 没有调用方的配置项、常量、开关
- 为“将来可能需要”预留的扩展点、字段、钩子
- 两处在做同一件事的代码
- 新增的依赖:标准库或已有依赖能不能替代
输出一张表:条目 | 类型 | 删掉之后会发生什么 | 建议(删 / 留 / 合并)
规则:
- 判断标准只有一条:删掉之后行为会不会变。不变就是可删。
- “为了以后好扩展”不算理由。说得出现在就存在的第二个调用方才算。
- 每个建议“留”的条目,用一句话说清它当下解决的真实问题。
- 不要顺手改格式、改命名、补日志。这一轮只给删除建议。
最后给个数:建议删掉多少行,占总行数的百分之多少。
三、写进项目规则文件,省得每次重复
放进 AGENTS.md / CLAUDE.md / .cursorrules,让它成为默认前提而不是每轮对话的叮嘱:
## 奥卡姆剃刀约束
- 默认不新增文件、配置项、抽象层、第三方依赖、进程。要加,先在回复里说明理由。
- 能在配置层或脚本层做完的,就不要写到需要编译和重启的那一层去。
- 两次重复是巧合,三次才抽象。只有一个实现时不要定义接口。
- 一次改动只做一件事;不顺手改格式、命名和日志。
第三段最管用:前两段得你每次记得用,第三段你忘了它也在。
剃刀也会砍过头
把五件不相干的事塞进同一个 Lua 文件,用一个万能配置项开关五种行为,所有逻辑挤进一个两千行的 dialplan——这些“看起来实体很少”的做法,实际上是把耦合藏起来了。表面上剃刀挥得很勤,债一样要还,只是还的时候更难受。
奥卡姆那句话的重点在“如无必要”的“必要”二字上,不在“勿增”。
判断标准只有一条:这个实体,对应现实里一个真实存在的边界吗?
- 两条 SIP trunk 本来就是两条物理链路、两套鉴权、两家运营商——那它们理应是两份配置。硬合成一份“通用配置 + 一堆 if”,反而是在跟现实较劲。
- 两段“看起来有点像”的处理流程,在第三段出现之前,不值得为它们抽一个公共基类。两次重复是巧合,三次才是模式。
一条土办法:实体的数量,应该跟你能数清的现实概念数量对得上。 多出来的那些是过度设计,少掉的那些是耦合。
总结:砍的不是代码,是三年后的维护工作量
AI 把“不怕麻烦”这件事做到了极致。它不会偷懒,不会漏写边界条件,不会跳过错误处理——这些是真便宜,该用。它唯一做不到的是替你判断:这个麻烦,值不值得你今后一直养着。
下次让 AI 动后端服务之前,过一遍这四问:
- 不加任何新东西,现有机制能不能做完?
- 这个方案新增了几样实体?分别是哪一类——函数、配置、抽象、依赖,还是进程?
- 能不能放在脚本层(XML / Lua / 路由脚本)?说不出非写进 C 层不可的理由,就别写。
- 这几样新增的东西,各自对应现实里的哪一个边界?对不上的,删。
《道德经》里有句“为学日益,为道日损”——求学问是天天往上加,悟道理是天天往下减。AI 已经把“日益”这半句做到头了;“日损”那半句,还得你自己来。
全文思维导图
@startmindmap
<style>
mindmapDiagram {
node {
BackgroundColor #F8F9FA
RoundCorner 10
Padding 10
FontSize 13
}
:depth(0) {
BackgroundColor #1E3A5F
FontColor white
FontSize 18
FontStyle bold
}
:depth(1) {
FontSize 15
FontStyle bold
}
:depth(2) {
FontSize 13
}
}
</style>
* AI 编程的奥卡姆剃刀
** 为什么 AI 天然啰嗦
*** 多写的边际成本对它为零
*** 边界不清时覆盖所有分支最优
*** 它优化单次回答,你优化长期维护
** 剃刀砍的是实体
*** 函数:便宜,影响局部
*** 配置项:要文档、要兼容
*** 抽象层:人人都得先理解它
*** 依赖:CVE、升级、供应链
*** 服务:部署监控告警全套,最贵
** 后端自带的分界线
*** C 核心 + 热更新脚本层
*** XML 改错:掉一通电话
*** 脚本改错:局部报错,可回滚
*** C 模块改错:整个进程一起掉
*** 该写 C 层的四个正当理由
** 实操
*** 先要方案,再要代码
*** 提示里先划层,约束写进规则文件
*** 先问“不加东西行不行”
*** 做减法 review:删了会怎样
*** 一次 diff 只做一件事
** 三段现成 Prompt
*** 方案阶段:列实体清单 + 最小改动方案
*** 代码阶段:减法 review,只问能删什么
*** 规则文件:常驻约束,忘了也生效
** 别砍过头
*** 重点在“必要”,不在“勿增”
*** 实体数对齐现实概念数
*** 两次是巧合,三次才是模式
@endmindmap

本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。 欢迎在我的个人网站 https://www.fanyamin.com 访问原文并评论。