本文由 莫潇羽@无忆研习社(www.wuyi.wiki)撰写,转载请注明出处。
快速摘要
AI 没有让代码变便宜,反而让"烂代码"变成了你能背的最贵的债。 我自己用 AI 写了将近两年的项目,趟过 vibe coding 的坑,也试过把规格文档当源代码的"specs-to-code"路线,结果都不漂亮。真正让我把 AI 用顺的,是回到一组几十年前就被反复验证过的工程基础:让 AI 先反过来拷问你的方案、给项目建一份统一语言的术语表、用 TDD 控制每一步的步幅、把代码组织成"接口简单、内部丰满"的深模块。这四件事背后是同一个目标——管理认知负荷,让你这个"战略家"始终拽得住 AI 这个"战术执行者"。本文把这四根支柱拆给你看,附上我自己日常在用的开源 Skill 仓库、流程清单和实操样例,所有方法都可以今天就装进 Claude Code、Codex 或者你手头任何一个编码 Agent。想看完整拆解,往下翻。
一、引子:AI 写得快,不等于做得好
我从 2024 年底开始用 AI 编码 Agent 写真实项目,到现在每天有一多半的代码是和 Agent 一起完成的。一个直观的感受是:写代码的速度被拉得非常夸张,过去要花一上午的事,现在二十分钟就能拿到第一个能跑的版本。
但越往后越发现一件事——速度上来了,质量并没有跟着上来。
我自己有过这样几次很真实的体验。一开始尝过甜头:丢给 Agent 一句"帮我做一个能上传 CSV、自动统计字段类型、生成图表的内部小工具",二十几分钟就能跑起来。第二次同样思路,我让 Agent 在已有项目里加一个新模块,结果它写出来的东西看起来对,跑起来也对,但翻一翻代码——满地都是重复实现、命名各说各话、依赖关系绕得像麻花。等到第三次再让它在这堆代码上加东西,它自己都已经看不懂了,给我返回的修改方案里全是"我建议重写这个文件"。
这就是行业里现在反复在讨论的那个问题:AI 让代码的"产能"暴涨,但代码本身从来没有变便宜过。代码会腐烂、会积累技术债、会拖累后面的每一次迭代,这件事和谁写的没有关系,人写的会烂,AI 写的也一样会烂。
我后来认真去翻了一些资料,发现已经有不少资深开发者在公开场合反复强调这件事——工程基础在 AI 时代不是变得没用了,而是变得更重要了。最让我有共鸣的一种说法是:糟糕的代码库,是现在所有想用 AI 提效的团队头上最贵的那笔账。因为代码乱了之后,AI 也跟着发挥不出来,你越想用它救场,它就越加速整个系统的崩坏。
这篇文章我想把这件事说清楚:
- 为什么 vibe coding 和 specs-to-code 都不是出路;
- 哪些"老掉牙"的工程方法,恰好是 AI 时代你最该拿出来重新用的;
- 怎么把这些方法落到日常开发里,配上我自己每天都在用的几套开源 Skill。
如果你也用 AI Agent 写过几个月的真实项目,肯定多多少少都被它"过度产出"的毛病坑过。这套思路,至少能帮你把方向盘抢回来。
二、两条死胡同:Vibe Coding 与 Specs-to-Code
要讲清楚什么叫"工程基础回归",得先讲清楚我们正在告别的是什么。我自己观察下来,眼下用 AI 编码的开发者大致都掉进过下面两个坑里的一个。
2.1 第一种极端:Vibe Coding(凭感觉投喂)
Vibe Coding 这个词在 2025 年前后火起来的,意思也很直白:你不用太管细节,凭感觉把需求说一句,让 AI 去写,写完跑一下,跑不通就让它再改一改。整个过程是"感觉驱动"的——你不读它写的代码,不知道它为什么这么写,只关心结果能不能跑。
这种方式在玩具项目、一次性脚本、demo 演示里特别香。我自己也用过:临时想画一个数据可视化,半个小时之内能从想法到上线截图,效率高到不像话。
但只要你想把这种方式推到稍微正式一点的项目里,问题立刻冒出来:
- 出了 bug 你根本不知道从哪查;
- 改一个需求会牵动你完全没意识到的其他模块;
- 项目大到一定程度,你和 AI 都看不懂这个项目在做什么了。
vibe coding 的本质是把"理解"从工作里删除。短时间看起来轻松,长时间下来你会失去整个项目的把控。
2.2 第二种极端:Specs-to-Code(把规格当源码)
这一派的思路在 2025 年也很流行,大意是:既然 AI 写代码已经接近免费,那我们就只维护高层规格文档,代码完全由 AI 根据规格生成。如果系统有问题,那就改规格、重新生成,不要碰生成出来的代码。
这套打法的潜在假设是——代码是可以一次性丢弃的产物,规格才是有价值的资产。
听起来很美好。但只要你真去试一遍,就会发现一个反直觉的事实:反复让 AI 根据规格重新生成的代码,质量是在下降的,而不是上升的。第一遍可能还行,第二遍开始打补丁,第三遍就成了一锅粥。我自己在一个内部知识库项目上认认真真试过两个月这套思路,最后得出的结论是——它低估了"代码本身的演化连贯性"这件事。
这背后其实是软件工程里一个老到不能再老的规律:软件熵增。每一次"忽略已有结构、重新生成",都是在向无序方向多走一步。第三章我会专门展开这一段。
2.3 为什么两个极端都站不住脚
你可以发现,Vibe Coding 和 Specs-to-Code 看起来一个粗一个细,但它们其实有一个共同点:都试图把"思考"这件事从开发者身上彻底卸掉。
- vibe coding 把思考交给"感觉";
- specs-to-code 把思考压缩进一份"规格",假装代码不存在。
但软件本质上就是一个长期演化的复杂系统,它需要持续的设计判断、命名判断、边界判断。这些判断不可能完全压缩进任何一份静态文档里,也不可能靠运气堆出来。
|
路线 |
假设 |
致命缺陷 |
适用场景 |
|
Vibe Coding |
"感觉对就行" |
失控、不可调试、不可演化 |
一次性脚本、demo |
|
Specs-to-Code |
"代码可丢弃" |
反复生成会熵增、规格本身也会失真 |
极小项目原型 |
|
工程基础回归 |
"AI 是执行者,人是设计者" |
需要持续投入设计能力 |
真实生产项目 |
2.4 真正可行的路线
我现在自己的定位是这样的——我是战略家,AI 是战术执行者。
我不指挥 AI 怎么打字,但我决定要打什么、打到哪、打的边界在哪、出问题怎么知道。AI 的输出由我来约束、由测试来验证、由架构来收口。
这条路线没什么新东西,它本质上就是回到几十年前已经被打磨成熟的那批工程方法论,只是这一次的搭档从同事变成了 AI 而已。
我自己花了不少时间才彻底搞清楚这件事。一开始我也偏爱 Vibe Coding,毕竟当时确实快。后来在一个稍大的项目上吃了苦头——一个用户反馈"我点提交按钮没反应"的 bug,我和 AI 一起花了将近两天才定位到。原因是早期我让 AI 写表单提交逻辑的时候没说清楚"提交失败要保留表单数据",它默认了"提交失败就清空",而清空逻辑又被埋在一个名字毫不相关的工具函数里。两天的排查里,我前后让 AI 改了 11 次代码,每次它都"很自信"地告诉我修好了,但实际上每一次都引入了新的小问题。
这件事之后我开始反思——我和 AI 是不是从一开始就没在讨论同一个问题?答案是肯定的。后面这些章节里讲的每一个方法,本质上都是在回答这个问题。
接下来四章,就是我自己最常用的四根支柱。
三、把"软件熵增"摆回桌面
讲四根支柱之前,先把那个最容易被忽略的底层概念讲清楚——软件熵增。
3.1 什么是软件熵增
熵增最早是物理里的概念,搬到软件工程里大概意思是:一个软件系统在每一次修改之后,如果你不主动投入额外的设计成本,它的有序度都会下降。
你可以把它想象成一个房间。刚搬进去的时候东西摆得整整齐齐,每过一周做一点小修小补,每次你都觉得"放哪儿都差不多",半年后你会发现这房间已经完全没法住了——但你没法指出某一次具体的"坏决策",是无数个小决策的累加把它毁了。
代码也是一样。每一次"我先把它塞这里,以后再重构",每一次"先注释掉这个测试,等下再写",每一次"这个变量名我先这么叫吧",都是在向熵增方向多走一步。
3.2 AI 编码并没有破解熵增
AI 让"修改一次"的成本变得极低,听起来好像有助于反熵增——因为重构变便宜了嘛。但真实情况恰恰相反:修改成本越低,人就越倾向于做局部修改,而不去做整体设计。
我自己有过特别典型的体验:因为让 AI 改一个文件只要 30 秒,所以我会一天之内对同一个文件做十几次小改动,每一次都看起来"挺合理",但十几次之后这个文件就不像我熟悉的文件了。最终我不得不停下来,让 AI 给我画一张依赖图,才搞明白它已经被改成了什么样子。
AI 没有破解熵增,它只是加快了熵增的速度。
3.3 反复重生成的代价是隐性的
Specs-to-Code 那一派的核心问题也在这里——反复重新生成这件事本身就是一种极端形式的熵增加速器。每一次生成,AI 都只看到当下的上下文,看不到这个系统过去为什么这么演化、上一版为什么做了某个权衡。它会按当下最容易的方式来组织代码,于是每生成一次,结构就被"打散"一次。
这个代价不在生成的那一刻显形。它会潜伏几个迭代周期,在你试图加一个稍微复杂的新功能时,才以"AI 完全没办法接住这个新需求"的形式爆出来。
所以真正能让 AI 持续帮你写代码的前提,是你愿意每天投入一部分精力,去对抗这个系统的熵增。这就需要工程基础里那些"看起来很慢、其实长期最快"的方法。
下面正式进入四根支柱。
四、第一根支柱:先让 AI 把你拷问到底(Grill 你的方案)
4.1 沟通这件事的天然漏洞
软件工程里有一个观察特别准——最常见的失败模式不是技术不行,而是"对齐失败"。你以为开发者懂你想要什么,他写完了你才发现,他理解的根本不是那回事。
这件事在你和 AI 协作的时候,被放大了不止一倍。因为 AI 没有你和它共享的隐性上下文:它不知道你这家公司有什么惯例、不知道你这个项目过去做过什么权衡、不知道你嘴里那个"用户"到底是注册用户还是访客。
我刚开始用 AI 写代码的那阵子,最常出现的剧情是这样的:
- 我描述一个功能;
- AI 立刻给我一份方案;
- 我说"看起来还行";
- 它开始写;
- 写到一半我看出问题,但已经写了 300 行;
- 我让它改,它改完又出了新问题;
- 来回三轮之后,我比自己写还累。
问题出在哪?出在"我描述一个功能"那一步,我没说清楚,它也没追问。
4.2 设计决策树
我后来读到一个概念,叫"设计决策树"。意思是任何一个设计任务都不是一个孤立的决策,而是一棵嵌套的树:
做一个搜索页
├─ 用高级搜索界面 还是 简单文本框?
│ ├─ 选高级搜索
│ │ ├─ 需要哪些筛选条件?
│ │ │ ├─ 时间范围
│ │ │ ├─ 分类
│ │ │ └─ 状态
│ │ └─ 默认排序方式?
│ │ ├─ 按相关性
│ │ └─ 按时间倒序
│ └─ 选简单文本框
│ ├─ 是否支持模糊搜索?
│ └─ 是否需要历史记录?
└─ 后端怎么实现?
├─ 数据库 LIKE
├─ 全文索引
└─ 走搜索引擎
每一个分支都是一个具体的决策点,每一个决策又会牵出新的分支。你脑子里可能模糊地有"我要做搜索"这个想法,但这棵树有 80% 没有被展开。没展开的部分,AI 都会替你做主——而它做主的方式,未必是你想要的。
4.3 Grill 思路的精髓:让 AI 反过来追问你
我自己现在的做法是:在 AI 动手写任何代码之前,先让它把这棵决策树的每一个分支都问我一遍。
这件事的关键是——问的人是 AI,答的人是我。
不是我去给 AI 写一份完美的需求文档,而是我把一个粗糙的想法丢给它,让它像一个挑剔的资深工程师一样不停追问。每问一个问题,它会给出它的推荐答案;我可以直接说"按你推荐的来",也可以否决并给出我的理由。问到我们俩"对齐"为止,再开始写代码。
这种打法在 AI Coding 圈子里有一个很贴切的名字——Grilling Session,烧烤式拷问。
它的好处特别明显:
- 把所有隐性假设都摆到台面上;
- 让 AI 的内部模型和你的脑内模型对齐;
- 写出来的代码不需要回炉重做。
4.4 一个可直接用的中文版 Grill 提示词
下面是我自己整理的一个 Grill 提示词,可以直接复制到你的 AI 编程助手里:
请把自己当作一个挑剔但善意的资深工程师,对我下面这个想法发起一次彻底的拷问。
要求:
1. 一次只问一个问题,问完等我回答再问下一个。
2. 每个问题都附上你自己的推荐答案以及推荐的理由。
3. 优先从"模糊点、边界条件、依赖关系、数据模型、错误处理、可观测性"这几个方向追问。
4. 如果某个问题可以通过查看代码库本身回答,直接帮我查代码库,不要问我。
5. 当你觉得这套方案的所有关键决策都已经对齐之后,告诉我"已经达成共识",并给出一段总结。
我的想法是:
<这里写你的需求>
我用这套提示词,最少的一次被问了 12 个问题,最多的一次问了 50 多个,整个 Grilling 过程持续了快一个小时。但写代码的时候 AI 几乎没返工。
这里有一个细节值得展开说——让 AI 给推荐答案这件事,看起来不起眼,实际上节省了大量时间。如果 AI 只是问问题不给建议,每个问题都需要你从零开始思考;但如果它先给你一个"我会这么选,理由是 xxx"的推荐答案,你大部分时候只需要点头说"按你说的来",遇到不同意的地方再展开讨论就行。这一个小改动让整个 Grill 过程的效率至少提升了一倍。
我也试过把这种思路用到非编码场景。比如我在筹划一篇新文章的时候,会先让 AI 用 Grill 的方式问我:"你这篇文章的目标读者是谁?是已经懂 AI Coding 的工程师,还是完全没接触过 Agent 的开发者?两者的笔触会有明显差别——你倾向哪个?我的推荐是前者,因为你的过往内容都偏中高级。"这种问法把我脑子里很多模糊的预设都逼到了台面上,比我自己冥思苦想要快得多。
4.5 把对齐结果沉淀下来
光问还不够,问完之后这些对齐结果会随对话窗口消失。我的做法是在项目根目录建一个 CONTEXT.md 文件,把每次 Grill 出来的关键决策、术语定义、约束条件全部记进去,下次新开对话直接把这个文件喂给 AI。
我会把 CONTEXT.md 写成下面这个结构:
# 项目上下文
## 术语表
- 读者(Reader): 在系统里完成手机号注册并通过实名认证的自然人
- 借阅(Loan): 一个读者对一本书在一段时间内的占用状态
## 关键约束
- 单个读者同时持有的借阅数 ≤ 5
- 逾期未还自动冻结新借阅
## 已经达成共识的设计决策
- 搜索走 Postgres 全文索引,不引入 Elasticsearch
- 用户身份用一张 users 表统一管理,不做读写分离
这个文件就是我和 AI 共享的"项目记忆"。我每开一个新会话,都会把它带上。
莫潇羽@无忆研习社 写这种文档已经成了一种肌肉记忆——Grill 一次,落档一次,这是把 AI 协作从"凭运气"升级到"凭流程"的关键一步。
五、第二根支柱:统一语言(让人、代码、AI 说同一种话)
5.1 这个概念来自哪里
"统一语言"这个说法,最早是 Eric Evans 在 2003 年那本《领域驱动设计》里提出来的。原话的意思是——团队所有人和代码本身,都应该使用同一套来自业务领域的词汇。
听起来理所当然,但你回头看看你自己手头的项目,大概率是这样的:
- 产品文档里管它叫"用户";
- 数据库表里叫
customer; - 后端代码里叫
User; - 前端组件里叫
account; - 客服那边管它叫"会员"。
五个词指的是同一个东西,但谁也说不清这五个词之间到底有没有区别。等到你想加一个"VIP 会员升级"的功能,所有人都开始打架——因为大家其实没有在讨论同一个概念。
5.2 在 AI 协作里这件事被放大
在和 AI 协作的时候,这个问题至少被放大了三倍。
第一,AI 没有你团队的隐性语境,它看不出"客户"和"用户"在你这个项目里到底是不是同一个东西,只能猜。
第二,AI 有一个特别明显的毛病——啰嗦。如果你不告诉它"在我们这里,下单的人叫买家、卖东西的人叫卖家",它就会用"用户 A、用户 B、那个发起交易的实体、与之对应的供给方"这种绕来绕去的方式描述同一件事。每一次它输出这种繁复的描述,都是在消耗你和它的 token 配额,也在加大你阅读的成本。
第三,如果你的术语没有统一,AI 生成的代码会出现命名漂移:今天叫 userId,明天叫 accountId,后天又冒出来一个 memberId,越写越乱。
5.3 一份术语表能换来什么
我现在每开一个新项目,第一件事就是逼自己写一份项目术语表。这份术语表的格式可以非常朴素,一个 Markdown 表格就够:
|
术语 |
英文标识 |
精确定义 |
别名/同义词 |
反义/对比 |
|
读者 |
Reader |
完成实名认证、可发起借阅的自然人 |
用户 (口语) |
访客 (未注册) |
|
借阅 |
Loan |
一次读者对一本书的占用记录,有起止时间 |
借书 |
归还 (Return) |
|
副本 |
Copy |
一本书的物理实例,同一本书可有多个副本 |
- |
书目 (Book Title) |
|
馆员 |
Librarian |
后台运营人员,有管理权限 |
管理员 |
读者 |
这份表的几个细节很关键:
- 每个术语都有英文标识,方便直接用在代码里;
- 写明别名和反义,AI 拿到这份表之后,知道遇到"用户"该自动翻成"读者";
- 优先描述行为/边界,不描述实现,"完成实名认证、可发起借阅的自然人"比"users 表里的一行"更稳定。
5.4 一个亲身经历的小冲突
我自己跑过一个真实的例子。我在一个内容管理项目里,一开始有一个叫 Article(文章)的概念。后来产品想加一个"草稿"功能,问题立刻来了——
- 草稿算不算 Article?
- 一个 Article 在被发布前后,是同一个对象,还是两个不同的对象?
- 数据库里要不要分表?
如果我没有统一语言,这种问题会扔给 AI 让它"自由发挥",它的处理方式有时候是新建一张 Drafts 表,有时候是在 articles 表里加一个 status 字段。两种方案没有谁对谁错,但项目里一旦混着出现,就会变成大灾难。
我的做法是当场停下来,让 AI 帮我把"Article"的定义重新捋一遍,然后写进 CONTEXT.md:
## Article 的精确定义
- Article 指任何"具备标题 + 正文 + 作者"的内容对象
- Article 有状态字段 status: draft | published | archived
- 状态变化通过同一张表的 status 字段管理,不分表
- 一旦 published,Article 拥有不可变的 publishedAt 时间戳
把这段写进去之后,后面所有的对话和代码生成都围绕这个定义转。AI 不再会冒出一个新的 Draft 类,前端也不会出现"草稿和文章是两个独立的东西"的错觉。
5.5 把术语表喂回 AI 后会发生什么
这是我个人觉得最有意思的部分——术语表不是给人看的,是给 AI 看的。
我把 CONTEXT.md 设成一个固定的上下文文件,每次新建对话都会塞进去。带上这份文件之后,AI 的行为有几个明显的变化:
- 输出变短了,因为它不用再绕着圈子描述概念;
- 命名变稳定了,因为它有标准答案可以参考;
- 设计建议变靠谱了,因为它知道项目里哪些边界是硬约束。
这件事很难量化,但用过之后回不去。我现在每开一个新项目都会强制要求自己——先建术语表,再写一行代码。
莫潇羽@无忆研习社 自己有一个总结:术语表是项目的"宪法",它一旦定下来,后面所有的设计、命名、讨论都要服从它。AI 协作时代,这件事不仅没过时,反而是必修课。
六、第三根支柱:TDD 不只是测试,是步幅控制
6.1 在 AI 协作里重新理解 TDD
TDD(测试驱动开发)这个词早就被讲烂了,2000 年前后就在传,到了 2010 年代变成"过时的方法论",被很多人束之高阁。
但有意思的是,在 AI Coding 时代它又被翻出来了——因为它解决了 AI 协作里一个非常具体的问题:步幅失控。
我先讲清楚 TDD 的标准三步法:
flowchart LR
A[Red<br/>写一个会失败的测试] --> B[Green<br/>写最少的代码让它通过]
B --> C[Refactor<br/>不改行为,优化结构]
C --> A
很多人理解的 TDD 是"先写测试再写代码",但在 AI Coding 语境里,TDD 真正在做的事情是控制步幅——每一步小到一个测试,每一步都能立刻验证,每一步都不允许 AI 一口气写出 500 行你没看过的代码。
6.2 AI 最大的毛病:它"太能写"
我用 AI Agent 最常遇到的问题,不是它写不出来,而是它写太多。
你让它"加一个登录功能",它会一口气给你:
- 一个登录表单组件;
- 一个表单验证 hook;
- 一个登录 API;
- 一个 JWT 工具类;
- 一个用户上下文 provider;
- 一个权限路由守卫;
- 一个忘记密码的页面;
- 一份测试套件。
看起来很贴心,但你没法验证它。哪一段是对的、哪一段有 bug、哪一段是它瞎编的,你都没法快速判断。你只能跑一下试试,跑通了就以为它都对了——这就是埋雷。
The Pragmatic Programmer 那本书里有一句话特别贴切,大意是——"你的反馈速率就是你的速度上限"。AI 看起来在加速你,但如果你没办法快速反馈"对不对",那它就是在加速你掉进坑里。
6.3 红绿重构循环是怎么压住 AI 的
我现在用 AI 写代码的标准节奏,是把它困在"红-绿-重构"里:
Step 1(红):先让 AI 写一个失败的测试。这个测试要描述清楚我想要的行为,但还没有任何实现。这一步必须失败,否则就是写错了。
// loan.test.js
import { describe, it, expect } from 'vitest';
import { createLoan } from './loan.js';
describe('createLoan', () => {
it('一个读者最多同时有 5 个借阅', () => {
const reader = { id: 'r1', currentLoans: 5 };
expect(() => createLoan(reader, 'book-6')).toThrow('已达借阅上限');
});
});
Step 2(绿):让 AI 写最少的实现让这个测试通过。注意是"最少"——它不需要顺手帮我把整个借阅系统都写完。
// loan.js
export function createLoan(reader, bookId) {
if (reader.currentLoans >= 5) {
throw new Error('已达借阅上限');
}
return { readerId: reader.id, bookId };
}
Step 3(重构):在测试一直绿的前提下,让 AI 优化结构。比如把上限"5"抽成常量、把异常换成更具体的类型等等。
Step 4:回到 Step 1,写下一个测试。
这个节奏看起来慢,但有几个好处难以替代:
- 每一步都是可验证的;
- AI 永远不会跑出你的"灯光范围";
- 测试就是文档,写完一遍系统就有了一套行为规范;
- 重构变得真的安全,因为有测试网兜底。
6.4 反馈环路:不止测试一种
TDD 是反馈环里最显式的一种,但反馈环本身是一个更宽的概念。我自己在 AI 协作里至少保持下面三种反馈环同时在线:
|
反馈环 |
用途 |
工具样例 |
|
静态类型 |
在写代码的瞬间就能发现类型错配 |
TypeScript、Pyright |
|
自动化测试 |
描述行为正确性 |
Vitest、Jest、Pytest |
|
运行时观测 |
跑起来之后看真实行为 |
浏览器开发者工具、日志、tracing |
少一种,AI 都更容易跑偏。
我自己有一条铁律——没有静态类型的代码,我不让 AI 写超过 100 行。因为没有类型,AI 的"自由发挥空间"太大,它会在变量类型上做出一堆我不知道的假设,到最后一定崩。
6.5 别让 AI 越过你的"灯光范围"
我特别喜欢 The Pragmatic Programmer 里的一个比喻——开车不能开过你大灯能照亮的距离。意思是,每一步都要走在你能验证的范围内。
AI 协作里这条铁律变得更重要。AI 不会自己停下来,它有多少 token 就能写多少代码,你不主动给它划线,它能从一个登录功能写到整个 SaaS。
TDD 就是那条线。每一个测试都是一盏灯,把下一步要走的那一小段路照亮。
莫潇羽@无忆研习社 实测过——用 TDD 节奏写 AI 协作的代码,整体效率反而比"放飞"模式高。因为返工少了,bug 少了,重构敢动手了。这件事不需要你信,亲自试一周就知道。
我顺便提一个心理层面的观察。很多人之所以排斥 TDD,是因为"先写测试"看起来很反直觉——明明还不知道实现长什么样,为什么要先写测试?但你换个角度想,TDD 的"先写测试"其实是在"先写需求"。你在写测试的那一刻,被迫把"这个功能完成的样子"具像化成一段可以验证的代码。这段验证代码就是你给 AI 的最清晰的目标。AI 比任何时候都需要清晰的目标。
七、第四根支柱:深模块(让 AI 看得懂你的代码)
7.1 复杂度才是真正的敌人
斯坦福教授 John Ousterhout 在他那本《软件设计的哲学》里有一个核心观点——软件工程的本质是与复杂度战斗。
他把复杂度的来源拆成两个主要原因:
- 依赖:模块 A 要正常工作,必须知道模块 B 的某些细节;
- 不透明:你看到一段代码,无法立刻知道它为什么这么写。
复杂度会以三种症状显示出来:
- 变更放大:改一个小需求,要改很多个文件;
- 认知负荷:理解这段代码需要你脑子里同时装太多东西;
- 未知的未知:你不知道有什么东西不知道,bug 总是从你想不到的地方冒出来。
每一种症状在 AI 协作里都会被放大——因为 AI 也有它自己的"认知负荷"上限,那就是上下文窗口。
7.2 深模块 vs 浅模块
Ousterhout 给出的一个最重要的设计原则就是——追求深模块。
什么叫"深"?看下面这张对比就明白了:
浅模块 深模块
┌─────────────────────┐ ┌──────────────┐
│ 10 个参数的接口 │ │ 1 个简单接口 │
│ ───────────── │ │ ────────────│
│ 20 行实现 │ │ 500 行实现 │
└─────────────────────┘ └──────────────┘
接口比实现还重 接口轻,实现重
→ 复杂度暴露在外 → 复杂度被藏起来
深模块 = 功能丰富 + 接口简单。它把复杂的东西藏在背后,对外只暴露一个简洁的契约。
一个特别经典的深模块例子是JavaScript 的垃圾回收。它内部实现复杂得吓人——分代回收、增量标记、并发回收,每一项都能写一本书。但它对你这个开发者暴露的接口是什么?什么都没有。你写代码的时候根本不需要管它,它在背后默默运行。
反过来,一个典型的浅模块长这样:
// 浅模块: 参数比逻辑还多
function processData(
data,
format,
encoding,
separator,
skipFirstRow,
trimWhitespace,
parseNumbers,
dateFormat,
...
) {
// 实际上里面就是几个 split 和 map
}
接口非常复杂,但实现没多少东西。这种模块的存在本身就是一种"分摊出去的复杂度"——它没把复杂度消化掉,只是把它转嫁给了调用方。
7.3 AI 为什么偏爱写浅模块
我自己观察 AI Agent 生成的代码,有一个非常一致的倾向——它特别喜欢拆文件。
你让它做一个"加购物车"的功能,它会给你:
addToCart.tsvalidateCartItem.tscalculateCartPrice.tsupdateCartCount.tspersistCart.tsnotifyCartChange.ts
六个文件,每个 20 行。看起来"模块化做得很好",实际上每个文件都只干一件小事,互相通过参数传来传去。
这种代码组织对 AI 自己有一个致命问题——它跨文件理解能力很差。当你下次让它"在购物车里加一个满减逻辑"的时候,它得同时把这六个文件读进上下文,才能搞清楚整个流程。如果文件再多一点,它就开始猜,开始遗漏,开始改错。
浅模块对 AI 极不友好。
7.4 怎么把"一堆小文件"重构成"几个深模块"
我自己摸索出来的一套通用做法:
第一步:找"经常一起改"的代码。如果你发现每次改一个功能都要同时改 ABC 三个文件,那 ABC 三件事大概率应该属于同一个模块。
第二步:把它们合并到一个模块,对外只暴露最少接口。哪怕这个模块内部很长也没关系,长不要紧,接口窄才是关键。
第三步:把这个模块当成黑盒来测试。如果你的测试不需要管内部细节,只测接口就能覆盖关键场景,那就是一个合格的深模块。
举个我自己的真实例子。我有一个项目里有一套"支付"流程,原来被拆成了:
payment/
validatePaymentInput.js
generatePaymentToken.js
callPaymentGateway.js
parseGatewayResponse.js
recordPaymentLog.js
notifyOrderUpdate.js
refundIfFailed.js
七个文件,调用方要按顺序自己组装。这个流程改一个小细节都要改三四个文件。
后来我让 AI 帮我把它重构成一个深模块:
// payment/index.js
// 对外只暴露一个函数,所有复杂性都藏在内部
export async function processPayment({ orderId, amount, method }) {
// 内部串起来 7 个子步骤
// 调用方只关心 "成功 / 失败 / 失败原因"
...
}
接口就这么一个函数,输入是订单 ID、金额、方式,输出是结果。对外暴露的复杂度大幅下降。
后续每次让 AI 改支付相关逻辑,它只需要读这一个文件就能搞明白整套流程,跨文件遗漏的问题基本消失了。
7.5 深模块对测试也更友好
深模块还有一个隐性好处——它的测试边界天然就在接口上。
你不需要去测 validatePaymentInput、generatePaymentToken 这一堆小函数,你只需要给"支付模块"一个输入、检查它的输出。内部怎么实现是它自己的事。
这就形成了一个良性循环——深模块 → 接口少 → 测试少 → 重构更敢动 → 模块越来越深。
|
维度 |
深模块 |
浅模块 |
|
接口数量 |
少 |