首页 / 资讯中心 / 文章详情

基于BERT重构测试用例智能检索:从关键词到语义理解的工程实践

基于BERT重构测试用例智能检索:从关键词到语义理解的工程实践 ★ FEATURED ARTICLE
我第一次意识到测试用例库的检索问题已经不能再拖是在一次版本回归排期会上。当时我们负责的业务线光手工用例就有十几万条自动化脚本又是另一套体系大家明知道库里沉淀了大量历史边界场景却因为“搜不到”而反复重写。有同事想找“连续下单导致库存超卖”的用例在老测试案例库里用“重复提交”“超卖”“库存扣减”轮番试折腾了近半个小时最后还是靠一个老员工回忆起来那条用例当年的标题叫“并发请求扣减异常”。也就是从那次之后我把“知识沉淀”和“智能检索”当成一个正经工程问题来对待并在后续落地了这套基于BERT重构测试案例库智能检索体系的方案。如果你也在维护一个几十万条用例的测试平台如果你的用例搜索还是靠SQL LIKE、靠Excel筛关键字、靠老员工记性那么这篇内容就是一个可复用的工程参考。我不会花大篇幅讲注意力机制公式重点说清楚用BERT做什么、为什么选它、数据怎么清洗、索引怎么建、坏结果怎么过滤以及“测试用例”这种特殊语料带来的那些坑。1. 为什么传统检索拿不下测试用例库这座“知识富矿”1.1 造词的人不一样关键词匹配一开始就偏了关键词搜索的本质是字符匹配它要求“用户输入的词”和“用例里存的词”在字面上对得上。但测试用例库是最典型的“同义词重灾区”产品经理说“登录”开发在代码里写“认证”用例作者可能写“登入”线上问题描述写着“支付失败”老用例标题却是“交易异常逆向流程”。这不是谁对谁错而是同一件事在不同角色、不同时期、不同文档里的自然表达差异。举个例子一个典型的查询是“连续提交订单导致库存扣减异常”这串字在老系统里会被分词器切开再去倒排索引里找相同词项。结果呢“连续”可能只有两条用例“提交”可能命中一堆无关按钮点击用例“库存扣减”“异常”还得刚好出现在同一条用例里才可能排到前面。而真正相关的那条用例写的是“重复点击下单按钮后校验库存流水是否幂等”字面完全对不上关键词系统几乎不可能把它捞出来。更麻烦的是测试用例文本往往由“前置条件”“操作步骤”“预期结果”拼成步骤里满是步骤编号、测试数据、按钮名称全篇重复词非常多。关键词匹配要么召回一堆只是“步骤里都提到下单”的噪声要么因为缺少某个同义词直接漏检。这种“作者造词”和“用户造词”之间的断层不是靠多维护几张同义词表就能解决的。1.2 人工维护的标签和模块树维护成本正在吞噬效果很多团队的用例库不是没有分类而是越分类越乱。模块树一开始按功能架构划分但随着版本迭代支付渠道从一个模块拆出八个子模块有的用例还挂在五年前的旧节点下。标签体系也没有好到哪里去早期要求每条用例打标签后来执行节奏加快新用例的标签越打越敷衍我抽查过一批用例标签为空的占比超过两成剩下有标签的又大量使用“回归”“新增”“线上问题”这种高泛化词过滤价值非常低。这带来一个恶性循环因为标签和模块树不可信老手搜索时不敢只信过滤条件只好把范围拉大再靠肉眼一条条看新人倒是信任了模块树结果一路点进去发现用例早就挪窝了最后只能怀疑自己找错了地方。靠人工维护组织信息的系统更新速度永远赶不上用例增长和业务变化的速度这是我认为要重构检索体系时首先要认清的现实与其继续押注人工分类不如让算法先从文本语义层面把门打开。1.3 我在踩坑中试过的“轻量方案”最后都倒在哪我在做BERT方案之前先后试过三条“看起来成本更低”的路结果都只解决了表面问题。第一招是维护同义词词典。把“登录登入认证鉴权”这类关系人工打成表查询前做词项替换。问题是测试领域的新词、缩写、业务黑话更新太快每次大版本都有一批新词漏进来词典维护本身变成了一项无底洞工作。第二招是文本相似度用Jaccard系数或者TF-IDF向量算余弦效果比关键词好一点但毕竟还是停留在“词面统计”层面稍微换个说法就失灵而且对高频步骤词的干扰非常敏感。第三招是上ES做更精细的查询语法搞多字段加权、german词干之类调参一时爽换个业务语料又得重调本质还是和关键词匹配同一个思路。这几轮试下来我最大的体会是用例检索难难的不是“某个词搜不到”而是“用户脑子里想的和库里写出来的根本不在一个表达体系里”。要跨过这道坎只能让计算机理解语义而不是继续在字符表面打转。这也是我们后来下决心引入BERT的核心原因。2. BERT在智能检索中到底做了什么2.1 不懂注意力机制也能用抓住“预训练、上下文、向量”就够了做工程落地的时候不需要对着多头注意力机制推导半天。实际用BERT只需要理解三件事一是它在大规模通用文本上做了预训练已经学会了很多词与词、句与句之间的搭配规律二是它建模的是上下文同一个词在不同句子里会被理解成不同含义三是它可以把任意一段文本变成一个固定维度的向量。向量这个东西我们可以理解成一个“语义坐标”。如果两句话表达的意思接近它们映射出来的坐标在向量空间里就应该靠得很近。比如“用户认证失败”和“登录校验不通过”字面上没有完全相同的词但BERT会把它们映射到相近的位置因为它在预训练时见过太多类似表达知道“认证”“登录”“校验”“失败”“不通过”这些词在相关语境里的关系。要做测试用例检索我们真正需要的就是这种“跨表达”的能力。普通关键词搜索是拿“原词表”去对“原词表”BERT是拿“语义向量”去对“语义向量”它对表达差异的容忍度要高出一个量级。这也是它和传统统计文本相似度最本质的区别。2.2 双塔结构让自然语言的查询和用例住进同一个向量空间实际工程里我们用“双塔”结构来组织检索。所谓双塔就是两个编码分支共享同一个BERT模型一个分支输入用户查询比如“连续下单导致库存扣减异常”另一个分支输入一条完整的测试用例文本。两个分支各自吐出一个向量然后我们用余弦相似度衡量它们的语义距离。这样做的好处是检索阶段可以完全离线化。十几万条用例每条都能提前算好向量并存入索引用户来查询的时候只需要在线计算查询那一句的向量然后去索引里做近邻检索就行。这个结构把最重的大规模计算放在了离线批量阶段在线服务只需要几十毫秒就能给出结果。我当时构建用例文本时并不是只用标题而是把“标题 前置条件 操作步骤 预期结果”拼接成一段完整文档再送入模型。标题重复一次作为加权预期结果尤其不能丢因为很多用例光看标题很普通但预期结果里藏着真正有用的验证点。这套构造方法直接决定了向量空间里的布局相当于给每条用例定义了它在语义空间里的“住址”。2.3 为什么我没有一上来就上交互式精排懂行的人可能会问BERT双塔召回精度有限为什么不用Cross-Encoder直接做交互式精排我的判断是检索系统要先用起来而不是一版做到最重。Cross-Encoder把查询和候选文档拼在一起过一遍完整模型精度确实更高但计算成本也高十几万候选不能全量做还得先靠双塔筛出几百条候选再做交互式精排。更关键的是我们内部用户的搜索场景对响应时间很敏感在用例评审、回归排期这种边翻边搜的场景里超过一秒就会有明显的割裂感。所以第一版我选择了“BERT向量召回 结构化过滤 轻量去重”的简化路线。等用户确实把检索用起来以后再在召回Top50范围内引入精排或者Rerank模型收益更明显。这个取舍的核心思路是架构上留出升级空间第一版先保证链路通、体验顺、结果可用。3. 落地一套基于BERT的测试案例库智能检索体系3.1 第一步也是最重要数据清洗统一成可用语料我见过不少团队兴致勃勃地跑了一遍嵌入模型效果差得离谱回头一看全栽在数据上。测试用例库从多个平台汇过来有的字段叫“前置”有的叫“预置条件”有的把步骤用“1. 2. 3.”写在同一个文本框里还有的用例里挂着大段环境说明、数据库连接串、附件路径。这些脏数据对BERT的干扰极大直接拼接成文档向量会被代码、IP、数字带偏。我做的第一件事是统一字段建模抽出六个核心字段用例编号、标题、前置条件、操作步骤、预期结果、所属模块。然后做文本规范化def build_search_doc(case: dict) - str: title clean_text(case[title]) pre clean_text(case.get(preconditions, )) steps clean_text(case.get(steps, )) expected clean_text(case.get(expected_result, )) return f{title} {title} {pre} {steps} {expected}clean_text里做这几件事去掉无意义的换行和全半角干扰、将具体测试数据替换为“参数占位符”、剔除IP地址和URL、把类似“订单号1234567890”这种长数字统一成“订单号变量”。这一步非常关键因为这些数字在向量空间里会产生极强的误导性相似不同用例只要步骤里都出现过同一个IP就可能被判定为高度相关这是我在后续评测里看得最清楚的坑之一。提示不要跳过数据清洗直接嵌入。宁可花一半的工期在处理文本上也不要让模型去消化充满代码片段和长数字的原始用例。3.2 嵌入模型选型与领域微调策略模型选型要看场景语言和硬件条件。我们的用例以中文为主选择的是开源的中文通用嵌入模型综合效果和参数量都比较均衡。通用模型在“日常表述”上没问题但测试用例里有大量业务术语和系统交互描述直接拿来用会有点“泛”所以我们做了轻量级的领域微调。微调用的是对比学习思路。我从真实需求中抽了大概六千对“查询-用例”样本正样本是真正会被复用的用例负样本则是看起来相关但实际上不合适的用例。其中一个关键技巧是主动加“硬负样本”把这些用例从原先关键词检索的Top结果里捞出来人工标注为负例强制模型学会区分“表面相似”和“真正相关”。train_data [ (查询: 用户连续提交订单导致库存扣减异常, 正例: 重复点击下单按钮后校验库存流水是否幂等), (查询: 用户连续提交订单导致库存扣减异常, 负例: 订单列表接口连续分页查询返回数据一致性检查), ]我还做了严格的正负比例控制避免模型产生“偷懒”倾向也就是只靠一部分高频词判断相关。微调后的模型需要在下游评测集上对比通用模型的效果如果评测指标下降可能就是负样本难度过大、引入噪声需要回调训练数据。3.3 向量索引、增量更新与检索服务搭建向量索引我选择了比较经典的FAISS方案因为我们的数据量在几十万条量级用IndexFlatIP做暴力近邻检索足够快。在线阶段把查询向量归一化后与库内向量做内积就等价于余弦相似度。这是一个“架构能跑、不用过度设计”的务实选择。from sentence_transformers import SentenceTransformer import faiss import numpy as np model SentenceTransformer(m3e-base) docs [build_search_doc(c) for c in cases] vectors model.encode(docs, batch_size64, normalize_embeddingsTrue) dim vectors.shape[1] index faiss.IndexFlatIP(dim) index.add(np.asarray(vectors, dtypenp.float32)) faiss.write_index(index, cases.index)索引建立不是一次性工作用例库每天都在增长所以我单独维护了一个离线增量任务。用例发生变更或者新增时只重新计算受影响用例的向量然后追加或替换索引文件每天晚上再跑一次全量重建兜底防止增量过程中出现数据不一致。检索服务我用一个轻量接口包一层from fastapi import FastAPI app FastAPI() app.get(/search) def search(q: str, module: str , k: int 20): rewritten rewrite_query(q) q_vec model.encode([rewritten], normalize_embeddingsTrue) raw_hits index.search(q_vec, kk * 5)[1][0] hits post_filter(raw_hits, modulemodule) return to_json(hits)这层服务要同时兼容来自测试管理平台、IDE插件和命令行工具的调用输入输出结构化方便上层业务接入。我不建议把语义检索逻辑写进某个平台的内部代码里独立成服务以后后续换平台、换模型都只需要改一个接口。3.4 查询改写、结构化过滤与“轻量级”结果排序很多人以为有了向量召回就万事大吉实际上用户的自然语言查询往往又短又含糊。比如他搜“登录失败”到底想问登录失败的测试集中在哪还是想知道历史失败案例如何复现所以我在检索前加了一个查询改写层做三件事去掉“请问”“怎么测”这类无意义表达把口语说法展开成业务术语比如“登不上”改写成“登录失败”对明显同义的关键词做一轮轻量替换只替换高频常见词不做大规模词典维护。向量召回top结果以后我不主张直接把相似度从高到低堆给用户而是先走结构化过滤按模块、用例状态、优先级筛掉不合适的对象。这里有个细节早期版本里我们没有先过滤结果搜“订单支付”召回的用例里混着大量已废弃的老支付渠道用例排序还特别靠前就是因为它们文本相似度确实高。后来把结构化过滤放在语义召回之后误用率下降了一大截。最后一步是做结果多样性去重。同一个“登录失败”的场景库里可能躺着十几条几乎一样的用例如果Top10全是它们的变体用户会觉得系统只会搜到一个场景。我的方案是做一个轻量的贪心去重按向量相似度高于阈值的用例聚成组每组最多保留两三条并让未命中的相邻模块用例有机会进到列表里。这个细节极其影响主观体验比单纯提高零点零几个召回率还管用。4. 效果怎么验证从评测集到线上反馈4.1 评测集先用真实需求标注出“标准答案”做检索系统最怕拍脑袋说“效果好了”。模型返回的结果到底准不准需要一套相对客观的评测集。我们从真实迭代中抽取了200条搜索意图让三位测试工程师分别去库中找相关性最高的用例只保留至少两个人一致认为相关的用例作为标准答案。这一步没有想象中快前前后后花了两三个人天但它非常重要后面所有模型调整、阈值设置都以这套评测集为准而不是听某个人说“我觉得现在结果还行”。评测集里除了“相关用例”我还记录了一部分“看似相关但实际不该复用”的反例。因为后续调参时模型很容易为了拉高召回率而变得宽松把“登录页面样式检查”和“登录鉴权异常”混为一谈。有明确的负例标注才能逼着模型理解测试用例里真正的验证目标。4.2 跑分对照三套检索方案的召回与排序差异我们用同一套评测集分别跑了三种方案原平台的关键词搜索、Elasticsearch加BM25排序基线、以及BERT向量召回加过滤去重。这里的数据是基于我们项目评测集的相对结果供参考方法不代表所有场景都会得到同样的绝对值方案Recall10MRR10P5关键词搜索原平台39.5%0.3245.2%BM25 排序基线55.1%0.4461.0%BERT 向量召回 过滤去重86.7%0.7382.4%BERT方案在“要找的用例是否出现在前十名”这个指标提升非常明显这也是我被用户感知最强的一项以前翻好几页都找不到现在基本都在第一页。MRR的提升说明不只是搜得到而是排得靠前这直接影响用户愿不愿意多点几次。P5相对低一些说明Top5里还是会有少量不够精准的选项这也是我后来想把精排加进来的主要原因。我不知道你会不会遇到类似情况纯粹看模型指标BERT方案远超关键词搜索但真拿给用户试用时还是有人说“搜出来很多相关的但我想找的那条没在第一屏”。这个差距来自检索的“回忆率”和用户“心里那条具体用例”并不完全一致所以除了指标优化还得在交互上给用户更多可判断的上下文把标题、模块、最近执行时间展示出来让用户快速扫一眼就知道要不要点进去。4.3 把指标翻译成业务收益回归排期与新人上手指标真正打动人是在业务流程里体现出来的。我们观察了几轮版本回归的排期过程以前测试人员拿到需求列表要先在用例库翻找已有覆盖场景平均一个人要花半小时到四十分钟在这个环节熟悉系统的人快一些新同事经常折腾一个小时以上还不确定自己找全了没有。上线检索系统之后步骤变成把需求描述的一句话黏贴进搜索框系统返回Top10到20条候选用例再由人来判断哪条复用、哪条修改。我统计到的实际变化是老手在检索相关用例环节平均时间降到了五分钟以内新人虽然还是会多花一点时间阅读用例内容但基本不再需要全程追着老员工问“那条用例叫什么来着”。这个收益其实是双份的一份是时间另一份是组织记忆的释放。老员工终于不用再当人肉检索机能把精力放到更有价值的测试设计上。我还特意看过一个边界回归场景测试人员在搜索框输入“重复提交订单库存有没有可能扣两次”系统把一条标题为“并发请求扣减异常”的五年老用例排到了第一位。放在过去这条用例几乎只能靠口口相传才能被翻出来。这个案例在我做内部分享时反复被提到它比任何跑分数据都更直观地说明了语义检索对知识沉淀的价值。5. 踩坑实录与排查速查表5.1 最容易被带偏的坑数字、IP和代码片段这是我认为整个项目里最值得单独拎出来讲的一个坑。测试用例的步骤里天然存在大量测试数据IP地址、订单编号、用户ID、金额、时间戳。而这些数据在不同用例中重复出现的概率非常高BERT会把它们当作显著的语义特征于是出现这种情况两条用例同样引用了某个测试环境的数据库地址明明测的完全是两个功能向量相似度却高得异常。我第一次跑完整库召回时搜“优惠券发放失败”结果挤进了好几条“订单批量查询超时”的用例人工排查后才发现它们只是在步骤说明里都写着一串相同的库表地址。后来我把所有环境标识、IP、长数字统一替换为“变量占位符”代码片段和SQL语句也做了弱化处理这类误召回立刻大幅下降。这也是为什么我前面反复强调数据清洗的优先级。我还给排查阶段准备了一个“错误样本收集”机制每周随机抽五十条搜索失败的记录对比用户最后实际点击了哪条用例分析是否因为某个特定字段在干扰向量。这个机制帮助我发现了一个隐藏很深的坑标题里带有“APP”和“移动端”的用例在语义空间中聚合得特别紧密导致用户搜“小程序”时也会把它们召回后来通过加入业务域前缀才缓解。5.2 语义检索不是万能结构化过滤必须前置BERT很强但不是万能的。它理解的是“文本语义”但用例库里的很多约束条件并不在文本里。比如一条用例被测状态是“废弃”另一条状态是“有效”文本几乎一模一样语义检索会同时召回再比如用例所属模块有严格权限控制不同团队不应该看到彼此的用例但语义检索本身根本不认识权限。这些结构化规则必须放在检索链路里硬性过滤。向量召回后面跟一道规则过滤闸门是一个可靠且必要的设计。我在设计过滤器时特别提醒自己不要只按“模块”过滤因为前面说过模块树本身的准确性存疑。所以过滤器的优先级是状态过滤、权限过滤、时间范围过滤最后才是模块过滤。模块过滤做成可干预项用户如果明确指定模块就执行如果不指定宁可让它跨模块召回不要再因为模块树过时把本应命中的历史用例漏掉。5.3 比精度更重要的机制让团队默认使用检索入口技术系统跑通之后我碰到的最大阻力不是性能而是习惯。很多测试人员的肌肉记忆里查找历史用例就等于“进入某个平台展开某个模块树一层层点进去”他们根本不会想到在搜索框里输入一句完整的需求描述。这个习惯不改变再准的语义检索也只是演示系统。我采取的机制是“把检索入口嵌入日常动作里”。不只是在平台首页放一个搜索框而是在新建用例的页面旁边放“你可能想复用以下历史用例”在关联缺陷的表单里增加“搜索相似用例”按钮在回归排期模板里直接预置检索结果区域。当用户每次写用例、提缺陷、排回归时系统都主动把候选用例推到他面前他才真正意识到“原来这东西能帮我少干活”。团队的反馈也反向推动了系统改进。有人提出搜索结果里最好展示这条用例最近一次执行时间和命中缺陷数后来加上了有人希望支持“排除某些模块”再搜索也加上了。这个互动过程让我确定了一点知识沉淀不是一个搜索框的事而是一整套让人愿意“找旧东西、复用旧资产”的工作流。检索算法负责把门打开但最终能不能让大家走进来拼的是产品机制。最后回来说一句实操体会。整个项目走下来让我印象最深的不是BERT模型本身多么神奇而是“检索”这件事在测试领域的特殊性测试用例是一类高度结构化、又充满业务黑话的文本算法再强也救不了脏数据评测集再严谨也替代不了真实用户的使用习惯。如果你也想做类似的重构我建议按这个顺序来先把用例字段梳理干净再建一套人工标注的评测集然后用通用嵌入模型跑通召回链路最后才谈微调和精排。别想着一步到位上最重、最复杂的模型先把一条能让用户用的链路打通后面所有的优化才有意义。
阅读完成 · 觉得有帮助?
咨询建站