🟡 进阶

该停下来的信号:怎么判断一次改动是「结构问题」而不是「bug 问题」

你不写代码,所以看不见「结构」。但你能数出一个数:同一件事,这次被改在了几个地方(本篇管每一个这样的地方叫一个「落点」)。数到 3,就该停——这篇给你那份信号清单、停下来之后能直接说的话,以及哪些情况其实是真的 bug、照常修就行

📅 更新于 2026-09-16

3
同一件事散落的处数——到这个数,问题就不在这一处了
1
判据的单位:是「同一件事」有几份,不是「你改了几个文件」
0
收落点这件事,允许改变的行为数:一件都不行
15 倍
低质量代码里的缺陷数量,是高质代码的 15 倍强(观察性统计,口径见文末)

0030 秒速览:三句话,然后决定你从哪一节开始

先说清这篇解决的是什么。它不教你怎么写代码,也不给你一套新工具。它只补一个判断:眼前这一次改动(就像「点了没反应」这种没按你想要的样子表现的问题,也就是平常说的 bug),是该继续修,还是该停下来回头看结构。

你以前没有这个判断,所以你只有一个动作——继续修。而「继续修」这条路有个特点:它永远不会自己告诉你该停了。 这篇就是把那个「该停了」的信号,从感觉变成一个你能数出来的数。

如果只记三句话

🎯 记住了,后面都是细节

① 问题不在 bug 本身,在「同一件事被存在了几个地方」。一次改动,同一件事只落在 1 处,那是普通 bug;落在 3 处以上,那是结构问题——这时候接着修,只会引出下一个问题。

② 该停下这件事,只能由你判断,AI 判断不了。它每次只看得见它读到的那几个文件,手里没有一张「这个系统长什么样」的图。所以它的每一次修复都是局部最优(只看眼前那几行,改得对;放进整体,经常有害)。这不是它不努力,是它拿不到那张图。

③ 停下来 ≠ 重写(重写 = 丢掉旧的、按新思路再写一遍)。停下来的第一个动作小得多:把「同一件事的 3 个落点」收成 1 个。收完,这件事就回到「改一处就够」了。

如果你只有 5 分钟:看这三节

01 · 起手动作(60 秒,立刻能测出你手上这个项目有没有这个问题)→ 04 · 五条信号(对照你现在正卡着的那一次改动)→ 05 · 反面(防止你从此看什么都像结构问题)。

这篇假设你手上有什么

诚实说在前面:这篇需要一个你自己能跑起来的项目,哪怕很烂。没有项目的话,它对你就是一份说明书,读了也用不上。 具体要什么、缺了怎么办,下一节第一段就说清。

01起手动作:读完 60 秒内做的那一件事

上面那三句话你未必信。没关系,你可以现在就测一次——用你自己上次那次「改完还是不对」的经历测,不用信我。

第一个动作:把这句话发给 AI,然后只数一个数

打开你和 AI 的上一次对话。就是那一轮:你说了哪里不对,它改完,你还是觉得不对(或者它说改好了,回头又坏了)。把下面这段原样发过去:

先别动代码。只回答三个问题:
1. 刚才那次改动,你一共动了几个文件?一个一个列出来。
2. 每个文件里你改的,是「同一件事」的哪一份?
3. 这件事在项目里现在一共存在于几个地方?

不确定的地方就说「不确定」,不要猜。

然后你只做一件事:数第 3 问的答案。

1 处这是普通 bug。这篇你可以先收起来,按平常的方式修就行。
2 处先记下来。还不到该停的时候,但下一次同一件事再找上你,就该停了。
3 处以上你刚才遇到的不是 bug,是结构问题。继续修下去,你还会遇到下一个——原因见 03。
💡 为什么千万不能跳过第 3 问

第 3 问才是判据,第 1、2 问是为了让你能验证它的答案。AI 说「动了 3 个文件」可能只是它手脚大(那是另一个问题,见 08 误判二);只有第 3 问答出「这件事现在有 3 处」,你手上才真的是结构问题。

做这个动作之前,你手上必须有什么

两样,缺一不可。这一栏比上面那一栏重要——它让你在花时间之前就能判断这篇对你有没有用:

① 一个你自己能跑起来的项目

不用好。能打开、能点、能看见它现在哪里不对,就够了。半途停下的也算——这篇恰恰是给那种项目写的。

