我用AI编程写了大量代码以后,最强烈的感受不是“真快”,而是技术债开始像分期账单一样准点出现。以前手写代码,债还得过一段时间才暴露;现在AI生成代码,债务往往在第一次合入时就悄悄埋下了。一个功能第一版半小时就能跑通,但第二期需求改动却要花三天——负责改代码的人可能还是当初那个只负责“复制粘贴确认”的人,这种反差非常值得警惕。
这篇文章想聊的是:当AI生成代码已经成为日常,我们怎么在享受速度的同时,不让代码库三年后变成谁都不敢碰的遗留系统。我结合自己实际项目和团队落地的经验,把AI代码产生技术债的根源、典型症状、源头预防、合入拦截和存量治理都过一遍,适合正在用AI辅助开发的工程师,也适合团队里负责代码质量和架构评审的人参考。
1. AI写代码为什么“跑得越快,改起来越慢”
技术债不是AI编程发明的概念,手写代码同样会欠债。但只要对比一下两种生产方式,就会发现AI时代的技术债在三个维度上发生了质变。
1.1 从“人写代码”到“AI生成代码”,债务的生成速率完全不同
过去一个工程师写一万行代码,一天能写两三百行已经很快了,写之前要理解业务、要构思设计,潜意识里可能已经过滤掉很多不合理方案。AI生成代码则把“写”这个环节压缩到几分钟,真正制约速度的变成了需求澄清和代码审查。问题恰恰在这里:很多人的工作流没有跟着改变,还在用“写完就能跑 = 完成”的老标准验收,结果一天生成几千行代码,本质上是把一周的债务压缩进了一小时。
我处理过一个案例。团队让AI生成一个订单导出的功能,它一次性吐出了一个包含四个类、六个工具方法的模块,跑起来没问题,导出字段也正确。等到对接方要求增加一个“按渠道拆分sheet”的需求时,团队成员发现数据组装逻辑被分散在三个类里,每个类里都对订单来源做了分支判断,而且判断依据的口径还不一样。改一个地方,另外两个地方就会漏。原本三十分钟的改动,最后用了两天做回归和修复。这就是典型的“速度快掩盖了结构烂”。
1.2 AI优化目标的偏差:它求的是“单次正确”,不是“长期可维护”
为什么同样的工作交给人做,债不会这么多?这里的关键不在代码能力,而在优化目标。AI在生成代码时,本质上是在对一个输入上下文做“最合理的补全”,它优先满足的是语义通顺、逻辑自洽、和当前Prompt匹配,很难主动考虑“三个月后另一个维护者会怎样读这段代码”“这段逻辑是否和代码库里另一个函数重复了”“未来业务变化会落在这个抽象上还是那个实现上”。
用人来类比,AI更像一个每次都能考高分但从不复习错题的学生。它解答当前题目时很出色,但不会意识到自己上次已经在同一类题上写过一种解法,这次又用另一种约定写了一遍。当代码库中出现了两套并行的约定,维护成本就开始指数上升。
这也引出一个根本认知:AI生成代码不是“有人帮你写代码”,更像是“有人帮你快速写出第一版草稿”,草稿的价值在于把骨架搭好,但后续的结构整理、语义统一、债务清理必须由人来完成。如果把AI的输出直接当成终稿,技术债就不可避免。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三种最典型的AI技术债病灶:语义失真、过度抽象、逻辑重复
聊技术债不能只停留在概念层面。我观察过不少AI辅助开发的代码库,发现高频问题其实很集中,主要集中在三种病灶上:命名掩盖了真实语义、抽象过度超前、同一份逻辑在代码库中反复出现“变体”。把这三个问题认清楚,后面的防债手段才有意义。
2.1 病灶一:语义失真的命名——代码读起来通顺,改起来处处是坑
很多人觉得AI生成代码的命名质量不错,变量名和函数名都挺像英语母语者写的。但实际上,AI特别擅长制造一种“表面合理、实际失真”的命名。
举一个很常见的例子。AI生成下面这段代码时,函数名叫 apply_discount,看起来没问题,但函数体里其实做了三件事:判断订单是否满足折扣条件、判断用户会员等级、再按等级计算折后金额。
python复制def apply_discount(order_id):
order = get_order(order_id)
if order.total < 100:
return order.total
member_level = get_member_level(order.user_id)
if member_level == "gold":
return order.total * 0.8
if member_level == "silver":
return order.total * 0.9
return order.total
只看名字,你以为是“应用某个已算好的折扣率”,读函数体才发现它把规则判断也一起承担了。过两个月,产品说“普通用户满300减20,黄金会员不打折”,你要么在函数里塞更多if,要么就得重命名并拆分逻辑。这还只是函数级的问题,更麻烦的是类名和模块名也变得模糊,比如 OrderService 里既有价格计算又有库存扣减还有消息通知,AI生成时为了“归类”而硬凑,后续定位问题就会非常低效。
我的建议是,不要因为AI取的名字“顺眼”就放过。拿到生成代码后,我会把每个public函数名单独拎出来问一个问题:如果我只看到这个名字,不看函数体,能不能准确说出它对外承诺了什么?如果说不太清,就说明语义边界模糊,改动迟早会踩雷。
2.2 病灶二:为了省事而来的过度抽象——三层包装只包住一个if
另一个高频问题是“抽象过度”。人类的过度抽象通常是因为没有想清楚未来方向,AI的过度抽象则往往是因为它从训练语料里学到了大量“模式化设计”,只要场景沾点边,就会生成一堆工厂、策略、事件总线。
比如我见过有AI为了更好地处理订单状态变更,自动生成了一套完整的“状态机 + 发布订阅”结构。实际业务只有下单、支付、取消三个状态,点击“取消订单”后发一条消息而已,结果代码里出现了事件接口、事件发送器、事件订阅器、三个状态处理器类,外加一堆配置。为了应付一个if能解决的需求,建立了十五个文件。
这种债务的危害在写代码时看不出来,等项目进入维护期就变成灾难。改一个状态流转,要跨七八个文件,新增一个状态类型,要把事件、处理器、配置表全部同步一次。这里要说明一下,我并不是说工厂模式、策略模式不好,而是这些模式应该由“业务压力的重复出现”催生出来,不应该由“AI觉得这里可能需要”提前引入。如果生成出来的某个抽象只有一个实现、只有一个调用点,那它就不是抽象,是负债。
我把这个原则写进了团队约定:凡是AI生成代码中出现了“接口、抽象类、工厂”等结构,评审时必须追问一句——现在是否已经有至少两个不同的实现场景?如果没有,砍掉,用普通类或函数顶着。等第二个场景出现时再抽象,成本并不高,而且那次的抽象才会是贴合业务的。
2.3 病灶三:AI之间的逻辑重复——同一个业务规则在代码库里出现多个版本
还有一个特别隐蔽的坑是逻辑重复。AI每次都是基于当前上下文重新推理,如果上下文里没有明确提示“代码库里已经有一个函数可以完成这件事”,它就很容易“凭印象”新写一个。于是同一个运费计算、同一套积分规则、同一个金额格式化方法,会被不同的AI会话分别实现,而且细节经常分叉。
我调试过一个问题:用户下单时显示的预估运费和最终结算时的运费不一致。排查后发现代码库里有三个函数在计算运费,一个叫 get_shipping_fee,一个叫 calculateFreight,另一个藏在工具类里叫 shipping_price_by_weight。三个方法的主体逻辑都是从重量表里查价格,但其中两个没有处理偏远地区加成,另一个对续重的计算逻辑和主流程差了半公斤。AI不是在同一时刻把这三个函数写出来的,而是三个不同需求、三次不同对话分别生成的。每个生成当下都通过了对应的单点测试,但合在一起,业务规则就互相打架了。
这种债比命名失真更难发现,因为编译不会报错、单测也可能都过。等到线上出bug时,定位已经不是“哪行代码错”,而是“哪一份逻辑才是业务想要的标准”。要防住这类问题,必须在AI生成的入口处建立对现有代码的搜索和复用机制,这个我下面会展开讲。
3. 在生成源头堵债:提示词约束与任务拆解策略
聊完了“债长什么样”,再来说说怎么从源头上让AI少生债。很多人以为防债主要靠事后review,但我的经验是,Review拦得住错别字,拦不住结构性隐患,真正高效的做法是在生成前给出足够的约束,把AI的自由发挥空间压到合理范围。
3.1 把“不要做什么”写进约束清单,比描述需求更重要
给AI写提示词时,大家习惯详细描述“要做什么功能”,却很少描述“不要做什么事情”。AI是一种对上下文极其敏感的生成机器,它不会主动遵守你没写的规则。我在项目里用了一套“约束清单”式的提示词,效果比只说需求好得多。
text复制我要新增一个会员积分抵扣功能,请遵守以下约束:
1. 不改变订单模块现有的公共接口
2. 不允许新增抽象类或接口,除非你先说明这是为了解决什么具体问题
3. 先搜索订单金额计算相关代码,复用已有函数,禁止复制一份新实现
4. 生成代码不得包含未被调用的辅助函数或未用到的配置项
5. 异常处理只捕获你能处理的分支,处理不了的向上抛,不允许吞异常
6. 在给出完整代码前,先把你准备使用的变量和函数名列出来,并说明各自的职责
这段提示词看着很啰嗦,但非常有效。因为它把AI最容易犯的三类毛病——过度抽象、逻辑重复、死代码——都在源头打了预防针。第4条尤其管用,AI为了“代码完整”往往生成一堆自己调用自己但实际上没入口接进来的函数,明确禁止之后,代码干净很多。
3.2 把一次性大生成拆成多次小生成,债务才不会集中爆发
很多人让AI写功能时喜欢给一个大而全的Prompt:“请帮我实现一个完整的优惠券系统,包括创建、领取、核销、过期处理、统计报表,使用贴近生产的工程结构。”AI确实能生成,而且看起来很完整,但这种完整正是债务集中爆发的原因。一次生成上千行代码,里面藏了无数假设,你根本来不及检查每一处。
我更推荐按“功能切片”的方式推进,每个切片都小到可以完全审查。比如做优惠券,先让AI设计数据模型和接口定义,确认没问题后,再让它实现创建和领取,接着再实现核销。每一步之间都插入一次审查与修正,而不是等所有代码生成完再统一看。一次切片如果超过300行,我就会觉得偏大了,会再把需求切得更细。
这样做的代价是多花几次Prompt交互时间,但换来的是每一段代码的来龙去脉都在掌控中。小步快跑不只是敏捷开发的原则,它也适用于AI辅助编程——生成的步子越小,AI犯错的面积越小,出问题后定位的范围也越小。
3.3 上下文越完整,AI越不容易自作主张
AI生成代码时其实很像一个没有摸清团队历史的新人。如果你不告诉它代码库里已经有了什么约定,它就会按自己熟悉的方式重新发明一套。很多人写Prompt只把当前需求贴上去,完全没有提供“哪些代码已经存在、哪些函数应该复用、当前模块遵循什么样的设计风格”,这等于放一个新人进代码库,还不给他看组织架构图,他当然会到处造轮子。
要解决这一点,我习惯在Prompt里额外加一段“相关代码索引”。例如:
text复制代码库中运费计算函数位于 services/shipping/calculator.py,入口函数是 calculate(region, weight, product_type)。
会员等级判断可以通过 services/member.py 中的 get_level(user_id) 获取。
本模块统一使用 dataclass 作为参数对象,不直接传多个独立参数。
请先阅读以上文件再开始修改,不要新建同名功能函数。
这些信息哪里来?直接从IDE的代码索引、文件树或自己的记忆里抄过来。只要喂了这样的上下文,AI就不会再自己造一套“运费规则”。尤其当代码库已经有大量沉淀时,这一步是防止重复逻辑最有效的开关。
3.4 要求AI先给方案再写代码,避免直接落入实现细节
大多数人让AI干活都是“请帮我实现一个xxx”,AI也乐于直接输出代码。但如果你需要的是可以长期演进的功能,多一步“先给方案再实现”会显著降低潜在债务。
我通常会让AI先输出一个问题拆解和实现方案,例:
text复制会员积分抵扣功能涉及订单金额计算、积分余额校验、扣减与流水记录四个环节。
请先列出你的实现方案,包括:
- 每个环节放在哪个类或函数中
- 是否复用已有的订单金额计算逻辑
- 关键边界情况如何处理
- 哪些地方会影响存量功能
方案确认之前不要写具体实现代码。
这一步的价值在于,它在“生成大量代码”之前设置了一道人工审批闸门。AI方案写得不好,你还能让它换思路;如果它已经把实现代码全生成了,再想让它推倒重来,代价就高得多,人也容易因为“已经能跑了”而将就接受烂方案。
4. 合入之前:给AI代码设置防债审查关卡
无论源头提示词写得多么好,AI生成的代码仍然需要人工把关。我的经验是,不能拿着传统的逐行评审标准来审AI代码,而应该建立一套专门针对AI生成特征的审查关卡。这套关卡不一定能挡下所有债,但至少能把最伤筋动骨的那几类拦在合入之前。
4.1 我目前使用的AI代码审查检查表
我把日常审AI代码时遇到的问题收敛成了一张表。每次代码评审,我会按表逐项过,而不是漫无目的地看diff。你会发现问题率一下子高了不少,因为很多“看起来正常”的代码其实是踩了表中某一条。
| 审查项 | 判定标准 | AI生成的常见翻车特征 |
|---|---|---|
| 命名与语义 | 函数/类名能准确反映职责边界,而不是一个“万能桶” | 名称广泛,如 handle_event、process_data |
| 依赖方向 | 单向依赖清晰,不存在调用方反向依赖 | 为了处理一个分支,工具类反过来依赖业务类 |
| 边界处理 | 空值、重复提交、异常路径都已覆盖,而不是只写正常路径 | 主流程写得很全,else分支用注释带过 |
| 抽象时机 | 每个接口至少有两个实现,且都有真实调用方 | 工厂、策略类只有一个实现 |
| 重复逻辑 | 和已有代码库做了对比,不重复造轮子 | 同一种计算逻辑存在多套不同实现 |
| 死代码 | 没有未被调用的私有函数、未使用的配置参数 | 生成了一堆“以后可能用到”的辅助方法 |
| 注释的真伪 | 注释描述的是当前代码真实做的事 | 注释讲的是旧逻辑,代码已经被AI改成新逻辑 |
这张表不是一次性定死的,团队可以按自己领域微调。我建议在最开始不要贪多,挑最容易踩的五项强制检查,等习惯之后再逐步加码,否则评审负担太大,流程很快就会被绕过。
4.2 用自动化工具拦截原本只能在评审中发现的债
人脑当然能发现很细腻的问题,但AI生成代码的量很大,如果全靠人看,一是慢,二是每个人标准不一致。实际上,有些AI代码债完全可以通过静态分析和质量门禁拦截,让机器先做第一道筛选。
我会给代码仓库配几条强制门槛:单方法圈复杂度超过10就打回重写,单方法行数超过60行不允许直接合入,代码重复率超过5%进入人工检查,未使用的导入和变量算作编译告警阻断合入。这些阈值本身不神奇,但它能解决AI生成代码里最典型的“超大函数”和“复制粘贴式变体”问题。
有一点要说清楚:自动化的作用不是替代人工,而是把所有代码拉到同一条质量基准线上。AI生成的代码虽然逻辑各不相同,但在复杂度、死代码、重复率这些维度上具有统计共性,门禁可以很高效地识别出来。我们把这些静态指标称为“债务探测器”,虽然不能告诉你这样设计是不是合理,但能提醒你“这里看着就有隐患,先想清楚再往下走”。
4.3 把AI当作反向评审员,让另一段AI来“挑刺”
关于AI生成代码的审查,我还有一个反直觉但很实用的做法:在代码评审阶段,让另一个AI来读AI生成的diff,专门负责挑刺,而不是让它改代码。我会把一个Pull Request的描述和代码diff丢给它,限制它只输出“发现的问题清单和理由”,不允许给出修好的代码。
这样做的原因并不复杂。第一段AI在写代码时处于“补全模式”,它的目标是让代码在语法和逻辑上尽量完整,很容易屏蔽掉一些潜在问题。而第二段AI处于“批判模式”,不承担责任压力,因此愿意点出各种风险。实际操作中它经常能发现命名失真、异常处理不足、以及和同模块其他文件风格不一致的问题,而这些恰恰是人审代码时最容易被“代码能跑”的错觉带偏的地方。
当然,让AI当反向评审员不等于它可以代替技术负责人拍板。它能提供问题清单,但问题到底改不改、怎么改,还是要由掌握业务上下文的人做决定。总的原则是:生成和审查都由AI承担一部分,但最终对代码负责的必须是人。
5. 债已经酿成时,如何低成本拆解AI历史债务
前面聊的都是怎么防新增债务,可现实中的很多团队,AI已经用了一段时间,代码库里的债早就堆起来了。看哪些文件都不敢动,一跑测试红一片。存量债怎么处理,其实比防新增更考验功力。
5.1 动代码之前,先给债务分个类,不是所有债都值得马上还
技术债这个词容易让人焦虑,好像欠了就一定要立刻清偿。但把债务看成统一的“财务问题”会误判优先级。有些债是“信用卡债”,比如线上bug、数据一致性隐患、安全问题,这类必须尽快还,多放一天就多产生利息。还有一些债是“房贷式债务”,比如为了赶上线而采用的临时方案,虽然不够优雅,但它已经稳定运行,此刻拆掉它反而可能在没准备好时引入重大回归,这类债可以规划一个周期慢慢还。
拿到一个存量AI代码模块,我的第一步不是打开代码文件,而是先列一个债务清单:目前这个模块最让人痛苦的是什么?是改需求时容易改漏?是测试跑得太慢?是逻辑根本没人读得懂?还是每次上线都出幺蛾子?痛感最强的那个问题,才是优先处理对象。把问题“翻译”成具体的代码修复任务之后,再去动刀。千万不要抱着“把这段代码重写一遍就干净了”的念头去做大范围重写,那种做法在AI时代尤其危险——你很可能让AI重写出第二套同样有债但风格更陌生的代码。
5.2 给AI生成的历史代码补上“语义地图”
AI代码很典型的特征是从局部看挺工整,但整体上没有“知识连续性”。人类维护者接手时,往往要花很长时间才能拼出它的设计意图。与其靠人肉翻代码去还原,不如给这个模块补一份“语义地图”,把代码不好表达的背景知识写出来。
我所谓语义地图不是传统的那种“类说明注释”,而是一段放在模块头部、面向后期维护者的短文,类似:
text复制# Module: member_benefit.py
# 目的:计算会员积分抵扣后的应付金额。
# 调用方:checkout.py、mobile_order.py、invoice.py。
# 依赖:user_service、payment_service。
# 关键规则:
# 积分抵扣只在订单实付前生效,不作用于运费;
# 修改抵扣阈值时必须同步修改 tests/test_member_benefit.py。
# 已知债务:当前保留了 legacy_pay_type 分支,用于兼容旧客户端的过期请求,
# 计划在旧版本下线后移除,迁移前不要新增对该分支的依赖。
别小看这几行注释。它把AI生成代码里最稀缺的“为什么”信息补上了,之后的维护者无需重新考古一遍调用链。我一度把语义地图当作AI代码库的急救措施,哪块代码最难改,就在哪块头部先补上它。几个月下来,团队接手AI代码时问“这个函数为什么存在”的次数少了很多。
5.3 让AI做单点重构,而不是大爆炸重写
存量AI代码里有很多“分散在各处、但逻辑相似”的重复实现。传统做法是把它们统一收拢成一个函数,这是一件技术操作上可行但风险很高的事。一个更稳妥的方法是把重构拆成单点任务,逐个做,并且让AI参与每一个单点的迁移。
比如要统一运费计算逻辑,我不会让AI“把所有运费计算函数合并成一个”,而是这样操作:先让AI梳理出所有调用运费计算的位置和差异点,列成一张清单;然后选择一个调用量最少的位置作为试点,把它切到新统一的函数上,跑测试验证;确认无误再切下一处。整个过程保持旧函数在大部分调用点继续工作,直到最后一处迁移完,再把旧函数删除。这种“增量替换”的方式,即使某个环节出错,也只影响一个小范围,回滚也容易。
在这个流程里,AI的用武之地不是“一次性重写”,而是帮我们生成迁移清单、为旧函数补齐测试用例、写迁移后的diff。AI擅长处理确定性的重复劳动,让它去整理“哪些位置用了哪个实现”、生成单元测试来兜底,比让它直接大规模重构要安全得多。
5.4 技术债真正失控的原因,是AI被赋予了超出边界的影响力
最后想强调一个容易被忽略的管理因素:AI工具在代码库上拥有多大的改动范围,决定了它制造技术债的上限。如果AI被允许每次都能扫描整个代码库并自由修改多个模块,它的产出量会让任何人审查不过来,债务必然积累。
我现在对AI的使用范围做了限制,每次只能处理一个明确任务域。如果它说“这个问题要改到另一个文件”,必须额外征得同意并解释理由,禁止它顺手“优化”和任务无关的代码。这个约定执行起来有点费事,但对代码库的健康影响非常大。AI的每一个改动都应该是被看见、被理解、被批准的,而不应该是“它顺手改的”。
最后再分享一个控制债务的小习惯
踩过AI生成代码带来的各种坑之后,我养成了一个朴素但有效的习惯:每次让AI写完一段代码,我都会强迫自己回答一个问题——这段代码如果三个月后由我来改,我看到第一眼会不会想骂人?如果答案是会,那不管它现在跑得多好,我也会当场让它重写或者自己调整。这个方法听起来有点玄学,但它比任何审查工具都先到一步,因为它在代码生成的瞬间就在提醒我,AI生成的不是最终成品,而是一个必须经得起时间考验的初稿。技术债的关键从来不在“用不用AI”,而在于我们有没有把自己当成那个最终为代码负责的人。
