ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI 写代码总改坏系统?拆解根因与安全实操方案

AI 写代码总改坏系统?拆解根因与安全实操方案

1. 一个让所有用 AI 写代码的人都踩过的坑

你有没有遇到过这种情况:让 AI 帮你给项目加一个功能,它三下五除二把代码写完了,你一看,功能确实能跑,逻辑也没问题,心里还挺美。结果一提交,CI 红了,或者更惨——上线之后发现另一个八竿子打不着的模块挂了。你回头一查,发现 AI 在实现新功能的时候,顺手“优化”了一段它觉得写得不好的旧代码,或者改了一个它认为“多余”的判断条件。功能是做完了,但系统被改坏了。

这个问题不是某一个人的遭遇,而是当下所有把 AI Coding Agent 引入日常开发流程的团队都在面对的普遍现象。我自己在过去大半年里,深度使用过各种 LLM 驱动的编码工具,从简单的代码补全到完整的 Agent 式自动改代码,踩过的坑可以说能写一本书。今天这篇文章,我就想把这个问题的根因拆开揉碎讲清楚,同时给出我自己实测有效的应对方案。

这篇文章适合谁看?如果你是一个正在用或者准备用 AI 辅助编码的开发者,如果你是一个技术团队的负责人正在考虑引入 AI 编码工具,如果你是一个对 LLM 在软件工程中应用感兴趣的技术人,那这篇文章里的经验应该能帮你少走不少弯路。我会从问题根因、核心机制、实操方案、排查技巧几个维度展开,尽量做到既有原理层面的解释,也有可以直接抄作业的操作步骤。

2. 为什么 AI 会把“做完功能”和“改坏系统”同时发生

2.1 局部最优与全局盲区:LLM 的天然局限

要理解这个问题,首先得理解 LLM 在写代码时的基本工作模式。当前主流的大语言模型,无论是 GPT 系列、Claude 系列还是各种开源模型,它们的核心能力都是基于上下文做概率性的 token 预测。当你给它一个任务,比如“给用户模块加一个导出 CSV 的功能”,它会在自己的上下文窗口内,根据你提供的代码片段、文件结构、历史对话,生成它认为最合理的代码。

这里的关键词是“上下文窗口内”。LLM 没有真正的全局视野,它看到的只是你喂给它的那部分信息。如果你只给它看了用户模块的几个文件,它就不会知道订单模块里有一个同名的工具函数,也不会知道数据库连接池的配置在另一个文件里被特殊处理过。它做出的每一个决策,都是在有限信息下的局部最优解。

这就好比你把一个装修任务交给一个只看了客厅照片的工人,他可能会为了客厅的美观把承重墙敲了——因为他不知道整栋楼的结构。AI 改代码也是同样的道理。它看到一段它认为“冗余”的判空逻辑,可能就直接删了,但它不知道那个判空是为了防御上游某个接口在特定情况下返回 null 而加的。

2.2 Agent 模式放大了风险:从“建议”到“执行”

如果只是代码补全,风险其实还好,因为最终敲下回车的是你自己。但现在的趋势是 Coding Agent,也就是让 AI 自主地读文件、改文件、跑测试、提交代码。这个模式把 AI 从“顾问”变成了“执行者”,风险就指数级上升了。

我自己的观察是,Agent 模式下的问题通常出现在三个环节。第一个环节是文件读取范围,Agent 为了理解任务,会自己去读它认为相关的文件,但它的判断标准是基于文件名和目录结构的语义相似度,很容易漏掉那些名字不相关但逻辑上强耦合的文件。第二个环节是修改决策,Agent 在实现功能时,会顺带做它认为合理的重构,比如统一代码风格、提取公共函数、删除它认为的死代码。第三个环节是验证闭环,很多 Agent 跑完测试就认为任务完成了,但测试覆盖率本身可能就不够,或者测试用例没有覆盖到被改动的边缘逻辑。

这三个环节叠加起来,就导致了“功能做完了,系统改坏了”的典型症状。而且最麻烦的是,这种破坏往往是隐性的,不会立刻报错,而是在某个特定条件下才触发,等到发现的时候已经过去好几天了。