② 一次刚刚真的发生过的改动

不是假设的场景,是你和 AI 之间真实发生过的那一轮。因为它留下了痕迹:AI 当时确实动过那些文件,你现在去问,它答得出。你拿一个编出来的场景去问,它只会编一个同样编出来的答案——数出来的数没有意义。

⚠️ 如果你连一个能跑起来的项目都没有,今天先别读这篇

不是你的问题,也不是这篇的问题:这份判断需要一次真实的改动当素材。没有素材,它读起来就是一份说明书,读完还是用不上。

缺的是项目,不是这篇。先去把某一个旧项目弄到「能打开、能点」的状态(那是另一件事,不在这篇的范围内),再回来做上面那个动作。

想现在就用上:把同一个问题提前问一次

上面那个动作是回头测。你也可以在让 AI 动手之前先问一遍——只要 30 秒,而且比事后追问更值:

这件事你打算改哪几个文件?先列出来,先别动手。
如果同一件事在项目里存在多处,把它们一并列出来。

这么做的好处是它给你一份预期。等它真的改完,你看到实际改动范围,超没超你一眼就知道——不用读代码,你只需要对比两份清单。这一步解决的是「它顺手改了我没让它碰的东西」,和这篇要解决的结构问题是两件事,但两件都值得做。

02「结构问题」是什么:一个你能数出来的数

先破掉一个误会:它跟代码好不好看没关系

你大概听过「代码乱」「代码丑」「屎山」这类说法。它们听着像同一件事,其实不是。「丑」是审美,「结构问题」是算术。

一份代码可以写得很丑,但同一件事只有一处——那它在改动上是健康的,不是结构问题。反过来,一份代码看起来很整齐(每个文件都短短的很干净),但同一条规则存在四个地方——那它就是结构问题。判据跟美丑无关,只跟「同一件事有几处」有关。

这个区别对你很实际:因为你看不见美丑(你不读代码),但你能数得清处数。这篇从头到尾都在用你能拿到的那一个量。

正向的定义:同一件事被存在了不止一个地方

一句话:同一件事在项目里存在几处,决定了你改一次要付出几倍的力。

当它存在 1 处,「改对」只需要改一次。当它存在 3 处,「改对」就变成了「三处都记得改」——而「记得」不是一种能力,是一种运气。你之前那些「改完还是不对」,绝大多数不是改错了,是漏了一处。

这件事在软件工程里有个名字,也有出处。Martin Fowler 在《重构》第一版(1999 年)第 3 章「代码里的坏味道」里,把它叫做霰弹式修改。(《重构》是一本书的书名,指的是「改变代码的内部组织方式,但外部行为一个字不变」这类做法;「坏味道」= 那些一看就知道不对劲、但还不能算错的征兆。这两个词 06 节还会细讲。)他的原话大意是:每次你想做一种改动,都不得不对很多个地方各做一点小修改;当改动散得到处都是的时候,它们很难被找全,而漏掉重要的一处太容易了。

他在同一节里还给了反面:一个地方因为好几种互不相干的原因被反复修改,叫发散式变化。这一节的收束句值得抄下来——理想状态下,一种常见的改动,应该只对应一个地方。

📌 引用口径

上面引的是《重构》第一版(1999)。第二版(2018)把这一章改了大约四分之三(Fowler 自己说的原话是「我猜四分之三变了,但那是感觉不是实测」),两个名字和含义保留,措辞有差异,所以别把这段话当成他的最新表述。

为什么阈值是「3」,而不是「2」

同一个来源里还有一个经验法则,叫三次法则(Rule of Three)。Fowler 说这是 Don Roberts 给他的,原话大意是:第一次做某件事,你直接做;第二次遇到类似的事,你会为重复皱一下眉,但还是照做一遍;第三次再遇到类似的事,你就该动手整理了。

为什么 2 不够、3 才算:两处相像,可能只是相像。 两个不同页面上的按钮恰好用了同一个颜色,那不是「同一件事」。第三处出现,说明有一条规则被抄了三份——它是个模式,不是巧合。

⚠️ 3 是闹钟,不是定律

