ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

如何让AI生成测试用例不重复:从等价类到Embedding相似度的去重实践

2026/9/9 3:54:44 拓冰建站 浏览量
如何让AI生成测试用例不重复:从等价类到Embedding相似度的去重实践 不得不说第一次把 AI 接进测试用例生成的时候我是有点兴奋的。输入一段需求描述几十条用例唰一下就出来了格式工整、步骤齐全看起来很专业。但等我把这批用例交给测试组评审人家看完第一页就皱眉了这个“密码错误”的用例你已经写了五遍了字不一样逻辑是一模一样的。我回去统计了一下第一批用 GPT-4 生成的 60 条用例里有明显重复语义的大概有 25 条左右其中字面几乎一样的就有 12 条。换句话说模型非常勤奋地帮我们把一条用例翻译成了各种说法。这个问题不解决AI 生成测试用例的效率优势会被人工筛选成本完全吃掉。所以后来我花了大概三周时间专门做了一件事让 AI 生成的测试用例不重复。这篇文章把我这次改造中用到的策略、算法、提示词以及踩过的坑都整理出来希望对正在做同样方向的 QA、测试开发以及 AI 应用开发者有帮助。1. 重复问题出在哪先理解AI为什么会造出“看起来不一样”的重复用例1.1 AI不是“记不住”它压根没有一张用例清单先说个容易被忽略的事实绝大多数大语言模型在做生成的时候并没有一个真正的“已生成用例清单”可以查询。它的工作方式是逐 token 预测概率输入一段需求文本模型在语义空间里按概率采样生成内容。你第一轮问它“请为登录功能生成用例”第二轮再问一次只要有细微的随机性它给出的第 7 条用例和第 1 轮的第 7 条用例就可能长得完全不同。这跟人写用例是两回事。一个人写测试用例时脑子里会有一个隐形的检查单正常流程、异常流程、边界值、必填项校验……写完一条就会在清单上勾一下避免下一轮再踩同样的分支。但模型没有显式维护这个清单它靠的是训练时学到的“回答这类问题通常要覆盖哪些点”而不是“当前上下文里我已经覆盖了哪些点”。所以当模型连续输出多条用例时它其实是在同一个语义空间里不断“复述”它认为重要的场景。温度参数调得越高句子形态差异越大但底层测的都是同一段业务逻辑。这就是重复用例的第一个根源无状态生成。1.2 “字面不同”不等于“逻辑不同”等价类视角的重复第二个根源更隐蔽也更关键AI 生成的重复绝大多数不是字符串级别的重复而是等价类级别的重复。举一个特别经典的例子被测功能是一个“手机号输入框”规则是 11 位数字。AI 可能同时生成这样两条用例用例 A输入 11 位数字“13812345678”系统校验通过。用例 B手机号输入 11 位有效数字正常提交。这两条句子在文本层面几乎没有任何共同点哪怕你用最严格的文本相似度算法得分也低得可怜。但是在测试设计视角下这两条用例执行的是同一条代码路径合法等价类通过。这就是等价类划分的思想。我们在手工设计用例时会把输入数据划分成若干个等价类有效等价类测一个代表值就够了无效等价类里的每种错误类型测一个代表值就够了。但大模型没有这种经济性意识它在训练语料里看到过各种“手机号正确”的不同表述于是它倾向于把这些表述全部生成出来好显得“覆盖全面”。因此在做 AI 生成用例的去重时如果只做文本判重基本是白做。你必须先意识到重复不是文案重复而是业务逻辑等价类重复。1.3 组合空间大不代表都要测伪覆盖与正交设计前两类重复已经很难处理了第三类更让人头疼叫“伪覆盖”。当被测功能有多个输入条件时AI 特别热衷于穷举组合。比如一个找回密码功能涉及手机号、验证码、新密码三个输入项AI 会生成下面这一堆用例手机号正确 验证码正确 新密码符合规则手机号错误 验证码正确 新密码符合规则手机号正确 验证码错误 新密码符合规则手机号正确 验证码正确 新密码为空表面上看这是四条不同的用例组合各不一样。但从测试成本的角度看很多组合没有独立价值。比如“手机号错误”和“验证码错误”分别触发的是独立校验逻辑它们不需要和第三项组合在一起成为新用例。如果按照全排列去生成一个只有 3 个条件、每个条件 4 种情况的输入域就能组合出 64 条用例而其中绝大多数都是重复覆盖同一段分支判断的“伪覆盖”。这也是我后来在提示词里刻意加入“正交实验设计”、“边界值分析”思想的原因。让 AI 按条件粒度去生成用例而不是按条件组合去生成用例能从源头上大幅降低用例数量并且不会丢失覆盖率。2. 把去重前置提示词层面的三层约束2.1 防重Prompt第一件事把“重复”定义写清楚很多人以为在提示词里加一句“不要生成重复的测试用例”就够了。实际上大模型对“重复”的理解和测试工程师对“重复”的理解完全不一样。你说“不要重复”它理解的可能是不要生成格式一模一样的句子于是它反而会努力换措辞让重复变得更隐蔽。我建议在提示词里给模型一个可操作的重复判定标准。这个概念最早是我在要求手下的测试工程师评审 AI 用例时总结出来的后来直接写进了 Prompt。下面是我实际在用的一个版本判定两条测试用例是否重复按以下优先级执行 1. 如果两条用例操作的是同一个页面元素或接口字段且输入数据的类型相同 例如都是“长度不足”或“格式非法”则视为重复。 2. 如果两条用例的执行步骤序列完全相同只有操作数据的具体取值不同 且该取值属于同一个等价类则视为重复。 3. 如果两条用例触发的是同一个错误提示或同一个异常分支则视为重复。 4. 如果两条用例的差异仅仅体现在描述语气、措辞、大小写或中英文表达上则视为重复。注意这个定义里出现了“等价类”“分支”这些词它们都是测试领域的专业概念模型是能理解的。你把定义写得越接近测试工程师的评审语言模型输出的重复率就越低。2.2 给每条用例打上“等价类标签”让模型按边界值去发散第二条约束是我认为全流程里收益最高的一步让 AI 在生成每条用例时显式输出一个等价类标识符。等价类标识符可以理解成给用例盖一个章标注它覆盖的是哪条业务规则和哪个边界。例如登录模块可以定义如下标签体系AUTH-LOGIN-VALID正常凭证登录成功AUTH-LOGIN-EMPTY-USER用户名为空AUTH-LOGIN-EMPTY-PASS密码为空AUTH-LOGIN-WRONG-PASS密码错误AUTH-LOGIN-LOCKED账号被锁定AUTH-LOGIN-DISABLED账号已禁用我在提示词里要求模型生成用例前必须先根据需求拆解出等价类清单然后再针对每个等价类生成用例最后在每条用例的“等价类ID”字段里标注它属于哪一类。生成结束后还要做一轮自检如果两条用例的等价类ID相同且动作序列相同就删除其中一条。这样做的好处不只是便于后处理去重更重要的是让模型在生成阶段就建立“归类意识”。它不再是随意地往语义空间撒点而是先规划再填充。实测下来光加这一步同一批需求的语义重复率就下降了差不多一半。2.3 让输出可编程JSON Schema 结构化字段如果说前两步解决的是“模型不知道什么是重复”那第三步解决的是“就算模型知道你怎么在代码里拿到它”。测试用例天然是高度结构化的数据包含前置条件、操作步骤、测试数据、预期结果、优先级等字段。如果让 AI 自由发挥生成大段散文后处理阶段的人工解析成本和误判率都会非常高。我强烈建议用 JSON Schema 强制约束输出结构。在代码层面我一般会用函数调用或输出格式约束确保每条用例都返回固定 JSON 结构。下面是我常用的一份简化版 schema{ 用例列表: [ { 用例ID: TC-LOGIN-001, 等价类ID: AUTH-LOGIN-WRONG-PASS, 所属功能模块: 登录, 前置条件: 已进入登录页面, 操作步骤: [输入已注册手机号, 输入错误密码, 点击登录], 测试数据: {手机号: 13800138000, 密码: wrong_pass}, 预期结果: 页面提示“密码错误您还可以尝试4次”, 优先级: 高 } ] }这里有个很重要的设计细节不要只给 AI 一个 JSON 模板要同时给一个字段填写说明。尤其是“等价类ID”这个字段AI 第一次很可能不知道怎么写你需要提供几个示例明确告诉他这个值不是随便编的而是前面拆解需求时列出的等价类清单中的一项。有了结构化输出后处理阶段就可以直接对字段做判重而不是把整段文字丢给算法。3. 特征计算与相似度判重从字符串到业务语义即便提示词设计得再好也不可能把重复率降到零。模型偶尔还是会突破约束生成一些语义高度相似但没有显式标记的用例。这时候就需要一套后处理判重机制对生成结果做第二道甚至第三道防线。3.1 文本层粗暴去重归一化、MinHash与simHash很多人做文本去重会想到各种 Hash 算法但用在测试用例上有几个坑我一个个说。首先是归一化。在做任何计算之前一定要先清洗文本。测试用例里的中文标点、全角半角空格、大小写、括号种类都会干扰相似度计算。我在项目里写过一批归一化规则核心包括把全角字符转半角、把中文逗号句号统一成英文分隔符、删除所有空格、把“账号”和“账户”这一类同义词替换为统一词根、把手机号/身份证号等具体值替换为占位符。归一化做完之后如果你只是想去掉“几乎一模一样的重复”用 simHash 就够了。它的思路是把文本映射成固定位数的指纹然后比较两个指纹的海明距离。但 simHash 对短文本效果不稳定测试用例通常也就几行字很容易出现“分母太小、指纹抖动大”的情况。我在实际项目里更喜欢用 MinHash Jaccard 相似度来兜底处理操作步骤这一类短文本。思路是把用例的操作步骤序列拆成 n-gram例如把输入手机号、点击登录拆成二元组然后计算两个集合的交集比例。n-gram 的 n 需要根据步骤数量调整步骤一般很短n 取 2 比较合适。下面是一个很简洁的实现from datasketch import MinHash, MinHashLSH def build_minhash(text, num_perm128): tokens text.split( ) # 假设已经做了归一化和分词 m MinHash(num_permnum_perm) for t in tokens: m.update(t.encode(utf-8)) return m不过要提前说明文本层算法能抓的是“字面上就很像”的重复真正棘手的语义重复还得靠下面几层。3.2 语义层判重Embedding相似度的取舍对归一化之后仍然“字面不同”但语义相同的用例文本算法无能为力这时我会用一个嵌入模型把用例文本向量化然后计算两个向量之间的余弦相似度。这里有一个经验不要直接对整条用例做向量化。整条用例通常包含前置条件、步骤、数据、预期结果四个部分。步骤部分和预期结果部分在语义空间中是两种完全不同的表达混在一起算相似度会把分数拉偏。比如两条用例前置条件都是“用户已登录”这个片段占整条文本长度很大比例结果两条用例可能因为前置条件相同余弦相似度就偏高尽管核心操作完全不同。解决办法是分字段计算。我更推荐把操作步骤和预期结果拆开分别向量化然后取加权分数。操作步骤相似度权重给 0.6预期结果给 0.4。因为在实际测试设计中只要操作路径一致预期结果是否同一句话反而是次要的。from openai import OpenAI client OpenAI() # 初始化你的模型客户端 def embed_texts(texts): resp client.embeddings.create(modeltext-embedding-3-small, inputtexts) return [item.embedding for item in resp.data] def cosine_sim(a, b): dot sum(x * y for x, y in zip(a, b)) na sum(x * x for x in a) ** 0.5 nb sum(y * y for y in b) ** 0.5 return dot / (na * nb)在向量化之前建议对数值型测试数据先做脱敏。例如“输入密码 123456”和“输入密码 abcdef”本质都是“密码错误”这一等价类但向量化之后可能被认为是完全不同的内容。把具体取值替换成它的数据属性比如错误密码统一替换为“WRONG_PASS”合法手机号统一替换为“VALID_PHONE”这样向量空间里比较的才是业务语义而不是具体字符。3.3 结构层判重把用例拆成“对象-动作-数据-期望”再比较我一度觉得 Embedding 是终极大法直到我碰到一种很刁钻的情况。有一个输入框的规则是“6到20位字母数字组合”AI 生成了这几条用例用例 1输入 5 位字符提示长度不足用例 2输入 6 位字符通过用例 3输入 20 位字符通过用例 4输入 21 位字符提示长度超出从语义相似度的角度看这四条都是围绕“密码长度”操作向量可能非常接近。但作为测试用例它们是完全不同的——分别是边界下、下边界、上边界、边界上的一组经典边界值分析。如果只按向量相似度直接合并就会把正确且必要的测试设计误杀。所以我把这套判重流程设计成树状结构先用结构解析做一次规则判断再利用向量相似度做第二层召回最后再对应保留策略。结构层做的事情是解析出每条用例中的“操作对象”“动作”“测试数据属性”“预期结果类型”然后把这些结构拼成一个专门用于相似度计算的短文本。关键一步是数据要做边界标记。上面的四个长度用例会被解析成如下形式[登录密码输入框][输入][长度为5_低于下边界][报错_长度不足] [登录密码输入框][输入][长度为6_等于下边界][通过] [登录密码输入框][输入][长度为20_等于上边界][通过] [登录密码输入框][输入][长度为21_高于上边界][报错_长度超出]当结构层发现两条用例的“对象、动作”完全一致但“数据边界点”不同就绝不能判重复。这一步弥补了 Embedding 看不到边界语义的缺陷。4. 工程落地生成、判重、复审的完整闭环4.1 生成前的“历史用例召回”只靠生成后判重只是补救真正的工程化做法是把去重前置到提示词拼接阶段。我在项目里的做法是为每个功能模块维护一个历史用例库在生成新用例之前先从库里召回与该需求最相似的 N 条已有用例把它们的等价类ID和结果类型拼接进系统提示词。具体实现上用向量检索成本最低。提前把历史用例按模块做 embedding 索引当新需求进来时用需求描述查询相似历史场景找到当前模块已覆盖的等价类 ID。提示词里可以这样写以下是该模块历史用例中已经覆盖的等价类请在本次生成时避免重复生成相同等价类的用例 - AUTH-LOGIN-EMPTY-USER - AUTH-LOGIN-WRONG-PASS - AUTH-LOGIN-LOCKED这个方法的本质是让模型拿到“外部记忆”。上下文窗口允许的情况下效果比给模型装上再强的能力都明显。它解决的不只是单次生成内部的重复问题还顺带解决了多次迭代、多个需求版本之间的重复问题。4.2 生成后的三级判重通道生成后我设计了三道判重关卡每道关卡只做自己能做好的事第一关是规则精确查重。对新建用例做字段级归一化然后和当前批次里所有用例比对如果操作步骤的归一化字符串完全一致直接标记为重复。这一步实现简单、零成本能拦截大概 20% 的同义表达重复。第二关是等价类 ID 查重。如果两条用例的等价类 ID 相同并且核心操作对象相同进入候选重复集合。这里不是直接删除因为边界值分析场景下同一个等价类可能同时包含“等于下边界”和“等于上边界”两条用例它们共用 ID 但不能删除。第三关才是向量相似度判重但只处理没有被打上等价类标签的用例。可疑相似度高于阈值的送入人工复核队列让测试人员决定是否冗余。整体流程串起来大概是规则过滤掉非常明显的赘余ID 过滤掉业务等价类的重复向量只负责兜底那些模型在语义空间里偷偷制造的“换皮”用例。4.3 和现有测试管理流程接起来的落地姿势很多团队担心接入 AI 去重引擎会打乱现有测试管理流程。其实这套判重机制完全可以做成一个旁路服务不动原来的用例存储结构。我们项目里就是把它封装成一个 Python FastAPI 服务对外只暴露两个接口一个是“提交需求文本生成用例”另一个是“对已有用例集去重”。输出方面可以接多种格式。团队如果还在用 Excel 管用例就输出一个结构化的 CSV 或 XMind如果已经在用 TestRail、PingCode 这类平台就调用它们提供的批量导入接口。我通常建议在输出时把“等价类ID”放在独立列这样测试人员审查时一眼就能看出哪些用例职责重复。哪怕是人工维护的历史用例也可以定期跑一遍去重程序找出库里的存量冗余。4.4 效果复盘一次真实需求的数据变化最后晒一组数据让大家有一个体感。我在一个“用户中心-安全设置”的需求上做过一次前后对比功能点涵盖修改密码、绑定手机号、开启两步验证。使用无约束提示词直接生成第一批用例模型共输出 158 条。人工评审后被认为存在明显重复或覆盖不到新分支的用例有 66 条重复率约 41.7%有效新用例只有 92 条。加入提示词约束、等价类 ID 标注和三级判重流程后同一需求再次生成 180 条。规则精确查重拦截了 11 条等价类 ID 查重发现了 23 对疑似重复人工复核后确认其中 15 对确实冗余向量相似度又额外召回 4 条高度相似却没有标注 ID 的用例。最终进入用例库的是 146 条人工再筛出的重复只有 5 条左右有效率达到 79.4%比第一版提高了差不多一倍。当然这不是说模型变聪明了而是流程上把冗余卡住了。一旦把去重责任从模型身上转移到流程身上效果就稳定了。5. 避坑指南调阈值、模型选择与中文环境下容易翻车的细节5.1 阈值不是拍脑袋定的要用标注集校准很多人做语义相似度去重最纠结的是阈值该设成多少。我一开始也一样先取 0.9发现有两条明显重复的用例相似度只有 0.88于是调到 0.85。结果误杀了 3 条操作对象完全不同的用例。后来我明白了阈值和测试数据强相关没有一个万能值。正确做法是准备一个标注集。拿历史项目里已经由人工评审过的用例对把其中被人工判定为“重复”的标为 1判定为“不重复”的标为 0然后遍历不同阈值计算精确率和召回率找到 F1 最高的点。这个标注集不用太大一两百对用例就能让阈值稳定下来。另外更精细的做法是按用例类型分开设阈值。功能流程类用例通常比较长重复时相似度会非常高阈值可以设 0.92异常分支类用例描述本来就很接近容易误伤阈值设在 0.96 以上更稳妥接口参数类用例数据占主导文本相似度不可靠主要靠结构层判断。5.2 长提示词反而制造更多重复我一开始“为了防止重复把所有规则一股脑塞进上下文”提示词写到三千多字。结果模型严重跑偏开始反复使用我举例子的措辞重复率反而更高。后来我意识到提示词里的示例是一把双刃剑模型会模仿示例的结构去生成但如果示例不够典型它会把“按示例仿写”本身当成任务。所以示例数量控制在每组 2 到 3 个就够。更重要的是示例之间要有对比性一个来自正常等价类一个来自边界值类让模型理解同一字段在不同条件下要单独成例。总提示词压缩在 1500 字以内效果反而更好。千万不要试图把需求文档全文丢给模型再让它去重它处理长上下文时注意力会被稀释。5.3 中文归一化的坑全角半角、量词、同义词中文测试文本的归一化比英文麻烦得多。我踩过的坑包括“用户名为空”和“用户名称为空”其实是一回事但“账号为空”和“账号为0”完全不同。另外全角括号、中文引号、省略号这些字符如果不统一会直接打乱 MinHash 的 n-gram 切分。我的建议是维护一张领域同义词映射表把需求文档和以前测试用例里出现过的同义表达都存进去。第一次跑的时候这个表很小四十来个词跑了一个月后它会自己长大。注意映射表不要做得太激进。把“账户”“账号”“用户名”映射成统一词根没问题但不要把“余额不足”和“余额为零”混为一谈后者是两条不同的业务规则。5.4 Embedding模型也会“翻车”给相似计算加护栏Embedding 模型在处理领域术语时的稳定性并不像厂商宣传的那么好。就拿“锁定”这个词来说账号锁定和按钮锁定在向量空间里距离可能非常近但业务语义完全不同。我遇到过两条用例“密码输入错误次数过多账号被锁定”和“点击锁定按钮后账号被锁定”向量相似度高达 0.94险些被误杀。这就是为什么我在第三层前面加了结构层护栏。结构层先比较操作对象和动作如果对象一个指向“系统账号状态”另一个指向“页面按钮”那无论向量相似度多高都绝不能判重复。向量相似度只能用于“结构已经判定为同类”的用例之间做进一步排序不能单独作为删用例的依据。5.5 检测到重复后应该保留哪一条可能有人觉得最后一步最简单重复就删呗。实际上保留策略很讲究。我们的经验是按这样排序保留优先保留覆盖边界点的用例其次保留包含明确测试数据的用例再次保留预期结果描述更具体的用例最后才看描述是否简洁。比如两条用例都是“手机号格式错误”这一等价类一条写着“输入 12345”另一条只写“输入一个格式错误的手机号”我们必须留前者因为它的测试数据可以真正执行另一条还需要测试人员自己去猜。去重不只是做减法本质是在保证覆盖度的前提下让剩下的每一条都有足够执行价值。6. 关于这套方案的补充答疑我整理几个项目协作中同事问得最多的问题直接做成一份速查表方便你们对照排查。问题原因我的处理建议相似度阈值总是不准直接套用别人的阈值没有按项目标注用一两百对人工标注用例跑一遍 F1 曲线再定阈值提示词加了去重规则后重复反而变多示例太多或例子太单一模型在“仿写示例”而不是“生成用例”压缩示例到 2-3 个并拉开示例之间的差异两条整段文字都不同的用例被判定重复Embedding 在比较全句步骤和期望混在一起干扰结果拆字段后分别向量化再按权重组合向量模型把“账号锁定”和“按钮锁定”判定为重复领域术语在语义空间中存在歧义先跑结构层判断操作对象再加向量做排序用例库越来越大判重耗时剧增每次都做全量两两比对用 MinHash LSH 做候选召回只在候选集里计算向量人工复核队列积压严重可疑用例过多且没有优先级按重复概率高到低排序抽样复核代替全量复核从这套改造里我个人感受最深的一点是去重这件事模型的能力远远不如流程设计重要。你不需要换一个更强的模型只需要让现有模型在生成前看到历史覆盖情况、在生成中遵循等价类标记、在生成后经过多层判重重复率就能肉眼可见地降下来。如果你们团队刚好也在尝试 AI 生成测试用例我建议按照这个顺序落地先改提示词加重复定义和等价类 ID再定义结构化输出最后再考虑 Embedding 和向量检索那些看起来高级的方案。后面两个方案只在你有几千条存量用例、已经有成熟测试管理体系的阶段才值得投入。先把前面的基本功做好你就已经解决了大部分问题。