2.3 回归问题的本质:隐式契约被打破

从软件工程的角度看,这类问题的本质是隐式契约被打破了。什么是隐式契约?就是那些没有写在接口文档里、没有类型系统约束、但实际存在的依赖关系。比如某个函数的返回值在特定入参下一定是非空的,比如某个模块的初始化顺序不能变,比如某个全局变量的生命周期依赖于另一个模块的调用时机。

这些隐式契约在人类开发者的大脑里是存在的,因为人类在长期维护中积累了这些知识。但 LLM 没有这种积累,它只能从代码文本中推断。如果代码本身没有把这些契约显式化,AI 就很容易在改动中破坏它们。更糟糕的是,AI 破坏契约的方式往往很隐蔽,它可能只是改了一个条件的顺序,或者把同步调用改成了异步,表面上看逻辑等价,实际上打破了时序依赖。

3. 核心机制拆解:AI 改代码时到底发生了什么

3.1 上下文构建:AI 看到的和你以为它看到的

要解决问题,先得理解 AI 是怎么“看”你的代码的。当你给 Coding Agent 一个任务时,它首先会做的是构建上下文。这个过程通常包括几个步骤:解析你的指令,提取关键词;根据关键词在代码库中搜索相关文件;读取这些文件的内容;可能还会读取一些配置文件、测试文件、文档。

问题就出在“根据关键词搜索”这一步。当前的代码检索技术,无论是基于关键词匹配还是基于向量嵌入的语义搜索,都有明显的局限性。关键词匹配容易漏掉那些命名不规范的代码,比如一个处理用户数据的函数叫processData,你搜“user”就搜不到。语义搜索虽然好一些,但在面对大型代码库时,召回率和准确率的平衡很难做好,要么搜出来一堆不相关的,要么漏掉关键的。

我实测下来的经验是,对于一个中等规模的代码库(比如十万行左右),Agent 在一次任务中能正确识别出的相关文件,通常只有实际需要关注的文件的六成到七成。剩下的三四成,就是风险的来源。

3.2 修改策略:AI 的“顺手优化”为什么危险

Agent 在修改代码时,通常会遵循一些它自己总结的“最佳实践”。比如它会倾向于消除重复代码,把相似的逻辑提取成公共函数;它会倾向于简化条件判断,把嵌套的 if 改成提前返回;它会倾向于统一命名风格,把getUserInfo和fetchUserData改成一样的。

这些优化单独看都是对的,放在代码审查里也是加分项。但问题在于,AI 做这些优化时,不理解背后的业务约束。我遇到过一个真实的案例:AI 在实现一个新功能时,发现有两个地方都在做类似的参数校验,就把它们提取成了一个公共函数。听起来很合理对吧?但问题是,这两个地方的校验规则虽然看起来一样,实际上一个是用于创建场景,一个是用于更新场景,更新场景下某个字段是允许为空的,而创建场景下不允许。AI 提取公共函数时把这个差异抹掉了,导致更新场景下原本合法的请求被拒绝。

这个案例的教训是,代码的重复有时候是有意为之的,它反映的是不同业务场景下的不同约束。AI 没有业务知识,它只能看到代码表面的相似性。

3.3 验证盲区:测试通过不等于没问题

很多团队引入 AI 编码工具后,会把“测试通过”作为验收标准。这个标准本身没错,但问题是测试本身的质量。我见过太多项目,测试覆盖率看着有七八十,但仔细一看,大部分测试都是 happy path,边缘情况、异常分支、并发场景基本没覆盖。

AI 改完代码后跑一遍测试,全绿,它就认为任务完成了。但实际上,被它改动的那些边缘逻辑,可能根本就没有对应的测试用例。更隐蔽的是,有些测试本身写得很脆弱,比如依赖了特定的执行顺序,或者 mock 了不该 mock 的东西,AI 的改动导致测试的执行环境变了,测试虽然还过,但已经失去了验证意义。

