ARTICLE DETAIL

资讯详情

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

AI写代码“功能做完系统坏了”:根源与三道防线

AI写代码“功能做完系统坏了”:根源与三道防线 先说个亲身经历。上个月我们业务系统出了个线上事故报警群半夜两点弹消息支付回调连续失败。排查下来原因很尴尬——一位同事用AI助手重构了一个工具函数功能测试全部通过但那个函数被另外三个隐藏调用点依赖AI只按主调用点改了返回值结构另外两处直接拿到脏数据。功能确实做完了系统却实实在在被改坏了。这不是个例。我观察了团队里大量AI辅助开发的案例所有功能做完系统坏了的事故背后几乎都指向同一个原因AI在局部上下文里给出了正确解但在系统全局约束下这是个错误解。而且最关键的麻烦在于——AI写出来的代码看起来极其正常正常到代码评审都想直接点通过。这篇文章我想把这类问题的根源拆开把我们团队这半年沉淀下来的配套打法完整梳理一遍。内容适合正在用AI写业务代码、又担心稳定性的技术负责人和一线工程师也适合准备把AI引入正式项目但还在观望的团队。1. AI 写代码真正的风险点不在能不能写而在它看不见的全局状态很多人讨论AI编程时喜欢争论AI写的代码质量好不好。这个命题其实从一开始就问歪了。AI当下写单点功能的质量比如一个排序算法、一个正则表达式、一个CRUD接口水平已经相当稳定至少在能跑这个层面上问题不大。真正的风险在于AI对新代码接入位置周围的隐式系统状态一无所知。1.1 AI 是照着葫芦画葫芦不是照着架构画架构你可以把现在的AI编程助手理解成一个超强模式的搜索引擎加自动补全器。它训练的语料来自全网公开代码仓库里面堆积了大量相互矛盾、质量参差的写法。当你给它一个明确的局部任务——写一个从订单列表里筛选待支付订单的函数——它会按照语料中最常见的模式生成一段代码。这段代码从局部看完全正确变量名合理逻辑清晰甚至附带了类型注解。可一旦把这段代码放进你的系统问题就来了。你的系统可能规定所有订单状态变更必须走状态机不能直接改字段AI不知道你的系统可能在Redis里缓存了订单状态直接改数据库字段会造成缓存与库不一致AI不知道你的业务逻辑里订单有冻结、预占、异常终态等自定义状态AI更不知道。我后来复盘时经常打一个比方AI像一个手艺很好的木匠你让它打一张桌子它手艺没问题但它不知道你家客厅有多宽、门有多高、装修是什么风格。它打出来的桌子单独看是好的放进你家里就格格不入。1.2 上下文窗口再大也装不下系统的隐式约定现在AI编程工具的上下文窗口——也就是它一次能看到的代码量——已经从几K扩展到了上百K甚至更多。这带来一个错觉我让它先读一遍整个项目的代码再动手它就能理解全局了。这个错觉很危险。我们做过一次实测让一个AI助手通读我们一个中大型业务模块的全部代码约两万行然后让它重构其中的一个核心服务。从会话记录看它确实引用了不同文件里的函数和类型表面上理解了全局。但最终生成的代码里它还是踩了三个全局约定上的坑——一个是改了事务边界把原本应该在一个事务里的两步操作拆成了两段另一个是在消息队列的消费逻辑里加了同步HTTP调用直接把消费吞吐拖垮第三个是错误处理逻辑覆盖了上层已有的重试机制。问题不在于上下文窗口不够大而在于系统的很多关键约束根本没有写在代码里。它们写在文档里、写在评审意见里、写在产品需求里、写在老同事的脑子里。上下文窗口装得下代码装不下这些。1.3 最常见的一类改坏场景命名误导与副作用从实际踩坑的统计看AI改坏系统最集中的场景不是复杂逻辑而是两类看起来毫不起眼的地方命名相似导致的错误关联。AI看到updateOrderStatus这个名字就会自然假设它是更新订单状态的地方往里插业务逻辑。但很多老系统里名字叫这个的函数可能只是更新某个内部字段真正状态流转在另一处。AI被命名误导改动落在错误的抽象层上。函数副作用被悄然改变。一个函数原本是纯查询、无副作用AI为了优化加了缓存写入一个函数原本默默吞掉异常AI补齐了异常抛出——逻辑上更合理了但系统依赖原来的静默行为。这两类问题有个共同特征代码评审里极难发现因为AI写的改动看起来太正常了正常到每个评审点都挑不出毛病。最有效的防御手段不在评审环节而在更早的策略层面。这部分后面专门讲。2. AI 改坏系统最常踩的是这四个坑为了讲清楚为什么改坏我把团队内部记录的几十次AI引入的事故做了归类。归纳下来绝大部分落在四类模式里。如果你看懂了这四类基本上就掌握了80%的防御思路。2.1 局部最优解为了新功能破坏已有行为这是最常见的一类。AI接到给用户模块增加XX功能的任务时它会找到用户相关的代码在最近的地方插入逻辑。它很少先去分析这个模块还有哪些地方在依赖当前这段逻辑。举个具体例子。我们有段代码def get_user_info(user_id): user db.query_user(user_id) return {user_id: user.id, name: user.name, email: user.email}同事让AI给返回值里加一个phone字段方便前端展示。AI改完之后def get_user_info(user_id): user db.query_user(user_id) return {user_id: user.id, name: user.name, email: user.email, phone: user.phone}看着没毛病吧但问题在于这个函数是给内部RPC调用用的返回的是字典下游有十来个服务在用字段白名单做序列化。白名单里没有phone消息直接报错。功能上加字段的需求被完成了但系统层面因为它改了公开契约的一个骑墙点整个下游链路全挂了。这类问题根子上是AI的最小改动直觉它默认改动是增量的、无副作用的但现实系统里很多返回结构本身就是不稳定接口牵一发动全身。2.2 隐式约定被绕过超时、幂等、鉴权与并发控制第二类坑更隐蔽。业务系统里最值钱的经验几乎都是隐式的AI看不到但它会以最标准、最干净的方式去写代码于是常常把隐性约定破坏掉。举几个我们真实遇到过的例子超时约定。内部服务间调用约定是800ms超时AI生成的HTTP客户端配置里写了个很舒适的10秒超时一旦下游抖动原本应该快速失败的调用变成了拖垮整个调用链的长尾请求。幂等约定。在写消息处理逻辑时AI写了个先查再插的实现但没加唯一索引兜底。如果消息重复投递逻辑上不会插入重复记录但极容易在并发场景下双双通过查询、双双插入——幂等性被绕过。鉴权粒度。老代码里get_user有内部调用和外部调用两个入口通过参数控制鉴权。AI简化了代码合并了两条路径结果外部接口绕过了鉴权校验。这个属于最严重的一类。这些约定没有一条写在需求文档里也没有一条在AI的训练语料里有明确体现。它们只存在于系统运行一年多、踩过无数坑之后沉淀下来的桌面下知识里。AI编程工具不可能凭空获得这些知识所以这些坑必然得由人来兜底。2.3 过时或编造的API用法第三类相对好发现但依然造成过线上事故。AI的训练语料存在明显的时间滞后加上不同版本的代码差异极大它很容易生成在你当前依赖版本里已经废弃甚至根本不存在的API调用。典型的一次事故AI生成了一段使用某个消息队列SDK的代码用的是旧版本API。本地编译都没报错因为依赖传递关系里兼容层还在但线上运行到那个路径就直接抛NoSuchMethodError。这类问题看起来低级但一旦混在大批量AI生成的代码里人工逐行排查的难度非常高。我建议团队在每个项目里固定住核心依赖的版本并且在AI辅助编码时把关键依赖和版本号直接写进提示词里让AI在指定版本下检索API用法而不是靠它自己记忆。2.4 无注释、无分层、无扩展点的三无代码这个坑不直接导致故障但它是后续所有维护性风险的温床。AI生成的代码结构上高度平滑——一个函数从头写到尾没有分层没有接口没有预留扩展点。单独执行没问题但当你需要在这个模块上迭代第三个功能时因为AI生成的代码没有留任何缝隙你只能要么重写、要么在外层包裹越来越多的特判。传统老代码虽然丑但人类写代码时会自然地留一些因为上一个需求所以这里有个分支的痕迹这些痕迹是后续迭代的重要上下文。AI不这么做。它在语义完整之外对这个系统曾经发生过什么、未来可能要变成什么完全没有感知于是它在代码组织上会系统性忽略可演进性。3. 三道防线让AI写代码不破坏系统的工程配套AI本身没有好坏之分它就是个放大器——你给它清晰的边界、可靠的回馈机制它能帮你成倍地提高产出你让它在一个没有约束、没有评审、没有测试反馈的环境里自由发挥它也能成倍地帮你制造混乱。所以我真正想分享的不是怎么让AI更聪明而是怎么用工程手段构建让AI安全干活的防线。3.1 第一道防线把AI的改动圈在手术区里外科手术前要消毒、划定手术区防止感染扩散。AI改代码也一样。我们现在的强制要求是不允许AI助手直接在主分支或长期分支上工作必须为每一次改动单独开一个短命的feature分支。这个约束很深的意义。AI在生成代码时无法自己区分这是我改的和这是别人改的。一旦混在一起出问题时连排查的切入点都找不到。单独开分支至少保证了一点diff的范围就是AI行为的边界。你随时可以看清这次AI改动动了哪些文件、哪些行这是后续一切防御动作的前提。我们甚至有同事把这一步做得更细在提示词里直接声明只允许修改src/payment/目录下的文件其他目录一律只读。3.2 第二道防线代码评审从看对不对转向看为什么大多数团队做代码评审时评审者默认的审视角度是这段代码逻辑对不对、写得干不干净。这套传统标准在AI时代不够了。AI生成的代码逻辑往往是对的、风格往往是干净的问题恰恰出在它为什么要这样做的上下文缺失上。所以我们把评审清单改成了一套追问式结构这个改动为什么必须落在这个文件、这个函数里有没有更贴近现有抽象的落点这个改动影响到的所有调用方有没有被逐一核对改动里有没有使用本系统已有的工具函数、公共组件还是AI另起炉灶写了套新的对系统隐式约定超时、幂等、鉴权、状态流转有没有触碰这三条里第一条其实约束的是AI最常见的就近落点毛病第三条针对的是AI从语料里捡一个通用解法的习惯。绝大多数时候评审者在这几个追问下都能快速发现问题。3.3 第三道防线行为测试优于覆盖率测试AI生成代码时它自己会顺带做一件事为了让你觉得测试过了补一批只测自己新代码的单元测试。这些测试单独看都对但恰恰因为只测自己它们对集成行为根本没有保护作用——这也就是为什么覆盖率100%但线上照样挂的情况越来越多。我们现在的策略是对AI改动的代码要求必须附加一条基于调用方的集成或行为测试。比如它改了get_user_info的返回值就必须加一个测试模拟下游用白名单消费这段返回的完整链路。这类测试用代码把系统依赖的隐式约定固化下来只要AI再次破坏测试立刻报警。说实话让AI写这种行为测试并不容易它天然不了解系统层面的集成行为所以这部分的成本必须由人来承担。可一旦写进测试价值是长期复利——下次不管人来改还是AI来改这条防线永远起效。4. 让AI在既有架构里安全干活的提示词工程聊完防线聊聊最实操的部分提示词怎么设计才能让AI生成的代码从一开始就更贴合系统全局我们团队用下来这五条规则基本成了写提示词的铁律。4.1 上下文先行让AI先读再写很多人用AI写代码上来就是帮我改一下XXX。AI会直接按照它脑内的通用系统开写。正确做法是强制它先检索并阅读你指定的文件确认它读完后再让它给出改动方案。一个基础示范先阅读以下文件再回答不要直接写代码 - src/services/order_service.py - src/repositories/order_repo.py - src/api/schemas/order.py 阅读完后先列出你对order_service.update_status这个函数的理解特别是它对事务和缓存的处理方式。我确认无误后你再给出重构方案。这一步的成本极低但收益极大。你会发现AI在读完文件后至少会模仿文件里的风格、使用文件里已有的辅助函数而不是凭空创建一套新的。我们实测粗暴要求先读再写能让AI改动的陌生感下降一半以上。4.2 把约束前置让AI在进入细节前先看到不可违反清单AI只要没看到约束它就会走通用最优解路径。所以最有效的做法是在提示词最前面写清楚本系统的硬性约束。我的常用模板是这样的你在修改一个已经稳定运行的系统。以下约束必须严格遵守 1. 不得改变已有函数的公开签名和返回值结构除非我明确要求。 2. 所有对外HTTP调用必须保持现有的超时设置。 3. 数据库所有写操作必须在同一事务内禁止拆成多段提交。 4. 禁止给API新增无授权的公开入口。 5. 优先使用src/utils/下的现有工具函数不要自行实现重复能力。 任务...出现效果非常明显——AI从自由发挥的天才变成戴着镣铐还能干活的工匠。它会在生成代码时反复检查自己的方案是否违背这些清单出错率大幅降低。4.3 小步生成一次只说清楚一个改动一次让AI重构整个模块基本等于拿系统的稳定性赌博。我们的经验是把大任务拆成多个小任务每个小任务的diff尽量控制在50行以内逐个验证、逐个合入。为什么50行我们从实际事故统计里得出来的AI生成代码的出错率与改动行数呈明显的非线性增长。50行以内人眼可以逐行Review任何理性判断都来得及一旦超过这个量级Review就会变得粗糙各种隐藏问题自然漏过去。另一个小技巧小步任务里每完成一步人都要在本地把测试跑一遍并且确认页面或接口行为没有回归然后才让AI进入下一步。这相当于给AI的每一次动手都加了一个瞬间反馈回路它能从反馈里学会这个系统的代码应该这样组织。4.4 让AI自己解释影响面这是我认为最被低估的一个能力。AI其实完全有能力分析自己的改动影响面只是大多数人从来不这么用它。每次AI给出代码后我们强制它追加一段影响面分析格式大致如下这次改动会影响到以下调用方 - xxx函数因为它直接调用了被修改函数且它依赖返回值中的xxx字段。 - xxx服务因为它在事件中消费了被修改数据的结果。 风险点 - 用户模块的缓存可能短暂不一致需要追加缓存刷新。这个动作有两个不可替代的价值其一它强迫AI模拟运行一遍自己的改动很多AI在模拟过程中会自己发现前面的错误然后主动纠正其二它把AI能感知到的局部影响暴露给人类评审者让评审者能对照检查AI的理解是否与系统实际相符——绝大多数改坏事故在这个环节就会被截住。5. 已经改坏了怎么办一套可复现的快速恢复打法防线再严密也有失手的时候。真线上出问题也别慌关键是有一套不需要现场脑暴的恢复路径。我们把这套流程沉淀成了事故恢复三问团队里所有人都能照着执行。5.1 先看时间线是合入即坏还是发布后渐渐坏线上异常出现后第一件事不是打开代码看而是先确定异常出现的时间点与最近一次发布、最近一次改动之间的对应关系。如果异常是在发布后几分钟内集中爆发大概率是某次改动的直接副作用如果异常是发布后逐渐加剧则更可能是新改动放大了系统原本存在的性能瓶颈或队列积压。AI改坏系统的场景绝大多数属于前者——合入后就立即出问题。这时候不用犹豫直接进入下一步。5.2 用diff而不是记忆来定位改了什么很多工程师在排查线上问题时喜欢凭我记得这次发布改了哪些内容来缩小范围。但AI时代这个策略失效了——一次发布可能包含了几百上千行AI生成代码人的记忆根本靠不住。我们的做法是直接对比发布分支与上一个稳定分支的git diff按目录、按函数调用关系快速分层。具体来说# 先看改动的文件清单按规模排序 git diff --stat 原稳定分支..发布分支 # 找出与报错链路相关的文件改动细节 git diff 原稳定分支..发布分支 -- src/payment/ service拿到diff之后顺着报错堆栈的调用链反向查报错发生在哪个函数它被哪个改动波及。AI改坏系统的高频模式是改了一个底层函数导致多个上层调用一起挂所以一旦发现报错链路里有被改动过的底层函数几乎就能锁定真凶。5.3 回滚与止血优先恢复服务而不是解决代码这里我想特别强调一个团队早期反复踩的坑线上出故障时最大的诱惑是立刻写代码修复。在AI辅助开发时代这个诱惑更是加倍了——反正让AI改个代码很快。但我们的铁律是先是止血再谈根治。确认问题来自某次改动后最快的动作永远是回滚代码到上一个稳定版本把线上服务恢复然后再开分支分析问题。理由很务实AI定位和修复一个全局性问题需要时间而线上每多一秒故障都是在损失真金白银且回滚后你手里的现场报错日志、调用链快照没有变化不会影响事后分析。5.4 事后复盘把这次的教训固化进提示词和测试最后一步是目前多数团队最欠缺的。每次AI引入的事故恢复后我们都会把这次事故的原因抽象成一条配置沉淀到团队的AI辅助开发约定文档里这次是哪个隐式约定被破坏了把它写进公共提示词模板的不可违反清单里。这次哪段系统行为没有测试保护给它补上行为测试。这次是提示词缺了什么信息导致的更新项目级提示词明确把相关信息前置。用这样一套循环我们团队的AI引发事故率在三个月内肉眼可见地下降。坏消息是AI编程的坑踩不完好消息是每个坑踩完之后都能转化成免费的防御能力。说到底我对AI写代码这件事现在的态度是用而且放开用但永远把系统的全局约束掌握在人手里。AI是一个知识极广、但对你这个系统一无所知的协作者它在局部可以做得比人快在全局则必须有人的调度、约束和兜底。如果你也在用AI开发我最后一条实操建议是从今天开始每次让AI生成代码前先花两分钟写清楚这个系统里什么不能碰。就这一点改变能帮你避开八成以上功能做完了系统改坏了的糟心事。
返回列表