九月底刷那份月度盘点,有个标题我扫过去又倒回来——大意是"我们好像都有两个 AI,一个正经干活的,一个用来瞎玩的"。
画外音:这不就是我那台常年开着两个窗口的编辑器吗。
我盯着这话看了半分钟。被总结命中的感觉,翻过车的人都懂。
先把现场交代一下。我干活的机器上一直有两套东西:一个正经工作目录,接着仓库,历史干净,跑测试,提 PR;另一个是 ~/scratch——一次性脚本、半成品、各种想试的东西,扔进去就不怎么管。这习惯 AI 进来之前就有了,那时候两个目录的区别很单纯,一个"重要",一个"不重要"。
用了 agent 之后,这区别在我脑子里悄悄换了个词。不再是"重不重要",变成了"要不要审"。
工作目录里的产出,我一行一行看 diff。scratch 里的产出,我不看,跑通就行——反正它不进仓库。
问题就出在"反正"上。
摸鱼那一半的产出,是会走路的
那篇帖子的本意是聊工具分工:写文章用一个,写代码用另一个,两个脑子别混着使。这个我认,工具按任务挑没什么毛病。但我想说的不是选型。
我想说的是,当我在工具层面分了工,我顺手在流程层面也签了张空白支票——我提前决定了哪半产出来自"不用审"的区域。注意,这个决定是在看到产出之前就做好的。
然后支票就被兑了。
上个月我要给一个配置读取函数补个默认值。这活本该在正经目录里写,可手里刚好有半年前在 scratch 写的一段,懒得重写,直接粘了过来。粘的时候我没细看——它属于"不用审"那一类,我潜意识里就是这么归档的。
这行 import 就是这么进的仓库:
// 从 scratch 目录粘过来的,当时只想让它先跑起来
import { normalizeConfig } from "./legacy/config-helpers";
它跑起来了,测试也过了。故事在函数体里:
function normalizeConfig(raw: RawConfig) {
try {
return parseConfig(raw);
} catch {
return raw; // 出任何事,就当没解析过
}
}
一个空 catch。任何解析错误被它静默吃掉,然后返回一个"看起来能用"的原始值。它不是我手写的——骨架确实是当年那个实习生生成的,但那个空的 catch 是我自己觉得"反正能跑"留下的。它在 scratch 里待着的时候,"反正"完全成立。它进了主流程之后,不成立了。
整个过程里没有一步是错的,每一步都很合理。这才是最烦人的地方。
反面也一样,别把"没用 AI"当勋章
同一个月,我还看到有人特意在文末标了一句"这篇没用 AI 帮忙"。那篇数据不错。
我没有要挖苦谁的意思。愿意写、愿意标,比藏着掖着强,这点我站他。但那个标注本身暴露了一件事:我们默认把风险安在了工具身上,而不是安在"有没有人看过"这件事上。
这跟我开头那个空 catch 是同一枚硬币的两面。一边是"产出来自摸鱼 AI,跳过审查";另一边是"产出没让 AI 碰,所以没问题"。两边都拿同一个变量——产出从哪来——去代替了真正该看的那个变量:这段东西有没有被一双眼睛认真过一遍。
手写的空 catch 和 AI 生成的空 catch,出事的时候一样精彩。我审 AI 生成的代码从来不手软,但"没让 AI 写"不自动等于审过了。这个等号很危险。
边界一定会漂,而没人替你看着它漂
我前面其实补了一句没展开的话,这里接上。
一开始,scratch 里真的都是些一次性的东西:一条正则,一行 shell,临时验证个想法。用"不用审"对待它们,合理,投入产出也划算。我没做错。错的是我以为这条线是固定的。
它不是。今天放进去的一个小函数,明天会因为"懒得重写"被粘到别处;再后天,它被第二个地方 import 了。每一次都没越界,每一步都很自然。等你发现的时候,"一次性"这个前提早就不成立,但"不用审"这个习惯还在原地站着。
// 三个星期后,第二个文件也引了它
import { normalizeConfig } from "./config-helpers";
工具不会提醒你边界漂了。agent 只看到你让它写、你让它跑、你点了同意。这条线从头到尾是你在脑子里独自维护的,而它偏偏在你最忙、最想走捷径的时候才派上用场——也就是最守不住的时候。
这周期的实习生表现其实都挺好。出问题的是带实习生的人,是我没告诉它这块地后来变成了主流程的一部分。
萎缩不发生在"用 AI"的那一半
那个月还有篇讲"工程师认知在悄悄萎缩"的,评论区有人总结得特别好,说那种累,是你在追一条一直往后跑的终点线。
现象我认。但结论我不同意——很多人顺着这篇往下推,落点是"少用 AI"。我的落点正好反过来。
萎缩不是发生在"用 AI"的那一半,是发生在"摸鱼 AI"那一半。因为那一半里,我问的问题最少。不较真,不追问,不挑战那个默认值为什么偏偏是 raw。我以为省下来的脑力会花在架构判断上——结果那半年我也没见着架构判断在哪儿。扯远了,回正题。
对了,那篇评论区里有人推了个东西,我顺手点进去看了,印象特别深。发帖的人用 prompt 把一个完整的 3D 空间"说"了出来,效果很爽,底下清一色"这就是 vibe coding"。续集是:为了让这玩意真能用,他自己动手写了一个关卡编辑器。
画外音:vibing 出来的东西,最后总得有人给它配一个编辑器。
这例子比我的空 catch 大得多,但结构一模一样——生成那一步有多爽,维护那一步就有多"正经",而且维护那步没法 vibe。没人能替你判断"这个关卡放这儿崩不崩",模型也不行,它连你的历史都不知道。工具能给你一个能跑的东西,给不了你一个可以不用管的东西。
我的补丁
补丁很土,就是把两个窗口合成一个。
不是把 scratch 删了——删了我过两天还会再建一个。是让它不再是"免审区"。现在不管产出从哪个窗口来,进仓库之前走同一套:同一份 rules,同一个 review 队列,同一条 dry-run。
规则文件里我删掉了一行,那行大概长这样:
<!-- 实验目录,产出不用管,能跑就行 -->
换成了:
<!-- 唯一规则:进了这个仓库的,就是被审过的。来源不写进判断。 -->
然后那个空 catch,被真正看了一遍:
- } catch {
- return raw;
+ } catch (err) {
+ logger.warn("config 解析失败,回退原始值", err);
+ return raw;
它要的就是这一眼。三秒钟的事,我拖了半年。
到现在也没想清楚的
中间那条线该画哪儿,我至今没有标准答案,也不想硬凑一个出来。工具要不要按用途分开、agent 要不要分权,这些我都在试。但有一条我现在能守住:你分几个 AI 都行,review 只能有一个入口,而来源永远不写进判断里。
补一句,我中间那句"工具从来不是变量"是错的,写到这儿自己打了脸 😅。真正致命的不是分工本身,是你顺手把"哪半不用看"也一起分配了出去。工具只是那块被分配的地,画地的人还是我。
本期缴税:一个空 catch 从"试验品"一路躺进主流程,因为我在它出生那天就把审查关了。