真正的判据是「这是不是同一件事」,3 只是个提醒你抬头看一眼的数字。别把它当成硬性标准:同一件事出现 2 次、但它已经在悄悄不一致了(一处的规则已经和另一处不一样),那第 2 次就该停。反过来,3 个地方改的是 3 件不同的事,那 3 是噪音,不是信号。

它和「技术债」「代码烂」是什么关系

那些词是笼统的说法,你没法拿它们做决定——你没法数「有多少债」。这篇给的是一个能数的量:同一件事现在有几个落点。能数出来,才能比较、才能判断该不该停。你以前的困境不是缺判断力,是缺一个可数的东西。

03为什么「再修一次」只会让事情变多

这一节解释的是你最想搞明白的那件事:为什么不管怎么修,问题都会涌现出来。

先把你走过的那个循环写出来

如果你有过这种感觉——发现一个 bug,让 AI 去改,它改完,问题没解决,反而多出来两个;再改,又多出来——这不是你运气差。这是一个稳定的循环,走进去的人都会走到同一个地方:

不敢改
上次改坏过
→
只能往上加
加新的不碰旧的
→
更乱
同一件事又多了一份
→
更不敢改
回到起点,但更差

关键在后面那一步:「加新的」这个动作本身,就在给系统添东西。 你不是在失败,你是在按这套循环的规则正确地运行。所以问题不在意志力,也不在「底子太差」。

机制一:改症状,不改根因 → 错从别的地方冒出来

同一件事存在 3 处,AI 只改了它读到的那 1 处。另外 2 处还按老规矩运行。于是你看到的现象是「修好了 A,B 又不对了」——那个 B 不是新问题,它是同一件事的另一份,一直就在那里,只是你上次没走到那条路上。

机制二:一次改太多处 → 出了新问题,你也不知道是哪一处引起的

很多 AI 会顺手把另外两处也一起改了,有时还顺手改了点别的。这时候它「改好了」,但出了问题你无法定位:你没有「一次只改一件事」这个对照。 三个地方同时变,错在哪一处,只能靠猜。

机制三:它只看得见它读到的文件 → 每次都是局部最优

这是三条里最根本的一条。

AI 手里没有一张「这个系统长什么样」的图。它每次开工,只读到被打开的那几个文件,然后在这几个文件的范围里给出一个最优的修改。只看眼前那几行,它改得是对的;把这几行放回整个系统里,这个修改经常是有害的。

🚨 两个真实观察到的形状

一是越界:你只让它「包一层判断」,它顺手把周边代码也重整了一遍,还加了一条你从没要求过的行为。二是级联扩大:一个小的报错让它觉得「底下那个约定写错了」,于是改了那个约定;为了让所有用到这个约定的地方都对上,它把每一个相关文件都改了——一个文件的修补,变成二十个文件的整体改动。这两种形状本站另一篇已经收过成套的一手证据(issue = 用户在公开代码仓库里提的问题记录;见《用 skill 维护老项目》§08 与 §03 的护栏样例),这里只用来解释你遇到的那一类。

更麻烦的是它分不清两件事:「完成你交代的任务」和「因为它发现了更好的做法、所以重塑这个项目」。 在它那边,继续做看起来就等于进展。一个有经验的开发者知道什么时候不该写代码——这个判断目前还不在它那一侧。所以「该停了」只能由你说。这句话不是推给你,是它真的给不了。

于是「越修越多」不是意外,是算术

把三条合起来看:每修一次,如果落点没有减少,你就多了一处「下次必须记得的地方」。 下一次改动要动的处数只会增加,不会自己减少。这就是为什么修到后来,你的信心比代码先垮——因为你无法预测下一步会不会更好。信心来自「我能预测下一步会发生什么」,而落点数不清,就没法预测。

有一条实证能说明「在乱的地方继续修」这条路成本是往上走的(口径是观察性统计,不是因果证明):2022 年的一项研究统计了低质量代码与高质量代码的对比——低质量代码里的缺陷数量是高质代码的 15 倍以上;在低质量代码里解决问题,平均要多花 124% 的时间,最长处理周期长 9 倍(Tornhill & Borg, Code Red, arXiv:2203.04374)。

所以这一节要给你的不是安慰,是一个算术结论:你不是在修复,你是在给下一次的「必须记得」名单上加一行。

04五条信号:该停下来的那一刻长什么样

