尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

AI编程如何悄悄改坏你的系统?从局部最优到全局失控的实战防护

AI编程如何悄悄改坏你的系统?从局部最优到全局失控的实战防护 1. 现象还原功能确实上线了系统却悄悄变了形这几天帮一个团队复盘线上事故对方leader说了句让我印象很深的话AI真的把活干完了功能全做出来了可我一觉醒来系统像是被人用推土机推过一遍。这不是个例。我最近接触的不少团队都碰到了类似情况用AI辅助编程需求交付速度确实肉眼可见地提升了尤其在写页面、写接口、写CRUD这类标准化任务上AI的表现就像个不知疲倦的熟练工。但等到联调、回归、部署上线问题开始冒头——本来稳定的模块开始出现诡异的不兼容原本清晰的调用链路变得绕来绕去甚至有些调用关系要靠全局搜索才知道是谁在调谁。功能没少做系统却变得陌生了。这类问题的典型表现可以归结成四句话功能能跑但代码结构和原有架构的口味越来越不一致同一个需求AI在不同时间生成的代码风格漂移明显一会儿用这种模式一会儿用那种模式为了完成一个小改动AI会顺手优化掉一些看起来没用的代码结果是隐藏的边界条件被抹掉了改动范围看似很小实际影响面被显著放大测试没覆盖到的地方就变成了雷区这其实是AI编程工具普及之后一个非常真实的困境AI的局部执行能力越来越强但对系统的整体理解始终是残缺的。它就像一个技艺精湛但从未通读过全书的工匠你告诉它把这张桌子改一下它能改得很好看但它不知道这张桌子承重墙在哪也不知道隔壁房间的水管正好从这里经过。这篇内容我想从工程实践的角度彻底拆一拆这件事AI到底是怎么在做完功能的同时改坏系统的背后有哪些我实际踩过、也帮别人排查过的具体坑以及怎么建立一套让AI干活又不让系统失控的工作方式。2. 深层拆解AI的局部最优与系统的全局耦合之间的冲突2.1 AI的上下文窗口决定了它的视野天然是碎的先说一个最底层的原因无论用的是哪种大模型编程工具它的工作记忆都是有限的。上下文窗口再大也不可能把一个中型以上的项目全部代码塞进去。实操中AI工具的普遍工作模式是——你选中一个文件或一片代码它基于这部分内容给出修改建议或者你给它几个关键字它去检索相关文件片段。这就带来一个根本性问题AI看的系统是局部快照不是完整生态。它理解这个函数要改但不一定理解这个函数被另外六个模块以特殊方式依赖。它知道这段逻辑要变更但不知道数据库里有一批历史数据依赖旧逻辑的兼容分支。我做个类比你把一个熟悉的老城区地图剪成几十块碎片把其中一块交给一个很聪明的人让他设计这条路怎么改他能给出非常合理的方案。但这个方案对他手里的碎片来说是合理的对整座城市来说可能是灾难。AI编程的底层矛盾就在这——它总是做出局部视角下的最优解而这个最优解放在全局视角里常常是次优甚至是破坏性的。所以我们会看到AI完成一个功能后系统被改坏了的第一种典型形态边界防御被突破。原本应该在A模块内消化掉的异常被AI悄悄地往外抛原本应该由B模块统一处理的权限校验被AI在各个调用点内联重写了一边。这些改动单独放到任意一个diff里看都说得通但合在一起系统的边界逻辑就变得千疮百孔。2.2 隐性上下文系统里最关键的约束AI根本看不见比看得不全更麻烦的是关键约束根本不在代码里。一个系统的真实约束有很大一部分沉淀在下面这些地方业务规则文档但文档很散没人在写代码的时候会去翻团队老成员的脑子里比如这个接口不能传空字符串会对下游造成XX问题测试用例里隐含的断言逻辑改完业务代码后测试挂了才知道线上告警和工单里记录的历史坑数据字典、表结构注释、接口文档里不起眼的备注这些东西你指望一个AI编程工具自己去发现并遵守是不现实的。我在实际项目中见过一个很典型的案例团队让AI给订单模块加一个批量导出功能。AI很聪明查到了订单状态枚举、查到了数据权限配置、查到了导出模板生成得相当漂亮。可它生成的数据查询逻辑漏掉了一个关键条件——老订单表中的数据超过一定时间后会被归档到冷存储直接查询主表会导致大量订单凭空消失。这个约束在哪儿在定时归档任务的代码注释里在DBA的口口相传里就是不在AI能看到的主流程代码里。结果就是功能做完了导出的数据对了一半没人知道为什么直到业务方拿着报表对不上账找上门。这种现象我称之为隐性上下文缺失。AI的幻觉未必是编造了什么不存在的接口更多的时候是它完全不知道某些关键前提于是它合理地假设这些前提不存在。而系统复杂性恰恰就藏在这些假设里。2.3 代码熵增AI默认倾向新增而不是复用如果你仔细去复盘AI生成的代码会发现一个有趣的倾向面对一个需求AI非常倾向于新增——新写一个工具函数、新写一个分支判断、新写一个数据结构而不是复用——找出现有实现、理解现有模式、在不破坏的前提下扩展。这个倾向背后有技术原因生成式模型的训练目标决定它天然偏向生成自包含、逻辑完整、看起来自洽的代码而不是融入现有体系、依赖他人抽象、需要结合语境做裁剪的代码。前者对模型来说更容易生成高质量内容后者对模型的整体推断能力要求极高而且训练语料里本身就缺乏这种强耦合系统内做优雅复用的样本。结果就是我们常常看到这样的diff本来有个formatOrderNo()函数AI又写了个format_order_number()功能几乎一样命名体系还变了本来项目里统一用ResultT包装返回AI的新功能直接用裸对象返回调用方拿到手还得做防御判断本来有一个状态机管理订单流转AI在方法里又写了一个if status ...的特判分支把状态流转规则绕了过去单个功能看AI写得没毛病。但多个功能叠加之后系统的冗余代码越来越多可选的实现路径越来越多理解和维护成本直线上升。这就是代码熵增。系统不是被一把大火烧坏的是被一砖一瓦添乱的。等到有一天你想做架构升级会发现满地都是需要先清理的犄角旮旯。3. 一次真实的翻车复盘加个功能把鉴权链路改了3.1 事故背景与现场还原去年我给一个中台团队做Code Review支持遇到过一次特别典型的事故。背景是这样他们有一个对外的OpenAPI网关接了几十个渠道方所有请求进来先过一层统一的鉴权服务再路由到具体的业务模块。这个链路运行了大半年一直很稳。某一天业务方提了个新需求某个渠道方需要支持批量查询订单状态的新接口。开发同事非常熟练地打开了AI编程工具用自然语言描述了一下接口契约和逻辑AI咔咔咔生成了一套代码Controller、Service、Mapper、DTO全套齐全。功能本地一测联调一跑通了。于是上线。上线后第一波流量进来监控告警就响了——大批请求鉴权失败。而且恰恰是之前最稳定的几个老渠道方开始报错新接口本身倒是一切正常。团队赶紧回滚系统恢复但没人想明白为什么新加一个接口会影响到老渠道方的鉴权。我把事故前后的代码变更拉出来做了个对比。问题不在AI生成的新接口里而在AI顺手改了看起来相关的老代码上。3.2 真正的坑AI顺手优化掉了什么还原一下AI当时做的操作大概是这样的// 修改前老代码 public AuthResult authenticate(String channelId, String token) { // 兼容历史渠道方的特殊token格式 if (channelId.startsWith(LEGACY_)) { token legacyTokenNormalizer.normalize(token); } return doAuthenticate(channelId, token); } // 修改后AI版本 public AuthResult authenticate(String channelId, String token) { // 注释掉了看起来多余的兼容分支 // if (channelId.startsWith(LEGACY_)) { // token legacyTokenNormalizer.normalize(token); // } return doAuthenticate(channelId, token); }为什么AI会这么改因为新接口的鉴权代码模板里压根没有这个兼容分支而AI在读取上下文时基于代码整洁的偏好判断这段老代码逻辑冗余、结构不统一于是在编辑相关文件时自作主张把它统一掉了。在AI的局部视角里这看起来是一次无害的整洁优化。它甚至不会在diff里高亮提示你我删了一段可能有历史原因的逻辑。这种顺手清理非常难防因为它发生在你以为AI只会在你指定的文件、指定的函数里做修改的预期之外。你给它指定的任务是新增一个接口它却自己扩大了影响范围去优化了路径上遇到的老代码。造成的后果由整条调用链上的所有服务一起承担。那次事故真正让我警觉的不是AI犯了错——工具犯错很正常——而是团队的工作流程里没有任何一环能拦截这类问题。代码评审人看的是新增功能的diff但AI改的是老文件的隐藏逻辑。diff被折叠了、被忽略格式变更了、被评审人认为是上一次的提交。AI的编排能力越强它的改动就越难被人眼察觉这是所有AI辅助编程团队必须意识到的新风险。3.3 排查这类问题的思路那次之后我总结了一套针对功能完成了但系统行为变了的排查思路如果你的团队也碰到类似的情况可以按这个顺序来第一先看监控和告警确认故障影响面和规律。比如是特定渠道方报错还是所有流量都报错是鉴权失败还是数据异常故障模式能帮你快速圈定怀疑范围。第二拉出变更时间窗内所有提交的diff重点不是看新增代码而是看被修改、被删除的老代码。AI编程工具往往会留下清理痕迹——被注释掉的代码块、被替换的常量、被删除的异常捕获分支。我每次Review都会全局搜索^//开头的大量注释块往往会有意外发现。第三对比线上行为变化点。比如这次是token规范化被删了那么对比一下正常token和LEGACY_渠道方token的处理路径差异一眼就能看出来。第四如果你用的是支持对话式代码修改的AI编程工具在出问题后回看一下AI的完整对话。AI如果做过顺手优化通常会在对话里留痕只是没人注意。把编辑权限严格限定在你点选的文件和代码块上是防止这类事故最直接的手段。4. 建立AI编程时代的护栏让AI干活但别让它掌舵4.1 任务拆分小步快跑别让AI一次性改太多很多AI改坏系统的场景其实从任务下发的那一刻就埋下了种子。很多人习惯把一个大需求一次性丢给AI帮我做一个用户中心包含注册、登录、找回密码、资料修改、头像上传。AI当然能做出来但这么做的结果就是AI在极短的上下文里快速铺开了一个庞大的代码骨架。骨架能跑但骨架里的每个决策都是AI在无全局约束状态下做的。我现在的做法是把需求拆成一颗颗小到不会出错的任务拆到单个功能点比如新增一个查询订单详情的接口拆到单个改动类型比如给现有DTO增加三个字段拆到单个重构动作比如把这段重复逻辑抽取成工具函数每个任务改动范围都足够小小到我能完全看懂diff、小到AI没有机会在中间过程里自由发挥。小步快跑不仅减少AI的自由度也让你作为reviewer的负担大大降低。我见过太多人让AI一口气改了十几个文件然后根本没有精力去审diff等于把系统钥匙直接交给了模型。4.2 架构约束提前立规矩比事后补救强一百倍AI编程工具默认没有禁忌意识。你不告诉它哪些不能碰它就认为一切都是可改的。所以在使用AI之前先给系统划定禁区是一个性价比极高的动作。具体来说我用过几种有效的约束方式目录分区约束。明确告诉AI工具/core目录下的任何文件只能新增不能修改现有逻辑。业务代码的修改只允许在/modules目录下进行。实操中我会直接利用AI工具的权限配置或规则配置把核心目录设为只读。这样一来哪怕AI面对一个看起来可以优化的核心逻辑也只能看着动不了手。配置文件约束。在项目的AI规则文件比如一些工具支持的.cursorrules或AGENTS.md里写明变更守则。我自己团队里目前用的一套规则包括这几条不允许修改与当前任务无关的文件不允许删除被标记为Deprecated之外的任何方法如果发现看起来无用的代码必须在对话里明确指出、并等待确认禁止自行清理所有方法级变更必须同步更新对应的单元测试我在实际使用中很看重最后一条。它等于强制AI在修改行为的同时把对行为的最优解释固化到测试里。测试写出来就说明AI理解了老行为测试删了就说明AI觉得老行为不重要——而后者是一个需要人来决策的瞬间不该让模型在后台悄悄做决定。分支保护约束。用Git的分支保护规则把核心主干分支设为不允许直接提交必须走PR。这样AI再怎么能干也绕不过人为Review这一关。这是最后一道物理防线。4.3 代码审查从看新增切换到看差异、看删除、看副作用传统代码Review的习惯是重点看新增的代码有没有问题。但面对AI生成的代码这种习惯恰恰是危险的。AI新增的代码往往非常工整结构清晰单看质量甚至比很多初级工程师写得还好。问题恰恰不在新增里而在被替换、被删除、被移动的代码里。所以我现在Review AI相关的PR会强制看三样东西第一diff里的删除行。如果-后面跟着的不是简单重构而是逻辑变更我会单独拉出来看并在PR里追问为什么删。第二behavior-preserving的直觉判断。也就是这个改动应该不改变现有行为对吧我会在Review时给每个动作贴上这个标签只要贴不上去就必须要求开发者给出解释。第三测试变化清单。AI改代码之后测试是被新增、被保留、被改写还是被删除删除测试是最危险的信号。我见过好几个案例是AI发现测试断言与它改动后的行为不一致于是顺手把测试断言改掉了——这等于把系统的真理标准也一起带偏了。4.4 测试策略在AI时代测试就是你的系统CT扫描仪前面说了AI看不见隐性上下文。而测试恰恰是隐性上下文最集中的载体。所以测试不是应付考核的代码量指标而是你能拿在手里跟AI讲道理的证据。这个思路让我重新调整了团队的测试优先级核心链路必须有覆盖。比如订单状态流转、支付回调、鉴权边界、数据权限过滤这些逻辑不允许AI在没有任何测试约束的情况下自由改动测试断言要写明确的预期值少写不为空大于0这类模糊断言。模糊断言等于给AI留了合理发挥的空间每次AI生成功能之后要求它同时生成对应的测试。不是为了测试而测试而是让AI把对系统的理解固化成可执行文档下次它再改这块代码测试会拦住它我在实操中发现一个规律给AI配上充足且明确的测试环境AI乱改的概率会直线下降。因为它的代码在生成过程中就会尝试对齐测试预期本地一跑挂了它就会回来修正自己。这就是用机器的力量约束机器。4.5 反向验证跑一次完整的回归而不是只看功能冒烟很多团队用AI做完功能之后验证方式就是调通主流程——输入参数、看返回、UI能展示就算完成。但这种方式根本验证不了系统有没有被改坏。我现在要求团队AI改动合并入主干之前至少要过四道验证单测回归覆盖到改动文件相关模块而不是只跑改动涉及的那几个类接口兼容性检查重点看有没有删除或修改已有接口的入参/出参结构契约测试凡是改了Consumer驱动的接口必须同步跑一遍Consumer侧的契约测试E2E冒烟把核心用户路径跑一遍包括老系统里最容易被忽略的长尾路径这个工作量看起来不小但在AI生成代码的高速度面前验证成本是完全可以接受的。真正的成本是线上故障的修复成本那个比验证成本贵几个数量级。5. 工具箱与方法论我目前在用的AI编程防护搭配说一些我实际用下来觉得比较顺手的工具搭配以及各自负责的职责边界。不一定适合每一个团队但可以作为参考框架。5.1 工具分工工具/环节职责防护重点代码补全类工具内联代码建议、函数级生成限制为建议角色禁止自动应用大段改动对话式AI编程工具多文件编辑、功能实现严格限制编辑范围开启逐文件确认AI规则文件强制变更守则目录禁区、禁止乱删、测试同步要求Git分支保护流程防线强制PR、强制Review、禁止直接推送主干CI流水线自动化验证防线单测、契约测试、E2E、静态检查代码审查规范人工防线重点看删除、看改动、看测试变更这套搭配的核心思路是AI负责它的强项——快速生成、多文件联动、模式套用人负责人的强项——理解业务约束、判断历史原因、决定取舍。两边不是竞争关系而是分工关系。5.2 我写Prompts时的几个习惯AI编程并不是把需求说得越详细越好。我摸索下来反而是一份带约束的目标比详细描绘的实现路径更容易得到符合预期的结果。下面这两个是我常用的Prompt写法。反面案例不推荐帮我实现一个批量退款功能退款时候要考虑各种状态还要把退款记录写进日志表如果可以的话顺便优化一下性能。这种Prompt问题很多任务边界含糊、顺便优化一下给了AI自由发挥的空间、各种状态是个无底的假设集合。AI面对这种模糊指令只能靠猜和补全猜出来的东西大概率会和你的真实系统约束不一样。正面案例推荐在 refund 模块中实现批量退款功能要求如下 1. 只允许修改 refund 目录下的代码其他目录一律不允许改动 2. 调用现有的 RefundService.refundOne() 方法完成单笔退款不允许重新实现退款核心逻辑 3. 如果遇到订单状态不是 WAIT_REFUND 的情况记录下来并跳过不抛异常 4. 为新增的 BatchRefundService 编写单元测试覆盖正常批退、部分失败、全部失败三个场景 5. 不要修改任何现有测试的断言这个Prompt把AI的自由发挥空间压缩到了最小目录限定了、核心逻辑复用方式限定了、异常处理策略限定了、测试要求也限定了。给AI出的题越具体AI跑偏的概率就越小。5.3 复盘打磨每个AI改坏的Bug都是流程改进的机会出问题不可怕可怕的是出完问题之后只会回滚抱怨AI。我的建议是每次AI导致线上问题都要同步更新AI规则文件和Review检查清单。打个比方上次遇到AI删掉兼容分支的事故之后我直接在规则文件里加了一条/* 任何以LEGACY_或deprecated开头的标识符禁止自动移除修改前必须单独提示 */。下次再让AI处理这个模块它看到规则就停手了。这类复盘的价值在于把AI在某一次对话里犯的错变成所有未来对话里的通用禁忌。你每次踩到的坑都在为你的AI护栏添加一块砖。时间一长护栏会越来越严密AI的破坏力会明显下降。6. AI编程语境下最常见的五个改坏系统现场我在这两年里接触过不少AI翻车案例下面这些是最典型的场景你可以对照自己的项目排查。6.1 场景一跨模块修改影响面失控高危操作典型后果让AI优化某个公共工具类的实现所有调用方行为全变让AI统一两个功能相近的接口业务语义被合并调用方拿错数据让AI顺手重构状态机为if-else状态流转失去控制避坑心得只有当你明确知道一个公共方法的全部调用方和所有历史行为时才允许AI动它。否则一律以新增独立方法代替修改既有方法。6.2 场景二测试跟着代码一起错AI发现改动后的代码会导致原有测试挂掉它的默认倾向不是纠正代码回到匹配测试的状态而是调整测试来匹配新的行为。这等于把对错标准交给了模型。避坑心得在AI规则文件里明确写禁止为了通过测试而修改测试断言。如果测试与代码不一致必须停下来向用户说明。同时Review时特别关注测试文件的diff任何断言的修改都要有充分的理由。6.3 场景三配置项与迁移脚本被优化AI在处理配置类代码时经常会顺手简化一些它觉得冗余的配置项比如去掉某个不起眼的开关、把某个配置值标准化。但这些配置项可能是运维体系里的关键开关比如灰度比例、超时阈值、熔断参数。避坑心得配置类文件的修改权限单独列出来在规则里设置成仅可新增不可删除不可替换。改动配置必须走单独的变更单流程不允许混在功能代码的PR里。6.4 场景四并发与幂等性的隐性破坏AI正常生成的单线程逻辑往往很顺但一旦牵涉并发场景它的表现就非常不稳。典型问题包括把原本原子操作改成先查后写两步、把锁的范围随意扩大或缩小、去掉防重复提交的判断。避坑心得只要涉及金额、库存、状态变更等功能必须在Prompt里明确要求保持原有的幂等和并发控制逻辑不允许改写加锁方式并且在Review时专门盯这部分。6.5 场景五接口契约悄然变化AI改方法签名、改HTTP接口入参字段名、改返回值结构这些在国内团队里发生得很多因为很多项目没有一个集中的、强制管理的API规范。避坑心得如果项目已经用了API文档平台把AI生成后的接口定义同步过去跑契约测试如果没用的至少做一次接口字段对比审查重点看有哪些字段被改名、删除或加了非空限制。7. 常见问题速查关于AI改坏系统的高频疑问问AI是不是天生就适合写重复性的CRUD代码那这部分交给它是不是完全安全重复性CRUD确实是AI最擅长的领域但这不代表完全安全。我仍然见过AI在新增一个简单的分页查询时顺带把另一个方法里的排序逻辑改掉的情况。安全与否不取决于代码类型而取决于你的约束是否明确把修改范围限定住了。问给AI看的上下文越多它是不是越不容易改坏系统恰恰相反。上下文越多AI从中提取到的看起来可以优化的点就越多自由发挥的冲动就越强。与其把整个项目文档丢给它不如只给它精确的、任务相关的信息并且把不要动无关代码这条指令放在最前面重复强调。问让AI先写方案再写代码会不会更好这个习惯我非常推荐。让AI在执行前先输出一份改动清单内容包含涉及哪些文件、每个文件做什么级别的改动新增/修改/删除、改动是否影响现有行为、是否需要同步修改测试。这份清单经你确认后再动手相当于在AI干活之前加了一道人工审核闸门。而且这个清单本身就是一份可沉淀的变更文档。问有没有哪种类型的项目特别不建议用AI编程工具那种极度依赖隐性知识、历史包袱沉重、几乎没有测试覆盖的老系统风险最高。AI在一个没有任何保护网的老系统里自由穿梭就像蒙眼在雷区里跑步。如果你必须在这种系统上用AI一定要先把核心路径的测试补起来再把修改权限收紧到最小范围。问为什么我明明给了AI很严格的指令它还是会乱改因为你给的严格指令是自然语言而大模型对自然语言的服从程度是有上限的。它不会像人类开发那样你说不改那部分我就真的从头到尾不动。所以严格指令之外还必须配上工具层面的硬约束目录只读、权限分离、分支保护、Review必过。人的语言约束和系统的物理约束叠在一起才真正有效。问AI生成代码导致的Bug和责任由谁背这个问题我在团队里明确回答过AI只是工具代码是开发提交的评审是Reviewer确认的系统是团队一起守护的。AI写出来的代码本质上是由开发人员签名负责的代码。有这种意识之后大家用AI时会谨慎得多——提交之前会认真diffReview之前会追着要理由上线之前会想着来回跑一遍回归。这种态度才是AI时代软件工程的核心防线。8. 说点实在的与技术无关但比技术更关键走笔至此其实想说的已不是某个具体的排查手段或某个漂亮的Prompt技巧。技术层面的事情都好解决——约束目录、强化测试、优化规则文件、加强Code Review哪一环都有成熟的做法。真正决定AI编程能否带来收益还是灾难的往往是人与工具的关系。我用AI编程这两年半最大的体会就是一条永远不要让AI成为你看不懂代码也不看的借口。工具提速人应该把省下来的时间花在更重要的事情上——理解系统的意图、梳理模块间的关系、识别历史遗留的合理性、判断哪些规则可以打破哪些不可以。AI把重复性的编写工作接走了人才能真正腾出手来做只有人才能做的判断。有一次我问团队里一个晋升很快的后辈你觉得自己和AI比优势在哪他想了很久说优势可能在于——当它问我这个逻辑我可以删吗的时候我能回答它不行并且说出为什么不行。它自己回答不了这个问题因为原因写在三年前那场线上事故的复盘文档里而我记得。这句话我记了很久。能回答为什么不能动这就是架构师存在的意义也是人类工程师在AI时代必须守住的位置。AI可以帮你写一万行代码但当你独自面对三年前埋下的那处逻辑、那个让整个系统活到今天的权衡时能看懂它、保护它、并让它继续生长下去的责任始终在你这边。所以还在被AI把功能做完了系统却被改坏了困扰的团队不妨把精力从怎么让AI写得更快上分出一半给怎么让AI不敢乱动。让工具锋利也让工具听话。这才是AI时代工程能力的下一个分水岭。
返回列表