上周三下午三点,我在工位上盯着一个线上告警发呆。
告警内容很简单,用户下单偶发超时,错误率从0.01%飙到了2%。听起来不高对吧?但这是我们支付链路,2%意味着每一百个下单用户里有两个付不了钱。

我点进去一看,报错栈指向一个叫 BatchQueryService 的类,第47行。我打开代码看了一眼,愣住了。
这段代码,是我两周前用 Copilot 写的。
准确地说,是我用 Copilot 写完、Code Review 时同事看了觉得「逻辑没问题」、我自己也觉得「挺漂亮」的那段代码。
它跑了两周才出事。
两周。你品品这个时间跨度。
事情的起因是这样的。当时我在做一个批量查询用户订单状态的接口,逻辑不复杂,就是调用订单服务的批量接口拿到数据,然后组装返回。Copilot 给我补全的代码用了一个 CompletableFuture.allOf() 的写法,把多个查询并发执行,最后汇总结果。
代码写出来那一刻,我内心是满意的。结构清晰,用了并发优化,注释也到位,连变量命名都很规范。我当时还跟同事开玩笑说,「AI 写的代码比我自己的还体面。」
同事看了一眼说,「确实不错,用的 allOf 挺标准的。」
我们就都没多想,合进去了。
结果呢?两周后出事了。
排查了大半天,最后发现问题出在一个特别隐蔽的地方。CompletableFuture.allOf() 本身没问题,但 Copilot 给我补全的异常处理逻辑里,当其中一个 Future 抛出异常时,它用了一个 getNow(null) 来获取其他已完成 Future 的结果。
getNow(null) 这个方法,如果 Future 还没完成,会直接返回你传的默认值,也就是 null。正常情况下不会有问题,因为 allOf 等的就是全部完成嘛。但在异常场景下,allOf 的 join() 会抛出第一个异常,而此时其他某些 Future 可能还在执行中,getNow 拿到的就是 null,下游拿到 null 去做业务逻辑,就炸了。
这个 bug 妙就妙在,它不是一个语法错误,不是一个空指针这种一眼能看出来的问题。它是一个「在特定并发时序下才会触发」的逻辑缺陷。平时压测跑不出来,只有线上流量够大、并发够高的时候,才会偶尔冒出来。
我当时蹲在工位上改完这个 bug,盯着屏幕发了好一会儿呆。
不是因为 bug 难改,而是因为我在想一个问题。
如果这段代码是我自己写的,我会用 getNow 吗?大概率不会。我自己写的话,可能会用 get() 加超时控制,或者干脆用 join() 等它完成。因为我自己写代码的时候,对异常路径有一种本能的警惕,会下意识地想「如果这里炸了会怎样」。
但 Copilot 给我的代码太「漂亮」了。漂亮到我没有产生任何怀疑。它看起来就是一个标准的并发编程范式,allOf + getNow,在 StackOverflow 上能看到无数类似的写法。我甚至觉得,如果我自己写,可能还写不出这么「教科书」的版本。
就是这种「它写得太像对的了」的感觉,让我放下了警惕。
后来我复盘了一下这件事,发现一个很有意思的规律。
AI 生成的代码,如果写得烂,你一眼就能看出来,反而安全。变量名乱起,逻辑一塌糊涂,你肯定会改。真正危险的是那种「看起来正确、运行起来也正确、但边界情况下会出问题」的代码。
这种代码的特征是,它在99%的场景下都是对的。它能通过 Code Review,能通过单测,能通过压测。但那1%的边缘场景,它处理得不对,而且不对的方式非常隐蔽。
说实话我后来又仔细看了那段 AI 生成的代码,发现 getNow(null) 这个用法,在 Java 的官方文档里确实是一个合法的 API。它不是错误用法,只是不适合这个场景。你不能说 AI 写错了,它只是写了一个「在理论层面完全正确,但在工程实践中有隐患」的版本。
这让我想到一个更大一点的事情。
我们这一代程序员,可能是历史上第一批需要跟「比自己更会写代码」的工具共事的人。Copilot、Cursor、Claude Code,这些工具写出来的代码,在「表面质量」上已经超过了大多数初中级程序员。变量命名规范,注释清晰,结构合理,甚至连设计模式都用得很到位。
但这恰恰是最危险的地方。
我一直觉得,程序员审查代码时最重要的能力不是看懂这段代码在做什么,而是判断这段代码在什么情况下会不工作。这种能力不是靠语法知识,是靠踩过的坑、踩过坑之后形成的直觉。
AI 没有踩过坑。它的代码是从海量开源项目里学来的统计规律,它知道「在大多数情况下 getNow 是安全的」,但它不知道「在你这个特定的业务场景下,getNow(null) 会变成一颗定时炸弹」。
那问题来了,我们该怎么跟 AI 协作?
我自己摸索出来的经验是这样。
让 AI 帮你写「骨架」,你自己填「血肉」。什么意思呢?就是那些标准化的、跟业务逻辑无关的部分,放心交给 AI。比如一个 Controller 的 CRUD 脚手架,一个 DTO 的转换逻辑,一个配置类的模板代码。这些代码的正确性可以靠编译和单测来保证,AI 写出来基本不会有问题。
但涉及到并发控制、异常处理、事务边界、数据一致性这些地方,你得自己来。不是说 AI 写不了这些,而是这些地方出了问题,排查成本太高了。你得用你自己踩坑换来的直觉去判断「这里的异常该怎么处理」「这个超时时间设多少合适」「如果下游挂了我这边要不要降级」。
说得再直白一点,AI 写代码像刚毕业的985学生,基础扎实、学习能力强、代码规范,但没有上过战场。你不会让一个刚毕业的新人去写支付链路的核心逻辑,对吧?不是他不聪明,是他没见过那些诡异的线上场景。
同样的道理,你也不应该让 AI 独自负责那些「出了问题要通宵排查」的代码。
当然我也知道,很多人会说「我用 AI 写代码效率提升了好几倍」。这话我不否认,效率确实提升了。但效率提升和质量保证是两件事。你用 AI 花10分钟写出来的代码,可能要花30分钟去 review 和测试。这30分钟你省不掉,因为它省掉的代价可能是线上的 P0 事故。
我有时候觉得,现在行业里对 AI 编程的态度有点像当年自动驾驶刚出来那阵子。大家都在说「AI 已经能自己开车了」,但没人提 L2 级自动驾驶最危险的地方恰恰是「它让你以为可以不看路了」。
AI 写代码也是一样的。最危险的不是它写错了你没发现,而是它写对了99次之后,你在第100次的时候不再仔细看了。
那次线上事故之后,我给自己定了一个规矩。
AI 生成的代码,凡是我没有逐行理解的,一律不提交。不是不信任 AI,是不信任那个偷懒的自己。
这个规矩执行起来其实挺累的。有时候 Copilot 给你补全了一段很长的代码,你逐行看一遍,发现确实没问题,会有一种「这时间不是白花了吗」的感觉。但下一次,你可能就会在第三行发现一个微妙的问题,然后庆幸自己没有直接 Tab。
做程序员这些年,我越来越觉得写代码最重要的不是「快」,而是「确定」。你写的每一行代码,你都得知道它在干什么,出了问题你怎么排查。AI 能帮你快速生成一个「看起来对」的版本,但「确定它真的对」这件事,只能你自己来。
就像我那个用 getNow 的 bug,如果我在提交前多想一步「如果某个 Future 还没完成会怎样」,就不会出这档子事了。
但人就是这样,面对一段看起来完美无缺的代码,你很难保持怀疑。
这大概就是我们这代程序员要修炼的新功夫吧。不是学怎么用 AI 写代码,而是学怎么在 AI 写的代码面前,保持一个老程序员该有的警觉。
夜雨聆风