这五条有一个共同点:都是你能亲眼看见的,都不需要你读代码。 按「最容易先撞上」的顺序排——第一条是主信号,后面四条是它的各种变形。

信号 1(主信号)· 同一件事落在 3 处以上

你看见什么:AI 说「我改了 order.ts、cart.ts、checkout.ts」——三个文件(.ts 结尾的是代码文件,一个文件大致相当于一页纸,里面装着一部分代码),而这三个文件里改的是同一条规则,比如「满 199 免运费」。

它说明什么:不是「三个文件」多,是同一条规则有三份。规则本身只有一条,项目里却存了三份,所以你每改一次规则,都要记得三处。

这一刻你该做什么:用 01 节那三问,把第 3 问的数字拿到手。数字 ≥3,停。

别搞错的地方:改了 3 个文件、但改的是 3 件不同的事(比如三个页面各自的文案),不算信号。单位永远是「同一件事」。

信号 2 · 同一处坏了第二次

你看见什么:上个月修过的地方,这个月又坏了;或者你从另一个入口进去,发现它还是坏的。

它说明什么:bug 修好了就不该回来。回来只有两种可能——上次修的是症状(另外几处没动),或者你从另一个入口进去碰到的是同一件事的另一份。两种情况都是结构问题。

重要的数字:第二次就够了。不用等第三次。同一处坏第二次,是这五条里最快能确认的一条,因为它不需要你问 AI 任何问题——你记得,你上次就修过这里。

信号 3 · 修 A 引出 B

你看见什么:你说「按钮点了没反应」,它改完,按钮好了,表单提交坏了。

它说明什么:不是它手笨,是 A 和 B 共用着同一处东西,而那一处只有一份、被两件事同时拉扯。你改那一处,代价由两处一起承担。这个现象在软件工程里被专门研究过,有个名字叫「引发修复的改动」——确实有一类改动,它本身会引出后面的修复(Śliwerski / Zimmermann / Zeller, MSR 2005)。

这一刻你该做什么:先别让它「再修一次 B」。修 B 会让共用那一处继续被拉扯,下一次坏的是 C。先问:「A 和 B 是不是共用同一处东西?如果是,那一处现在有几份?」

信号 4 · 你要把同一条规则交代第二遍

你看见什么:「别忘了订单页也要加」「上次说的那个,这里也适用」。你说过一次,又得说第二次。

它说明什么:你说第二遍的那一刻,就证明了那条规则在项目里没有唯一的存放处——它散着,所以每一次都得重新告知一遍。这条是所有信号里最早出现的,也是最容易被忽略的,因为它不疼:只是多说一句话而已。但它是在替项目记住一件事,而那份记忆不在项目里,在你脑子里。

信号 5 · 你说不清「这个功能在哪几个地方」

你看见什么:有人问「这个功能在代码里在哪」,你答不上来,只能说「AI 知道」。

它说明什么:你答不上来,AI 也只能靠猜(它只读得到被打开的文件)。这一条是其他四条共同的源头:那个「系统长什么样」从来没有存在于任何地方——不在你脑子里,也不在任何文件里。这就是 03 节「机制三」里说的盲区的来源。

这一刻你该做什么:注意,这一条的解法不是「读懂代码」。这个动作对你不成立,也不需要成立。可做的动作是问 AI 要一段文字答案:

这件事(比如「运费怎么算」)现在在项目里存在于几处?
每一处在哪个文件、大概负责什么?
用中文回答,不要贴代码。

你能判断这段文字对不对——那是你的项目,你知道运费是怎么回事。你判断不了的是代码写得好不好,那不需要你判断。

五条信号速查

读第二遍的时候直接查这张表。第三列是这一刻该做的事,和上面的正文不重复:

信号你亲眼看见什么这一刻该做什么
同一件事落在 3 处以上它改了三个文件,改的是同一条规则问 01 节那三句,拿到处数;≥3 就停
同一处坏了第二次修过的地方又坏了不用问任何人,直接按结构问题处理
修 A 引出 B按钮好了,表单坏了先别修 B。问 A 和 B 是否共用同一处
同一条规则要交代第二遍「别忘了那边也要加」先记下来,别立刻动手;这是最早的信号
你说不清功能在几处你只能答「AI 知道」让它用中文文字回答,不贴代码

05反面:哪些是真的 bug,照常修就行

