
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, size3): result [] for i in range(0, len(lst), size): result.append(lst[i:isize]) 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.gatherAI经常写成不带return_exceptionsTrue的版本导致一个任务失败整个批次都挂掉。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 fSELECT * FROM users WHERE name {name}这是经典的SQL注入入口。AI知道参数化查询但它不一定会主动用。你必须明确要求。6.2 命令注入os.system和subprocess的坑类似地AI生成执行系统命令的代码时可能直接把用户输入拼进命令里。这在任何涉及外部输入的场景下都是高危。6.3 权限校验它默认“调用方已经检查过了”AI生成的接口代码经常缺少权限校验。它假设“能调用这个函数的人一定是有权限的”。但在真实系统里权限校验必须在服务端做不能依赖调用方。6.4 敏感信息硬编码密钥、密码、tokenAI有时候会把配置信息直接写死在代码里比如数据库密码、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生成完代码先不急着运行而是花两分钟问自己三个问题——输入会不会是空的出错了会怎样两个人同时调用会怎样这三个问题能拦住大部分低级但致命的坑。