跳到正文

代码重构与调试 ​

一句话:AI 能在行为不变的前提下快速重排代码结构,也能把一屏错误信息收敛成几个可验证的假设;换来的是吞吐,代价是它分不清“改结构”和“改行为”,所以安全网必须由人在动手前铺好。

为什么需要 ​

存量代码的坏味道——重复、长函数、大类、深嵌套——人工清理很慢,AI 读完整个文件只要几秒。问题出在验证端:AI 一次输出几十处 diff,中间夹着一处“顺手优化”的逻辑改动,人眼几乎看不出来。调试是同一个矛盾的另一面:堆栈、日志、跨文件调用链 AI 读得比你快,但它给的修复可能用了不存在的 API,也可能用 try-catch 把异常吞掉——报错消失了,问题还在。

所以流程目标不是“让 AI 改得更多”,而是把人的验证成本压到最低:改动小到一眼看懂,失败能定位到一步之内,结论能被测试或日志证伪。

核心机制 ​

安全重构:五步循环 ​

重构的定义是“不改变外部行为,只调整内部结构”,这把验收标准说死了:成败不看代码好不好看,只看行为有没有变。而行为只有两种表达方式——测试,或人肉对比。AI 一次改几十处时人肉对比必然漏,所以顺序只能是先有安全网,再动代码——没有测试的重构是赌博,改完你不知道行为变没变。

图里两个回退点的范围不同:测试没绿时退回补测试,单步改完变红时只回退这一步。范围越小,越能判断是哪一处破坏了行为;一次改完整个文件再跑测试,红灯意味着几十处变更里有一处错,定位成本接近重写。第一步的“特征测试(characterization test)”不断言代码应该怎样,只记录代码现在怎样——给定这组输入,当前输出是这些值。对遗留代码这是唯一可行的安全网,因为你不知道正确行为是什么,只知道不能变;代价是会把现有 bug 一起锁住,明显荒谬的断言要人过一眼。其余四步是:AI 列坏味道、人选定一种模式、单函数小步改、每步跑测试后审查并单独提交——一次一种重构,范围永远是一个函数。

六种重构模式 ​

模式触发信号代价 / 风险
提取函数函数超 30 行(经验值,提取候选;到 50 行则必须拆分)、有明显逻辑块、同段代码多处出现函数过多、调用链变长;名字起不好反而更难读
重命名名字有歧义、缩写、看不出意图全局替换会漏掉字符串、反射和配置;对外改名是破坏性变更
消除重复两段逻辑几乎相同、只差参数把“看着像但本质不同”的代码强行合并,制造假耦合
拆分大类/长函数类超 300 行、函数超 50 行(必须拆分;30 行起为提取候选)、一个类做多件事边界划不清时拆完更难懂;跨类调用增加
条件逻辑改多态switch/if-else 按类型分支,新增类型要改多处类数量膨胀;只有两三个分支时反而更啰嗦
引入设计模式能明确指出当前写法的痛点在哪个调用方没有痛点就是过度设计;多一层间接,调试和阅读成本上升

两个重构 prompt ​

text
【提取函数】对下面这个函数做重构:识别可以独立成块的逻辑,每段提取成新函数,函数名说明它做什么。原函数改为调用新函数,行为必须完全不变——不改算法、不优化逻辑、不动异常处理、不改公开签名。只改这一个函数及其新增的私有函数,在改动处加注释。代码:<粘贴>

【消除重复】找出下面代码中逻辑相同的代码段,列出差异点,把公共逻辑提取为函数、差异点作为参数。如果两段看着像但本质不同(如一处业务校验、一处数据清洗),不要强行合并,说明理由。保持行为和公开接口不变,只改这个文件。代码:<粘贴>

AI 重构的五个坑 ​

坑表现怎么防
顺手改行为重构时“顺便优化”了逻辑prompt 写明“只改结构,不改逻辑”;每步跑测试
一次改太多一次重构整个文件或模块一次一种模式、一个函数;diff 超出范围就拆
过度设计简单分支被套上策略模式、工厂先让 AI 说出当前痛点和调用方,说不出来就不重构
破坏公共接口改了签名,调用方全崩要求公开签名不变;必须改时加兼容层并按版本废弃
没有测试就动手改完不知道行为对不对先补特征测试;补不出来就先只读代码、不改

