跳到主要内容

让 AI 写代码,最怕的不是错,而是看起来对

· 阅读需 4 分钟
一介布衣
全栈开发者 / 技术写作者

补档说明:本文属于「AI 工程落地周记」系列,计划发布时间为 2025-07-02 09:10。当前先保留为草稿,后续补充真实案例、代码片段和复盘细节后再发布。

我越来越觉得,AI 写代码最大的风险不是它会明显报错,而是它经常会生成一段“看起来特别像对的东西”。这种代码危险的地方就在于,它不会像语法错误那样立刻被发现,反而更容易一路混进 review、测试,最后在边角场景里才炸开。

我印象最深的一次,是一个缓存键生成函数。AI 给出的实现非常自然,变量名也不错,测试样例甚至都能过。但真正上线后我们才发现,它把两个不同参数集在某些顺序下算成了同一个 key,导致缓存串了。

这类问题最麻烦的,不是它难修,而是它在第一次看代码时太像“经验丰富的人写出来的东西”了。

为什么“看起来对”比“明显错”更危险

明显错误其实很好处理:

  • 编译不过
  • 测试挂掉
  • linter 报警

团队会立刻停下来修。

真正危险的是下面这种情况:

  • 代码能跑
  • 名字看起来合理
  • 路径样例也通过
  • 只有在隐藏边界条件下才会露馅

这时候人最容易放松,因为系统没有给出任何强提醒。

一个典型例子

当时那段缓存键代码大概是这种味道:

function buildCacheKey(params: Record<string, string>) {
return Object.values(params).join(':')
}

看起来非常简洁,甚至很“优雅”。问题是,它只拼值,不拼字段名,也不保证字段顺序。于是:

  • { city: 'beijing', type: 'weather' }
  • { type: 'beijing', city: 'weather' }

在某些情况下就可能撞出奇怪的结果。

正确实现当然也不复杂:

function buildCacheKey(params: Record<string, string>) {
return Object.entries(params)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}=${value}`)
.join('&')
}

但重点不是“正确答案长什么样”,而是第一版错误实现太像正确答案,导致大家一开始都没警觉。

为什么 AI 特别容易生成这种代码

因为 AI 擅长的是:

  • 局部模式
  • 表面合理性
  • 常见写法拼装

它不天然擅长的是:

  • 你这个业务里真正危险的边界条件
  • 哪个约束是项目特有的
  • 哪个地方一旦错了会造成系统级副作用

所以它特别容易生成“局部逻辑顺、系统边界弱”的代码。

我后来怎么降低这种风险

1. 先让 AI 写测试点,不急着写实现

很多时候让 AI 先列“这段代码最容易在哪些边界条件下出错”,比直接让它写实现更有价值。

2. 人审时不只看代码可读性,还要问边界

我现在看 AI 代码时,会强迫自己额外问:

  • 这段代码在重复输入下稳定吗?
  • 在顺序变化下稳定吗?
  • 在缺字段或空值时稳定吗?
  • 在副作用场景里会不会重复执行?

3. 对“过于简洁”的实现更警惕

AI 特别喜欢给出很顺手、很短、很像最佳实践的代码。但越是这种一眼看上去很漂亮的实现,我现在反而越会多看两遍。

一个更有用的 review 清单

只要是 AI 生成的逻辑代码,我现在至少会过这 5 个问题:

  1. 它是不是只对示例成立,而不是对边界成立?
  2. 它有没有隐含依赖输入顺序?
  3. 它是否忽略了空值、重复值或异常值?
  4. 如果涉及副作用,是否考虑了幂等?
  5. 它是不是只是“像正确答案”,而不是真的满足业务约束?

总结

让 AI 写代码,最怕的不是明显错误,而是那种足够像正确实现、足够整洁、足够能通过浅层验证的“伪正确代码”。

真正有效的防线,不是单纯更信任或更不信任 AI,而是把 review 的重点从“写法顺不顺”转移到“边界和约束有没有真的被满足”。只有这样,AI 代码才更像助力,而不是隐形风险放大器。