先问一句:你有没有经历过这种场景——让AI改一段代码,第一次报错A,它给你改了;跑一下变成报错B,再让它改,变成报错C;改到第五轮,你发现项目里多了好几个没用的函数、几处重复的逻辑,连原来能跑的部分都开始报错了。你开始怀疑AI是不是能力不行,但真正的问题其实是:你从一开始就喂了一个错误的问题给AI。
我自己也被这个问题折磨了小半年。直到某天凌晨三点,对着一个越改越乱的接口,我把报错从头到尾读了一遍,突然意识到一件事:报错不是拿来直接丢给AI的,报错是要先被压缩成最小示例,AI才能真正帮上忙。那天之后我养成了一个习惯——任何报错,先花十分钟缩成一个可以独立运行的小例子,再开口问AI。效果不是好了一点点,是好了非常多。
这篇文章就讲讲这个思路:为什么AI改代码会越改越乱,最小复现示例为什么能终结这个循环,以及具体怎么把一个几十层深的项目报错压成两三段代码。适合所有正在用AI辅助写代码、但又觉得AI经常越帮越忙的开发者。
1. 为什么AI改代码会越改越乱:不是模型变笨了
先说结论:AI在“改代码”这件事上,并没有你想象的那么强大。它既没有完整的项目记忆,也没有边运行边观察的能力。它所有的判断,都建立在你给它的那段文字和代码上。你的上下文给得越乱,它猜得就越离谱。
1.1 上下文碎片化:你以为AI看到了全部,其实它只是在猜
这个是很多人的第一直觉误区。你会觉得,我把报错贴给你,把相关代码段也贴给你,再加上我对需求的描述,已经够多了吧?但AI看到的并不是一个“项目”,它看到的是一堆彼此之间可能有冲突、有缺漏的文字片段。
举个例子,你说“用户列表接口超时,帮忙看下”,然后贴了controller层的代码、service层的代码、还有一段配置文件。AI要怎么处理?它只能自己去推断这些片段之间的关系。它不知道这个接口在真实运行环境中是怎么被调用的,不知道数据库里有多少数据,不知道Redis缓存有没有生效,更不知道那段配置是不是已经废弃了。
于是AI就处在一个高度不确定的“猜谜”状态。每一次修改,它都会基于自己的猜测建立一个新的假设。这些假设之间不一定兼容,甚至会和之前改过的逻辑冲突。一轮、两轮没问题,第三轮开始,你面对的就不再是“修改代码”,而是一个自己给自己叠buff的混乱现场。
说白了:你喂给AI的上下文越碎片化,AI的每一次修改就越像是赌博。
1.2 错误漏斗效应:你看到的报错往往只是表层
另一个容易忽略的点是:生产环境里的报错,很少是“单纯的错误”。报错信息是一条链,外层是框架的报错,中层是业务逻辑的问题,最里面才是真正的根因。
我记得有个项目的报错,前端看到的是HTTP 500,后端日志里是空指针异常,再往下看才是某条SQL查出来的数据是null,然后那个null被直接塞进了对象。如果你把这个500的报错丢给AI,AI大概率会告诉你“加个try-catch”。它能解决吗?能让请求不报500了,但数据还是不对,甚至可能因为catch吞掉了错误,后面连排查点都找不到了。
这个我管它叫“错误漏斗效应”:同一时间浮上来的错误表象可能有几十个,但真正的根因往往就一两个。你越是在表象层反复修改,新问题冒出来的速度就越快。因为你改的任何一层,都在扰动下面层的输入。所以AI看起来像是越改越乱,其实是它一直被误导去修表象,压根没有碰到内核。
2. 最小复现示例到底做了什么:把“推理题”变成了“证明题”
那为什么要缩小成小例子?核心原因是,小例子能同时砍掉“上下文碎片化”和“错误漏斗效应”这两个恶化因素。
2.1 它先替AI清掉了大半的“隐性冲突”
当你把一个报错缩成一个最小示例时,你会主动做这些事:把外部依赖换成固定值,把网络请求换成mock数据,把框架配置删掉,把业务逻辑简化成直白的输入输出。表面上你是在“简化”,本质上你是在消除所有让AI产生歧义的背景信息。
我之前遇到过一个问题:让AI帮我优化一个数据清洗的函数,结果它改完之后把空值处理逻辑彻底删了。我气得不行,后来才发现,我贴过去的代码里,空值处理逻辑在一个完全无关的旧分支里写着,AI以为那是废弃代码。这个场景太典型了——不是AI不想好好干活,是你给的信息让它以为自己在干另一件活。
小例子没有这个问题。里面没有几十个变量、没有状态依赖、没有运行时上下文。AI只需要处理一件事:给定这段输入,这个函数为什么不符合预期。它不再需要同时回答“问题是什么、代码哪里错、怎么修才不破坏别的功能”三个问题,它只回答最后一个。任务从“推理题”变成了“证明题”,出错面小了非常多。
2.2 缩到多大才算够:我自己的判断标准
这可能是大家最关心的问题。不是所有问题都能缩成三行代码的,尤其是涉及框架、数据库、消息队列的问题。我的经验是:缩到你可以在30秒内向一个同事口头复述清楚,并且对方不需要看完整项目就能理解“哪里出错了”的程度。
具体一点,有三个硬指标:
- 一是能独立运行,不依赖原项目里其他模块。
- 二是输入数据是写死的,不依赖数据库、缓存、外部API。
- 三是预期行为和实际行为之间的差异,一句话能说清楚。
这里多说一句:不要追求“越小越好”而走极端。你完全没必要把Spring Boot整个应用缩成一个main函数,那是另一种浪费。只要上面三条达到,AI需要的信息就已经足够少了。
2.3 压缩过程本身就在帮你发现真相
有个非常有意思的副产物:很多时候你还没走到问AI那一步,问题就自己浮出来了。
压缩报错的过程,本质上是“把代码里的自由度一个个删掉”的过程。你删掉外部依赖,用固定值传参,把循环体拆出来单独跑,每删一次,程序的行为就更可控一些。大部分真正的bug,在这样反复剥离的过程中会直接暴露,因为你会发现“哎?我换成固定值之后,这个报错消失了”“咦?这一步算出来的数不对啊”。
这就是所谓的橡皮鸭效应。你按步骤把一个复杂的漏洞拆解时,其实是在用逻辑强制自己重新走一遍代码路径。很多藏在业务逻辑深处的“低级错误”(比如字段名拼错、类型传反、状态没更新),在这一步就能抓出来。我自己大概有三分之一的问题,是在压缩过程中解决的,根本没走到AI那一步。
3. 把报错压缩成小例子的标准四步流程
那具体怎么压?我总结了一套固定流程,现在一直用,大概花十到二十分钟,视情况而定。
3.1 冻结环境:先把变量锁死
这一步很多人忽略。你以为代码一样,但环境不一样,跑出来的结果就是不一样。所以第一件事不是改代码,是先把环境“冻结”下来。
我的做法是:单独建一个虚拟环境,把项目依赖装好,然后记录当前Python版本、依赖版本、数据库schema版本和关键配置项。如果用的是容器化开发,这一步更简单,直接用当前的镜像id记录就行。然后在压缩过程中,坚决不做任何升级依赖、改配置的操作。环境一固定,报错的可复现性就有了保障。
一旦你边排查边升依赖,报错可能会从一个变成另一个,到时候你根本分不清是代码问题还是环境问题。
3.2 剥洋葱:把业务代码换成假数据
接下来处理代码层。原则是从外到内,逐层剥离:外部接口先mock掉、数据库查询替换成内存字典、配置文件里的连接串删掉,一步一步把项目代码“变成”一个普通脚本。
具体操作上,我会找一个最简单的主干路径:从入口函数进,一路走到报错点,把所有跟主路径无关的分支全部删掉或注释掉。删完外部依赖后,再把你怀疑出问题的那个函数单独拎出来,放到一个新建的测试文件里,配上几组写死的输入数据。
这一步的关键是别贪心。一次不要尝试同时处理两个问题,先锁定报错信息里说的那个位置,做一个最小信心测试,跑通了再往内走一层。
3.3 二分定位:找“第一次算错”的那个点
代码剥离到一定程度,你可能还要进一步定位,尤其是在报错不明显、只是“结果不对”的情况下。我推荐二分法,而不是逐行看代码。
先从入口到出口把整条链路画出来,找中间的一个判断点,打印出这个点上的关键变量值。如果这个值已经不符合预期,问题就发生在它之前;如果它是对的,那问题就发生在它之后。这样每跑一次,排查范围就缩一半。几轮下来,问题点通常会非常集中。
注意,这一步不要加太多日志。很多人一上来就在几十个地方加print,结果一跑输出几百行,反而分不清重点。我一般只加三个断点:入口值、中间关键值、出口值。三个值一对,方向就明确了。
3.4 写出一份AI能直接执行的复现说明
最后一步是整理“成果”,把它变成一份可以直接投喂给AI的包。这不是让你写一篇长报告,恰恰相反,越精简越好。
我的格式基本是固定的:
- 第一行:一句话说明现象——“当传入参数X时,函数返回了Y,期望是Z”。
- 第二段:可运行的最小代码片段,包含必要的import、输入数据和触发报错的那几行。
- 第三段:完整报错栈或实际输出结果。
这样一份材料,AI不需要问任何前置问题就能开始分析。如果你的问题是框架层的,框架的启动入口和报错栈还是要保留,但业务逻辑务必替换成假数据。如果你的问题是业务逻辑层的,只要保留函数体和输入输出就行。
在我实践了一阵子之后,一个很真实的感受是:压缩报错,其实60%的时间不是在为AI节省时间,而是在为你自己节省时间。因为你亲手把环境、代码、输入输出都理清楚了,AI只需要做最后的推理判断,效率自然高。
4. 一次从乱到治的完整实录:接口超时排查手记
讲理论太多容易飘,我给你还原一个我实际踩过的坑。这个案例很典型,几乎每个后端开发者都会遇到。
4.1 乱在哪儿:三轮修改,五个新问题
某天某项目里一个“根据部门ID查询用户列表”的接口,在数据量上来之后开始超时。我顺手把报错和整个service层的代码丢给了AI,AI很快给出了第一轮修改建议——说要加缓存。我改了,部署,不超时了,但第二天发现数据不更新了。因为缓存没有失效策略。
于是第二轮,我告诉AI“缓存导致数据不更新,帮我加失效逻辑”。AI加了失效逻辑,这次数据更新了,但出现了新的问题:在某些并发场景下,缓存击穿,接口耗时反而比以前更不稳定。第三轮,我继续让它调,它改了一版加锁逻辑,结果锁粒度过大,接口直接退化成串行请求,白天高峰期又超时了。
到这个时候,项目里多了三个新的缓存的工具方法、一个分布式锁的配置、以及一套我快看不懂的嵌套判断。接口状态比最初还要糟糕。我当时的念头就是:这AI怎么越改越乱。
4.2 压缩之后发生了什么:问题浮出水面
冷静下来之后,我把这个问题用上面那套流程压缩了一遍。
环境冻结没花太多时间,就是测试环境当前的依赖版本组合。然后我把代码里跟部门查询相关的分支剥离出来:去掉权限校验、去掉操作日志、去掉列表转树形结构的部分,只保留最核心的“根据部门id查用户列表”这个动作。外部依赖全换成假数据,数据库查询保留,但只保留一张表。
接着我跑了一次,看到耗时还是挺高。再往内拆,单独把那个循环体抽出来测试,我突然看到问题了:查询是在for循环里执行的——每查到一个部门,就发一次数据库请求。部门多了之后,这个N+1查询的耗时直接爆炸。而这个循环体就在service层那段代码的中部,前面包着权限逻辑,后面包着DTO转换,AI从头到尾都没看出来,因为我把一整个服务层丢给它时,它的注意力全被大结构带跑了。
我重写查询逻辑,把循环外的批量查询放进去,接口耗时从800ms降到了30ms。甚至没有用到任何缓存或锁——根因就是一条SQL的事。
4.3 同样的AI,两问两答的对比
我做了一次对比测试,用同一个AI,分别问两个版本的问题。
第一个版本,我贴出整个service层代码加接口超时描述,AI的回答是三段式的:建议加Redis缓存、建议做异步削峰、建议换数据库连接池配置。每一句都像是正确的“大厂方案”,但没有一个指向真正的N+1问题。
第二个版本,我贴的是压缩后的最小例子:几行代码、一个循环内查询、几组假数据、实际耗时数据。AI的回答非常明确——直接指出这个循环里有N+1查询问题,建议用批量查询替代,还顺带给出了具体实现。
同一台AI,同一个模型,对比如此明显。不是第一轮AI能力不行,是我给的上下文让它根本没机会看到那个循环。
5. 最小化思维的两个延伸用法
理解了压缩报错的逻辑之后,我发现这套思维不仅适用于修bug,还能辐射到AI协作的其他场景。
5.1 代码评审:一次只问一个“决策单元”
很多开发者喜欢把一大段代码贴给AI,问“这套代码有什么问题”。这是个典型的错误用法。一大段代码里包含的隐性决策太多了:数据从哪来、生命周期是什么、哪个分支是主路径、哪部分是遗留逻辑。AI全去分析一遍,最后给你的都是宏观层面的空话。
我现在做AI辅助代码评审,一次只问一个“决策单元”。比如“这个函数在传入空list时会怎么走”“这两个分支的条件在什么情况下会重叠”“这个异常被捕获后会不会吞掉应该暴露的信号”。一个问题对应一个明确的小函数或一段小逻辑,AI的回答准确率和落地性都高很多。
5.2 让AI输出错误树,而不是一次性答案
另一个技巧是:在小例子的基础上,让AI先列出“可能出错的原因树”,而不是直接给一个答案。
比如你有一个小函数运行结果不对,不要直接问“怎么修”。先问:“基于这段最小示例,按照从常见到罕见排序,列出可能导致这种输出偏差的所有原因。”AI会输出一个结构化的原因列表,你从中挑出最接近实际的那个,再针对具体原因去问解决方案。
这样做的好处是:你不会被AI第一次给出的“看起来合理但其实是错的”方案带偏,同时你也能从它的原因列表里学到一些盲区。压缩后的例子本身就小,AI列原因也列得完整,这个场景下它甚至有点像在给你做知识科普。
6. 我现在的工作习惯:先把报错缩到我能看懂为止
最后分享一点个人习惯。现在我不论遇到多复杂的报错,第一反应不是贴给AI,而是先执行那套四步压缩流程。环境冻结、剥洋葱、二分定位、写复现说明。这个流程本身帮我解决了很多“不需要AI也能解决”的问题,更重要的是,它为AI准备好了极其干净的输入,让AI真正回到“帮你思考”的位置上。
有读者可能会问,这不是把活都自己干了吗?其实不是。压缩报错准备的是“问题的上下文”,而AI负责的是“从上下文到结论的推理”。前者讲究的是筛掉噪音,后者讲究的是推理深度。你筛得越干净,AI的推理就越集中在关键点上,产出的正确率也就越高。
我现在甚至会为每个正在进行的项目单独维护一个小例子文件夹,把每次排查时缩出来的最小复现示例按问题分类归档。下次遇到类似报错,先翻这个文件夹,往往不管用再新建新的。时间久了你会发现,报错的种类远没有你想象的那么多,你真正需要AI深度介入的场景,其实越来越少了。