1. 为什么AI写的代码总在“看起来没问题”的地方翻车
用AI写代码这件事,现在几乎成了日常。不管是补全一个函数、生成一段正则,还是让它从零搭一个CRUD接口,速度确实快得离谱。但真正把AI生成的代码放进项目里跑起来,很多人都会经历同一个心理曲线:第一眼觉得“写得真漂亮”,第二眼觉得“好像哪里不对”,第三眼就变成了“这玩意儿怎么敢这么写”。
我自己的体会是,AI生成的代码有一个非常鲜明的特征:它在“语法正确”这件事上几乎不会犯错,但在“语义正确”这件事上经常偷懒。换句话说,它写出来的东西编译能过、单元测试可能也能过,但一旦放到真实的数据流、真实的并发场景、真实的边界条件下,坑就一个接一个冒出来。
这篇文章想聊的就是这些坑到底埋在哪里。不是泛泛地说“AI代码要小心”,而是把最容易出问题的几类场景拆开,讲清楚它为什么会这么写、我们该怎么改、以及怎么在流程上提前拦住。适合已经在用AI辅助编程、但还没形成一套稳定审查习惯的开发者,也适合团队里需要制定AI代码规范的技术负责人。
先给一个总体判断:AI生成代码的高风险区,集中在边界条件、错误处理、并发与状态、依赖与版本、安全与权限这五个方向。下面逐个拆。
2. 边界条件:AI最擅长写“理想路径”,最不擅长写“万一”
2.1 空值、空集合、零值:它默认世界是完美的
让AI写一个“从用户列表里找出年龄最大的用户”的函数,它大概率会给你这样的东西:
def find_oldest_user(users): oldest = users[0] for user in users: if user.age > oldest.age: oldest = user return oldest逻辑没错,但users为空列表时直接IndexError。你如果追问“空列表怎么办”,它会立刻补上判断,态度还很好。问题在于:你不问,它就不写。这不是它不会,而是它的默认假设是“输入是合理的”。
这个行为模式在真实项目里非常危险。因为真实世界的数据永远是不干净的:接口返回空数组、数据库查不到记录、用户没填某个字段、配置项缺失。AI生成的代码往往把这些情况全部交给“调用方保证”,而调用方往往也是AI写的,于是整条链路都在假设“不会有空值”。
我的做法是:凡是AI生成的函数,第一件事就是看它的入参有没有做防御。具体检查这几类:
- 列表/数组:是否处理了空集合
- 字典/对象:是否处理了键不存在
- 字符串:是否处理了空串和
None - 数值:是否处理了0、负数、超大值
- 时间:是否处理了时区、闰秒、跨年
提示:不要指望AI主动写防御性代码。你要在提示词里明确说“对所有入参做空值和类型检查”,否则它默认走理想路径。
2.2 循环边界:<还是<=,它经常“看起来对”
AI写循环的时候,最容易出问题的是边界判断。比如让AI写一个“把数组分成每3个一组”的函数,它可能写成:
def chunk(lst, size=3): result = [] for i in range(0, len(lst), size): result.append(lst[i:i+size]) return result这个其实是对的。但换成“查找第一个大于目标值的元素位置”这种二分变体,AI就经常在mid的更新上出错,写出死循环或者漏掉边界元素。原因是二分查找的边界条件有太多等价写法,AI在训练数据里见过各种版本,生成时可能把两种写法的片段拼在一起。
实测下来,AI生成的循环代码,我重点看三个地方:
- 循环变量的初始值和终止条件是否匹配
- 循环体内是否有可能跳过或重复处理某个元素
- 是否存在死循环风险(尤其是
while循环)
2.3 分页与偏移:AI经常忘记“最后一页”
分页逻辑是边界条件的重灾区。AI写分页查询时,经常只考虑“中间页”,忘记处理:
- 总数为0时返回什么
- 请求页码超过总页数时返回什么
- 最后一页数据不足
page_size时的行为 - 偏移量计算是否用了
(page-1)*size还是page*size
这些细节AI不会主动覆盖,因为它生成的代码通常是“典型情况”的缩影。你得自己补。
3. 错误处理:AI写的try/except经常是“假装在处理”
3.1 吞异常:except: pass的变体满天飞
AI生成错误处理代码时,有一个非常顽固的倾向:把异常捕获了,但不做任何有意义的事。常见形态包括:
try: result = do_something() except Exception: pass或者稍微“好看”一点:
try: result = do_something() except Exception as e: print(e)这两种在真实项目里都是灾难。第一种让错误彻底消失,第二种把错误打到标准输出,在生产环境里等于没打。AI这么写的原因很简单:训练数据里有大量示例代码、教学代码、快速原型代码,这些代码的错误处理本来就是“占位”性质的。
我的处理原则是:AI生成的每一个except块,都要问一句“这里到底应该怎么处理”。是重试、是降级、是向上抛、是记录日志后继续,还是直接让程序崩溃?这个决策AI做不了,必须人来定。
3.2 异常类型太宽:Exception一把抓
AI很少写具体的异常类型,基本都是except Exception。这在快速开发时没问题,但在需要区分“可重试错误”和“不可重试错误”的场景下就是坑。比如网络请求,超时应该重试,但参数错误重试一万次也没用。AI生成的代码往往把两者混在一起。
一个实用的改法是:把except Exception拆成具体的异常类型,至少区分出:
| 异常类型 | 典型场景 | 处理策略 |
|---|---|---|
| 超时/连接错误 | 网络请求 | 重试+退避 |
| 参数/格式错误 | 输入校验 | 直接返回错误 |
| 资源不存在 | 查询操作 | 返回空或默认值 |
| 权限错误 | 鉴权操作 | 拒绝并记录 |
| 未知异常 | 兜底 | 记录完整堆栈并上报 |
3.3 错误信息丢失:raise的时候把上下文丢了
AI在重新抛出异常时,经常写成:
try: process(data) except Exception as e: raise ValueError("处理失败")这样原始的异常堆栈就丢了,排查问题时只能看到“处理失败”,完全不知道底层发生了什么。正确的做法是用raise ... from e保留因果链,或者至少把原始异常信息带上。
4. 并发与状态:AI对“同时发生”这件事缺乏直觉
4.1 共享变量:它默认代码是串行执行的
让AI写一个计数器、缓存或者连接池,它很可能写出没有加锁的版本。比如:
class Counter: def __init__(self): self.count = 0 def increment(self): self.count += 1单线程没问题,多线程下+=不是原子操作,计数会丢。AI不会主动加锁,因为它的默认执行模型是“一行一行顺序执行”。
类似的问题还出现在:
- 全局字典的读写
- 文件的同时写入
- 数据库连接的共享使用
- 缓存的并发更新
4.2 异步代码:await漏掉或者乱放
AI写异步代码时,常见问题是:
- 该
await的地方没await,导致协程没执行 - 不该
await的地方加了await,导致性能下降 - 在同步函数里调用异步函数,或者反过来
- 忘记处理
asyncio.gather的异常
尤其是asyncio.gather,AI经常写成不带return_exceptions=True的版本,导致一个任务失败整个批次都挂掉。
4.3 状态机与顺序依赖:AI假设“步骤不会乱”
如果业务逻辑有严格的顺序依赖,比如“先创建订单,再扣库存,再发消息”,AI生成的代码可能把这些步骤写成并行的,或者没有处理中间步骤失败的回滚。它理解的是“这些事都要做”,而不是“这些事必须按顺序做,且前面失败后面不能做”。
5. 依赖与版本:AI的“知识截止”是个隐形炸弹
5.1 库版本对不上:它用的是“记忆里的API”
AI的训练数据有截止时间,它生成的代码可能用的是某个库的旧版本API。比如某个函数在新版本里改了签名、某个参数被废弃、某个模块被拆分。你直接跑,轻则警告,重则报错。
我遇到过好几次:AI生成的代码用了某个库的写法,但当前安装的版本已经不支持了。解决办法是生成后先查官方文档确认API是否仍然有效,尤其是那些更新频繁的库。
5.2 依赖缺失:它不会告诉你“需要先装什么”
AI生成的代码片段经常默认某些包已经安装,但不会告诉你包名和版本。比如它用了requests但没提安装,用了pandas但没提版本要求。在团队协作里,这会导致“在我机器上能跑”的经典问题。
5.3 隐式依赖:它引入了你没注意的东西
有时候AI会引入一些“顺手”的依赖,比如为了一个简单的功能引入了一个大库,或者用了一个不常见的包。这些依赖会增加项目的维护成本和安全隐患。生成代码后,检查import列表是一个很好的习惯。
6. 安全与权限:AI不会主动帮你防攻击
6.1 SQL注入:字符串拼接是重灾区
AI生成数据库查询代码时,如果提示词里没有明确说“用参数化查询”,它有可能写出字符串拼接的版本:
query = f"SELECT * FROM users WHERE name = '{name}'"这是经典的SQL注入入口。AI知道参数化查询,但它不一定会主动用。你必须明确要求。
6.2 命令注入:os.system和subprocess的坑
类似地,AI生成执行系统命令的代码时,可能直接把用户输入拼进命令里。这在任何涉及外部输入的场景下都是高危。
6.3 权限校验:它默认“调用方已经检查过了”
AI生成的接口代码,经常缺少权限校验。它假设“能调用这个函数的人一定是有权限的”。但在真实系统里,权限校验必须在服务端做,不能依赖调用方。
6.4 敏感信息硬编码:密钥、密码、token
AI有时候会把配置信息直接写死在代码里,比如数据库密码、API密钥。这在示例代码里无所谓,但如果你直接复制到项目里就麻烦了。
7. 怎么在流程上拦住这些坑:我自己的审查清单
7.1 生成阶段:提示词里就要把要求说清楚
与其生成后再改,不如在提示词里就明确要求。我常用的提示词模板包括:
- “对所有入参做空值和类型检查”
- “用参数化查询,不要拼接SQL”
- “异常处理要区分具体类型,不要用裸
except” - “如果有并发访问,加锁或使用线程安全的数据结构”
- “不要硬编码任何密钥或配置”
- “注明需要的依赖和版本”
7.2 审查阶段:按风险等级过一遍
不是所有AI代码都需要同等强度的审查。我一般按这个优先级:
| 风险等级 | 代码类型 | 审查重点 |
|---|---|---|
| 高 | 涉及资金、权限、外部输入 | 安全、边界、错误处理 |
| 中 | 业务逻辑、数据处理 | 边界、状态、并发 |
| 低 | 工具函数、格式化 | 边界、依赖 |
7.3 测试阶段:专门针对AI代码写测试
AI生成的代码,测试要重点覆盖:
- 空输入、极端值、非法输入
- 并发场景(如果涉及)
- 异常路径(不只是正常路径)
- 依赖缺失或版本不匹配的情况
7.4 运行阶段:加监控和日志
即使审查过了,AI代码上线后也要有监控。重点看:
- 异常率是否异常升高
- 是否有未捕获的异常
- 性能指标是否偏离预期
- 日志里是否有“吞掉”的错误
8. 几个我踩过的真实坑和修复过程
8.1 一个“看起来对”的缓存,把内存吃爆了
之前让AI写一个简单的内存缓存,它给了这样的实现:
cache = {} def get_data(key): if key not in cache: cache[key] = fetch_from_db(key) return cache[key]逻辑没问题,但没有过期机制、没有容量上限。跑了一段时间后,缓存越来越大,最后OOM。修复方案是加上TTL和LRU淘汰。这个坑的教训是:AI生成的“简单实现”往往缺少生产环境必需的约束。
8.2 一个异步任务,因为漏了await一直没执行
AI生成了一段异步代码,其中有个地方调用了协程函数但没加await,导致那个协程根本没被执行,但代码也不报错。排查了很久才发现。修复就是补上await。这个坑的教训是:异步代码要特别检查每个协程调用是否被正确等待。
8.3 一个分页接口,最后一页返回了重复数据
AI写的分页逻辑用了offset = page * size而不是(page-1) * size,导致第一页和第二页有重叠。这种错误在单元测试里如果只测中间页是发现不了的。修复就是改偏移量计算,并补上边界测试。
9. 把AI当成“写得很快的初级工程师”来用
说到底,AI生成代码的问题,本质上和带一个初级工程师的问题很像:它写得快、语法熟、但缺乏对真实世界的敬畏。它不知道数据会脏、网络会断、用户会乱输入、并发会打架。
所以我的心态是:AI负责“写出来”,我负责“让它能活下来”。生成只是起点,审查、测试、加固才是真正的工作量所在。把这几类坑记在心里,每次生成后按清单过一遍,能省掉大量后期排查的时间。
最后分享一个我一直在用的小习惯:每次AI生成完代码,先不急着运行,而是花两分钟问自己三个问题——输入会不会是空的?出错了会怎样?两个人同时调用会怎样?这三个问题能拦住大部分低级但致命的坑。