☰
用历史Bug数据训练AI生成回归断言:实战经验与踩坑指南
2026/10/20 11:43:40 网站建设 项目流程

前阵子我干了一件不一定算得上聪明、但绝对够折腾的事:把项目里一百来个历史 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 都值得喂。我定了三条硬性筛选规则:

  1. 必须能复现。只写了“用户反馈下单失败”这种话的记录,直接淘汰。
  2. 必须有修复 diff,有 diff 才能看到问题代码长什么样。
  3. 修复后必须补了测试,有测试才能提炼出“当时该拦住什么”的答案。

按这个标准筛下来,最初找到的三百多条记录只剩一百一十几条,有一半根本没法用。

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(): # 历史Bug:user_level为空时,VIP判断直接抛异常 with pytest.raises(TypeError): calculate_final_price(100, 0.8, None)

这些断言当然不是全部正确,比如 negative_discount 那条,我们的业务当时并没有明确禁止负折扣,模型有点“太超前”。但整体上,它的覆盖维度比我预想的高,至少它知道要盯着 None、负数和零值。

4.2 接入CI:让断言加入回归集

人工审完模型生成的断言之后,我做了三件事:

  1. 把通过的断言统一放到test_regression_ai_generated.py,这个文件有一个独立的目录,方便统计哪些是 AI 生成的。
  2. 在 CI 流水线里加上一个环节,专门跑这批 AI 生成的回归断言。
  3. 断言失败时,不直接发警报,而是先进入一个“疑似回归”的待确认队列。

为什么要搞第三步?因为模型写的断言偶尔会写成“强断言”,本来只是轻微的性能抖动或者合理的边界行为,它却当成故障报警。人工确认一次,判断是“真实回归”还是“断言太强”,这个判断结果再回填给模型,模型的准确率会一轮比一轮好。

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 开始,人工写十条对应断言,再把这些交给模型当“范文”。你会发现,模型这东西,真的是你喂什么它就长什么,喂得干净,它就给你干活,喂得糊弄,它就给你写一堆漂亮但没用的废话。

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

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

立即咨询