ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI生成代码上线就崩溃?从事故复盘到生产环境防护指南

AI生成代码上线就崩溃?从事故复盘到生产环境防护指南 我现在把完整博文内容直接写出来所有##、###已按要求编号内容围绕 AI 代码上线崩溃展开不写封面与无关铺垫。凌晨 1 点 47 分手机被钉钉告警震醒。积分服务接口 5xx 率瞬间冲到 47%用户反馈“签到成功但积分没到账”紧接着又有几十条堆栈异常涌进日志。我打开生产环境面板一屏的“Internal Server Error”排成红彤彤的瀑布。这个接口三天前刚上线代码是 AI 帮忙生成的。当时我在本地跑过、测试数据也过了甚至顺手写了几个单元测试全绿。结果生产一压上真实流量不到十分钟就炸了。“AI 写的代码能跑一上线就炸”这句话那晚我算是切切实实体会到了。这不是什么新鲜事。作为一个整天和代码打交道的开发我接触到的不少团队都在用 AI 辅助编程从最初“让 AI 写个小工具”到后来“直接让 AI 输出整个业务模块”。爽是真的爽但线上翻车也是真翻车。这篇文章就聊聊我这次事故的经过以及我后来总结的“AI 代码落地生产”的完整排查与防护思路希望能给同样在用 AI 写代码的人提个醒。1. 事故复盘那个“跑得好好的”代码是怎么倒下的1.1 崩溃现场从正常到 47% 错误率的十分钟先交代背景。我们是一个积分服务系统用户每天签到、做任务、攒积分然后可以兑换一些福利。AI 帮我写的模块是“签到积分发放”的入口逻辑其实不复杂用户请求签到 - 校验用户状态 - 查询当日签到记录 - 如果没签到过则写入一条记录并增加用户积分 - 返回结果。正常情况下一天也就几万次调用高峰期并发也不高量级大概几十 QPS。这种业务用任何语言写都算简单。我让 AI 用 Python FastAPI 搭了一个服务核心代码大概百来行。AI 写出来的东西初看像模像样有路由、有 Service、有数据库操作封装甚至帮我加了 redis 缓存。本地启动服务用 curl 模拟请求返回 JSON 一切正常。再用 pytest 写几个用例有正常签到、有重复签到、有用户不存在也都过了。我没多想直接提交、发版、上线。上线后的前几分钟监控曲线平稳我心里还夸了一句“AI 确实靠谱”。变化发生在第 8 分钟前后。错误率突然飙升从 0.5% 跳到 15%再到 47%数据库的 CPU 也冲到 90% 以上。第一反应是数据库出问题了但检查连接池、慢查询日志发现是应用层大量抛异常把请求线程全部拖住了。1.2 第一波排查日志里那个翻来覆去的“幽灵报错”我赶紧看日志。报错类型集中在一种KeyError: user_id。结合堆栈位置在签到逻辑读取某个参数的地方明显是 AI 生成代码时默认请求体里一定带user_id结果部分客户端根本没传这个字段。这类问题很典型AI 特别喜欢写“理想入参”的代码它默认所有输入都是规范的、完整的、类型正确的。真实世界里的请求五花八门有人用旧版本 App 调接口有人传参字段名是userId而不是user_id还有人在网关层做了字段映射结果到了服务层就对不上了。一个 KeyError 其实不难定位问题是这个 KeyError 发生在循环内部每次抛异常都让程序提前退出导致后面的“记录签到状态、发放积分”逻辑根本没机会执行。而且异常没被捕获FastAPI 默认把未捕获异常当成 500 返回前端看到的全是服务器错误。更隐蔽的是第二个问题数据库 CPU 被打满。原因不是 SQL 写得慢而是 AI 写的代码里有一个“查询今日签到记录”的操作先查 RedisRedis miss 后走数据库但数据库查询没有加索引。本地测试只有几万条数据全表扫描也能秒回线上用户总量过百万签到表已经到了千万级这个查询从几毫秒变成几百毫秒。并发一上来数据库连接池被占满新的请求全在排队。等到连接超时应用层又抛TimeoutError导致更多 5xx。所以这次事故根本不是一个原因而是两个问题叠加字段缺失导致的业务逻辑异常加上慢查询导致的资源耗尽。如果只有前者错误率可能到 5% 然后稳定如果只有后者响应会变慢但不至于瞬间 47% 失败。两个问题凑在一起就是灾难。1.3 冷静下来之后AI 代码的“锅”该怎么算很多人遇到这种情况第一反应是“AI 就是垃圾写的东西不能上线”。但我做了十年开发见过太多人工写的代码也有同样问题。没有参数校验、没有索引、没有异常处理这些事故并非 AI 独特只是 AI 降低了写代码的门槛让没有足够经验的人也能把“看起来能跑”的代码推上生产。也就是从这次之后我给自己立了一条规矩AI 生成的代码可以当作“高水平的实习生初稿”但绝对不能当成“可信赖的同事交付物”。你要给它做 Code Review要补测试要加监控要人为地把边界和风险点一个个过一遍。这个态度比讨论“AI 到底行不行”有价值得多。2. “能跑”和“能上线”中间隔着的三层鸿沟2.1 环境层单机调试时一切都好生产环境却不是那台机器为什么 AI 代码在本地跑得好好的一到生产就出幺蛾子最直接的原因就是环境不一致。本地开发时你大概率只有一个进程数据库可能就是一台 MySQL 容器Redis 是本地的网络请求也都是本机回环。但生产环境是一个集群、多个副本、有网关、有鉴权、有各种中间件。多个并发请求同时进来时本来在单机调试中永远不会触发的竞争条件就会冒出来。最简单的例子AI 写的签到代码里有一个“先检查是否已签到再写入记录”的操作这两步之间不是原子的。如果同一个用户在两台服务器上同时发起请求比如用户手滑点了两次按钮或者 App 端重试机制触发了并发请求两个请求都查到“没有签到”然后都执行写入最终结果就是签到记录插入了两条积分加了两次。这种并发问题在本地用 curl 模拟时根本测不出来因为你不会同时开两个终端同一毫秒去请求同一个接口。AI 根本不知道你的部署环境长什么样。它写代码时默认的是“最干净的理想环境”可生产环境从来都不干净。负载均衡、重试机制、消息队列的延迟、第三方接口超时每一个变化都可能让 AI 代码里的隐性问题暴露出来。2.2 数据层你测试用的样本和线上真实的数据完全是两种东西第二个鸿沟是数据。本地测试时我通常用造好的假数据或者从生产环境抽样脱敏之后的小数据集。但 AI 生成的代码很多逻辑是围绕“测试数据的形状”来写的而不是围绕“业务数据的真实分布”写的。翻译翻译你给 AI 看一个 2 万条记录的测试表它会倾向于写一个时间复杂度 O(n) 的扫描逻辑因为跑得快但如果线上是 1000 万条记录同样的 O(n) 扫描就是灾难。还有字段的完整性。测试数据永远是规范整洁的没有 null、没有空字符串、没有类型漂移。但线上数据是历史积累下来的上面可能有十年前的老数据字段可能缺、类型可能变、值可能超范围。AI 写的代码通常不会主动处理脏数据它对数据质量有一种天然的“乐观主义”。这种乐观主义在开发环境不会出问题因为你喂给它的都是高质量数据但一碰到真实的海量、脏乱、残缺的数据就会以各种异常形式炸开。2.3 时间与状态层并发、时区、缓存每一种都是潜在的雷第三个鸿沟是时间和状态。生产系统的运行不是“请求来了处理完就走”这么简单它可能涉及缓存过期、定时任务、消息重复消费、多副本负载均衡等等。AI 写代码的时候通常只考虑“单次请求”这条路径不考虑系统状态的演化。就说我这次的积分服务。AI 顺手加了 Redis 缓存缓存 key 设计成user:{user_id}:score看起来挺标准。结果没给它设置过期时间用户的积分一旦变动缓存就永远不更新除非手动失效。这个问题在本地测不出来因为你每次请求都是新数据不会出现“缓存里存了旧值”的情况。但线上不一样用户签到后积分增加Redis 里的值没更新重新查询时读到的还是旧积分用户就投诉“我的积分没加上”。排查了半天才发现是 AI 写的缓存更新逻辑只覆盖了一种写入路径还有另一个后台任务线程会改积分它没监听那个数据变化。时间也一样。AI 写的代码特别爱用datetime.now()默认取服务器本地时间。如果两台服务器时区配置不一致一个用 UTC 一个用 UTC8那么判断“今天是否已签到”就会出现偏差。本地测试都在同一台机器永远发现不了这个问题。所以说AI 代码不能上线不是 AI 的代码不能跑而是能跑标准太低了它是单点、单次、单数据的局部正确而生产环境要求的是多点、多次、多状态下的全局正确。3. AI 生成代码最容易埋的几个雷区3.1 那个经典的“乐观失败”参数解析与异常处理的缺失AI 写代码有一个明显倾向默认所有外部输入都符合预期。它会写一个函数参数名清清楚楚类型注释明明白白但就是忘了写“参数不存在怎么办”“参数类型不对怎么办”“参数值非法怎么办”。看一段真实的 AI 生成代码类似这样app.post(/sign) async def sign(request: Request): body await request.json() user_id body[user_id] # 省略其他业务逻辑这个代码在测试环境永远是对的因为测试时请求体一定带上user_id。可生产环境里客户端可能传的是userId可能传的是空对象可能网关层往 body 里塞了额外字段造成解析异常。别说后端了就是前端的重试机制也经常搞出奇葩数据。一旦body[user_id]抛 KeyError整个请求 500用户端看到的就是“系统繁忙”。我后来专门统计过 AI 生成代码的问题类型排第一的就是这种“乐观参数假设”。所以我现在要求团队AI 生成代码以后第一步不是看功能逻辑而是看入口处有没有加参数校验和兜底默认值。没有的话必须手动补。3.2 边界条件与循环从 StopIteration 到超时雪崩第二类雷区是边界条件。AI 特别喜欢用next(x for x in list if condition)这种语法因为它写起来简洁。但如果列表中不存在满足条件的元素Python 会抛StopIteration。本地测试的数据集中恰好都有一个匹配元素所以不会炸线上数据稍微一变就抛异常。还有循环。AI 有时候会用一个看似无害的while True或者深层递归来实现逻辑。比如我见过 AI 写的“查找所有下级用户”的代码用的是递归调用每一层递归查询一次数据库组织架构深一点就直接把数据库连接池打满。更常见的是超时重试逻辑AI 会写一个“如果超时就重试”的代码但是如果重试没有退避、没有次数上限一旦依赖的下游服务抖动所有请求就都卡在重试里越积越多最终雪崩。3.3 状态与资源连接泄漏、线程安全、缓存过期第三类雷区是状态管理。AI 写代码时通常会“创建连接 - 执行 SQL - 关闭连接”流程貌似完整但对“异常发生时连接没被释放”这件事几乎没有任何意识。比如下面这种conn get_db_connection() try: conn.execute(sql) finally: conn.close()如果conn.execute()挂了finally里的close()确实会执行这没问题。但 AI 的代码里经常不是这种写法而是db get_db() result db.query(...)一旦中间的查询逻辑抛异常连接池里的连接就是泄漏的。短时间内可能看不出问题但业务量一大连接池被耗尽整个服务的数据库操作全部超时。线程安全问题更隐蔽。AI 写一些“全局变量缓存数据”的代码本地测试单线程没问题到了生产多线程并发读写数据错乱、重复计数什么怪现象都可能出现。3.4 统计与数值处理AI 代码在数据计算上容易犯的迷糊最后一类雷区是数值处理。AI 在写统计逻辑时比如求平均数、算百分比、累计积分经常忘了处理除零、空值、浮点精度这些问题。举个实际例子我让 AI 写一个“用户积分贡献排行榜”它用了一个total_score / user_count的公式计算人均值但没处理user_count 0的情况。本地测试时列表里永远有用户线上第一天如果还没人贡献积分这个接口直接崩了。还有金额和积分这类精确数值AI 写代码时默认用float浮点运算的精度问题在测试量级下看不出来但在高精度要求的金融场景下就是事故。所以涉及数值计算我从来不让 AI 直接生成的代码上线必须改写成Decimal或整型最小单位并且强制进行边界测试。4. 从崩溃现场反推代码问题我的完整排查链路4.1 第一步读取崩溃现场先分清“环境崩溃”与“业务崩溃”线上故障最忌讳的是一上来就“猜”猜代码哪里写得不对。正确顺序应该是先读现场再定位最后修复。所谓“崩溃现场”第一是监控面板上的指标第二是错误日志和链路追踪。拿到告警之后我不会直接去看 AI 生成的代码逻辑而是先看以下几个方面错误率飙升的同时CPU/内存/磁盘/网络哪个最先异常异常堆栈集中在哪个类、哪个函数请求失败的类型是什么是超时、是连接被拒、还是业务异常用户分布是不是集中在某个特定入口。这些信息能帮你快速划定范围。比如我这次的故障错误日志明显指向业务异常KeyError同时数据库 CPU 异常那就要分两条线去看一条是代码逻辑问题一条是数据库性能问题。4.2 第二步用日志和链路追踪给问题定位而不是翻代码很多人一出问题就翻源码恨不得把 AI 生成的代码从头到尾读一遍。这个习惯在开发环境可以生产故障不行。因为生产环境的请求路径很长入口可能是网关、路由可能有多层、中间可能经过 MQ逐行读代码效率太低。我优先看链路追踪里某个请求的完整调用链哪一步耗时最多、哪一步抛了异常、数据长什么样一目了然。这次事故中我随手点开一条失败的链路追踪记录看到请求从网关进来经过服务 A再到积分服务接着调数据库。数据库查询阶段耗时 800ms远超正常的 20ms而业务逻辑层面异常发生在“读取用户 ID”那一步也就是传入的参数里根本没有这个字段。一个数据找不到一个查询慢两个问题就这样被链路追踪暴露出来。4.3 第三步本地复现的技巧与陷阱定位到问题之后当然要在本地复现验证猜测。但本地复现有一个陷阱你很容易用“干净的数据”去复现结果发现复现不了。比如这次的 KeyError我用 curl 加上user_id就能正常返回于是觉得“本地没问题”忽略了真实的客户端根本不会传这个字段。正确的复现方式是从崩溃现场抓真实入参。链路追踪系统里都有请求参数记录把失败的那批请求参数原样粘贴出来用那个真实的畸形 body 去打本地服务。如果本地报错和线上一致那就确认了。至于慢查询我直接把线上那张千万级表的结构和数据量导入本地库再执行一遍 AI 生成的查询语句发现索引失效、全表扫描问题就清楚了。本地复现必须贴近真实场景否则复现一百次都是徒劳。这个排查链路走下来差不多花了一个小时。真正修代码只用了二十分钟但这一个小时帮我彻底搞清了 AI 代码在生产环境里会以哪些方式失效。这种经验比代码本身值钱得多。5. AI 时代的新工作流怎么让 AI 写的代码真的能上线5.1 把“能跑”的验收标准改成“能上线”的三张清单经过这次事故我给自己和团队定了一个新规矩AI 生成的代码要走完三张清单才能上线。第一张是参数清单检查所有对外接口的参数有没有校验、有没有默认值、有没有处理缺失和类型错误。第二张是状态清单检查所有缓存有没有过期策略、所有并发读写有没有锁或原子操作、所有连接有没有在异常时关闭。第三张是数据清单检查所有查询是否命中索引、所有循环和递归是否有边界条件、所有数值计算是否处理了除零和精度问题。你可能觉得这太繁琐但我用下来发现这三张清单基本覆盖了 AI 代码最容易翻车的高发区。每次把 AI 生成的代码往生产推之前我都会打开这三张清单一项一项打钩。打勾的过程不一定要花很长时间很多时候十分钟就能过完但经历过那次半夜告警以后我再也不想省这十分钟了。5.2 测试策略从“验证能跑”变成“验证会炸”传统开发里测试是为了证明代码“能完成预期功能”。但面对 AI 生成的代码测试思路要反过来设计测试用例的核心目的是逼它崩。我会故意构造空列表、缺参数、极端值、超大并发看它会不会死。如果一个测试用例能让 AI 代码抛异常恭喜你你在上线前就发现了问题。我现在的习惯是写两类测试。一类是常规功能测试确保正常路径能走通另一类是混沌式测试随机删字段、随机传 null、随机重复调用同一个接口。后者往往比我精心设计的用例更能发现 AI 代码的隐藏问题。比如上次我让 AI 写一个“批量发放积分”的功能常规测试全部通过但我随机传了一个很大的batch_size结果内存一下爆了因为 AI 一次性把所有数据都 load 进 list根本没有分批处理。如果这个代码直接上线运营同学某天手滑导出一万条用户数据服务就得挂。5.3 给 AI 的提示词里要写清楚边界条件和例外情况很多人抱怨 AI 写代码质量差但我现在觉得问题有一半出在提问方式上。你以为“让 AI 写个签到接口”就够了但 AI 对业务的理解就是字面意思它不会主动考虑你隐藏的规则。想要 AI 写出更“皮实”的代码你必须把边界条件写进提示词里。我现在写提示词的固定模板是这样的先描述功能需求再列输入参数清单然后专门加一段“需要注意的边界情况”比如“用户 ID 可能为空请求体可能缺少字段同一个用户可能并发签到积分金额必须用整型最小单位存储数据库查询必须走索引”。这一小段话看起来不起眼但它能显著减少 AI 生成代码里那种“乐观主义”问题。有一回我让 AI 写一个抽奖逻辑一开始它只写了“根据中奖概率判断”上线后发现没有处理“库存不足”的情况用户抽中了但兑换失败。后来我把“库存不足、概率为 0、用户重复抽奖、并发超卖”四个边界条件写进提示词AI 生成的代码就明显严谨多了。5.4 强制 AI 生成代码带上自解释注释降低审查成本最后一点是代码可读性。AI 生成的代码往往有这样一个特点功能实现了但没有任何注释说明为什么要这样写。如果代码本身有问题你在审查的时候就很难看出来“它这么做是在处理什么边界情况”。我现在会给 AI 下一条指令“请为每一段关键逻辑补充注释说明它处理了什么边界情况为什么使用这种实现方式”。加了这条之后审查效率至少提升一倍。比如 AI 写了一个“先查 Redismiss 后回源数据库”的逻辑注释会告诉我“因为防止缓存穿透这里加了空值缓存”。那么我一眼就能知道这段代码有没有写空值缓存如果没写注释我还要一行一行去猜它的意图。这些规则看起来都是基本功但在 AI 生成代码的新节奏下执行起来就是完全不一样的体验。以前代码是自己写的心里有数审查是走形式现在代码是 AI 写的审查是在替一个“不熟悉业务的人”做质量把关每一步都不能省。现在我的团队已经形成了一套固定的作业流程AI 生成初稿 - 开发者根据三张清单改造 - 混沌测试补边界 - Code Review 聚焦异常路径 - 灰度发布。每次有新同学加入问“AI 代码能直接上线吗”我都会回答“能上线但要先准备好它在线上花式崩溃的方案”。这套流程跑下来我们 AI 辅助生成的模块事故率已经和后端同学手写的代码基本持平。但要说能完全省心那不可能。我自己那晚的教训是AI 写代码越顺手你的防御姿势越要摆好。永远不要带着“它刚才在本地跑通了”的满足感去点发布按钮因为本地能跑从来都不是上线的理由而只是万里长征第一步。
返回列表