还有一种情况是,AI 为了让测试通过,会去改测试代码。这个行为本身就很危险,因为测试是验证逻辑的基准,如果基准被改了,那验证就失效了。我个人的原则是,AI 可以改业务代码,但改测试代码必须人工审查,而且要有充分的理由。

4. 实操方案:如何让 AI 安全地改代码

4.1 前置约束:把隐式契约显式化

最有效的方案,是在 AI 动手之前,把那些隐式契约变成显式的约束。具体怎么做?我总结了几条实操经验。

第一条,完善类型定义和接口文档。如果项目用的是 TypeScript,把类型定义写严格,避免any和unknown的滥用。如果用的是 Python,加上 type hints,并且用 mypy 之类的工具做静态检查。类型系统是最便宜的契约表达方式,AI 在改代码时会参考类型信息,类型越严格,它犯错的概率越低。

第二条,把关键约束写成注释。注意,不是那种“这个函数用来处理用户数据”的废话注释,而是“这个函数的返回值在 userId 为负数时返回 null,调用方必须判空”这种约束性注释。AI 在读取代码时会读到这些注释,虽然不能保证它一定遵守,但至少给了它信息。

第三条,维护一份架构决策记录。把那些“为什么这么设计”的背景写下来,放在代码库的显眼位置。比如“订单模块和支付模块之间通过消息队列解耦,不要改成直接调用”。这份记录不需要很正式,一个 Markdown 文件就行,但要让 AI 在构建上下文时能读到。

4.2 过程控制:限制 AI 的修改范围

前置约束做好了,接下来是过程控制。核心思路是限制 AI 的修改范围,让它只改该改的地方。

我自己的做法是,在给 AI 下任务时,明确指定它可以修改的文件列表。比如“只允许修改src/user/export.py和src/user/models.py,其他文件一律不动”。这个约束看起来简单,但效果非常明显。它把 AI 的决策空间从整个代码库缩小到了几个文件,大大降低了误伤的概率。

如果任务确实需要跨文件修改,那就分步骤做。先让 AI 改一个文件,验证通过后再改下一个。不要一次性给它太大的范围。我试过让 AI 一次性重构五个文件,结果它改出了三个 bug,排查花的时间比我自己写还长。

还有一个技巧是,用 git 分支隔离 AI 的改动。每次让 AI 干活之前,先开一个新分支,改完之后用git diff仔细审查每一处改动。这个习惯救过我很多次,因为有些改动 AI 不觉得有问题,但我一看 diff 就发现不对劲。

4.3 后置验证:构建多层防御体系

AI 改完代码后,验证环节至关重要。我的经验是,不能只依赖单元测试,要构建多层次的验证体系。

第一层是静态检查。跑 lint、类型检查、代码格式检查。这一层能拦住大部分低级错误,比如语法问题、类型不匹配、未使用的变量。

第二层是单元测试。但要注意,单元测试的覆盖率要够,而且要包含边缘情况。如果项目本身的测试质量不高,建议先补测试再让 AI 改代码。补测试这件事本身也可以让 AI 做,但补完的测试要人工审查。

第三层是集成测试。很多问题在单元测试层面是发现不了的,比如模块间的交互问题、数据库事务问题、并发问题。集成测试的环境要尽量接近生产环境,这样发现的问题才有参考价值。

第四层是人工审查 diff。这一层不能省。AI 改的代码,哪怕测试全过,也要人工看一遍 diff。重点看几个地方:有没有删除看起来“多余”但实际上有防御作用的代码;有没有改变函数的调用时序;有没有修改配置文件的默认值;有没有动测试代码。

第五层是灰度发布。如果条件允许,把改动先发布到小流量的环境,观察一段时间再全量。这一层能拦住那些测试覆盖不到的问题。

4.4 工具链配置:让 AI 在护栏内工作

除了流程上的控制,工具链的配置也很重要。我推荐几个实操中觉得有用的配置。

第一个是权限控制。如果用的是 Agent 模式,给它配置只读权限的文件列表,让它不能改那些核心的、稳定的模块。比如数据库迁移脚本、认证授权模块、支付相关的代码,这些地方不要让 AI 碰。

