用历史Bug数据训练AI生成回归断言:实战经验与踩坑指南

📅 发布时间:2026/10/20 11:43:47
用历史Bug数据训练AI生成回归断言:实战经验与踩坑指南
前阵子我干了一件不一定算得上聪明、但绝对够折腾的事把项目里一百来个历史 Bug 的记录整理成结构化数据全喂给了一个开源的代码大模型。折腾了差不多三周现在它已经能在我提交新代码时直接给我输出一组能拦截回归的测试断言准确率比我团队里不少初级测试同学手写的还稳。这篇文章就聊聊我是怎么喂的、模型是怎么从 Bug 里学到“坏味道”的、以及这个过程中踩过的几个大坑。这套思路适合谁如果你维护的是一个老业务系统线上 bug 反复横跳测试用例又稀稀拉拉那你大概率能从这篇文章里找到点参考。哪怕你手上没有一百个 Bug先把历史故障记录整理清楚这件事本身就能让你对系统长出直觉。1. 先说说我为什么想给AI喂历史Bug我手上维护着一个老模块业务逻辑不算复杂但就是特别容易出回归问题。今天把订单状态机改了明天折扣计算就漏了一个分支后天把这个分支补上结果并发场景又炸了。代码评审很难看住所有边界单测覆盖率看着有七八成但真正能拦住线上故障的断言少得可怜。有一个周五的下午我盯着一条线上告警发呆同一个方法三个月内已经是第五次因为同样的空值问题出事故了。每次修复都是加一行判断每次新增的测试都只覆盖了当前这次“成功路径”。我当时的想法很简单我们缺的不是修 Bug 的时间缺的是一套能把“这个 Bug 以后别再犯”这句话翻译成代码的能力。我试着让团队继续补用例效果一般。补用例的人有惯性会盯着刚修的位置上手写写来写去还是那几个维度。我就想能不能找一个什么东西让它把历史 Bug 的特征学进去然后在看到新代码的时候主动把容易出错的位置用断言圈出来。于是有了这个项目给 AI 喂历史 Bug。这里说的 AI 不是那种在网页上聊天的大模型而是我本地部署的一个开源代码生成模型。我用项目里真实的故障记录、修复 diff、以及修复后补的测试用例做样本让模型学习“什么样的代码容易出什么样的错”最后输出的是断言不是修复代码。1.1 为什么选“写断言”而不是“自动修Bug”一开始我也考虑过让 AI 直接改代码修复 Bug。试了两版效果很吓人它会把函数逻辑改得“看起来更安全”但行为已经悄悄变了。比如它为了防空指针在参数为空时默默返回一个默认值结果下游系统静默接受了脏数据最后出的事故比原来还要隐蔽。后来我把目标从“修”改成“报”AI 不碰业务逻辑只在测试目录里生成断言。这些断言的作用是——当某个历史 Bug 的特征重新出现时CI 直接红。这样做有三个好处不侵入生产代码出问题的面很小。断言失败就是明确的信号不会像“自动修复”那样把问题掩盖掉。生成的东西天然就是测试资产可以进代码评审、进回归集沉淀下来。1.2 核心需求到底是什么这个项目的核心需求不是“用 AI 替代测试人员”而是“把团队脑子里那些没写出来的回归经验变成模型参数”。老系统里最贵的不是改代码而是“记得这里以前出过事”的人。这些人一走那段记忆也跟着走。一百个历史 Bug 喂进去本质上是在做一个团队经验的蒸馏。2. 怎么把100个历史Bug变成AI吃得到的数据说实话最耗时间的不是训练模型而是整理数据。我一开始天真地以为把 Bug 单号列表扔给模型就行结果模型学了个寂寞。后来花了大概十天才把一百条数据清洗成模型真正能用的样子。2.1 数据来源和筛选规则我收集数据的渠道有三个缺陷管理系统里的历史单子、Git 提交记录里带有 bug 修复标记的 commit、以及对应修复提交里面新加的测试用例。但不是所有 Bug 都值得喂。我定了三条硬性筛选规则必须能复现。只写了“用户反馈下单失败”这种话的记录直接淘汰。必须有修复 diff有 diff 才能看到问题代码长什么样。修复后必须补了测试有测试才能提炼出“当时该拦住什么”的答案。按这个标准筛下来最初找到的三百多条记录只剩一百一十几条有一半根本没法用。2.2 每条Bug样本的结构化格式我最后定下来的单条样本格式长这样前置条件调用方当时的输入、状态、环境特点触发输入出事时具体传进去的参数失败表现空指针异常、越界、计算结果错误、脏数据落库等相关代码片段出事函数或模块的“病前”快照修复 diff加了个判断、调整了分支、还是改了资源释放逻辑回归断言修复后补的测试用例哪怕当时写得再粗糙也要举个例子某条历史故障是“优惠券金额为负数时结算总价被算成了负数”。我整理的样本会把输入、堆栈、修复方式都写清楚最后写一行模型看得懂的指令“请针对结算金额计算函数生成能拦截负数优惠券导致总价异常的断言。”起初我觉得部分字段是多余的后来发现去掉“前置条件”之后模型生成的断言经常脱离真实调用链。加上前置条件后断言才开始像样。2.3 数据质量把控100条到底够不够一百条这个数量对于模型微调来说不算多但如果每条都足够“顺滑”效果绝对比五百条糊弄的数据好。我前前后后通过了一个朴素的标准把每条样本给团队里一个刚入职的同学看他看完能不能说出“这个 Bug 该写什么断言”。说不上来的样本就继续改描述直到说上来为止。类别分布也要看。我最初的样本里一大半是空值问题模型后来学歪了看到任何函数都想往“参数是否为空”上写。后来我手动平衡了类别把空值、边界值、资源释放、并发状态、脏数据、超时重试这几类压到差不多的比例效果立刻正常了很多。3. 让AI写断言的核心思路学Bug特征而不是修Bug数据准备好了接下来是怎么让模型“开窍”。我试了两条路线一是直接用大模型的上下文能力把历史 Bug 作为少样本示例塞进提示词二是在开源代码模型基础上做微调。两条路线各有各的定位。3.1 少样本提示词把历史Bug当作“参考答案”在不做任何训练的情况下直接在提示词里给模型看两个历史 Bug 的完整样本再让它对新函数写断言。这个方案灵活适合先快速验证想法。我当时用的提示词模板大概是这样你是一名资深测试工程师。请根据下面这段业务代码生成一组能够拦截历史回归问题的 pytest 断言。 业务代码 {新开发的函数代码} 历史回归特征参考 - 曾经由于 user_id 为 None函数直接抛出 TypeError。 - 曾经由于 status 为 closed查询返回空结果但调用方误以为查询失败。 - 曾经由于金额为负数导致结算总额被计算成负数。 要求 1. 只输出可以直接运行的 pytest 测试代码。 2. 每条断言必须配一句注释说明它拦截的是哪个历史回归场景。 3. 不要修改业务代码本身。这种办法的好处是原来就能见效适合我先拿到一份“模型输出样例”去说服团队。坏处是提示词越长模型越容易把注意力散掉而且当历史 Bug 超过五六个很多模型的上下文窗口就开始顾此失彼。3.2 微调路线把故障记忆压进权重里验证提示词方案可行之后我开始往微调方向走。选了一个参数量不大、可以在公司内网跑起来的开源代码模型用整理好的结构化作训样本做监督微调。我的训练样本组织方式不是传统的“代码-修复”而是“代码-断言”输入 函数 fetch_user_orders(user_id, status) 存在以下历史故障 - user_id 为空时 TypeError - status 非法时返回空列表 请生成该函数的回归断言。 输出 def test_fetch_user_orders_user_id_is_none(): with pytest.raises(TypeError): fetch_user_orders(None, active) def test_fetch_user_orders_invalid_status(): assert fetch_user_orders(1, not_exist) []这个“输入输出对”看起来简单但关键点是输出里如果只给正确结果不给失败模式说明模型就不知道这些断言是干嘛的。所以我在输出之前一定要把每条断言对应的“历史故障”也写进标注里。3.3 为什么我不推荐一上来就搞大规模微调如果你只有一百个 Bug我的建议是不要急着做全量训练。先用提示词方案跑通流程让模型生成、人工审核、接入 CI 形成一个环路。等你发现提示词方案里模型总是在同一个类型的问题上犯同样的错那时候再把这一类的数据扣出来做增量微调性价比最高。我实际跑下来的路线是先用提示词模板做试点收集了大概三十个函数上的生成结果人工标注了模型的低级错误再拿这些错误反哺微调数据。两轮迭代之后效果才开始稳定。4. 实操全流程从试点模块到接入CI路数讲完了下面说说我那边实际是怎么操作的。我挑了一个历史 Bug 最多、但业务影响面相对可控的订单折扣模块当试点没一上来就全系统铺开。4.1 试点模块挑一个bug最集中的地方下手订单折扣模块历史上有过不少幺蛾子比如折扣叠加顺序不对、会员等级传错、金额出现精度问题。我先把这个模块相关的二十来个历史 Bug 整理成样本再让模型对模块里三个核心函数生成断言。拿其中一个函数举例原来是这样的def calculate_final_price(price, discount_rate, user_level): if user_level vip: discount_rate discount_rate * 0.95 return price * discount_rate模型基于历史 Bug 生成的一组断言里有一段我当时一看就觉得“确实该写”def test_calculate_final_price_zero_price(): assert calculate_final_price(0, 0.8, normal) 0 def test_calculate_final_price_negative_discount(): with pytest.raises(ValueError): calculate_final_price(100, -0.1, vip) def test_calculate_final_price_none_user_level(): # 历史Buguser_level为空时VIP判断直接抛异常 with pytest.raises(TypeError): calculate_final_price(100, 0.8, None)这些断言当然不是全部正确比如 negative_discount 那条我们的业务当时并没有明确禁止负折扣模型有点“太超前”。但整体上它的覆盖维度比我预想的高至少它知道要盯着 None、负数和零值。4.2 接入CI让断言加入回归集人工审完模型生成的断言之后我做了三件事把通过的断言统一放到test_regression_ai_generated.py这个文件有一个独立的目录方便统计哪些是 AI 生成的。在 CI 流水线里加上一个环节专门跑这批 AI 生成的回归断言。断言失败时不直接发警报而是先进入一个“疑似回归”的待确认队列。为什么要搞第三步因为模型写的断言偶尔会写成“强断言”本来只是轻微的性能抖动或者合理的边界行为它却当成故障报警。人工确认一次判断是“真实回归”还是“断言太强”这个判断结果再回填给模型模型的准确率会一轮比一轮好。4.3 迭代反馈把新Bug回填形成闭环玩了大概两个礼拜之后我整理出一个最有价值的习惯只要线上有新的故障发生不管是不是跟试点模块相关都按照之前的格式记一条扔进数据集。有一次新的线上故障是“库存扣减成功但订单状态没更新”本质上是数据库事务的原子性和顺序问题。我把它按格式整理好之后再让模型看同类函数它生成的断言里果然多了一条“检查事务回滚时状态是否保持一致性”的断言。这不是我教它的是它从历史样本的相似结构里泛化出来的。经过三轮回填AI 生成断言的“真实回归捕获率”从最初补丁版的不到 60% 提升到了 80% 以上这个数字是我拿最近四个季度线上故障单做盲测算出来的。盲测方式很简单把断言语料先隐藏让模型对故障函数生成断言再看它能不能命中对应的异常。5. 实际效果和踩坑记录这部分说说我自己觉得最有参考价值的东西。没有踩过坑的分享都是在大白天说梦话我踩着坑一路走过来的。5.1 效果对比模型写断言值不值我拿三个方案做了横向对比人工手写、通用提示词方案、喂了历史 Bug 之后的微调方案。测试集是随机抽二十个历史函数看断言能否准确捕获已知故障。方案捕获率误报率平均耗时人工手写65%5%40分钟/20个函数通用提示词45%20%10分钟历史Bug微调80%10%8分钟含人工审核注意那个 80% 的捕获率前提是有人工审核兜底。如果完全没有人工看就直接跑误报率到 15% 起步CI 很快就会被刷屏最后大家就会对 AI 生成的断言视而不见。所以这个 80% 不是“模型多神”而是“人机配合”的结果。5.2 我踩过的四个坑一次说清楚第一个坑数据泄漏。早期我为了让模型学得更快在样本里放了部分函数“未来补丁版本”的代码。结果模型不是在做泛化而是在背答案。新函数稍微换一下名字它就把背过的断言原样输出完全没法用。后来我严格按“故障发生前代码快照”来取样废掉重练才好转。第二个坑断言过强或者过弱。模型容易把每一个整数都断言成“必须是正数”把每个列表都断言成“不能为空”。这种断言跑一百次绿真的遇见脏数据时又因为它太弱而漏掉。解决办法是每一批生成结果都做“断言语义消融”测试人为把一个正常输入改成非法输入看断言能不能变红。变不了红的断言删掉重写。第三个坑上下文截断。提示词里的历史 Bug 样例超过五个模型开始忽略中间部分最容易被漏掉的是“前置条件”里的环境信息。后来我把提示词顺序改成“最新故障案例放最前历史案例放后面”情况好了一点。再用的时候我发现把前后关键的样例拆成两条指令再拼结果比挤在一条指令里更稳。第四个坑类别不平衡。一开始我手头有四十多条空值相关故障其他类别加起来才六十条。模型生成十句断言里七句都在判断“是否为 None”其他类型的 Bug 完全不在输出里。后来我把每个类别的样本上限压到十五六条超出部分只保留典型模型输出的覆盖维度才正常。5.3 常见问题速查表现象可能原因解决办法生成的断言全是“不为空”训练样本类别失衡重平衡类别每个类型限取典型样本断言通过率 100%上线照样出事故数据泄漏或样本太干净检查是否有未来代码混入重置样本切分模型对老函数效果好对新函数失效函数模式偏离历史分布先少样本提示词再把新函数加入作训集生成结果总是很长很啰嗦指令里没有约束输出格式在提示词里明确“只输出 pytest 代码”断言偶尔把正常逻辑判断为 Bug提示词缺少业务约束在指令中加入“不要断言业务规则不存在的限制条件”6. 这套玩法能推广到什么场景这套方法不是银弹但对特定场景特别有效。如果是一个测试覆盖薄弱、历史 Bug 记录比较完整、而且团队成员流动性偏大的老系统用 AI 把历史故障“蒸馏”成回归断言效果立竿见影。它本质上是在做经验传承。反过来如果业务逻辑天天剧烈变化或者你自己根本不知道历史上出过什么事、连 Bug 记录都七零八落那就别折腾模型了先把故障记录体系建起来更重要。没有数据什么模型都白搭。我个人在实际操作中的体会是喂给 AI 的一百个历史 Bug真正的价值不是让模型学会“标准答案”而是让团队被迫把曾经说过无数次“这里以后肯定会出事就是不知道什么时候”的那种模糊直觉变成了明确的前置条件、触发输入和失败形态。做这个过程本身就已经是在把团队的最贵经验资产固化下来。后面我还打算把这条路延伸到别处比如用同样的样本让 AI 生成“异常输入数据工厂”或者让它自动给核心接口标注“不变量约束”再把这些约束转成代码评审时自动检查的规则。目前试了一小部分效果有点意思但还没成熟到能写成一篇文章等以后跑顺了再单独开一篇细聊。如果你手上也正好有一堆历史故障单可以参考我上面的路子先别急着上模型从清洗十来个 Bug 开始人工写十条对应断言再把这些交给模型当“范文”。你会发现模型这东西真的是你喂什么它就长什么喂得干净它就给你干活喂得糊弄它就给你写一堆漂亮但没用的废话。