☰
用最小复现例子驯服AI改代码,告别越改越乱
2026/10/20 4:08:28 网站建设 项目流程

AI改代码,最典型的翻车现场就是:你贴过去一整段代码,前面加一句“帮我修一下这个报错”,AI回复一个看起来很合理的修改,跑起来却换了新报错;你再贴回去,它又给你改一个新地方。来回几轮之后,代码已经乱到不敢合并,报错也从一条变成三条。我自己踩过这个坑,后来总结出一个特别土但特别有效的办法:不管报错多大,先动手把它缩成一个最小复现例子,再交给AI去改。这篇文章就把这个习惯背后的原理、手工构造方法、一次真实的对比案例和避坑清单完整写出来,适合所有用AI编程助手但被“改乱代码”困扰的人。

1. 先搞清楚AI为什么会越改越乱

1.1 上下文被噪声带偏:给得太多,AI反而看不见报错

很多人觉得“信息越多AI越聪明”,实际恰恰相反。当你把整个项目文件、一大段几十行的业务代码丢给AI时,AI会对所有内容平均分配注意力。假设这段代码有200行,跟报错真正相关的可能只有5行,剩下95%全是无关逻辑、注释、函数调用、配置读取。AI并不知道哪些是噪声,它会从中“读取”出各种潜在问题,然后开始过度联想。

最典型的表现就是“顺手优化”。我有一次让AI修一个字段类型错误,它第一轮就把函数签名改成了泛型写法,加了三个校验分支,还改了调用处的参数名。报错确实不报了,但改动量从预期的一行变成了十几个文件的diff。为什么?因为AI在上下文中看到了一些“写得不太好”的代码,它觉得既然是“帮我改代码”,那把这里也改一改应该没问题。可这对我们来说完全不是预期内的修改。

这就像你请装修师傅来处理墙角裂缝,他进门后开始评估全屋要不要重新刷漆、地板要不要换、门框要不要校正。你只想要一管胶把裂缝堵上,他给了你一个十万块的翻新方案。AI越改越乱,第一步通常就是输入里的无关信息太多,导致它分不清任务边界。

所以核心思路很简单:把报错相关信息在输入里的占比尽量提高,逼近100%。你要让AI面对的只有“这个函数、这个输入、这个报错”,而不是“整个项目、一堆历史包袱、各种边界情况”。

1.2 打地鼠循环:每修一次就产生一个新错误

第二个原因是AI对话式修改的固有缺陷。AI不是一次性把整个项目读进去做全量分析,它是在对话上下文中增量生成代码。第一轮的错误修完,第二轮它会基于第一轮改完的代码继续生成。一旦第一轮引入了新问题,第二轮就开始处理新问题。这时候就像打地鼠,旧的刚按下去,新的又从另一个洞里冒出来。

我见过一个很典型的循环:AI先把解析函数里的类型转换改掉,结果调用方拿到新格式后解析失败;它又去改调用方,改完发现另一个模块依赖旧格式;再回头改第一个函数,结果第一种报错又回来了。整个过程中,上下文窗口里累积了大量的旧报错、新报错、已修改的代码片段,AI对“最初那个目标”的记忆其实在慢慢模糊。

这就是所谓的“目标漂移”。第一轮目标是“修类型错误”,第二轮目标变成“适配一种新格式”,第三轮目标已经变成“重构整个模块”。等到第五轮,它可能已经在改配置文件的读取顺序了。你跟它说的是“修一个bug”,它最后在给你做“代码美化工程”。

还有一种更隐蔽的现象叫“幻觉修复”。上下文很长的时候,AI会误以为某个变量已经定义、某个工具函数已经存在,于是在修复代码里直接引用了一个根本不存在的东西。这不是它笨,而是它基于长期上下文做概率推断时,前后矛盾了。办法不是去“教育”AI,而是减少上下文里的变量数——变量越少,幻觉空间越小。

1.3 为什么“最小例子”能一次说中要害

既然问题出在上下文污染、目标漂移、幻觉修复,那反推回来就有一个天然解:不让AI看那么多,只让它看一个最小的、纯粹的、能触发同一个报错的例子。

最小例子的好处可以从四个角度看。从信息角度,它把报错相关的信息浓度拉满,AI不需要猜哪些代码有用。从调试角度,最小例子固定了输入和期望输出,AI能在脑子里“干跑”一遍代码逻辑,不需要真正执行就能发现矛盾。从验证角度,小例子运行成本极低,AI给了修复后你可以秒级验证对错,快速形成反馈闭环。从成本角度,上下文越小消耗的token越少,等待时间越短,就算AI改错了,重新来一轮的代价也低到可以忽略。