这一节和前面同样重要。这篇的目的不是让你从此看什么都像结构问题。 过度反应和反应不足一样贵,而且过度反应更容易发生——因为它感觉更专业、更有掌控感。你会开始说「这里得重构一下」,而其实只需要改一个错别字。

判据只有一句:下一次同类的事来了,你还要不要走同一遍

❌ 结构问题

第二件同类的事来了,你还是得改这几个地方。那就说明这几处是同一件事的多份,而不是碰巧都要改。

✅ 真的 bug

修完就结束了,不会再有第二件同类的事。那你改的那一处就是它唯一的家,没有第二份。

这一问的好处是它不需要你懂代码,只需要你回顾:「这种事我以前遇到过吗?以后还会遇到吗?」 这个你答得了。

五类「照常修就行」

1
只落一处的错

路径写错了、名字拼错了、少写了一步等待、某个依赖没装(依赖 = 别人写好的、你的项目要用到的东西)。改一处就好,那就是 bug。

2
改了 2 到 3 处,但它们是「配套的」,不是「重复的」

比如页面上要多显示一项内容,服务器那边就要多返回这一项。这是两件不同的事各改一次,不是同一件事的两份。 这一类你最容易误判成结构问题——因为「动了三个文件」和信号 1 长得一模一样。区分法:问它们是不是同一条规则。不是,就放行。

3
数据问题

那一条记录本身是脏的,或者某个文件是你自己手动改坏的。错的是数据,不是代码的组织方式。 改数据,或者删掉那条脏数据重来。

4
环境问题

换一台机器就不行、升级之后打不开、别人电脑上正常你这里不正常。这类问题的落点在配置(那些填写项和设置),不在代码怎么组织。

5
只在一个特定条件下发生、修完再不复现

比如只有数量刚好是 0 的时候才出错。修好之后你怎么试都不再出现——那它就是一次性的 bug。

另一个方向的警告:别急着「合并重复」

你已经受过一次「重复」的苦,所以很自然会产生一个冲动:一看到两处长得像,就立刻让 AI 把它们合并成一处。这个动作会做错,而且做错了比重复更难拆。

Sandi Metz 在 2016 年写过一篇《错误的抽象》,核心两句:重复远比错误的抽象便宜;而一旦你造出了错误的抽象,最快的出路是往回走——把合并出来的东西再拆回给每个用到它的地方(「调用方」),重新容忍重复。她还给了失灵的信号:合并出来的那份东西上,每次用它要传进去的东西(「参数」)越加越多、里面「如果是 A 就…否则就…」的分岔(「条件分支」)越加越多,就说明它已经不再代表「同一件事」了。

所以当 AI 说「这里有两处重复,我把它提取成一个公共函数吧」(公共函数 = 把几份一样的写法收成一份,让几处都去用它),而你手上还没有第三处证据的时候,你可以直接说:

先别合并。等第三次真的出现再说。
📌 这条的边界

它不是「永远别合并」。当三处都出现了、而且你确认它们是同一件事,那就该合并——那正是 06 节要做的事。这条警告针对的是「看到两处像就动手」这个冲动。

还有一个时机问题:别在「今晚就要用」的时候停

如果你现在正需要它工作——今晚就要用、明天要给人看——那就先打补丁让它能用,把「收落点」排到能用之后。

但有一个条件:延期要写下来。 不写下来的延期,会变成永久的「以后再说」,而「以后」不会自己到来。写一句话就够了:「运费规则现在散在三个文件里,等这版用完要收成一处。」——这句话写在哪儿都行,只要是你下次会看到的地方。

06停下来之后:把 3 个落点收成 1 个

「停下来」这三个字,在你以前的经历里很可能等于「重写」——因为过去你停下来之后的结果,就是项目烂尾。所以你会本能地抗拒停。但停下来不等于重写,这两件事之间差得很远。

先分清「重构」和「重写」

重构重写
做什么改变代码的内部组织方式丢掉旧的,按新思路再写一遍
外部行为变不变一个字都不变全部重新定义
中途能不能停下来用能,每一步之后它都还是能用的不能,写完之前它是坏的