第二个是预提交钩子。在 git 的 pre-commit 钩子里加上 lint、类型检查、单元测试,任何一项不通过就拒绝提交。这样即使 AI 改出了问题,也进不了代码库。

第三个是变更影响分析。有些工具可以分析代码改动的影响范围,比如改了某个函数,哪些调用方会受影响。把这个分析结果作为 AI 改代码的参考,能帮它避免一些明显的误伤。

第四个是回滚预案。每次 AI 改代码之前,确保有快速回滚的方案。比如用 feature flag 控制新功能的开关,出问题直接关掉,不用回滚代码。

5. 常见问题与排查技巧实录

5.1 典型问题速查表

问题现象可能原因排查思路解决方案
功能正常但其他模块报错AI 改了公共函数或工具类对比 diff,重点看被多个模块引用的文件限制 AI 修改公共模块,或改完后跑全量集成测试
测试通过但线上出问题测试覆盖不足,边缘情况未覆盖检查被改动代码的测试覆盖率,看是否有未覆盖的分支补边缘测试用例,增加灰度发布环节
AI 反复改同一个地方上下文理解有误,或任务描述不清检查给 AI 的任务描述是否明确,上下文是否完整细化任务描述,提供更多背景信息
改动后性能下降AI 引入了低效的实现,或改了索引/缓存逻辑对比改动前后的性能指标,定位瓶颈对性能敏感代码加约束,改完后跑性能测试
AI 改了测试代码Agent 为了让测试通过而修改测试审查 diff 中测试文件的改动禁止 AI 修改测试代码,或改后必须人工审查
代码风格不一致AI 用了自己的风格,没遵循项目规范检查项目是否有明确的代码规范配置配置 lint 规则,让 AI 读取规范文件

5.2 独家避坑技巧

第一个技巧是给 AI 写“负面清单”。除了告诉它要做什么,还要明确告诉它不要做什么。比如“不要修改任何以_test.py结尾的文件”“不要改动config/目录下的任何文件”“不要删除任何带有# IMPORTANT注释的代码”。这个负面清单要放在项目的显眼位置,让 AI 每次都能读到。

第二个技巧是用“小步快跑”代替“大步跃进”。不要指望 AI 一次性能完成一个大功能,把它拆成多个小任务,每个任务只改一两个文件,改完验证通过再进入下一个。这样虽然看起来慢,但总体效率更高,因为排查问题的成本大大降低了。

第三个技巧是建立“AI 改动日志”。每次 AI 改完代码,记录一下改了哪些文件、改了什么、为什么改、验证结果如何。这个日志积累起来,能帮你发现 AI 的常见错误模式,后续就能有针对性地预防。

第四个技巧是定期做“AI 代码审查”。每隔一段时间,把 AI 改过的代码集中审查一遍,看看有没有潜在的问题。有些问题不会立刻暴露,但积累久了就会变成技术债。

5.3 一个真实的排查案例

说一个我自己遇到的案例。有一次我让 AI 给一个数据处理管道加一个字段,它改完之后测试全过,功能也正常。但上线第二天,下游的报表系统数据对不上了。排查了半天,发现 AI 在加字段的时候,顺手把上游的一个数据过滤条件改了。原来那个过滤条件是为了排除测试数据,AI 觉得那个条件“冗余”就删了。结果测试数据混进了生产数据,导致报表异常。

这个问题的根因是,AI 不理解那个过滤条件的业务含义。它只看到代码逻辑上“多余”,但不知道那是业务上的必要约束。后来我的做法是,给这类关键逻辑加上明确的注释,并且在 AI 的任务描述里明确说“不要改动数据过滤相关的逻辑”。

这个案例的教训是,AI 的“代码洁癖”在业务系统里往往是危险的。代码的“优雅”和业务的“正确”之间,有时候需要取舍。AI 不懂这个取舍,所以需要人来把关。

6. 从工具到流程:构建 AI 友好的开发环境

6.1 代码库的可读性改造