说白了,构造最小例子的过程不是“给AI省事”,是“给自己省事”。你花十分钟把报错缩成一个20行的脚本,后面能省下两小时的拉锯战。

2. 最小复现:把报错缩成小例子的实操方法

2.1 先明确标准:一个合格的最小例子长什么样

很多人在这一步会问:什么算“最小”?是不是代码越短越好?不是。最小不是绝对意义上的短,而是“删掉任何一个元素之后,报错就不再复现”的那一刻。换句话说,它能稳定触发同一个报错,同时没有任何多余的部分。

我一般用四个标准来判断一个最小例子是否合格。

第一,稳定复现。同一段代码、同一个输入,运行100次,每次报的错必须和原项目里看到的一致,包括异常类型、错误消息里的关键片段、甚至堆栈顶部指向的行号。如果报错时有时无,说明你没找到真正的触发点。

第二,固定输入。所有导致报错的输入必须是写死在代码里的字面量,不能从命令行、配置文件、数据库或网络里读取。比如报错是因为一个字符串“2h30m”进入了解析函数,那你就在代码里直接写parse_duration("2h30m"),不要再去读某个Excel。

第三,最小依赖。不连数据库、不读配置文件、不碰网络接口、不启动消息队列。凡是外部的东西,一律用固定值或最简单的桩对象代替。这一步会逼你找出代码本身的逻辑错误,而不是环境配置错误。

第四,最小代码。只保留触发报错的函数以及必要的最小调用入口。函数体内与报错无关的分支可以删掉,不相关的参数可以先用默认值占住。一个合格的最小例子通常在10到50行之间。

还有一个很灵的自检方法:写完之后,尝试从里面删掉任何一行、任何一个参数,如果删掉之后报错消失了,说明你删过头了或者还有更小的版本;如果删掉之后报错不变,说明这行本来就是多余的,可以继续缩。重复几次,你会得到一个不能再小的骨架。

2.2 五步构造法:手把手把报错“剥出来”

直接上我平时用的固定流程。先说明一下,这个过程建议手工做,不要一边缩一遍让AI代劳——因为中间你会对这份代码建立起直觉,这才是后面诊断的关键。

第一步,复制完整报错。把异常类型、异常消息、完整堆栈全部复制下来。重点看堆栈里第一个指向你自己项目文件的帧,不要盯着最后一行。框架内部的帧大多是噪声,真正出错的是第一个进入你代码的调用点。那个文件路径和行号,就是你要找的起点。

第二步,抽函数。到那个文件、那一行,往上找到所在函数,把整个函数体连同def一行复制出来。如果报错跨了好几个函数,先把最内层那个函数复制出来,后面再逐层往外包。这一步先别管依赖,原样复制就行。

第三步,去依赖。检查函数体里访问了哪些外部的东西:全局变量、环境变量、数据库连接、另一个模块的函数调用。把能用固定值替换的,直接替换成字面量。如果某个外部对象太复杂,比如一个ORM模型实例,那就写一个只有必需属性的桩对象,或者干脆用一个字典凑合。目标是:这个函数现在可以脱离原项目单独运行。

第四步,固定输入。在文件底部写一行调用,把触发报错的那个具体输入值写死。比如parse_duration("2h30m"),然后print(...)看一眼结果,或者干脆不print,让它直接崩。注意不要模拟完整的运行入口,不要引入参数解析逻辑,入口越简单越好。

第五步,反复运行确认报错一致。执行这个脚本,确认报错类型、消息、堆栈关键行都和原项目一致。如果报错变了,说明你删掉了某个必需的前置状态,回去补上。如果一致,这个最小例子就合格了。

我习惯在文件顶部加一行注释# 最小复现,跑通后要么直接删掉,要么挪进一个专门放复现例子的目录。这样不会污染项目代码,也不会被人误以为是生产文件。

2.3 缩不动时的两个兜底方案

实际操作中你会发现,有些报错不是那么容易缩。最常见的是两类。

第一类,报错发生在框架启动阶段,比如初始化函数里依赖一大堆框架级对象:全局容器、中间件、配置中心。这时候不要试图原样启动框架,而是做“手工桩”。把那些框架对象替换成一个你自己写的、只有必要属性和方法的简化对象。比如原来代码里用config.get("timeout"),你就写一个class FakeConfig,里面固定返回30。目的不是完美模拟框架,而是让代码逻辑能走到报错那一行。

