AI生成代码如何避免变成技术债?从代码审查到工程实践的全面指南
那是我印象很深的一次重构。一套内部系统功能都正常接口也都通报表跑得比原来还快。但当我需要在一个看似独立的模块上加一个小需求时真正的噩梦开始了。每个函数都有一层我觉得有必要的封装所有状态都被抽到了看似合理的 store 里注释写得比业务文档还工整但没有任何一条能告诉我为什么当初要这么设计。项目是 AI 帮忙填出来的而且填得非常快——快到我意识到问题的时候整个代码库已经变成了一座漂亮但无人认领的迷宫。这不是个例。最近半年我在不少项目里都看到了同一个趋势AI 代码正在以前所未有的速度进入生产环境而团队对能跑和能维护之间的距离普遍缺乏感知。今天想系统地聊一聊这个话题AI 代码为什么特别容易变成技术债债务是怎么悄悄累积的以及我们能在哪个环节把它真正拦下来。这篇文章不聊工具排名也不聊大模型之间的对比只聊一件事——怎么让你手里的 AI 生产力不变成明天的还债压力。1. 技术债不是代码烂而是交付当天就生效的复利1.1 能跑和能维护中间隔着一条大沟很多团队对技术债的理解还停留在代码写得乱、没注释、结构差这个层面。但真正参与过长期项目维护的人会告诉你技术债的本质不是代码质量低而是当前系统的结构无法以合理成本支撑下一步的变化。举个例子。你让 AI 写一个处理订单状态的函数它几分钟就交付了所有的分支判断都对单元测试也过了。你高高兴兴把它合入主干。但三个月后业务方说要增加一个订单在退款流程中不可修改地址的规则。你打开那个函数发现它把所有状态判断全部隐式地嵌在了一连串 if 和 switch 里变量命名高度抽象分支之间还有大量你无法确认是刻意保留还是历史遗留的组合。你改了第一处第二处逻辑就出现潜在漏洞你顺着函数追到 store发现那里还缓存了三份互相不一致的订单副本。此时你付出的时间成本早就超过了让 AI 重新写一版然后自己从头 review的成本——这就是技术债的复利。代码能跑只代表它满足当下输入输出。代码能维护才决定它在下一次需求到来时是帮你节省时间还是把你拖进泥潭。AI 代码的问题恰恰在于它极其擅长生产能跑的代码但生产能维护的代码需要的是对业务上下文、历史约束、团队约定的深度理解而这些东西很难被完整地写进提示词。1.2 为什么 AI 会让债务翻倍累积我把大量 AI 辅助项目复盘之后发现一个很扎心的规律AI 写代码的速度会掩盖掉债务产生的大部分过程。传统开发里一个人写出一段烂代码至少会经历思考、编码、自测这几个步骤。每一步都给他机会发现自己逻辑不对劲。但 AI 是直接给出一个结构完整、注释齐整、看起来非常专业的答案。人对看起来专业的东西天然有信任感这种信任感一跳过代码审查就只能看个大概。更要命的是AI 版本迭代很快你这次生成基础的函数下次生成第二个模块两次生成之间没有上下文记忆。第一次生成的模块用了自己的一套命名习惯和状态管理方式第二次生成的模块用了另一种风格等到项目后期代码库里会出现大量平行宇宙。每个宇宙内部是自洽的但宇宙之间互相看不懂。你没法简单地说这是 bug因为它确实能运行。它只是让所有人都无从下手。债务最大的特点是它会自行生长。AI 生成的某个过度设计的抽象层会鼓励后来者继续往上叠加新的抽象AI 生成的一段复制粘贴代码会诱导后续维护时继续复制粘贴。等到这批代码积压到一定程度重构已经变成一项需要跨多个模块协作的工程——而这个工程本身又可能被分包给 AI。债务就这样被一笔笔滚大了。2. AI 生成的代码最容易在五个固定环节变形如果说技术债是结果那我们需要看清的是过程。复盘了大量 AI 辅助编写的项目之后我发现 AI 代码的问题高度集中在下面五个行业里。2.1 幻觉 API参数是编出来的你只是还没被报错教育过AI 最让人头疼的问题之一就是它会在接口调用上表现出惊人的自信。你以为它调用的是一个真实存在的方法实际上那个方法在第三方库里根本不存在或者路径差了一个层级或者参数名写错了只因为听起来很像。我见过一个项目里某大模型生成的支付回调校验代码调用了一个看起来非常合理的 SDK 工具方法。当时测试环境里那个方法碰巧存在一个兼容的旧实现所以没有报错等上了生产依赖升级那个旧实现被移除整个支付回调直接瘫痪。排查的时候所有人都盯着业务逻辑看没人想到问题出在一个看起来就是官方 API的调用上。应对这个问题的核心不是抱怨 AI 不靠谱而是建立一条接口使用底线任何 AI 生成的外部依赖调用、SDK 方法、系统命令必须人工去官方文档核对一次如果连着两次生成都对同一个接口产生了不稳定猜测立刻停止让 AI 直接写调用代码改成你先把接口签名贴给它让它在此基础上完成业务逻辑。2.2 过度抽象与过早隔离把简单问题拆成复杂宇宙这是 AI 代码里最容易积累长期债务的模式。你让它实现一个导出报表的功能它能给你拆出 IExporter 接口、抽象工厂、策略注册器、扩展点隔离层。逻辑上无懈可击设计模式教科书级但你的项目其实只有两种导出格式而且未来一年都看不到第三种的迹象。这种过度工程会让你后续每一次简单的改动都要跨过三层间接层才能触达真正的逻辑。查一个 bug需要先搞清楚策略是怎么注册的、扩展点是怎么生效的、接口是怎么被代理的。AI 为什么这么喜欢抽象因为它的训练数据里有大量企业级项目而企业级项目里处处都是这类严谨的冗余。我的判断标准很简单如果一段抽象暂时没有第二个真实的调用方那就不要抽象。一次只解决一个具体问题保留最简单的、直接能看懂的写法。新调用方出现时再重构成本比你提前铺好所有可能的扩展点要低得多。不要为了明天虚构的需求预支今天本不该存在的复杂度。2.3 风格漂移每次生成都是一套新口音团队协作中最隐蔽的 AI 债务是代码风格的彻底乱序。同一个项目里有的模块用函数式写法有的模块用类封装有的模块错误码是抛出异常有的模块错误码是返回 null有的模块状态存在于 store有的模块状态藏在全局变量。每一个模块单独看都说得通放在一起看却像好几个团队各自做了一部分然后强行拼接。这种问题单独拎出来往往不构成致命的 bug但它会显著伤害团队的长期效率。因为维护代码本质上是阅读代码而阅读代码依赖你对这个项目习惯怎么表达的快速判断。如果每次走进一个文件都要重新适应一套规则认知负担会成倍增加。我现在的做法是在项目开始前就准备好一份 AI 也能读懂的代码约定文件把命名风格、错误处理、数据流方向、文件组织方式全部写明白。每次让 AI 生成代码之前先把这份约定塞进上下文里并且在 review 时把风格一致性当作和功能正确性同级的检查项。风格漂移不是必须靠强制重构来解决的它是完全可以通过流程来预防的。2.4 注释自信但结论可疑AI 生成注释可以做到比绝大多数程序员都勤快。但勤快并不等于可靠。我经常看到这样的代码// 这里使用缓存以防止频繁调用外部服务 const data await fetchData();问题是下面根本没有任何缓存逻辑。或者更隐蔽一些// 此分支处理用户未登录时跳转登录页 if (user user.status active) { ... }而这个分支实际上处理的是用户被禁用时展示提示页的逻辑。注释和实现在最初的 AI 生成版本里可能是一致的但后续人肉修改时改了代码忘了改注释或者 AI 在生成时就把注释和实现的真实关系理解歪了。于是注释变成了一种看起来知道自己在说什么的噪声。我处理注释的策略比较激进AI 生成的注释默认不信任。凡是解释怎么做的注释可以直接删掉——好的代码应该通过命名和结构让自己不言自明。凡是解释为什么这么做的注释我要求必须有具体的历史判断或业务约束支撑比如「因为上游接口返回格式兼容旧版本这里做了一次字段映射要等旧版本下线才能移除」。这样的注释才有保留价值。2.5 死代码与重复样板看起来在干活实际在拖地另一种非常普遍的 AI 债是成套出现的无用代码。AI 经常会生成一个当前需求根本走不到的分支或者定义了某个可能有用的帮助函数却没有任何调用方。因为它要保证答案的完整性和健壮性所以倾向于多写而不是少写。死代码本身不产生 bug但它会在未来的每一次重构里成为障碍。你想做接口变更但不知道那个看似没用的函数是不是有其他隐藏调用方你想升级依赖还得先把所有引用踪迹重新追一遍。更麻烦的是AI 生成的重复样板比如每个文件都带一个 logger 初始化、每个组件都带上通用的错误处理逻辑——这些代码是必要的重复但它们一旦散落各处后续要修改统一逻辑时就会变成一个巨大的检索任务。我的经验是AI 完成交付后第一遍 review 先不读逻辑专门扫描有没有不能被验证为必需的代码。删掉所有找不到调用方的函数把所有重复的样板逻辑标记出来能合并的立刻合并不能合并的至少单独提取到一个公共包里统一管理。做这一步就是在给未来的自己减少债务。3. 我在合入 AI 代码前会过一遍的九条审查清单聊完了典型的变形模式接下来是最有价值的部分到底怎么审查 AI 代码才不会让自己变成高级代码搬运工。我把自己的审查习惯整理成了一张清单每一条都对应着我踩过或围观过的真实代价。把「编译通过」从验收标准里划掉。编译通过只说明语法正确有些 AI 代码会在运行时才暴露接口理解的错误。我会至少让 AI 生成的部分跑一遍最小场景的真实用例看到实际输入输出才敢往下走。凡是 AI 在使用接口时出现了任何猜测迹象——比如参数名不确定、返回值可能为空却不处理、错误类型模糊——我要求它先把该接口的官方定义贴出来把定义核对清楚再继续写业务逻辑。这一步能拦截绝大多数幻觉 API 引发的生产事故。追问为什么要这么写而不是这句话在干什么。我会挑出三个最关键的设计决策要求 AI 给出理由。如果 AI 给的是这样更健壮这样更容易扩展这类假大空的理由基本上就等于它自己也不知道为什么这时候就该简化它。所有解释怎么做的注释直接删。让代码自己表达结构只保留带有业务约束和历史决策背景的为什么注释。每一条保留下来的注释都必须通过第二道审查假设这条注释消失未来的人会不会把这段代码改成不正确的行为会才留。让 AI 自己写测试但我来挑测试的有效性。AI 生成的测试经常出现断言的只是它自己的实现细节而不是真正的业务约束——也就是说代码错了测试照样通过。我会先看几个断言是不是反映了需求层面的结果而不是实现层面的过程。看到重复代码先不急着接受。AI 很喜欢就地复制再微调参数的模式。凡是复制来的代码我要求立刻提取公共逻辑哪怕改动量稍大也要做因为每份复制都会成为日后修改时漏掉的那一个。改动范围越大拆分需求描述就要越细。如果我把一大段需求直接丢给 AI它生成出来的代码几乎必然带着大量过度设计反过来我把需求拆成小到不能再小的子任务一个任务一个任务地生成每个任务之间都做 review它的质量会明显稳定很多。合入之前我要确保这段代码有一个明确的 owner。没有人认领的 AI 代码是隐性债务的重灾区。哪怕我是一个人在做独立项目我也会给自己设一个认领的仪式——在提交信息里写清楚这段代码解决的真实问题以及后续可能受影响的模块。任何 AI 生成的代码至少要过一夜再看。这个习惯强烈建议纳入流程。AI 代码看起来往往太顺了顺到让你失去批判性。给自己离开屏幕一段时间的机会再带着问题回来重新读一遍你会发现很多之前被流畅感掩盖掉的疑点。这张清单看起来简单但我建议你一条一条落到实际流程里而不是靠自觉。一开始会有点繁琐坚持一个月之后你会明显感觉到代码库的手感不一样了。4. 团队层面那些比代码本身更难还的隐性债4.1 无人认领的代码每个人都能改每个人都不负责如果说是单兵作战时 AI 债务是自我透支那团队协作里 AI 债务更可怕的地方是责任边界彻底模糊。传统代码至少有一个作者出了问题你会知道去问谁。但 AI 生成的代码不是它是所有人共同接受又共同不认领的东西。我见过一个真实的排障场景某个服务的内存持续飙升排查到最后发现是一段 AI 生成的定时任务它每次执行都会把全量数据加载进缓存且没有任何去重和淘汰机制。全组五个人都 review 过这段代码都以为别人看懂了实际上每个人都只是扫了一眼缩进和命名心里想着既然是 AI 写的应该没问题吧。这个问题的解法不能停留在加强 code review 责任心这种口号上。要做的是在每个 AI 辅助提交里强制声明代码归属。在提交说明里标注这段代码是 AI 生成还是人工编写、AI 参与程度是多少、谁是实际审查者。有了这个标签合入之前自然会有人多问一句这段逻辑你确定吗。没有归属感的代码就像没上锁的仓库平时看不出问题出事的时候所有人都会同时后退一步。4.2 集体幻觉AI 错误共识是怎么形成的另一个团队层面的陷阱叫集体幻觉。当多个成员同时使用同一款 AI 工具时AI 犯下的同一类错误可能会被不同的人在不同模块里复制成多个版本。每个人都会认为既然上次也用这种方法没问题那这次应该也没问题。直到某个依赖升级或特殊业务场景触发所有隐患在同一个时间点集中爆炸。这种情况其实是知识管理问题。我建议团队里建立一份「AI 工具已知雷区清单」内容很简单哪些 API 容易被幻觉、哪些生成模式被反复改写过、哪些类别的逻辑会让 AI 产生过度设计。每个人在 review 时发现 AI 的典型错误就记一条进去。这份清单的价值不在于它多全面而在于它让AI 也会犯这些错的共识在团队里流动起来。最怕的就是每个人都觉得只有自己发现了问题别人都在顺畅地用 AI 飞快交付。4.3 Code Review 流于形式程序员的防御性下降Code review 本来是拦截 AI 债务最重要的一道关口但现实是AI 代码正在让 review 这件事变得更加形式化。因为 AI 生成的代码结构太规整了——缩进对齐、命名一致、注释齐全——人类的注意力会自动放松大脑会倾向于用看起来整洁代替逻辑上正确。我在自己团队里试过一种方法把 review 的关注重点从这段代码正不正确彻底改成这段代码引入的成本可不可接受。review 的问题变成这段代码的真实需求是什么有没有比这更简单的实现方式它有没有引入新的依赖它的结构是帮助了下一个修改者还是阻碍了下一个修改者这些问题引导下的 review 不会轻易被表面工整误导。顺便提一个很容易被忽略的点AI 生成的代码在 review 时应该以一个陌生同事提交的代码的标准去看而不是以工具吐出来的东西的标准去看。这个心理转换非常管用它会自动把你放回我有否决权、我要为最终后果负责的位置上。4.4 依赖账本不清版本升级时的定时炸弹最后一个隐性债务是 AI 代码带来的依赖税。AI 生成一个功能时往往会顺手引入某个第三方库实现效果确实好但它不会考虑你现有的依赖版本是否兼容、许可协议是否匹配、这个库的维护状态如何。多个成员各自让 AI 帮忙实现功能之后项目依赖会悄无声息地膨胀起来每个人引入时都有正当理由但整体账本没人负责。这种债务平时不痛痛的时候都特别致命。有一次我们升级主框架版本结果发现有四个 AI 引入的库都还在用基于旧框架协议实现的深层嵌套调用升级牵扯到的连锁改动远超预期。我现在要求AI 新增任何第三方依赖之前必须先经过一道依赖审查为什么要用它、它和项目现有能力能不能复用、它维护得怎么样、有没有更轻量的替代方案。没有这层审查AI 的便利性会不断用未来的稳定性来偿还。5. 给 AI 设护栏我目前用得顺手的落地做法5.1 用小步任务喂 AI不给它留自由发挥的空间我最近一个做得比较顺的项目里最大的变化是改变了和 AI 协作的方式不再一次性丢给它一个大需求而是把需求拆成足够小的步骤一次只让 AI 解决一个问题。为什么这样有用因为大需求意味着大上下文而大上下文意味着 AI 需要在信息不完整的情况下自己做大量假设。假设一多幻觉和过度设计概率就会指数上升。反过来当我让它把这段 JSON 转换成符合甲方接口规范的数组而不是实现整个数据迁移功能时它只需要处理一个明确的问题输出的质量要稳定得多。建议你实际操作中这样拆分以能单独测试为粒度标准。每个子任务完成后马上编译、马上跑测试、马上 review通过后再进入下一个子任务。这样 AI 的产出始终被控制在一个可逆的范围内即使有坏代码也只是一个节点的问题而不是一整条链路的崩塌。5.2 审查前三问在任何工具介入之前先问自己我自己做 AI 审查时有一个固定的开场三问它们和具体的代码无关而是帮自己校准心态的这段代码如果是我手写的我会接受这种写法吗这能避免面对 AI 时标准自动降低。三个月后的我能在没有注释的情况下看懂这段代码的意图吗这能过滤掉大量看起来聪明其实难懂的实现。这段代码在什么条件下可能会崩而我现在完全没考虑这能逼迫自己找出 AI 没提到的边界。这三个问题每次都能帮我发现至少一个隐藏在流畅感之下的缺陷。它们不需要任何工具但比很多 Lint 规则都管用。5.3 把债务量化让问题能看见了再处理还有个比较实用的做法用工具把代码库里难以维护的信号量化出来。常见的信号包括单个函数长度过高、循环复杂度超标、同类型代码重复率过高等。你不需要让人工去数这些指标借助现成的静态分析工具跑一遍就能拿到报表。有人会觉得这些指标是死板的教条但在这个阶段它们起到的真正作用是让技术债变成看得见的东西。AI 代码的问题恰恰在于它看似完美量化指标能撕开这层假象。我一般把复杂度阈值设得比常规工具默认值更严格一点因为 AI 生成的代码默认复杂度就比较容易超标有必要单独盯一下。5.4 渐进改造老模块不做一刀切重写最后想聊一聊存量代码的改造。如果一个现有模块已经因为 AI 代码堆积变得很难维护了我强烈不建议做全量重写。原因很简单重写过程中很多隐藏在旧代码里的业务细节——比如某个订单状态组合之间的微妙关系——会被误认为过时逻辑而丢掉。这种风险远比技术债本身危险。我更推荐的方式是绞杀者模式一次只替换一个边界清晰的子模块替换过程保留旧模块的所有字段和行为不变同时用真实流量对比新旧两版的行为差异。把每次替换的测试范围控制在可验证的颗粒度内每替换完一个部分技术债就收缩一圈。整个过程可能需要几个月但它带来的收益是确定的、可回滚的。从我自己的体会来说AI 时代的编程本质上不是在和工具赛跑而是在和自己的审查标准赛跑。工具会把一部分人的产出放大十倍也会把另一部分人偷懒的速度同样放量十倍。AI 代码进入项目并不可怕真正可怕的是我们慢慢习惯了接受一个看起来正确但没人真正理解的实现。保持那份不搞清楚就不合入的执拗是防止明天的技术债最好的手段。