跳到主要内容

它答得越顺,我越要往回翻一行

原石
原石

· 阅读约 5 分钟

先贴一段测试。

test('normalizeUser fills a default role', () => {
  const u = normalizeUser({ id: 1, name: 'x' });
  expect(u.role).toBe('guest');
});

绿的。跑一遍,过。提交进主干,半年后没人记得它为什么在这儿。

可它焊死的是一条 bug。role 本该由上游永远带着,那个 'guest' 是我某个分支里顺手写的兜底,为了让本地的假数据不炸。AI 扫了一眼 normalizeUser 的实现,把兜底读成了规格,顺手写一条测试钉死它。从那天起,谁想删掉那个兜底——那才是正确的事——先得跟这条测试打一架。

我拿三个工具当结对助手用了一个月,Cursor、Copilot、Claude Code,在既有项目里写、调、重构、写测试,不是让它们生成个斐波那契。这一个月里最不值钱的部分,是"谁生成得快"。

AI 默认照着当前实现写测试,不是照着应有行为写。你把函数贴过去说"来点测试",它读的是这段代码做了什么,不是应该做什么。这两件事之间正好差着一个 bug 的距离。而且是测试最该去抓的那个距离。

改口径之后不一样了。提示里必须明写:基于预期行为写,覆盖边界和失败场景,不要假设当前实现是对的。同一个模型,同一段代码,出来的套件是两个东西。原来那份是给现状拍快照,改完那份才在验证契约。

调试是另一码事,也是这几个工具真正拉开的地方。

生成代码差距很小。三个都能给你一版能跑的 getUserById,拉用户、判失败、校返回,都写得出来。差别只在清理和适配阶段谁少让你返工。调试不一样,调试直接决定这东西能不能留在你的工作流里。

UserList.jsx 第 42 行,读 undefined 的属性。报错就这一行。

// UserList.jsx:42
return <span>{user.profile.name}</span>;

崩溃行是症状。真凶在三个文件之外:API 层有一次失败返回了 { data: null },而不是抛出去;渲染层把这个 null 当成了用户对象;profile 就这么没了。

只盯着 42 行的工具,会给你一个 user?.profile?.name。报错消失,测试变绿。问题还在,只是从"崩了"变成"用户名那一栏空着"。这是最坏的一种修法——它把一个能看见的失败,改成了一个看不见的失败。

愿意往上追数据流的工具,会去翻 API 请求在哪发的、失败分支返回了什么、类型上 user 到底该不该可能是 null。我手头那个项目里,相关代码都在同一个工程的时候 Cursor 能追下来;跨文件、仓库级的,Claude Code 更稳。Copilot 处理当前编辑位置附近的问题很舒服,可一旦根因飘到别处,我就得手动往它嘴里塞更多文件。

差别不在模型多聪明。差别在它愿意跑多远。而"跑多远"这件事,恰恰是你最没法用提示词逼出来的——你都不知道根因在哪,怎么告诉它去看哪。

评论区有个人说我比的是三个 harness,不是三个模型。这话对,我前面那句"三个工具各自擅长的环节"说得糙了。同一个模型,换一层壳,行为就能变——它能看到哪些文件、能读多少、能不能自己决定去开哪个、什么时候停手,全是壳在管。那位说他在 Copilot 里换过好几个模型,体感趋同,讲的正是这件事。

真正的变量,是生成之前它手里有多少有效上下文。

这条比什么性能榜都好使。你在编辑器里敲个函数名,补全工具手里只有当前文件、光标上下几百行。你让它去整仓里找该改哪里,那是另一个量级的活,得能自己决定读哪些文件、读多少、几时停下来问。

返工量才是该盯的数。一分钟吐 200 行,然后花 30 分钟修假设、删多余代码、把变量名改回去、给漏掉的失败分支补错误处理——这不叫快。吐 80 行,三十分钟里二十五分钟在读 diff 和跑测试——这才叫快。

重构最典型。AI 给的大范围改动,光看着更干净不代表更安全。

// 原来
if (row.deleted && !row.parentId) return null;

// "重构"之后
if (row.deleted) return null;

下面这版干净多了。parentId 那个条件看起来像谁忘了删的垃圾。但它有用——软删除的子节点得留着挂到父节点底下。删掉它,一部分数据从列表里消失,没人会当场发现。

所以我现在就一条规则:AI 生成的大改动,没读过 diff 之前不接受。不是"看起来更干净"。是读过。

还有个更早的坑:需求没讲清的时候,AI 会自己把假设补上,补得还挺完整。这个月里最难受的几次会话,事后回想,多半不是因为模型差,是因为我根本没给一份共享的简报。评论区有人分享过一个做法,结对之前先写四行便签:一句话目标、模型不许自己发明的约束、一个验收检查、明确不在范围内的事项。四行。我几乎是把这一条直接抄进了提示模板。

最后说工具怎么选。我不看 benchmark,也不看功能表,看它在需求模糊的时候会不会停下来问一句。

有个人讲,安静的工具会用自信的错误代码把空白填满,吵的工具会先追问缺口。这句话我记到现在。

我一开始是按首次生成速度给这三个打分的。后来发现这个分数没用——不提问的那个当时显得快,到一天结束反而更慢,因为它替我把假设全填进去了,我还得一条条往回挖。

模型会越来越强,但"肯停下来"不会因为模型变强就自动发生。恰恰相反,越强、越自信,越容易顺手把空白填上,还填得好看。

那是人得守着的那块地方。生成可以外包,砍不能。

原石
原石

把代码当文章写的系统工程师,以源码立论、单线程式拒绝复杂度。

查看主页 →