禅道测试用例 RAG 系统 1:从 SQL 到智能问答,手把手搭建测试专家助手
禅道测试用例 RAG 系统 1:从 SQL 到智能问答,手把手搭建测试专家助手
你是否也曾面对海量的禅道测试用例,想要快速得到测试建议却无从下手?
本文带你从禅道的 SQL 导出文件出发,通过正则解析、向量检索、重排序与 DeepSeek 大模型,打造一个专属的测试用例智能问答系统。
一、背景:为什么要做这个系统?
在日常测试工作中,我们常遇到两个痛点:
- 用例复用难:禅道里成百上千的测试用例,想找一个“月租车空档期延期”的场景,搜半天找不到。
- 经验沉淀难:即便找到了用例,也需要人脑去理解、归纳,才能形成测试建议。
为了解决这个问题,我搭建了一个 RAG(检索增强生成)系统,让 AI 基于真实的历史用例,自动生成测试建议。
核心流程:
禅道 SQL → 正则解析 → 测试用例 JSON → Markdown 分块 → text2vec 向量化 → ChromaDB 存储 → 语义检索 → Cross-Encoder 重排 → DeepSeek 生成回答
二、代码实战:一步步搭建 RAG 系统
以下代码可直接运行,只需要替换你的
zentao.sql文件路径和 DeepSeek API Key。
第一步:SQL 解析 —— 从禅道导出数据中提取用例
禅道的 SQL 导出文件结构复杂,我们通过正则表达式提取 zt_case 和 zt_casestep 两张表,清洗出标题、前置条件和测试步骤。
import re, html, json, os
def clean_content(text):
if not text or text == 'NULL': return ""
text = text.strip("'").replace("\\'", "'").replace("\\r\\n", "\n").replace("\\n", "\n")
return re.sub(r'<[^>]+>', '', html.unescape(text)).strip()
def is_valid_text(text):
tags = {'feature', 'step', 'config', 'requirement', 'standard'}
if not text or text.isdigit() or text.lower() in tags:
return False
return True
def process_zentao_sql(sql_path, output_json):
case_map = {}
record_pattern = re.compile(r"\((.*?)\)(?=[,;])", re.DOTALL)
field_pattern = re.compile(r"'(.*?)'|(\d+)|''|NULL", re.DOTALL)
with open(sql_path, 'r', encoding='utf-8-sig', errors='ignore') as f:
# 提取 zt_case
for line in f:
if "INSERT INTO `zt_case` VALUES" in line:
for record in record_pattern.findall(line):
fields = field_pattern.findall(record)
actual = [f[0] if f[0] else (f[1] if f[1] else "") for f in fields]
if len(actual) > 11:
cid = actual[0]
title = clean_content(actual[10])
raw_pre = clean_content(actual[11])
precondition = raw_pre if is_valid_text(raw_pre) else ""
if len(title) < 2 or title.isdigit(): continue
case_map[cid] = {"title": title, "precondition": precondition, "steps": []}
# 提取 zt_casestep
f.seek(0)
for line in f:
if "INSERT INTO `zt_casestep` VALUES" in line:
for record in record_pattern.findall(line):
fields = field_pattern.findall(record)
actual = [f[0] if f[0] else (f[1] if f[1] else "") for f in fields]
if len(actual) > 6:
cid, desc, expect = actual[2], clean_content(actual[5]), clean_content(actual[6])
if cid in case_map and (desc or expect):
case_map[cid]["steps"].append(f"步骤:{desc} -> 预期:{expect}")
# 转换为 RAG JSON
rag_results = []
for cid, c in case_map.items():
if not c['steps'] and not c['precondition']: continue
content = f"测试项:{c['title']}\n前置条件:{c['precondition']}\n详情:\n" + "\n".join(c['steps'])
rag_results.append({
"content": content.strip(),
"metadata": {"source": "zentao_cases", "case_id": cid}
})
with open(output_json, 'w', encoding='utf-8') as wf:
json.dump(rag_results, wf, ensure_ascii=False, indent=2)
print(f"解析完成!生成有效语料: {len(rag_results)} 条")
第二步:JSON 转 Markdown —— 构建易读的中间格式
将清洗后的用例转换成 Markdown,方便后续分块和人工审查。
def convert_json_to_md(input_file, output_file):
with open(input_file, 'r', encoding='utf-8') as f:
data = json.load(f)
with open(output_file, 'w', encoding='utf-8') as md:
md.write("# 禅道测试用例库 (Markdown 版)\n\n")
for item in data:
case_id = item.get("metadata", {}).get("case_id", "N/A")
content = item.get("content", "")
lines = content.split('\n')
title = lines[0].replace("测试项:", "").strip()
md.write(f"## {title} (ID: {case_id})\n")
for line in lines[1:]:
line = line.strip()
if not line: continue
if "步骤:" in line:
formatted = line.replace("步骤:", " - **步骤**: ").replace(" -> 预期:", " - **预期**: ")
md.write(formatted + "\n")
elif "前置条件:" in line:
md.write(f"- **前置条件**: {line.replace('前置条件:', '').strip()}\n")
else:
md.write(f"- {line}\n")
md.write("\n---\n\n")
print(f"转换成功!")
第三步:文本分块 —— 保持用例完整性
每个用例作为一个独立块,保证检索时不会割裂逻辑。
def split_into_chunks(doc_file):
with open(doc_file, 'r', encoding='utf-8') as f:
content = f.read()
garbage_header = "# 禅道测试用例库 (Markdown 版)"
raw_chunks = content.split("---")
chunks = []
for c in raw_chunks:
clean_c = c.replace(garbage_header, "").strip()
if clean_c and len(clean_c) > 30 and "ID:" in clean_c:
chunks.append(clean_c)
return chunks
第四步:向量化 + 存储 —— 让用例“可搜索”
使用 shibing624/text2vec-base-chinese 模型进行中文向量化,存入 ChromaDB 持久化存储。
import torch, numpy as np
from sentence_transformers import SentenceTransformer
import chromadb
from typing import List
def embed_chunk(chunk, model):
return model.encode(chunk, normalize_embeddings=True).tolist()
def batch_vectorize(chunks, model):
embeddings = model.encode(chunks, batch_size=128, normalize_embeddings=True)
return embeddings
def save_to_chroma(chunks, embeddings):
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_or_create_collection(name="zentao_test_cases")
ids = [f"id_{i}" for i in range(len(chunks))]
for i in range(0, len(chunks), 1000):
collection.add(
documents=chunks[i:i+1000],
embeddings=embeddings[i:i+1000].tolist(),
ids=ids[i:i+1000]
)
print(f"存入完成!当前库内条数: {collection.count()}")
第五步:两阶段检索 —— 语义召回 + 重排序
先用向量检索召回 Top-K 候选,再用 Cross-Encoder 精排,提升相关性。
from sentence_transformers import CrossEncoder
def retrieve_test_cases(query, model, collection, top_k=5):
query_vector = embed_chunk(query, model)
results = collection.query(
query_embeddings=[query_vector],
n_results=top_k,
include=["documents", "distances"]
)
return results['documents'][0]
def retrieve_and_rerank(query, model, reranker, collection, top_k_initial=10, top_k_final=3):
initial_chunks = retrieve_test_cases(query, model, collection, top_k=top_k_initial)
pairs = [[query, chunk] for chunk in initial_chunks]
scores = reranker.predict(pairs)
scored_chunks = sorted(zip(initial_chunks, scores), key=lambda x: x[1], reverse=True)
return [chunk for chunk, score in scored_chunks[:top_k_final]]
第六步:LLM 生成 —— 输出智能测试建议
将重排后的相关用例作为上下文,调用 DeepSeek 生成结构化测试建议。
import requests
def generate_with_deepseek(query, chunks, api_key):
context = "\n\n".join(chunks)
payload = {
"model": "deepseek-chat",
"messages": [
{"role": "system", "content": "你是一位专业的停车场系统测试专家。"},
{"role": "user", "content": f"背景资料:\n{context}\n\n问题:{query}"}
],
"stream": False
}
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
response = requests.post(
"https://api.deepseek.com/chat/completions",
headers=headers,
json=payload,
timeout=60
)
return response.json()['choices'][0]['message']['content']
执行主流程
base_dir = r'E:\projs\codes\rag\rag\data'
sql_path = os.path.join(base_dir, 'zentao.sql')
json_path = os.path.join(base_dir, 'zentao_rag.json')
md_path = os.path.join(base_dir, 'zentao_cases.md')
process_zentao_sql(sql_path, json_path)
convert_json_to_md(json_path, md_path)
chunks = split_into_chunks(md_path)
print(f"提取分块: {len(chunks)} 条")
device = "cuda" if torch.cuda.is_available() else "cpu"
model = SentenceTransformer("shibing624/text2vec-base-chinese").to(device)
embeddings = batch_vectorize(chunks, model)
save_to_chroma(chunks, embeddings)
reranker = CrossEncoder('cross-encoder/mmarco-mMiniLMv2-L12-H384-v1', device=device)
query = "如何进行月租车测试?"
final_results = retrieve_and_rerank(query, model, reranker, collection)
answer = generate_with_deepseek(query, final_results, "your-api-key")
print(answer)
三、效果展示:AI 给出的测试建议

四、总结:这个系统的核心价值
- 保证用例分块的原子性:以单个用例为最小单位,避免信息割裂。
- 两阶段检索提升相关性:向量召回 + Cross-Encoder 精排,大幅提升 Top-3 准确率。
- 结合 LLM 提供可读的测试指导:不再是简单的用例匹配,而是生成结构化的、可直接执行的测试建议。
如果你也在做类似的 RAG 应用,欢迎交流讨论!
浙公网安备 33010602011771号