分界线只有一句话:「你能不能在每个中间状态都发布?」(中间状态 = 整理到一半、还没全部做完的那个时候;发布 = 把新版本放出去给人用)能,是重构;不能,是重写。(本站另一篇《用 skill 维护老项目》§05 的「三组最容易搞混的词」把这条讲得更细,可以对着看。)

但对你要补的只有一件:你不需要现在决定要不要重写。 你要做的第一步比这两者都小——小到不构成一个「项目」,只是一次改动顺手做的事。所以它不需要排期,也不需要信心。

第一步:收落点。验收标准是「行为一件都没变」

Kent Beck 在《Tidy First?》(2023)里给过一个很有用的区分:把改动分成结构改动和行为改动。他的说法是:结构改动大体上是可以回退的,行为改动往往不可逆,而把这两者分开,本身就有价值。

这条给你的直接好处是——它给了你一个能验收的标准。 你不需要判断代码写得好不好(那本来就不是你的位置),你只需要判断一件事:

🎯 收落点的验收标准

收完之后,你去点一遍,所有功能都跟之前一模一样。有一个功能的表现变了,那就不是结构改动,是改错了。

这就是「结构改动允许改变的行为数 = 0」的意思。它是你能百分之百验证的一件事,而且只有你能验证——因为你最清楚这个工具该有什么表现。

三步做完,以及每步该说的话

1
让它先回答,不动手

要一份文字答案:这件事现在在哪几处、每处的写法差在哪。这一步的产出是你能核对的一段话,不是代码。
很多情况下你会在这里发现一件更要紧的事:那几处已经不一致了。 见 07 节的例子——你之前遇到的 bug,往往就是这条不一致。

先别改。只回答:这件事在项目里现在存在几处?
每一处在哪个文件、写法差在哪?用中文说,不要贴代码。
2
选一处留下,其他地方改成「去问那一处」

选哪一处留着?选最符合你真实意图的那一处(如果它们已经不一致,你才知道哪个是对的)。其余几处不再自己算,改成去问留下来的那一处。

把这几处收成一处。留下的那一份要改得能覆盖现在所有情况。
要求:外部行为一个字都不变——所有功能的表现必须和现在完全一样。
3
你去点一遍,然后才删旧的

在确认之前,让两边同时存在、结果必须一致。你去点一遍(购物车、结算页、订单页,每个入口都走一次),确认行为一个字没变,再让它把旧的删掉。

现在这件事在项目里只剩一处了吗?
如果你不确定,就说「不确定」,不要猜。

最后那一句不是客套。「不确定」对你是好消息——它让你知道哪里还没被验证;「大概是吧」才是坏消息,因为它让没验证的东西看起来像验证过了。

下次直接用的话(都收在这里)

📋 存下来,下次直接粘

① 数落点(判断用)——就是 01 节那三问,它是全篇最该记住的一段,不在这里重复。

② 让它先回答再动手(收落点第一步)

先别改。只回答:这件事在项目里现在存在几处?
每一处在哪个文件、写法差在哪?用中文说,不要贴代码。

③ 收落点,并要求行为不变

把这几处收成一处。要求:外部行为一个字都不变——
所有功能的表现必须和现在完全一样。改完告诉我点哪里可以验证。

④ 防止它顺手扩大范围(和结构问题无关,但一起挡掉更省事)

只改我点名的文件。如果你觉得别的文件也该改,先列出来问我,不要直接动手。

最后这两句是本站《用 skill 维护老项目》§03 给的护栏原话(它放在 CLAUDE.md 里长期生效——那是给 AI 的一页常驻说明,每次开工它都会先读到),这里照抄,因为实测有效。

为什么不用专门排一个「整理时间」

一个很有用的提醒,来自 Fowler 在《重构》里谈「什么时候该重构」那一节。他大意说:几乎在所有情况下,我都反对专门腾出时间来做重构。 你不是决定去重构——你是因为要做别的事,而重构能帮你做成那件事,所以顺手做。

对你的意义很直接:不要给「整理」排期。 排期会把它变成一个没有截止日期的大工程,而那正是它一直没发生的原因。正确的时机只有一个:下一次你本来就要改这件事的时候。 那时候你本来就要动它,收落点只是顺手——而且不做的话,你这次又要改三处。

07最小例子:一条运费规则散在三个文件里

这是本篇唯一的完整走查。它演示的就是最要紧的那件事:结构问题一开始长得完全不像结构问题。

