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 一来一回的过程中真的用上。

  1. 先要方案,再要代码。 让它给两三个方案,并且每个方案都明确列出“新增了哪些实体”:新文件、新配置项、新依赖、新进程。这张清单一摆出来,很多方案自己就淘汰了——它自己写出“新增一个常驻进程”的时候,你和它都会犹豫一下。

  2. 在提示里先划层。 “只改 dialplan,不要新增文件”“在已有的路由脚本里改,不许加模块”“不引入任何新依赖”。层级约束说死,它就不会往外扩。这类约束应该写进项目的规则文件(AGENTS.md、CLAUDE.md、.cursorrules 之类),而不是每次对话重复一遍——重复的约束迟早有一次会忘。

  3. 先问一句“不加东西行不行”。 现有机制能不能覆盖?配置项能不能复用?已有的 hook 点够不够?这一问经常直接省掉整个方案。顺序很重要:先问能不能不加,再问加什么。

  4. 做减法 review,不做加法 review。 拿到 AI 的代码,别只检查“对不对”,要逐项问“删掉会怎样”。删了不出事的,就是废话。AI 最爱生成的三类废话:复述函数名的注释、永远进不去的防御分支、只有一个实现的接口。

  5. 一次 diff 只做一件事。 AI 很乐意“顺手”帮你改格式、改命名、补日志。顺手改的东西,review 时是噪音,出事时是干扰项——你盯着那三行真改动,旁边铺着两百行重排。

  6. 让它为每一个新增实体给出理由。 挨个问“这个为什么必要”。它通常会很爽快地承认“这个确实可以去掉”。它当初加上去,不是因为需要,是因为没人拦。

把“新增实体”变成一件需要申请的事:默认拒绝,给得出理由才批准。 对函数可以宽松,对服务必须严苛——价钱不一样,审批标准就不该一样。


三段可以直接抄走的 Prompt

上面那六条,落到实处就是三段话。抄走改改就能用。

一、动手之前:让它给方案,并且数清实体

在写任何代码之前,先给我方案,不要给实现。

需求:<写在这里>
现有代码 / 架构:<贴上相关文件、模块或一段架构说明>

要求:
1. 给 2-3 个方案,按“新增实体的总成本”从低到高排列。
2. 每个方案都要附一张实体清单,逐项写明:
   - 新增或修改了哪些文件
   - 新增了哪些配置项(含默认值、对老配置的兼容影响)
   - 新增了哪些抽象层(接口、基类、插件机制)
   - 新增了哪些第三方依赖
   - 新增了哪些进程 / 服务 / 部署单元
   没有就写“无”,不许省略这一项。
3. 必须包含一个“最小改动方案”:只用现有机制,不新增任何实体。
   如果确实做不到,明确指出是哪一个现有机制不够用、缺在哪里。
4. 每个新增实体后面写一句话理由。理由只接受这四种:
   性能热路径 / 现有机制表达不了 / 外部契约强制要求 / 安全隔离需要。
   这四条都说不出,就把这个实体从方案里去掉。
5. 最后给你的推荐,并用一句话说清:为什么不选更便宜的那个方案。

先别写代码,等我选。

二、拿到代码之后:做减法 review

对下面的代码做“减法 review”。不要评价写得好不好,只回答哪些东西可以删。

<贴代码或 diff>

逐项检查这些:
- 只有一个实现的接口、基类、工厂、策略类
- 只被调用一次、纯转发的中间层函数
- 永远进不去的防御分支、不可能为真的判断
- 复述函数名或参数名的注释
- 没有调用方的配置项、常量、开关
- 为“将来可能需要”预留的扩展点、字段、钩子
- 两处在做同一件事的代码
- 新增的依赖:标准库或已有依赖能不能替代

输出一张表:条目 | 类型 | 删掉之后会发生什么 | 建议(删 / 留 / 合并)

规则:
- 判断标准只有一条:删掉之后行为会不会变。不变就是可删。
- “为了以后好扩展”不算理由。说得出现在就存在的第二个调用方才算。
- 每个建议“留”的条目,用一句话说清它当下解决的真实问题。
- 不要顺手改格式、改命名、补日志。这一轮只给删除建议。

最后给个数:建议删掉多少行,占总行数的百分之多少。

三、写进项目规则文件,省得每次重复

放进 AGENTS.md / CLAUDE.md / .cursorrules,让它成为默认前提而不是每轮对话的叮嘱:

## 奥卡姆剃刀约束
- 默认不新增文件、配置项、抽象层、第三方依赖、进程。要加,先在回复里说明理由。
- 能在配置层或脚本层做完的,就不要写到需要编译和重启的那一层去。
- 两次重复是巧合,三次才抽象。只有一个实现时不要定义接口。
- 一次改动只做一件事;不顺手改格式、命名和日志。

第三段最管用:前两段得你每次记得用,第三段你忘了它也在。


剃刀也会砍过头

把五件不相干的事塞进同一个 Lua 文件,用一个万能配置项开关五种行为,所有逻辑挤进一个两千行的 dialplan——这些“看起来实体很少”的做法,实际上是把耦合藏起来了。表面上剃刀挥得很勤,债一样要还,只是还的时候更难受。

奥卡姆那句话的重点在“如无必要”的“必要”二字上,不在“勿增”。

判断标准只有一条:这个实体,对应现实里一个真实存在的边界吗?

  • 两条 SIP trunk 本来就是两条物理链路、两套鉴权、两家运营商——那它们理应是两份配置。硬合成一份“通用配置 + 一堆 if”,反而是在跟现实较劲。
  • 两段“看起来有点像”的处理流程,在第三段出现之前,不值得为它们抽一个公共基类。两次重复是巧合,三次才是模式。

一条土办法:实体的数量,应该跟你能数清的现实概念数量对得上。 多出来的那些是过度设计,少掉的那些是耦合。


总结:砍的不是代码,是三年后的维护工作量

AI 把“不怕麻烦”这件事做到了极致。它不会偷懒,不会漏写边界条件,不会跳过错误处理——这些是真便宜,该用。它唯一做不到的是替你判断:这个麻烦,值不值得你今后一直养着。

下次让 AI 动后端服务之前,过一遍这四问:

  1. 不加任何新东西,现有机制能不能做完?
  2. 这个方案新增了几样实体?分别是哪一类——函数、配置、抽象、依赖,还是进程?
  3. 能不能放在脚本层(XML / Lua / 路由脚本)?说不出非写进 C 层不可的理由,就别写。
  4. 这几样新增的东西,各自对应现实里的哪一个边界?对不上的,删。

《道德经》里有句“为学日益,为道日损”——求学问是天天往上加,悟道理是天天往下减。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

AI 编程的奥卡姆剃刀原则 - 思维导图


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

相关文章