第二类,报错依赖特殊数据,本地根本没有。比如同事那边有个奇怪的输入,你环境里造不出来。这种时候先用“结构等价”的伪造数据,按报错消息里的关键字段构造一个同类型的输入,先确认代码逻辑本身会报什么错,再拿真实数据去对齐。只要异常类型和关键消息对上了,就已经足够给AI诊断。

还有一个万能工具:二分裁剪法。把复现代码先保留一个能跑起来的状态,然后注释掉一半代码,看报错是否还在。如果还在,说明关键逻辑在前半部分;如果不在了,说明在后半部分。不断二分,通常十来分钟就能定位到最小集合。这个方法特别适合那种“代码量很大但不知道哪一行触发”的情况。

3. 一次实战:同一个报错,两条路线对比

3.1 问题背景

前阵子同事在处理一个内部工时统计工具,功能很简单,就是把“2h30m”这种时长字符串转换成分钟数。运行后抛了这么个错:

ValueError: invalid literal for int() with base 10: '30m'

堆栈指向的函数叫parse_duration。同事当时做的是把整个模块的代码连带调用方一起复制给了AI编程助手,来回改了八轮,越改越乱。我后来接手,先花了十分钟把报错缩成了一个最小例子,然后一轮解决。下面把两条路线的过程都复盘出来。

3.2 路线一:直接给AI整段项目代码,越改越乱的完整过程

当时的原始函数长这样:

def parse_duration(text: str) -> int: parts = text.split("h") hours = int(parts[0]) minutes = int(parts[1]) return hours * 60 + minutes

但同事没有只给这一个函数,而是把整个模块的300行代码都贴了过去,包括Excel读取、部门权限判断、配置文件加载。AI看到这堆东西之后,开始了它的“打地鼠之旅”。

第一轮,AI认为可能是输入字符串开头或结尾有空格,导致解析异常,于是在函数第一行加了text.strip()。跑了一下,报错依旧。因为"2h30m"本身根本没有空格。

第二轮,AI换了个思路,认为时间格式可能是“2:30”这种冒号形式,于是把split("h")改成了split(":")。这次parse_duration("2h30m")直接返回一个错误结果,而且其他依赖"h"格式的调用方全被打乱了。

第三轮,AI终于意识到问题出在"30m"转int失败,但它没去修解析逻辑,而是给整段加了try/except ValueError,让函数返回None。表面上看报错消失了,结果调用方在高高兴兴地做int(parse_duration(...)),新的TypeError又冒了出来。

第四轮,AI去改调用方,把parse_duration的结果做了默认值处理,解析失败就按0分钟算。这下程序不崩了,但业务数据全是错的,一个人明明干了两小时,统计出来是0分钟。

第五轮开始,AI已经彻底失去方向,开始改一些无关函数,比如顺手把读取Excel的代码改成用新库,还改了一个配置文件加载逻辑。整个diff已经没法看了。

我后来整理了这轮全过程的对比表格:

轮次AI的改动结果新增问题
第1轮加text.strip()报错未变无,纯无效修改
第2轮split("h")改为split(":")格式破坏多个调用方解析错误
第3轮加try/except返回None原报错消失调用方出现TypeError
第4轮改调用方做默认值程序不再崩溃业务数据错乱,静默错误
第5轮开始重构无关模块文件剧烈膨胀引入新的兼容性问题

问题不在于AI不聪明,而在于它没有“现场调试”的能力,只能根据上下文猜。上下文里信息越多,它猜偏的概率越大,每一步看起来都合理,叠加起来就完全失控。

3.3 路线二:先缩成小例子,一轮解决的正确姿势

我接手后做的事情很简单,把parse_duration连同固定输入单独抽出来,变成下面这个最小例子:

def parse_duration(text: str) -> int: hours = int(text.split("h")[0]) minutes = int(text.split("h")[1].replace("min", "")) return hours * 60 + minutes print(parse_duration("2h30m"))

然后我只把这段代码和报错发给AI,没有附带任何其他信息。这时候AI面对的输入里,报错相关信息占比是100%。

AI的回复也很直接:“2h30m”按"h"分割得到["2", "30m"],第二个元素是"30m",里面并没有"min"这个子串,所以.replace("min", "")不会生效,然后int("30m")就崩了。这个分析我一看到就知道它找对根因了。

跟它确认修复建议后,它给了这样的代码:

