📍 无忆研习社 › 开源解码 › AI Coding 时代的工程基础回归:从一团乱麻到可控协作的四根支柱与全套实操指南

AI Coding 时代的工程基础回归:从一团乱麻到可控协作的四根支柱与全套实操指南

摘要:AI编码速度暴涨,但代码质量没跟上,反而让“烂代码”成了最贵的债。本文作者莫潇羽用两年真实项目经验,揭示Vibe Coding和Specs-to-Code两条死胡同的陷阱,提出四根底层支柱:先让AI拷问方案、建统一术语表、用TDD控制步幅、把代码组织成“接口简单、内部丰满”的深模块。核心目标只有一个——管理认知负荷,让你始终掌控方向盘。附赠开源Skill仓库和可直接复制的流程清单。
字号 100%
行距 2.05
当前可见 60% 的内容
本文由 莫潇羽@无忆研习社(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 写代码的那阵子,最常出现的剧情是这样的:

  1. 我描述一个功能;
  2. AI 立刻给我一份方案;
  3. 我说"看起来还行";
  4. 它开始写;
  5. 写到一半我看出问题,但已经写了 300 行;
  6. 我让它改,它改完又出了新问题;
  7. 来回三轮之后,我比自己写还累。

问题出在哪?出在"我描述一个功能"那一步,我没说清楚,它也没追问。

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.ts
  • validateCartItem.ts
  • calculateCartPrice.ts
  • updateCartCount.ts
  • persistCart.ts
  • notifyCartChange.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 这一堆小函数,你只需要给"支付模块"一个输入、检查它的输出。内部怎么实现是它自己的事。

这就形成了一个良性循环——深模块 → 接口少 → 测试少 → 重构更敢动 → 模块越来越深。

维度

深模块

浅模块

接口数量

少

🔒
该内容仅对更高等级社区用户开放
请谨慎解锁时效性强且发布日期较早的文章
单篇解锁后若未显示全文请刷新页面
您当前:游客 · 可见 60% 内容 · 升级至 注册用户 可见 70%
👀
游客
可见 60%
✓ 当前
注册用户
注册用户
可见 70%
社区精英
社区精英
可见 100%
社区守护
社区守护
可见 100%
仅解锁本文,永久有效。如需PDF珍藏版,请联系站长获取。 当前单篇价格 ¥9.9
✏️ 发表评论

请先登录后发表评论

前往登录
📊 站点统计
今日发布0 篇
文章总数1107 篇
昨日发布1 篇
本月发布31 篇
建站时间456 天
🔍 搜索
站长微语

联系站长

QQ:2805463528
AIGC 技术社区
致力于解码 AI前沿技术 与经验分享
纯粹的技术应用社区

💡 欢迎您的建议与反馈,让社区变得更好

快速通道
联系站长
站长QQ二维码
AI交流群
AI交流群
仍在路上

那些寒夜里追赶过的方向

那些冷眼下没放弃的理想

一篇一篇写到现在

仍在路上

"不羁放纵爱自由"

—— 致敬 Beyond
持续创作中 莫潇羽 · 无忆研习社
▲