大型重构:绞杀者与分支抽象 ​

  • 绞杀者模式(Strangler Fig):在老系统旁建新实现,用路由或代理把部分流量切过去,逐步替换模块。适合不能停机、边跑边换的系统;代价是双系统并行期长,数据要同步,排查要看两套。
  • 分支抽象(Branch by Abstraction):先引入接口,老实现挂上去,调用方全部改为依赖接口,再写新实现并靠配置切换,稳定后删旧实现。适合替换 ORM、消息队列这类底层依赖;代价是抽象层本身成了负债,周期拖太久就会永久留下。两种模式里 AI 都是执行者不是决策者:拆几个服务、接口怎么切分由人定,AI 按既定边界做机械迁移,产出再用测试和流量对比验证。

AI 调试:五步法 ​

报错只说明“哪里炸了”,不说明“为什么这个值会到这里”。所以必须先把不可复现变成可复现,再让 AI 在完整信息上生成假设,最后由人用日志或最小脚本逐个证伪。

分叉点在“有没有堆栈”:有堆栈说明已定位到行,剩下的是追溯数据来源;没有堆栈的逻辑错误只能模拟执行,找第一处偏离预期的变量再往回追。两支最后都汇到“多个假设 + 逐个验证”,因为 AI 的第一个答案常是症状层面的,只接受一个假设等于放弃排除法。第五步也不能省:只修不补测试,同一根因会在下次改动里复活;只补测试不跑全量,可能改了 A 坏了 B。

六类错误与两个调试 prompt ​

类型特征AI 能帮到哪人的动作
编译/构建错误有错误码和位置高:贴完整输出即可定位确认版本与依赖是否匹配
运行时异常有堆栈,如空指针、越界高:定位行并追数据来源判断根因是否在调用方
逻辑错误不报错但结果不对中:逐步模拟找偏离点提供准确的输入、预期、实际
性能问题慢、内存涨、CPU 高中:解释 profile 结果先 profiling 定位瓶颈再问
并发问题偶发、难复现、竞态或死锁低:能列风险点压测、日志、代码审查
依赖/环境问题版本冲突、配置错、部署失败高:给版本和最近变更复现环境、锁定版本
text
【运行时异常】完整堆栈:<粘贴>。相关代码:<粘贴>。触发条件:<描述>。环境与版本:<…>。按顺序回答:异常在哪一行 → 根因是什么(追溯这个值从哪来、为什么是它)→ 修复方案 → 为什么之前没暴露 → 防御性建议。不要用 try-catch 吞异常或空值兜底来"消除"报错;只能这样处理时,明确说明这是临时缓解。

【逻辑错误】代码不报错但结果不对。函数:<粘贴>。输入:<值>,预期输出:<值>,实际输出:<值>。逐步模拟执行,列出每步中间变量,指出从哪一步开始偏离预期,解释原因,再给修复代码和回归测试。

调试的四个坑与信息清单 ​

坑表现怎么防
幻觉修复修复里用了不存在的 API 或参数查官方文档确认存在;改完必须真的跑
治标不治本加判空、加 try-catch 让报错消失追问“这个值为什么会出现”,追到数据来源
改了 A 坏了 B修复引入新问题跑全量测试、写回归测试、小步提交
只接受第一个答案AI 说什么就是什么要 2-3 个假设并排序,逐个验证

信息清单一次给全:完整堆栈(不是最后一行)、相关代码、输入数据、预期与实际、环境与版本、最近变更、复现频率。缺哪一项,AI 就多一次猜的机会。案例:首次登录偶发失败,按复现 → 收集日志 → 提出假设 → 验证 → 修复走完后发现,判空只是治标,异步落库尚未完成就返回才是根因——验证必须落到落库时序上,而不是停在报错消失。

实战场景 ​

事故复盘:一次“提取公共函数”改了行为,测试没拦住 ​

现象:季度结算批处理夜里正常跑完、无报错、CI 全绿,第二天对账发现金额缺口,差异只集中在“缺金额字段”与“未显式传币种”的那批订单,占比不到 1%,日间回归从未覆盖这两种输入。回滚后按提交二分,定位到前一天的 refactor: 提取结算公共计算段——一个不到 20 行的 diff,里面同时藏了三处行为改动,没有一处属于“重构”:

diff
 def settle(rows, currency="CNY", raise_on_missing=True):
-    if raise_on_missing and r.amount is None:
-        raise MissingAmount(r.id)              # 原来:缺金额直接上抛
-    return convert(r.amount, currency)         # 原来:币种逐单透传
+    return sum_amounts(rows)                   # ① 调用没带 currency,形参成了摆设
+
+def sum_amounts(rows, currency="USD"):         # ② 提取的新函数自带另一套默认值
+    if r.amount is None:
+        log.warning("missing amount %s", r.id) # ③ 上抛被降级成日志
+        return Decimal(0)                      #    并返回 0 继续累加
+    return convert(r.amount, currency)