事情的经过(注意每一次都「修好了」)

你让 AI 给一个小店后台加一条规则:满 199 免运费。

第一次

AI 改了算总价的地方(order.ts),加了一句「满 199 就免运费」。你试了一下,下单,运费是 0。好了。

两周后

你发现购物车页面上显示的运费不对——明明够了 199,它还收你运费。AI 找到购物车那块代码(cart.ts),也加了一句同样的规则。你试了,好了。

再过一周

结算页的运费还是不对。AI 改了结算那块(checkout.ts)。你试了,好了。

三次都是「改一处、好一处」。 所以它长得完全不像结构问题——没有一次改动落在 3 处,每一次都干净利落。你甚至会觉得这个项目挺健康的。

现在把 01 节那句话发给它

你问:「这件事在项目里现在一共有几个地方在算?」答案是 3。

然后你让它把这三处贴出来对比。你会看到类似这样的东西:

order.ts      满 199 免运费     →  if (total >= 199)
cart.ts       满 199 免运费     →  if (total >= 199)
checkout.ts   满 199 免运费     →  if (total >= 200)
🚨 这才是关键:三处已经不一致了

第三处写的是 200。可能当初是有人图个整数好看,也可能纯属手误。无论哪种,这就是你之前那些 bug 的来源——同一条规则,两个地方说 199,一个地方说 200。

所以「三处都记得改」从来不是一个可靠的方案:就算你每次都记得,它们也会自己走散。 这不是记性问题,是重复本身的问题。

收落点:留下最符合你意图的那一份

你确认「满 199 免运费」才是你要的(那两处是对的,200 那处是错的)。于是:

1
留下 order.ts 里那一份,并把它写清楚

门槛值 199,规则只在这一个地方定义一次。

2
另外两处不再自己算,改成「去问那一处」

购物车页和结算页不再各写一遍规则,而是去要同一个答案。外部行为必须一个字都不变。

3
你去点一遍

购物车、结算页、订单页,每个入口走一次,确认运费显示和改之前完全一样。注意:改之前那一处 200 是错的,统一到 199 之后,那个页面在「刚好 199」的情况下会变。 这是你要的行为变化——所以它不是「行为没变」,是你主动决定要变的。剩下所有不该变的地方,一处都不能变。

收完之后,什么变了

❌ 收落点之前

你要说:「把运费门槛改成 299。注意购物车、结算页、订单页都要改。」

——后面那半句,是你在替项目记住一件事。你成了那份记忆的存放处,而你会忘。

✅ 收落点之后

你说:「把运费门槛改成 299。」

——项目自己记得。你不需要记住任何东西,这就叫「这件事只有一处」。

🎯 留给你自己验的一件事(只有你能做)

这篇文章的其他部分你都可以读,但这一条只有你能做:挑一个你自己的项目,回想最近一次「改了又坏」或者「改完还是不对」的事,把 01 节那三句话发给 AI,把第 3 问的数字记下来。

如果数字是 3 或更大,你就亲眼看到了自己那十几个项目的失败模式长什么样——而且它和你以前以为的(「我底子太差」「模型能力不够」)很可能不是一回事。

08三个最容易误判的场面

这三处是「反应不足」和「过度反应」各自的高发区。每一条都给一个区分法,区分法都不需要你读代码。

误判一:把「代码丑」当成结构问题

你的感觉:「这一堆乱糟糟的,肯定有问题。」

为什么错:丑和落点数是两件事。一份很难看的代码可能同一件事只有一处,改起来很顺;一份看起来很整齐的代码可能同一条规则存在四处。

区分法:别问「它好不好看」,问「同一件事有几处」。前者你判断不了也不该判断,后者你数得清。

误判二:把「AI 顺手多改了几个文件」当成结构问题

你的感觉:「它动了 5 个文件,肯定是结构问题。」

为什么错:AI 常常顺手改格式、加注释、动一个你根本没提的文件。那叫越界,是范围问题,不是结构问题。它说明的是「你没有把范围框住」,不是「你的代码有结构问题」。

区分法:看这几个文件改的是不是同一件事。不是同一件事 → 那是范围问题,用 06 节第四句话(「只改我点名的文件」)挡回去就行。这也正是为什么 01 节那三问里的第 2 问(每一处改的是「同一件事」的哪一份)不能省。