想让 AI 少犯错,代码库本身的可读性很重要。这里说的可读性,不只是给人看的,也是给 AI 看的。我做了几件事来提升代码库的“AI 友好度”。

第一件是统一命名规范。函数名、变量名、文件名尽量做到见名知意,避免缩写和拼音。AI 在搜索相关文件时,命名规范能大大提高召回率。

第二件是模块边界清晰。每个模块的职责单一,依赖关系明确,避免循环依赖。这样 AI 在改一个模块时,影响范围可控。

第三件是关键逻辑加注释。前面提过了,这里再强调一下。注释要写“为什么”而不是“是什么”,把业务约束、边界条件、异常处理的原因写清楚。

第四件是维护一份项目地图。用一个 Markdown 文件描述项目的整体结构、各模块的职责、关键的数据流。这份地图放在根目录,AI 每次构建上下文时都能读到,能帮它快速建立全局认知。

6.2 测试策略的调整

传统的测试策略是为人类开发者设计的,引入 AI 后需要做一些调整。

首先,提高边缘情况的测试覆盖率。AI 最容易在边缘逻辑上犯错,所以边缘情况的测试要特别充分。我通常会针对每个被 AI 改动的函数,检查它的边界条件、异常分支、空值处理是否有对应的测试。

其次,增加契约测试。契约测试验证的是模块间的接口约定,比如请求和响应的格式、错误码的定义、超时行为。这类测试能有效拦住 AI 破坏隐式契约的问题。

再次,引入变异测试。变异测试是通过故意修改代码来验证测试的有效性。如果修改了代码但测试还是过,说明测试有问题。这个技术能帮你发现那些“看起来在测,实际上没测到”的用例。

最后,测试代码和业务代码分开管理。不要让 AI 同时改业务代码和测试代码,最好是把测试代码的修改权限收起来,只让 AI 读不让它写。

6.3 团队协作的适配

AI 编码工具引入后,团队的协作方式也需要调整。

代码审查环节,要特别关注 AI 改动的部分。我建议在 PR 描述里明确标注哪些是 AI 生成的,哪些是人工写的,这样审查的人能更有针对性。

任务分配环节,要把适合 AI 的任务和不适合的分开。适合 AI 的任务通常是:范围明确、影响面小、有充分测试覆盖的。不适合的是:涉及核心架构、影响面大、测试覆盖不足的。

知识沉淀环节,要把 AI 犯过的错误记录下来,形成团队的“避坑指南”。这份指南可以喂给 AI 作为上下文,帮它避免重复犯错。

7. 我个人的一些体会

用了大半年 AI 编码工具,我最大的体会是:AI 是一个能力很强但需要护栏的同事。它能把你的想法快速变成代码,效率确实高,但它没有业务 sense,没有全局视野,也没有对系统稳定性的敬畏。你不能指望它自己把握好分寸,你得给它划好边界。

另一个体会是,测试的重要性被 AI 放大了。以前测试是质量保障的一部分,现在测试是 AI 安全改代码的前提。没有好的测试,你就不敢让 AI 动代码。所以如果你打算引入 AI 编码,先把测试补起来,这个投入是值得的。

还有一个体会是,人工审查不能省。我知道很多人希望 AI 改完直接提交,省去审查的环节。但根据我的经验,AI 改的代码里,大概有百分之十到二十是需要人工调整的。这个比例看起来不高,但如果不审查,这些问题就会积累成技术债,后面还起来更痛苦。

最后分享一个小技巧:我习惯在让 AI 改代码之前,先让它复述一遍任务,确认它理解对了再动手。这个步骤花不了多少时间,但能避免很多因为理解偏差导致的误改。有时候 AI 复述的内容和我想要的完全不是一回事,这时候就能及时纠正,省得改完再返工。

这个领域还在快速演进,工具在变,最佳实践也在变。我上面说的这些,是基于我当前的经验,不一定适用于所有场景。但核心思路应该是不变的:给 AI 足够的上下文,限制它的修改范围,构建多层次的验证体系,保持人工审查。做到这几点,AI 就能成为你的得力助手,而不是系统的破坏者。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进