为什么没被拦住:签名没变,所以调用点和编译都不会报错;该模块的特征测试只断言了“金额齐全 + 显式传币种”的主路径,① 与 ③ 走的分支从未被记录。公开签名不变只是必要条件,不是充分条件。人 review 时读的是新结构好不好看,不是逐条对照旧行为——正文“五个坑”里的“顺手改行为”和“没有测试就动手”同时命中,缺的那两块安全网恰恰是事故发生的那两块。修法是重构三段纪律,顺序不可颠倒:

段动作产出
基线快照动手前记录当前行为:公开签名、每个默认值、每条 raise / return / continue 的触发条件列成一张表;补不出断言的分支先用特征测试钉住行为基线表(贴进 PR 描述)
AI 改写一种模式、一个函数,prompt 写死“不改公开签名、不改默认值、不动异常处理、参数必须继续传递”;diff 越界就整段退回重做单步 commit
diff 语义审查人只审行为差异,按下面清单逐条打勾审查记录 + 合入决定

diff 语义审查清单(每条都要指到具体行,不写“看着没问题”):

  1. 签名与默认值:参数顺序、可选性、默认值有没有变;新提取函数的默认值是否与调用处一致;参数是否真的被传递下去。
  2. 异常路径:原来 raise 的位置现在是不是变成了 log / return / 静默 continue;新增的 try 吞掉了哪些异常类型。
  3. 兜底与控制流:新增的 or、if not、默认值回填、continue / break / 提前 return 的数量与位置——多一个 continue 就多丢一批数据。
  4. 边界:循环上下界、时区与精度、舍入方向、空集合与单元素集合。
  5. 范围:diff 里有没有出现本次任务没点名的文件或函数。

任何一条命中,要么回退这一步,要么把它拆成独立的 fix: / feat: 提交并按行为变更走评审。它的本质是改了行为,只是借了重构的壳——把它们混在一个 refactor: 里,回滚时无法只回退一半(见 07-企业工作流与大代码库 的增量提交)。事故之后的固定动作是补一条能复现原问题的回归测试:让“缺金额字段必须抛错”“币种必须透传”这两条断言进 CI,而不是进下次记忆。

案例:AI 定位比人快的那个晚上,但修复是人写的 ​

现场:登录接口偶发超时,复现率约百分之几,无固定时间规律、无异常堆栈,只有一条网关侧的 upstream timeout。人工排查两小时没有结论——常规思路全跑过一遍:看监控(服务 P99 正常)、看错误日志(无报错)、看依赖(DB 慢查询面板干净),三条都指向“不是我们的问题”。切给 Agent 时只给了正文那份信息清单:完整堆栈与相关代码、触发条件、环境版本、最近一次发布记录、复现频率。

过程:Agent 没有直接改代码,按五步法走到“2–3 个假设并排序”,然后逐个证伪,每一轮插桩同时检验两条:

text
H1(高)认证依赖在新版本灰度机器上变慢   → 按 pod 版本分组统计 P99
H2(中)连接池等待而非下游慢             → 埋 pool.wait_ms / pool.in_use
H3(低)客户端重试把首屏请求挤掉         → 网关侧按 request_id 去重计数
第 1 轮:H1 否(新旧版本同分布);H2 显示慢请求的 wait_ms 从 300ms 涨到 4.2s
第 2 轮:H3 否(同一 request_id 只有一次上游调用);H2 补测 in_use 长期贴着 max
第 3 轮:对 H2 二分——把 pool.max 临时调高的灰度组超时率归零,根因收敛

全程约 20 分钟,命中点是“某条异常返回路径没有归还连接”:泄漏缓慢累积到池上限,表现为偶发、无堆栈、监控干净——正好是人最容易放弃的那类症状。它快不在模型更聪明,而在能并行写插桩代码、并行跑验证,把“提假设—取证—淘汰”这个循环的迭代间隔压到分钟级;而按正文的六类错误表,并发/资源类问题 AI 单独判断能力是“低”,这里成立是因为人先给了复现策略与观测入口。

但最后修复是人改的。 Agent 给的候选改法是 timeout=None 与加大 pool.max——前者把报错消除掉、后者把泄漏容量化:都是正文“治标不治本”那个坑。真正的修复要同时满足三个 AI 不掌握的约束:这个池被四个服务共享、DB 侧连接总量有硬上限、泄漏路径本身要归还连接。所以人把改动定在“异常路径统一进 finally 归还”+按 DB 容量重新核算池参数,AI 负责据此生成回归测试草稿(断言人审,防止同源锁错行为),最后跑全量。这套分工后来原文写进团队的排障值班手册:

环节交给 AI必须人做
复现写最小复现脚本、造负载定复现频率与采样开销能否上生产
假设给 2–3 个并排序、说明证据需求剔除业务上不可能的、补历史约束
验证写插桩代码、按结果二分、更新排序判断证据是否足以确认根因
修复给候选改法并解释代价在共享资源与容量约束里做取舍
收尾生成回归测试草稿人审断言、跑全量、单独 fix: 提交

一句话概括这条分工:定位是搜索问题,修复是取舍问题。搜索空间大、验证成本低的部分交给 AI;涉及跨服务约束、容量上限、回滚代价的部分留在人手里,并且 SOP 里写死“根因确认前不许改代码”,否则 AI 会在第一个看似合理的答案上直接动手。

最小上手 ​

前提:Python 3.10+ / pytest 8.x,其他语言换成对应测试命令。核心不是工具,而是顺序——测试先红,代码后改。

bash
git switch -c fix/login-new-user-null    # 独立分支,整段可回退
# 1. 先写能复现 bug 的测试,确认它红在 bug 上(不是红在导入错误)
pytest tests/test_login.py::test_new_user_login -q    # 预期 FAILED 500 != 200
# 2. 用「运行时异常」prompt 让 AI 给假设,先不让它改代码
# 3. 验证假设后只改根因:不加 try-catch 吞异常、不用空值兜底绕过
pytest tests/test_login.py::test_new_user_login -q    # 预期 PASSED
# 4. 全量回归,确认没改坏别处;5. 只提交本次修复涉及的文件,测试与修复一起提交
pytest -q && git add tests/test_login.py src/auth/service.py && git commit -m "fix: 注册后等待落库再返回,修复新用户首次登录失败"

重构走同一套骨架:pytest -q 全绿 → 指定一种模式和范围 → 改完再跑 → 全绿再 commit。

生产动作 ​

  • 门禁与粒度:CI 全绿才允许 AI 动代码,关键路径无测试覆盖的模块先补测试;一次重构一个 commit,用 refactor: 前缀,修复用 fix:,两者不混在一个提交里。
  • diff 审查:只看公开签名有没有变、异常处理有没有被吞、有没有范围外的文件改动。
  • 排障路径:重构后 CI 红 → git diff --stat HEAD~1 → 超出单个函数就拆 commit;线上新错误 → 对照最近提交和版本号 → 判断是否重构引入;修复可疑 → 搜 diff 里的 catch、??、|| 兜底 → 判断是修根因还是掩盖。
  • 回归测试必留:每个线上 bug 修复都配一条能复现原问题的测试,否则同类问题会换个形式回来。

边界 ​

  • 适合重构:行为稳定、有测试或能补出特征测试、坏味道能指到具体调用方的代码。不适合:完全无测试的遗留系统(先加日志和监控)、需求正在大改的功能(先改行为再重构)、性能优化(那是换算法,不是重构)。
  • AI 调试适合:有堆栈、输入可复现、单线程逻辑。不适合:竞态、内存泄漏、环境差异——这些要先压测和 profiling,AI 只能给方向,不能替代观测。
  • 代价与误区:小步重构更安全,但 commit 多、review 次数多;AI 提速明显,但验证成本从“写代码”转移到“读 diff 和跑测试”,测试基础差的团队净收益可能为负。最常见的误区是让 AI 生成测试来验证 AI 的重构——同源,可能一起锁错行为,关键断言必须人审;另一个是把重构当重写,行为一变所有测试失效。

讲清楚 ​

重构和调试是一件事的两面:AI 输出得快,但判断对错只能靠人,所以流程要把验证成本压到最低。重构的验收标准是行为不变,而行为只有测试能表达,所以第一步永远是补特征测试——它不判断对错,只锁住现状,代价是把已有 bug 也一起锁住。之后一次一种模式、只改一个函数、每步跑测试、单独提交,失败时回退范围就是一次 commit。AI 在这里是结构调整器,不是架构师。调试同理:AI 擅长读堆栈和生成假设,复现和验证必须人做。先稳定复现,再给全信息,让 AI 给出多个假设并排序,逐个证伪,最后修根因而不是加 try-catch 掩盖,并补一条回归测试。判断修复是否成立,看根因是否消失,不看报错是否不见。

适用:2026 年主流 AI 编程工具 | 初稿 2026-10

把 AI 工程讲到既懂也会用