误判三:把「难修」当成结构问题

你的感觉:「这个 bug 找了好久才修好,说明结构有问题。」

为什么错:难修的意思是找不着;结构问题的意思是找着了,但改一处不够。这两件事的解法完全不同:前者是定位问题(多看几处、把现象描述得更准),后者才是本篇讲的事。

区分法:问自己「找到了以后,改一处就够了吗?」找到了、改一处就够 → 那只是难找,不是结构问题。

三个场面对照

你看到的现象是结构问题吗判据
代码很乱很难看不一定丑是审美,跟你无关;数落点才算
它改了 5 个文件不一定看这 5 处是不是同一件事;不是就是越界
这个 bug 很难找到原因不是难修 = 找不着;结构问题 = 找着了但改一处不够
同一件事落在 3 处是主信号,直接停
同一处坏了第二次是第二次就够,不用等第三次

09术语速查:这几个词别混

都是本篇出现过的词。第三列回答的是「什么时候它会帮到你 / 别把它当成什么用」——如果你只需要记一件事,记着别把它们互换着说。

词在这里指什么为什么重要 · 别当什么用
落点同一件事在项目里被存放的某一处判断的单位是「同一件事有几个落点」;别数成「改了几个文件」
结构问题同一件事散落在 3 处以上,改一次要改多处是算术不是审美;别拿它当「代码丑」的同义词
bug程序没按你想要的样子表现bug 描述现象,不描述原因——落点散开引起的错,表现出来也叫 bug
局部最优只看眼前几个文件改得对,放进整体有害这是 AI 的默认状态,不是它不认真;别指望它自己跳出来
重构改内部组织方式,外部行为一个字不变验收标准是「点一遍,表现全一样」;别和重写混着说
重写丢掉旧的,按新思路再写一遍中途不能用,是它和重构的分界线;别用「重构」安慰自己做重写
霰弹式修改一次改动逼你在很多地方各改一点这是 Fowler 给主信号起的名字,不是你发明的感觉
发散式变化一个地方因为好几种不相干的原因被反复改主信号的反面,指同一个文件;判断时别和霰弹式弄反
三次法则第一次直接做,第二次皱眉还是做,第三次才整理它是「提醒你抬头」的闹钟,不是硬标准
公共函数把几份一样的写法收成一份,几处都去用它收落点就是造一个它;但对只有两处相像的,先别造

10场景索引:你现在的处境 → 该看哪一节

不用从头读。找到你现在那句话,直接跳:

你现在的处境先去哪为什么
我刚让 AI 改完,感觉它动了一堆东西01 → 08 误判二先数落点;如果只是越界,别误当结构问题
同一个地方坏了第二次04 信号 2 → 06最快能确认的一条,不用问任何人
修好 A 之后 B 坏了04 信号 3 → 03先弄清 A 和 B 是否共用同一处,别再修 B
我分不清这是 bug 还是结构问题05那一节给的就是这条分界线
我确认是结构问题了,但不知道跟它说什么06 的四句话直接复制,不用自己组织语言
我想干脆推倒重写06 的「重构 vs 重写」先确认它是不是真的不能「每个中间状态都发布」
我今晚就要用,现在不能停05 最后一节先打补丁,但要写下来延期,别让它变成永久
我连一个能跑起来的项目都没有01 的警告框这篇今天对你用不上,先去把某个项目弄到能跑

11来源与证据边界

一手来源

本篇没有核到的东西

时效性

这篇讲的是原理,不引版本号、价格、或者会变的费率,所以它不容易过时。唯一两处版本敏感的地方已经在上面标出来了(《重构》第一版 vs 第二版、Beck 那句话的转述问题)。

99一句话总结

🎯 核心要点

判断一次改动是结构问题还是 bug 问题,不看代码好不好,只看一个你能数出来的数:同一件事现在散落在几处。数到 3,就该停——因为再修一次只会给「下次必须记得的地方」再加一行。停下来的动作不是重写,是把那 3 处收成 1 处,验收标准是「点一遍,所有功能的表现一件都没变」。

最后一件事:这五条信号你不需要一次全记住。 下次再遇到「改完还是不对」,你只做 01 节那一个动作——把三句话发给它,数第 3 问的答案。数一次,比读十篇都管用。

⌂