AI生成的代码为什么经常"看起来正确"?
我统计过自己过去半年退回的 agent 产出,粗略分了一下类,发现一个反常识的结论:真正一眼就能看出写错的不超过两成,剩下的全是"读起来通顺、跑起来也不报错、过了两天才炸"的那一类。JetBrains 的调研里 90% 的专业开发者每周至少用一次 AI 编码 agent,这意味着这类隐性缺陷正在以极高的频率进入代码库。
要处理它,先得知道它长什么样。下面这六类是我遇到最多的,每一类都有对应的验证手段。
类型被悄悄放宽了
最典型的是用 any、类型断言、或者一个宽得离谱的联合类型把错误推到运行时。函数签名写着 Record<string, unknown>,函数体里直接当 string 用;或者给返回值加一句 as User[],编译就过了,但那个数组里可能混着 null。
验证手段很简单:开严格模式,把 any 和断言的数目当成代码评审的硬指标。更彻底一点,用类型层面的"不可能状态不可表示"来卡——把可选字段改成带标签的联合,让编译器替你挡住那些没处理的分支。类型不是装饰,它是唯一能在不看运行时的情况下否掉一段代码的检查器。
分支只写了 happy path
空数组、单元素数组、超长输入、重复键、null、undefined、网络超时、并发同一资源——这些情况在生成的代码里经常被省略,或者被一句 if (!data) return 笼统吞掉。
这里有个很实用的验证方法叫属性测试:不写具体用例,而是声明"对任意输入,输出应当满足什么性质",让框架自动生成几百组随机输入去找反例。排序函数就断言"输出长度等于输入长度、输出有序、输出是输入的一个排列";编解码就断言"decode(encode(x)) === x"。这类断言特别适合抓 AI 生成代码的漏洞,因为它不假设实现细节,只假设语义,而语义恰好是模型最容易漏的地方。
手写几条具体用例也能兜住一部分,但覆盖不到你没想到的输入。属性测试的价值在于它会去试那些你不会写进用例的组合:长度为一的数组、全是重复元素的数组、带不可见字符的字符串、刚好等于边界值的数字。模型生成的循环和下标计算在这些输入上翻车的概率远高于常规输入。
前端的边界情况——异步竞态、闭包捕获旧值、事件监听器没解绑——在 JavaScript 教程 的异步章节里有系统梳理,值得先补一遍。
数值和编码的默认假设
金额计算是重灾区。生成的代码里出现 0.1 + 0.2 === 0.3 为 false 这种事已经不新鲜了,更隐蔽的是"取整方式选错了":汇率换算该用银行家舍入还是四舍五入,分账时余数该给谁,这些模型不会问你,它会挑一个最常见的。金额一律走整数分或者十进制库,别让浮点数参与最终取值——Decimal.js 这类库存在的意义就是接管这部分语义。
时区和字符编码同理。跨月的时间差计算、“上个月最后一天”、UTF-16 里 emoji 占两个码元导致 slice 切出半个字符,都是"本地跑没问题、上线就出问题"的经典位置。
API 签名记错了
方法名是对的,参数顺序错了;库存在,但那是 3.x 的 API,你装的是 5.x;或者干脆是一个从没存在过的方法。这是幻觉最直白的一种,也最好查——但前提是你要真的去查。
我的做法是建一条硬规则:凡是 agent 引入的新依赖、新 API,一律先打开官方文档确认签名和最低版本,再把它写进 package.json。这一步不能省,也不能让 agent 自己确认——它确认自己产出的时候,倾向于给出肯定的答案。
单次调用对,放进循环或并发就错
这类最难发现,因为每一段单独看都是对的。一个函数在单次调用下返回正确结果,放进循环里就变成了 N+1 查询;一段状态更新单独跑没问题,两个请求同时进来就互相覆盖。
验证要靠场景级的测试,不是单元级的:把这段代码放到真实的批量和并发路径上跑一遍,看数据库里最终状态对不对、请求数是不是符合预期。涉及客户端重计算的场景,比如编解码、图片处理,把同一份逻辑用 WebAssembly 编译一份跑对照,两边结果必须逐字节一致——这个对照能非常有效地抓出"语义漂移"。
语义漂移:布尔方向反了
最后这一类最气人。disabled 被当成 enabled 用,isActive 和 isDisabled 混着出现在同一个判断里,函数名叫 validate 但实际会修改入参。代码风格完全正常,命名也很合理,就是意思反了。
对付它只有一个办法:用固定输入的快照。准备几组你手工算过预期结果的输入,把输出存成快照文件,每次改动都跑一遍。快照不关心实现漂到哪去,只关心结果有没有变。让 agent 自己写测试来验自己的实现是无效的,两边来自同一个模型、同一套假设,会一起错——测试必须由你或者由另一套独立实现来定义。
接进项目之前的那道工序
我现在的流程是固定的:先自己写测试(或者至少写下预期行为),再让 agent 实现,然后跑类型检查、跑属性测试、跑快照、手工验证一遍边界清单,最后才读它的实现。读实现放后面,是因为一旦先看了它写的代码,你就会被它的思路带着走,很难再独立判断对不对。
这套流程多花的十分钟,换掉的是三天后线上排查的两个小时。
JetBrains 数据引自其开发者生态调研报告中关于 AI 编码 agent 使用频率的部分。