上个月我review了一个PR,到现在想起来还有点后怕。
改动不大,一个结账页的小调整,顺手加了个pricing.ts。里面是一个formatMoneyCents(),代码写得非常漂亮:类型完整,函数拆得干净,单元测试带了三个用例,全绿。CI通过,构建通过,页面跑起来也没毛病。
我鼠标已经移到"Approve"上面了,突然觉得哪里不对。
不是代码有问题。是它太干净了。干净到不像这个仓库里长出来的东西。
我往下翻了两屏,在packages/currency/src/format.ts里找到了一个formatCurrency()。做的是同一件事,但多承担了一项职责------它执行的是财务在几年前一次真实的分币事故之后,签字确认的舍入规则。
不是四舍五入。是银行家舍入。
那个PR里没有任何东西提示"这里已经有一个函数了"。生成它的Agent在它能看到的文件里找到了一个"缺失的工具函数",然后把它补上了。在它的视角里,这是一次完美的、自包含的、最短路径的修复。
但它不知道那个formatCurrency为什么存在。 它不知道那几行舍入逻辑背后是一个真实的事故、一次财务的签字、一个再也不能出错的约定。它只是看到"这里需要一个函数",然后造了一个。
一、测试全过,是因为测试和实现是同一个脑子想出来的
我后来去查了这个PR为什么能一路绿灯。
formatMoneyCents()的单元测试用的是10.00、99.99、500.00、1280.00。全部通过。
但真正的分叉点不在这里。银行家舍入和四舍五入,只在恰好一半 的地方分道扬镳:0.005、2.675、5分钱。这些值在测试里一个都没有出现。
不是因为写测试的人偷懒。是因为写测试的人和写实现的是同一个模型。它不会主动去构造一个自己没想到的边界。它验证的是"这个函数自己跟自己一致",不是"这个函数跟系统里那个唯一的实现一致"。
更麻烦的是,这类缺陷不会报错。金额差一分钱,接口返回200,页面正常展示,监控安安静静。它只会在几个月后某一次对账时出现,那时候距离合并已经过去了好几周。
测试全过。但它会在你最不想被打扰的时候,安静地错。
二、AI爱"重复造轮子",不是因为笨,是因为它没有"归属感"
那篇技术评论给了四条解释,我在工程现场对得上每一条。
局部上下文偏好。 相关的工具在三个包之外,提示里没有任何东西指向它。Agent就在它能看到的那几个文件里解决问题。
命名不匹配。 既有函数叫formatCurrency,Agent想的是"金额分"。模糊搜索也会错过。
完成度奖励。 一个自包含的小函数,能让当前改动变绿,而且不用碰共享代码。这是通往"绿"的最短路径。
桶导出掩盖来源。 所有东西都从一个索引重导出,Agent意识不到它需要的函数已经在公共接口上了。
四条里没有一条是"模型能力不足"。全是检索和归属的问题。
它不知道哪些代码是"用血换来的",哪些抽象是禁区,哪些看似多余的几行逻辑背后压着一个真实的事故。它只知道"这里缺一个函数",然后补上。
这不是一个可以靠"请不要重复造轮子"的提示词解决的问题。
三、我现在看AI写的工具函数,会多问一句
"这个函数,仓库里是不是已经有一个了?"
不是"有没有类似的",是"有没有一个因为某个真实原因而存在的"。
我自己总结了一个很土的习惯:每次看到AI新建一个工具函数,先不读它的实现,先去搜一下同名的动词。format、validate、parse、convert,这些词最容易长出"看起来该新建、其实早就有了"的东西。
找到旧的那个之后,问一句:它为什么长这样?
如果旧函数里有任何一行看起来"多余"的代码------一个奇怪的舍入、一个别扭的默认值、一条绕路的错误处理------那大概率不是当初写的人水平不行,而是有人为这一行付过代价。
AI不知道这些代价。它只会看到"这里不够优雅",然后把它优化掉。
四、那篇评论最后给了测试侧的建议,我觉得说到了点上
金额类的边界值,不能等模型"自己想起来",得写成固定的小表 ,把0.005、2.675、5分这些分叉点硬编码进去。让测试去守那条线,而不是让实现去"理解"那条线。
但我觉得更深的一层在测试之外。
AI生成的代码,缺的不是正确性,是"来历"。
它不知道formatCurrency为什么用银行家舍入。它不知道那个看起来冗余的useEffect是为了绕开Safari某个版本的bug。它不知道那行"多余"的空值判断是两年前一次线上事故的补丁。
它只看到代码,看不到代码背后的事故、签字和约定。
而这些东西,恰恰是一个系统里最不能动、也最容易被"优化"掉的部分。
写在最后
那个PR我最后没有直接合。
我把formatMoneyCents删了,让改动人把逻辑接到formatCurrency上。改完的diff比原来丑一点,多了一个import,少了一个"干净的小函数"。
但它和系统里那个唯一的实现对齐了。它知道那行舍入逻辑是谁签的字。
AI可以把函数写得很干净。但它不知道哪些东西不该被写干净。