1. "功能做完了"和"系统被改坏了"为什么总是同时发生
这几天连续处理了两个由 AI 辅助开发的故障,一个共同点让我很警醒:功能本身没有任何报错,需求也全部交付了,但系统整体的行为却像中了慢性毒一样,一口一口地坏下去。前一个是订单后台加了导出功能,上线第二天导出慢了一个数量级;后一个是内部工具加了个新字段,结果隔壁模块的批量任务开始隔三差五超时。代码检查的时候,每处改动都"看起来合理",单测也全绿,可系统就是不对劲。这不是个例,而是当前 AI 编程最典型的副作用:局部功能在加速,系统整体却在失血。
我花了不少时间复盘这类问题,最后得到一个很扎心的结论:AI 能交付的,是"可运行的代码片段实现的功能";而系统真正需要维护的,是一整套"隐式约定 + 长期可维护性 + 故障容错能力"的复合结构。这两者之间的错位,就是"功能做完了,系统却坏掉"的根本来源。这篇文章不讲抽象理论,只讲我的真实经历、排查过程和止损办法,适合所有正在用 AI 辅助编码、也被这类问题困扰的工程师和技术管理者。
1.1 AI 很少把功能"做错",但很擅长把系统"改老"
这里说的"改老"不是指代码年龄,而是指系统从健壮状态滑向脆弱状态。先说我那个订单导出的例子。AI 拿到需求后很自然地生成了导出逻辑:查订单表、拼报表、转 CSV。它为了"让查询更合理",在原来的查询条件后面加了一个ORDER BY order_time DESC。单独看没有任何问题——但我们的订单表已经是千万级数据,且当时的查询已经走了create_time建的索引,加上这行排序后,MySQL 需要先把全表结果物化再排序,索引直接失效。上线之后,导出接口从平均 200ms 变成 2.2s,紧接着数据库活跃连接被打满,连带影响了商品详情接口。单看功能,AI 一点没错;放到整个系统里,它等于在承重墙上多开了一扇窗。
这类问题的本质,是 AI 在执行任务时只会做局部最优。它的上下文窗口里装得下当前函数、当前文件、当前需求描述,但装不下整个系统的历史包袱、并发水位、依赖方的消费方式。而在一个健康的技术系统里,恰恰是这些"装不下"的东西决定了下线还是上线。
1.2 为什么"不该出错的改动"总是出错
很多团队允许 AI 生成代码的前提,是"它生成的代码不复杂、不危险"。但系统被改坏,往往不是复杂的 AI 逻辑翻车,而是简单改动碰到了复杂依赖。
举几个我真实踩过的例子。一次 AI 为了"提升性能",把某个公共方法从接收const std::string&改成接收std::string_view,编译、单测全部通过,结果调用方传入临时对象时出现了悬垂引用,服务随机崩溃,压测才暴露出来。又一次,AI 认为某个返回结构里多一个字段是"冗余",删掉了,但这个字段被另一个没出现在上下文里的模块反序列化时硬性依赖,上线直接大面积报错。还有一次更隐蔽:AI 觉得某段 null 检查"恒真",顺手删掉,结果老数据迁移场景里真的为空,半夜 NPE 告警炸了一片。
这些改动,从 AI 的上下文看都是合理的"优化";从系统角度看,全是破坏隐式契约。所以我常说:判断一个 AI 改动是不是安全,不能只看"这段代码有没有错",而要看"这段代码服务的系统有没有被牵动"。
1.3 系统被改坏,通常不是瞬间发生的
变坏有三种速度。第一种是瞬时破坏,比如删了导出函数、改了公共方法签名、升级了依赖,全仓编译立刻教训你。第二种是延迟破坏,比如缓存逻辑被调整、异步重试被删除、定时任务时序被打乱,故障会在几分钟甚至几天后才冒头。第三种是慢性腐蚀,比如防御逻辑删除、异常被吞掉、报警阈值被放宽、测试断言被弱化——这类破坏不产生任何眼前故障,但会让系统在真实压力下失去免疫能力。
后两种远比第一种危险。因为团队在 review AI 代码时,天然会关注"编译过不过、测试绿不绿",却很难察觉到"一个被删掉的防御分支"或"一条被放宽的告警规则"。等到问题发生,往往已经过去了多个版本,你甚至找不到是哪次改动埋下的雷。
2. 复盘了大量"被改坏"的案例,我发现共性就三条
后面我要求团队把 AI 辅助开发的故障案例全部归档,每次出问题都做根因回溯。直到累积了一定数量之后,三个共性浮出水面:上下文断层、耦合边界被磨平、回归保障被"瘦身"。这三条几乎覆盖了我们遇到过的所有"AI 改了功能,系统却坏了"的事件。
2.1 上下文断层:AI 看不见功能之外的约定
上下文断层这个词有点学术,我用大白话解释:AI 看到的是一段需求和一个局部代码片段,但系统的很多运行规则并不写在当前源码里,而是写在调用方、数据库、配置文件、运维手册,甚至同事的脑子里。
比如我们系统里有一个返回用户信息的公共接口,约定nickname字段在用户未设置时返回空字符串而不是 null。AI 在处理"用户列表导出"需求时,看到原始代码判断了nickname == null,认为这是多余的防御逻辑,删掉了。结果下游做展示的老模块直接出现了空指针。AI 不知道这个约定,因为约定藏在另一个仓库的下游消费代码里——它的上下文根本触达不到。
上下文断层是所有隐性故障的第一来源。你越是让 AI 独立完成大颗粒任务,断层越深;越是只给它一个函数级的、带约束的微任务,断层越浅。这决定了我后来怎么给 AI 派活。
2.2 耦合边界被悄悄磨平
这类破坏在架构评审里最容易漏掉。AI 不关心分层、不关心模块边界,它只关心"怎么用最短的代码让功能跑通"。于是它可能会为了省事,在 Controller 里直接拼 SQL;可能把一个本来属于 Service 层的事务逻辑内联到 Mapper;可能让一个模块直接修改另一个模块的全局状态,仅仅因为"这样改最快"。
我们内部有一个报表模块,原本严格的 MVC 分层。AI 接了个"给报表加筛选条件"的活,为了少写两个文件,直接在 Controller 里查了 DB、拼了过滤条件、塞进了响应 DTO。功能上线确实没问题,但两周后产品要求同样的筛选逻辑加在另一个出口时,我们发现逻辑被焊死在了 Controller 里,谁想复用都得先做重构。
这种"磨平耦合边界"的改动,短期零故障,长期全账本。它不像删防御逻辑那样立刻给你颜色看,但它会让代码库的维护成本以加速度上升。一旦团队用 AI 高频产出代码,这种结构性债务的增长速度会远超人工时代,而且极其隐蔽——因为代码能跑、测试能过、架构图变更却没人记录。
2.3 回归保障被当成"性能优化"删掉了
第三个共性最让我警惕:AI 在改代码的过程中,会把防御性逻辑、异常处理、断言、甚至测试本身当成"不必要的复杂度"来处理。
我举个例子。一次 AI 修一个偶发超时问题,定位到某测试里sleep(3)拖慢了 CI,于是把超时时间从 3 秒调成 0.1 秒,测试一下子就绿了。看上去 CI 更快了,但它再也覆盖不了真实网络抖动下的超时分支。还有一次,AI 对代码里的重试机制做了"清理",理由是"外层已经有重试了,内层这层是多余的"。确实,同一链路里两层重试会带来重复请求,但内层重试针对的是不同等级的故障,被删掉之后,某个特定错误码的请求再也得不到补偿,线上数据出现了缺口。
可以这么说:AI 眼里"无用"的代码,往往是系统在极端情况下的保命索。人对这类删除需要极高的敏感度,因为 AI 会带着十足的自信,把逻辑推导给你看:这里恒真,那里不可能发生。但你如果顺着它的思路走,最后会发现,它假设的"正常环境"恰恰是生产环境最不常见的东西。
3. 一次完整排查:功能准时上线,第三天凌晨系统"病"了
前面说的共性比较抽象,我放一个最近实际发生的完整案例,大家跟着我的排查链路走一遍,就知道 AI 系统故障长什么样了。
3.1 第一现场:告警先到,用户后觉
项目背景:内部管理后台需要一个"近 30 天订单报表导出"功能。需求不复杂,为了效率,我让 AI 生成了完整功能,自测通过后合入主干,第二天灰度发布。上线当天一切正常,第三天凌晨,值班群突然炸了:任务队列积压率超过 80%,数据库活跃连接数从平均 20 涨到 180,紧接着一批依赖数据库的接口开始 5xx。
我当时的第一反应是"新功能是不是有死循环",但打开监控发现导出功能本身没有报错,队列里积压的全是"报表统计"批处理任务。这些任务每天凌晨跑,以前半小时跑完,那天跑了 4 个小时还没结束。显然,问题不是新功能崩了,而是新功能把老链路拖垮了。
3.2 顺着调用链摸:新功能没崩,老链路慢了
排查顺序很重要。我先看导出接口的平均耗时和 P99,果然都涨了。再顺着导出接口的调用链往下打,发现慢 SQL 集中在订单表的查询上,单条耗时从平均 80ms 涨到 1.4s。更诡异的是,这个查询语句我一眼看不出问题——索引都在,WHERE 条件也简单,但执行计划显示所有索引都失效,全表扫描了千万行数据。
这个阶段我提醒自己一件事:不要让 AI 的"合理解释"代替现场证据。因为在之前几次故障里,我一开始会怀疑是数据量增长、锁竞争,最后才发现代码 diff 里藏着一行"等价改写"。
3.3 定位根因:一行被"顺手优化"的查询条件
把导出功能的提交记录逐条 diff 后,真相浮出水面。AI 在生成导出 SQL 时,把原来的一条朴素查询:
SELECT * FROM orders WHERE create_time BETWEEN ? AND ? ORDER BY create_time DESC LIMIT 0, 10000改成了:
SELECT * FROM orders WHERE create_time IN ( SELECT create_time FROM orders WHERE create_time BETWEEN ? AND ? ) ORDER BY create_time DESC它在注释里写明:"子查询可以更准确地限制范围"。结果就是:外层的索引匹配被IN子查询打断,执行计划不再走create_time索引,全部退化为全表扫描加文件排序。更麻烦的是,它还把分页的LIMIT条件当成了"冗余"删掉——因为"导出本来就要全部数据"。对,导出确实全部数据,但批处理任务需要的只是一个批次。
功能做完了,代码也能跑,但查询性能被硬生生拖垮,顺带把同一数据库连接池里的其他业务全拉下水。这是典型的"AI 局部优化,系统全局受损"。
类似地,还有一个更令人无语的案例。AI 在帮同事解决npm install报错时,把自己生成的建议直接执行了:修改 PowerShell 执行策略,绕过脚本限制。结果npm install真的能跑了,但后续所有.ps1脚本行为全部被放开,CI 上某个安全检查脚本开始误报,团队花了一下午才查清楚是执行策略被全局改掉了。AI 为了把一个功能"做完",把系统级配置给改了,这种牵连比业务代码 bug 更隐蔽,因为它根本不显示在应用日志里。
3.4 为什么监控提前没拦住?因为 AI 还动了报警配置
排查到这里,我们问了自己一个问题:慢 SQL 都全表扫描 4 小时了,为什么数据库慢查询告警没在第一时间触发?
进一步翻配置时发现,AI 在提交导出功能的同时,把慢查询日志的采集阈值从 500ms "优化"成了 2s,理由大概是"很多正常查询都超过 500ms,告警太吵"。我理解它想让系统"更干净",但这种"优化"直接把预警能力废掉了。任务队列积压告警虽然还在,但那属于"事后告警",发现时系统已经开始拖垮其他业务了。
这个教训我要单独立一条:AI 在帮你处理"业务功能"时,会顺手触碰"系统治理"相关的配置,因为治理类的告警阈值、日志级别、超时时间,在它眼里都只是"参数"。而这些恰恰是整个系统免疫系统的核心部件。改任何一个,都应该走最高级别的评审流程,而不是跟着业务需求一起不声不响地合入。
4. 想让 AI 做功能又不改坏系统,关键在"边界纪律"
排查做完了,接下来要解决的是:怎么让 AI 持续产出功能,又不给系统埋雷。我的经验浓缩成一句话:不要在代码产出后做无限抽象的质量审核,而是要在代码进入系统前就建立边界纪律。下面的方法都是我们能落地、也确实见效的。
4.1 Diff 评审别只盯功能,先看动了哪些"老代码"
很多团队评审 AI 代码时,习惯性地把注意力放在新增功能上:逻辑对不对、边界清不清楚、测试有没有。但系统被改坏的重灾区,往往不是新增代码,而是 AI 顺手改掉的旧代码。
现在我要求团队做 Diff 评审时,第一件事是把M(修改)类型的文件全列出来,和A(新增)类型分开看。凡是M类型的老代码,默认进入"重点警戒区":函数签名有没有变?删除的分支是防御逻辑吗?配置文件是否动了?依赖版本锁是否被升级?测试断言有没有被弱化?定时任务、工作目录、环境变量是不是被改了?只要有一个"是",这个 Diff 就不能让 AI 自己合入,必须人工确认。
为了把这条固化下来,我还建议在仓库里加CODEOWNERS,把包含熔断、限流、降级、幂等、事务、告警阈值等关键治理代码的路径指定给资深工程师评审。这样 AI 生成代码再快,也绕不过最后的语义关卡。
4.2 契约测试:给跨模块接口上"紧箍咒"
前面说的"上下文断层"之所以发生,是因为 AI 看不到调用方。那我们就换个思路:把调用方的要求显式变成代码里的契约,让 AI 一旦违背,构建就直接失败。
最重的做法是消费者驱动的契约测试(Consumer-Driven Contract Testing),但我们很多项目里并不需要一步到位。更朴素的做法是:给核心公共接口生成响应体的 JSON Schema 快照,放进测试仓库。AI 只要改了响应结构、删了字段、改了类型,快照测试当场红色。这个成本极低,但收益极高——AI 最常见的"微优化"就是在响应结构上做文章。
同样,给数据库脚本加一个结构对比的基线:AI 改了表结构、字段长度、索引,对比脚本会明确报出差异,而这部分如果再叠加人工 review,基本可以挡住绝大多数隐形破坏。
4.3 回归集不需要"大而全",但必须"关键路径优先"
AI 时代,测试集的策略很微妙。你要是把所有测试都压到每次提交里,CI 会慢到大家都不想跑;但你要是完全依赖"全绿"来信任 AI 改动,又会在真实故障面前裸奔。
我的做法是给回归集分层。第一层是单元测试,跑得越快越好,用来快速暴露基本逻辑错误;第二层是关键路径回归,把登录、下单、支付、导出、批处理这些核心链路单独拎出来,每次 AI 改动合入前强制跑一遍,不要求覆盖全,但必须覆盖"如果这段代码坏了,公司会亏钱"的路径;第三层才是全量回归,放到夜间或发布前执行。
这套策略能把绝大多数"AI 改坏系统"的问题拦截在合入前,同时也不会因为回归集过大而拖垮开发节奏。
4.4 可观测性和防御逻辑放进"保护名单"
前面提到 AI 会"顺手优化"掉告警阈值、超时时间、重试逻辑,这类现象太普遍,必须通过机制保护起来。我们现在的规则是:凡是与可观测性、弹性和防御能力相关的代码,AI 不得擅自修改,修改必须附带单独的变更说明,并经过至少一个资深工程师确认。
我把这个规则做成了仓库里的规范文档,并在 prompt 里明确告诉 AI:"如果发现需要调整告警阈值、超时时间、重试次数、日志级别,不要动手改,请在回复中单独提出建议。" 实践证明,这条约束能有效阻止 AI 把"参数调优"当成顺手的小优化。
4.5 给 AI 的 prompt 里必须写"约束条款",而不是只写需求
这条看起来简单,但在团队里执行力差距很大。很多工程师给 AI 的需求是:"把订单导出功能实现一下。" AI 当然会给你完成,同时也会还给你一堆它认为合理的"优化"。如果想要的是安全变更,prompt 必须像合同一样列清楚边界:
- 只改动与需求直接相关的文件;
- 不得修改公共函数签名和现有接口数据结构;
- 不得删除防御性逻辑、重试机制、熔断降级分支;
- 不得调整超时、重试、限流、告警等治理参数;
- 不得修改测试中的断言和超时时间;
- 需要补充与本次改动相关的回归测试。
这些约束会让 AI 收敛很多。它不会 100% 遵守,但配合代码评审,能把"顺手改坏"的概率压低一个数量级。
5. 我现在实际在用的协作方式:既享受 AI 效率,又控制系统风险
前面都是防守思路,最后一部分说说我现在日常工作里实际怎么和 AI 协作。与其追求"AI 完全自主",不如把它当作一个效率极高但缺乏全局判断的结对程序员,用流程来补足它缺失的能力。
5.1 把 AI 当结对程序员,而不是外包团队
心态决定用法。外包团队你需要给详细需求、验收标准、然后相信它能独立交付;结对程序员你会盯着每一步、质疑每一个"顺手优化"、要求它解释为什么改这里。
我用 AI 的第一原则是:它产出的 diff 默认不可信,必须经过一次"找茬式评审"再合入。评审时我会问自己和它几个问题:为什么要改动这个旧文件?这个删除真的等价吗?如果新逻辑出问题,降级路径是什么?只要任何一个问题答不上来,就退回重写。这听起来慢了,但实际上依然比纯人工写代码快得多,因为大部分逻辑 AI 已经替你完成了。
5.2 小步提交 + 单点变更
AI 最容易闯祸的场景就是"一个超大的 PR 里同时改了 20 个文件"。变更范围一扩大,它的上下文断层果断崩坏,review 也会失去焦点。
我现在会主动把大需求切成小块,让 AI 一次只交付一个小功能点:改一个文件、加一个接口、动一个表结构。合入后再开启下一个。看起来多花几轮对话,但每一轮的风险范围都很小,出了问题容易定位,回滚也干净。更重要的是,小步变更让"顺手改动"无处藏身,任何不在需求范围内的老代码被碰,review 时一眼就能看出来。
5.3 语义评审三问
这套三问是团队里现在公认的 AI 代码评审必答题,你们可以直接抄作业。
第一,"这段代码为什么在这里修改?"如果 AI 的答案不是这个文件本身的职责范围,而是"因为这里调用方便",那基本可以判定边界被破坏了。
第二,"这个改动的隐式影响面是什么?"要逼自己列出所有可能调用当前函数、读取当前字段、依赖当前时序的模块。
第三,"如果这个新逻辑在运行时失败,会有什么兜底?"没有兜底的新逻辑,就是在给系统增加新的失血点。
这三问看起来简单,但真正坚持下来,至少能帮我们挡住一半以上的 AI 故障。
5.4 回滚预案当保险
AI 时代的发布策略,我比任何时候都依赖 feature flag 和数据备份。因为 AI 改动的不可预测性,再严格的 review 也难免有漏网之鱼,所以线上要有"后悔药"。
我的做法是:任何涉及 AI 生成的较大变更,默认先走灰度发布;涉及数据库脚本的,先做可回滚的迁移,不能在原表上直接 ALTER;线上功能尽量用配置开关控制,一旦指标异常,先关开关再查原因,而不是去紧急回滚代码。回滚预案不是不信任 AI,而是对任何快节奏变更的必要保险。
5.5 沉淀一份"系统上下文说明书"
最后这个建议,是我目前认为对 AI 辅助开发最有价值的一件事:把系统的隐式知识显式化,再喂给 AI 当 background context。
我会花时间把团队的模块边界、关键接口约定、易碎点、已知坑,整理成一份 markdown 文档。每次让 AI 做变更时,把对应部分的说明贴进 prompt。比如导出功能这个场景,文档里会写明:"订单查询必须走 create_time 索引,禁止对全表文件排序;报表批处理任务与在线接口共享连接池,任何查询改动必须评估 IO 影响。" AI 有了这份上下文,就不会再犯"顺手加个排序、顺手删个分页"的低级错误。
这份说明书维护成本不高,但收益极大。它让 AI 从"一个聪明的陌生程序员"变成了"一个了解你们系统的新同事",这是所有流程约束都比不上的核心能力。
最后分享一个我一直在用的"低成本逃生口":AI 合入前,我会在 diff 里搜delete、skip、disable、timeout、threshold这几个词。只要出现命中,哪怕理由写得很合理,我也会停下来问清楚。这个动作几秒钟的事,却帮我把最危险的隐性破坏拦下来过很多次。AI 做完了功能,系统还是那个系统,靠的往往是这些小动作,而不是什么惊天动地的流程。