import re def parse_duration(text: str) -> int: m = re.fullmatch(r"(\d+)h(\d+)m", text.strip()) if not m: raise ValueError(f"无法解析时长格式: {text}") hours, minutes = map(int, m.groups()) return hours * 60 + minutes print(parse_duration("2h30m"))

运行结果输出150,和预期完全一致。整个诊断和修复过程只花了一轮对话,前后不到五分钟。

4. 把“先缩成小例子”变成日常习惯

4.1 一套可以直接复制的报错提问模板

给大家一套我平时用的提问模板,复制就能用:

我有一个报错: [报错类型]: [错误消息] 最小复现代码: ```python [这里放最少可运行的代码]

固定输入:xxx 期望输出:xxx 实际报错:xxx

请只修改这个函数,保持函数签名不变,不要改动其他代码。先告诉我根因,再给修改后的完整函数。

这个模板里每一句话都有用意。“保持函数签名不变”是为了防止AI顺手改参数结构,导致调用方跟着崩。“不要改动其他代码”是明确任务边界,堵死它“顺手重构”的路。“先告诉我根因,再给修改后的完整函数”是逼它先做分析再动手,而不是直接给你一堆改动。 ### 4.2 约束AI的另外三个小技巧 除了提问模板,还有三个经验积累下来的小技巧。 第一个,要求AI只输出diff,而不是完整文件。完整文件的输出会让人放松警惕,不想逐行看;diff则强迫你看到每一个改动点,AI也不会因为要重新生成整个文件而带入额外变化。很多工具本身就支持diff模式,没有的话就在需求里加一句“请只输出改动部分的diff”。 第二个,如果AI在回复里附带“顺便建议这里也可以重构一下”这类内容,一概忽略。重构建议有时确实有价值,但绝对不应该混在bug修复里。正确的做法是先用最小改动把报错解决,然后另开一个对话专门讨论重构,那个对话里给足上下文。 第三个,一次只解决一个报错。如果项目里同时有两个报错,不要一起丢给AI。修完第一个、验证通过、确认不影响其他功能,再处理第二个。这能避免AI在一个对话里来回横跳,也方便你自己追踪每一步的改动。 ### 4.3 常见问题速查表 我把实际操作中经常遇到的情况整理成了一张速查表: | 现象 | 可能原因 | 解决动作 | |---|---|---| | AI改了之后报错没变 | 最小例子没有命中根因 | 检查最小例子和真实报错是否完全一致,重新缩 | | AI新代码引用了不存在的函数或变量 | 上下文过长引发的幻觉修复 | 恢复到上一版,明确要求只改指定行 | | 小例子不报错,但原项目报错 | 真实输入和示例输入不一致 | 把真实触发值替换进小例子,重看结果 | | AI越改越乱,涉及文件越来越多 | 目标漂移 | 立即回滚,重新缩成更小的例子再试 | | 不知道怎么缩 | 没找到第一个项目栈帧 | 复制完整堆栈,定位第一个自己代码的帧 | | 缩完发现报错变了 | 删掉了前置状态或依赖 | 逐个恢复被替换的外部依赖,直到报错一致 | 这张表我贴在很多项目文档里,团队里的人遇到同类问题,先看表再动手,能省掉大量来回试错的时间。 ### 4.4 什么时候可以不用小例子 最后说一个边界:不是所有报错都需要缩成最小例子。如果问题极其简单,比如变量名拼错、漏了个冒号、单行逻辑明显写反,直接把那一行代码和报错贴给AI就行,甚至不用贴报错就能改对。判断标准大概是:问题定位在一行内、根因一目了然、改动不会波及其他调用方。一旦报错涉及的代码超过十行,或者依赖了环境状态、外部数据、跨函数调用链,就老老实实先缩。 经验是:与其赌AI能透过表象看本质,不如先花十分钟让报错“无处可藏”。 我个人这几年带团队,每次有人抱怨AI越改越乱,我的第一句话都是:报错缩成最小例子了吗?大多数人缩完之后,问题其实已经解决了一半。因为一旦把报错缩小到最小范围,你往往已经知道了问题在哪,AI只是帮你把修复方案写出来而已。最后再分享一个小习惯:我会把每个最小复现例子连同修复后的代码一起放进项目文档里的复现案例目录,下次遇到同类报错直接翻出来参考,连prompt都不用重新写。这件事一开始做觉得麻烦,但攒了几个案例之后就发现,它比任何AI使用技巧都值钱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询