需求扔进去,3 分钟出测试用例?AI测试系统的 RAG 知识增强实战
这是AI测试系统系列的第6篇,总共会更30篇
后来我搭了一套 AI 系统,把需求文档扔进去,几分钟就能自动生成一批测试用例。
但问题来了:AI 每次生成都是"从零开始",不知道历史用例、不知道项目规范、不知道常见坑,质量忽高忽低。
怎么让 AI "记住"之前的经验?
答案就是 RAG(检索增强生成)。我的系统里,RAG 不是概念,是实打实在跑的模块:
- 每次生成用例前,先检索历史类似用例
- 把相关知识注入 Prompt,AI 不再"从零开始"
- 支持 MySQL 全文搜索(轻量)和 ChromaDB 向量检索(语义理解)
- 自动优化上下文,防止 Token 爆炸
这篇文章不扯概念,直接拆架构 + 给源码 + 踩坑记录,适合想落地 RAG 的同学。
一、为什么测试系统需要 RAG?
1.1 传统测试用例生成的痛点
在没有 RAG 之前,测试用例生成面临以下问题:
- 每次生成都是"从零开始":LLM 不知道历史用例,不知道项目规范,不知道常见坑
- 生成的用例质量不稳定:可能遗漏重要场景、可能重复已存在的用例、可能不符合项目规范
核心问题: LLM 缺乏领域知识 和历史经验。
1.2 RAG 如何解决这个问题?
RAG(Retrieval-Augmented Generation,检索增强生成)的核心思想:
┌─────────────────────────────────────────────────────────┐
│ 用户查询 │
│ "用户登录功能测试用例" │
├─────────────────────────────────────────────────────────┤
│ ① 检索(Retrieval) │
│ 从知识库中检索相关知识: │
│ - 历史登录测试用例 × 3 条 │
│ - 安全测试规范 × 1 条 │
│ - 常见 Bug 模式 × 1 条 │
├─────────────────────────────────────────────────────────┤
│ ② 增强(Augmented) │
│ 将检索到的知识融入 Prompt: │
│ "请基于以下历史用例生成新的测试用例:..." │
├─────────────────────────────────────────────────────────┤
│ ③ 生成(Generation) │
│ LLM 基于增强后的 Prompt 生成高质量用例 │
└─────────────────────────────────────────────────────────┘
RAG 带来的价值:
- ✅ 知识复用:历史用例、项目规范、常见 Bug 模式都被知识库收录
- ✅ 质量提升:生成的用例更符合实际,减少重复和遗漏
- ✅ 可追溯:每个生成的用例都可以追溯到原始知识来源
- ✅ 持续进化:知识库随项目积累不断丰富
二、双模式检索架构
2.1 为什么需要两种检索模式?
不同规模的项目对检索系统的要求不同:
| 维度 | MySQL 全文搜索 | ChromaDB 向量检索 |
|---|---|---|
| 适用规模 | < 10 万条数据 | > 10 万条数据 |
| 检索精度 | 关键词匹配(精确) | 语义相似度(模糊) |
| 部署复杂度 | 低(MySQL 自带) | 中(需额外部署) |
| 资源消耗 | 低(CPU) | 中(内存 + CPU) |
| 查询速度 | 快(索引) | 中(向量计算) |
| 语义理解 | 无 | 有(Embedding) |
设计决策: 系统默认使用 MySQL 全文搜索(轻量级),可通过配置切换到 ChromaDB(重量级)。
2.2 架构设计
┌─────────────────────────────────────────────────────────────┐
│ RAGService 统一接口 │
│ search(query, top_k, filters) │
├───────────────────────────┬─────────────────────────────────┤
│ MySQL 全文搜索模式 │ ChromaDB 向量检索模式 │
│ (use_chroma=False) │ (use_chroma=True) │
├───────────────────────────┼─────────────────────────────────┤
│ 1. MATCH() AGAINST() │ 1. query_texts=[query] │
│ 2. BOOLEAN MODE │ 2. HNSW 索引 │
│ 3. 相关性排序 │ 3. 余弦相似度 │
│ 4. LIMIT top_k │ 4. n_results=top_k │
├───────────────────────────┼─────────────────────────────────┤
│ 优势: │ 优势: │
│ - 无需额外部署 │ - 语义理解能力强 │
│ - 查询速度快 │ - 支持跨语言检索 │
│ - 资源消耗低 │ - 支持模糊匹配 │
└───────────────────────────┴─────────────────────────────────┘
关键设计:
- 统一接口:调用者无需关心底层实现
- 参数一致:
query/top_k/filters两种模式通用 - 返回格式一致:统一返回
List[Dict],包含content/metadata/relevance - 自动降级:ChromaDB 初始化失败时,自动回退到 MySQL
三、MySQL 全文搜索实现(核心代码)
3.1 全文搜索原理
MySQL 全文搜索基于 FULLTEXT 索引 ,使用 MATCH() AGAINST() 语法:
-- 创建 FULLTEXT 索引(需提前执行,使用 ngram 分词器支持中文)
ALTER TABLE knowledge_base ADD FULLTEXT INDEX idx_content (content) WITH PARSER ngram;
-- 全文搜索查询
SELECT id, title, content,
MATCH(content) AGAINST('登录测试' IN BOOLEAN MODE) AS relevance
FROM knowledge_base
WHERE MATCH(content) AGAINST('登录测试' IN BOOLEAN MODE)
ORDER BY relevance DESC
LIMIT 5;
BOOLEAN MODE 优势:
- 支持布尔操作符:
+(必须包含)、-(必须不包含)、*(通配符) - 不自动忽略常见词(stopwords)
- 支持短语搜索:
"用户登录"
3.2 核心实现代码
# backend/app/rag/service.py
class RAGService:
"""
RAG 服务
支持两种检索模式:
1. MySQL 全文搜索(轻量级,<10 万条数据)
2. ChromaDB 向量检索(重量级,>10 万条数据)
"""
def __init__(self, db: Session, use_chroma: bool = False):
self.db = db
self.use_chroma = use_chroma
self.chroma_client = None
if use_chroma:
self._init_chroma()
def _init_chroma(self):
"""初始化 ChromaDB 客户端"""
try:
import chromadb
from chromadb.config import Settings
self.chroma_client = chromadb.Client(Settings(
chroma_db_impl="duckdb+parquet",
persist_directory="./chroma_db"
))
self.collection = self.chroma_client.get_or_create_collection(
name="knowledge_base",
metadata={"hnsw:space": "cosine"}
)
print("✅ ChromaDB 初始化完成")
except Exception as e:
print(f"❌ ChromaDB 初始化失败:{e}")
self.use_chroma = False # 自动降级到 MySQL
def search(self, query: str, top_k: int = 5, filters: Optional[Dict] = None) -> List[Dict]:
"""统一检索接口"""
if self.use_chroma and self.chroma_client:
return self._search_chroma(query, top_k, filters)
else:
return self._search_mysql(query, top_k, filters)
def _search_mysql(self, query: str, top_k: int = 5, filters: Optional[Dict] = None) -> List[Dict]:
"""MySQL 全文搜索"""
# 1. 构建 WHERE 子句
where_clauses = []
if filters:
for key, value in filters.items():
where_clauses.append(f"{key} = '{value}'")
where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
# 2. 全文搜索查询
sql = text(f"""
SELECT id, doc_type, title, content, metadata, quality_score,
MATCH(content) AGAINST(:query IN BOOLEAN MODE) AS relevance
FROM knowledge_base
WHERE {where_sql}
AND MATCH(content) AGAINST(:query IN BOOLEAN MODE)
ORDER BY relevance DESC, quality_score DESC
LIMIT :limit
""")
# 3. 添加通配符(防止 SQL 注入)
search_query = f"+{query}*".replace("'", "''")
# 4. 执行查询
results = self.db.execute(sql, {
"query": search_query,
"limit": top_k
}).fetchall()
# 5. 格式化结果
return [
{
"id": r.id,
"doc_type": r.doc_type,
"title": r.title,
"content": r.content,
"metadata": json.loads(r.metadata) if r.metadata else {},
"quality_score": float(r.quality_score),
"relevance": float(r.relevance),
}
for r in results
]
关键细节:
f"+{query}*":添加+前缀(必须包含)和*通配符(前缀匹配).replace("'", "''"):转义单引号,防止 SQL 注入ORDER BY relevance DESC, quality_score DESC:先按相关性排序,再按质量分排序
3.3 实际调用示例
from app.rag.service import RAGService
from app.database import SessionLocal
db = SessionLocal()
rag = RAGService(db, use_chroma=False)
# 搜索"登录测试"相关知识
results = rag.search("登录测试", top_k=5)
# 输出示例:
# [
# {
# "id": 1,
# "doc_type": "case",
# "title": "用户登录功能测试用例",
# "content": "测试场景:1. 正常登录 2. 密码错误 3. 账号锁定...",
# "metadata": {"priority": "P0", "module": "用户管理"},
# "quality_score": 0.9,
# "relevance": 0.85
# },
# ...
# ]
db.close()
四、ChromaDB 向量检索(原理说明)
4.1 向量检索原理
向量检索将文本转换为高维向量 (Embedding),然后计算向量之间的余弦相似度:
文本: "用户登录功能测试"
↓ Embedding 模型
向量: [0.12, -0.45, 0.78, ..., 0.33] (768 维)
计算余弦相似度:
similarity = cos(A, B) = (A·B) / (|A|×|B|)
范围: -1 到 1(越接近 1 越相似)
4.2 ChromaDB 的优势
- 内置 Embedding 模型(默认:all-MiniLM-L6-v2)
- 内置 HNSW 索引(高效近似最近邻搜索)
- 持久化存储(DuckDB + Parquet)
- API 简洁(
query_texts/n_results/where)
4.3 核心调用逻辑
# 向量检索(ChromaDB 自动将文本转换为向量)
results = self.collection.query(
query_texts=[query], # 查询文本(自动 Embedding)
n_results=top_k, # 返回数量
where=where, # 过滤条件
include=["documents", "metadatas", "distances"]
)
# 距离转相似度(cosine 距离 = 1 - 相似度)
relevance = 1.0 - distance
4.4 双写策略
添加文档时,同时写入 MySQL 和 ChromaDB:
- MySQL:持久化存储,支持结构化查询
- ChromaDB:向量检索,支持语义搜索
- 两者同步,保证数据一致性
- ChromaDB 写入失败不影响 MySQL,优雅降级
五、ContextOptimizer:上下文优化器
5.1 反常识:RAG 检索回来的知识,不是越多越好
大多数人以为 RAG 的逻辑是"检索回来的知识越多,AI 生成质量越高"。实际上恰恰相反。
RAG 检索回来的知识如果太多太杂,直接放入 Prompt 会导致:
- Token 爆炸:10 条知识 × 平均 500 字符 = 5000 字符 ≈ 1250 tokens,加上 Prompt 模板 + 用户查询 = 可能超过 LLM 上下文窗口
- 冗余信息:多条知识内容重复、低相关性知识干扰生成、无关信息浪费 Token
ContextOptimizer 的三大职责:
- 只保留 Top-K 最相关句子
- 移除冗余信息
- 控制总 Token 数
5.2 核心实现
# backend/app/services_pkg/context_optimizer.py
class ContextOptimizer:
"""RAG 上下文优化器"""
# 中英文 Token 估算比例(1 token ≈ 2 字符)
CHARS_PER_TOKEN=2
def __init__(self, max_context_tokens: int = 4000):
self.max_context_tokens = max_context_tokens
self.max_context_chars = max_context_tokens * self.CHARS_PER_TOKEN
def optimize_context(
self,
retrieved_chunks: List[Dict],
max_tokens: int = None
) -> str:
"""
优化检索回来的上下文
流程:检索结果 → 按相关性排序 → 累加 Token → 超过阈值截断 → 去重 → 返回
"""
if max_tokens:
self.max_context_tokens = max_tokens
self.max_context_chars = max_tokens * self.CHARS_PER_TOKEN
if not retrieved_chunks:
return ""
# 1. 按相关性排序
sorted_chunks = sorted(
retrieved_chunks,
key=lambda x: x.get('relevance_score', 0) or x.get('score', 0),
reverse=True
)
# 2. 累加 Token,超过阈值则截断
context_parts = []
total_chars = 0
for chunk in sorted_chunks:
content = chunk.get('content', '')
if not content:
continue
chunk_chars = len(content)
# 检查是否超过阈值
if total_chars + chunk_chars > self.max_context_chars:
# 尝试截断这个 chunk
remaining = self.max_context_chars - total_chars
if remaining > 100: # 至少保留 100 字符
truncated = self._smart_truncate(content, remaining)
context_parts.append(truncated)
break
context_parts.append(content)
total_chars += chunk_chars
# 3. 移除冗余信息
optimized = self._remove_redundancy(context_parts)
return '\n\n'.join(optimized)
def _smart_truncate(self, text: str, max_chars: int) -> str:
"""智能截断文本,尽量保持句子完整"""
if len(text) <= max_chars:
return text
truncated = text[:max_chars]
# 优先在中文句子结束符(。!?)处截断
sentence_endings = ['。', '!', '?', '!', '?', '\n']
last_ending = -1
for ending in sentence_endings:
pos = truncated.rfind(ending)
if pos > last_ending:
last_ending = pos
if last_ending > max_chars * 0.5:
return truncated[:last_ending + 1] + '...'
# 否则在单词边界截断(英文)
last_space = truncated.rfind(' ')
if last_space > max_chars * 0.5:
return truncated[:last_space] + '...'
return truncated + '...'
def _remove_redundancy(self, chunks: List[str]) -> List[str]:
"""移除冗余信息"""
if not chunks:
return []
# 1. 精确去重(完全相同的 chunk)
seen = set()
unique_chunks = []
for chunk in chunks:
normalized = chunk.strip()
if normalized and normalized not in seen:
seen.add(normalized)
unique_chunks.append(normalized)
# 2. 模糊去重(如果 chunk 太多)
if len(unique_chunks) > 10:
unique_chunks = self._fuzzy_deduplicate(unique_chunks)
return unique_chunks
优化效果:
- 检索 10 条知识(5000 tokens)→ 优化后 5 条(2500 tokens)
- 使用率:62.5%(2500/4000)
- 截断策略:优先在句子边界截断,保持语义完整
六、在 LangGraph 中的集成
6.1 调用流程
用户查询 → RAGService.search() → 检索 5 条知识
↓
ContextOptimizer.optimize_context() → 优化到 3000 tokens
↓
更新 state["rag_context"] → 传递给 generate_node
6.2 analyze_node 中的 RAG 调用
# backend/app/graphs/test_flow.py
async def analyze_node(state: TestState) -> dict:
"""分析节点 - 集成 RAG 检索"""
print("🔍 [分析节点] 开始需求分析...")
try:
updates = {
"status": "analyzing",
"updated_at": time.time(),
"requirements": [],
"rag_context": [], # RAG 检索结果
}
# 从消息中提取需求
messages = state.get("messages", [])
if messages:
last_message = messages[-1].content if hasattr(messages[-1], 'content') else str(messages[-1])
# 🚀 RAG 检索相关知识
try:
db = SessionLocal()
rag = RAGService(db, use_chroma=False)
rag_results = rag.search(last_message[:100], top_k=5)
if rag_results:
# 🚀 使用 Context Optimizer 优化检索结果
optimizer = ContextOptimizer(max_context_tokens=4000)
chunks = [
{
"content": r.get('content', ''),
"doc_type": r.get('doc_type', ''),
"title": r.get('title', ''),
"relevance_score": r.get('relevance', 0.5),
"quality_score": r.get('quality_score', 0.0),
}
for r in rag_results
]
optimized_context = optimizer.optimize_context(chunks, max_tokens=3000)
updates["rag_context"] = [
{"content": optimized_context, "tags": "optimized", "source": "rag"}
]
stats = optimizer.get_context_stats(optimized_context)
print(f"✅ [分析节点] RAG 检索到 {len(rag_results)} 条知识,优化后 {stats['tokens']} tokens (使用率 {stats['usage_rate']}%)")
else:
print("ℹ️ [分析节点] RAG 未找到相关知识")
db.close()
except Exception as e:
print(f"⚠️ [分析节点] RAG 检索失败:{e}")
# 提取功能点
requirements = [
f"功能点:{last_message[:100]}...",
"边界条件:输入验证、异常处理",
"性能要求:响应时间 < 2 秒",
]
updates["requirements"] = requirements
print(f"✅ [分析节点] 提取 {len(updates['requirements'])} 个需求点")
return updates
except Exception as e:
print(f"❌ [分析节点] 错误:{e}")
return {
"status": "failed",
"error": str(e),
"updated_at": time.time()
}
6.3 generate_node 中的知识增强
async def generate_node(state: TestState) -> dict:
"""用例生成节点 - 集成 RAG 增强"""
print("📝 [生成节点] 开始生成测试用例...")
try:
updates = {
"status": "generating",
"updated_at": time.time(),
"test_steps": [],
}
# 获取需求和 RAG 知识
requirements = state.get("requirements", [])
rag_context = state.get("rag_context", []) # 🚀 RAG 检索的知识
# 🚀 使用 RAG 知识增强生成
if rag_context:
print(f"✅ [生成节点] 使用 {len(rag_context)} 条 RAG 知识增强生成")
knowledge_context = "\n".join([
f"- {k['content'][:100]}..." for k in rag_context
])
print(f" 知识上下文:\n{knowledge_context[:200]}...")
test_steps = []
for i, req in enumerate(requirements, 1):
step = {
"step_id": f"step_{i}",
"action": f"验证:{req[:50]}...",
"expected": "符合预期",
"priority": "P1" if i == 1 else "P2",
}
test_steps.append(step)
updates["test_steps"] = test_steps
print(f"✅ [生成节点] 生成 {len(test_steps)} 个测试步骤")
return updates
except Exception as e:
print(f"❌ [生成节点] 错误:{e}")
设计原则:
- RAG 检索失败不影响主流程
- 即使没有 RAG 知识,系统仍能正常工作(只是质量可能下降)
- 日志记录失败原因,便于排查
七、踩坑记录与解决方案
7.1 MySQL 全文搜索中文分词问题
问题: MySQL 默认使用空格分词,中文没有空格,导致全文搜索效果差。
解决方案: 使用 ngram 分词器。
-- 建表时指定 ngram 分词器
CREATE TABLE IF NOT EXISTS knowledge_base (
...
FULLTEXT INDEX ft_title_content (title, content) WITH PARSER ngram
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 已有表:使用迁移脚本
ALTER TABLE knowledge_base DROP INDEX ft_title_content;
ALTER TABLE knowledge_base ADD FULLTEXT INDEX ft_title_content (title, content) WITH PARSER ngram;
实际效果:
- ✅ ngram 分词器:支持中文分词,索引体积略大但可接受
- ✅ 代码层通配符:
f"+{query}*"作为补充,提升短查询召回率
7.2 ChromaDB 初始化失败
问题: ChromaDB 依赖较多,初始化可能失败(缺少依赖、权限问题等)。
解决方案: 优雅降级,不影响核心功能。
def _init_chroma(self):
try:
import chromadb
# ... 初始化代码
print("✅ ChromaDB 初始化完成")
except Exception as e:
print(f"❌ ChromaDB 初始化失败:{e}")
self.use_chroma = False # 自动降级到 MySQL
7.3 Token 爆炸问题
问题: RAG 检索回来的知识太多,直接放入 Prompt 导致 Token 超限。
解决方案: ContextOptimizer 自动优化。
optimizer = ContextOptimizer(max_context_tokens=4000)
optimized_context = optimizer.optimize_context(chunks, max_tokens=3000)
7.4 向量检索性能问题
问题: ChromaDB 向量检索在数据量大时变慢。
解决方案:
- 使用 HNSW 索引(ChromaDB 默认启用)
- 限制 top_k 数量(不要设置太大)
- 缓存热门查询结果(实际项目中可添加 Redis 缓存层)
八、总结
8.1 RAG 在测试系统中的价值
| 价值 | 说明 |
|---|---|
| 知识复用 | 历史用例、项目规范、常见 Bug 模式都被知识库收录 |
| 质量提升 | 生成的用例更符合实际,减少重复和遗漏 |
| 可追溯 | 每个生成的用例都可以追溯到原始知识来源 |
| 持续进化 | 知识库随项目积累不断丰富 |
| 双模式 | MySQL 全文搜索(轻量)+ ChromaDB 向量检索(重量) |
| 自动降级 | ChromaDB 失败 → 回退到 MySQL,不影响主流程 |
8.2 与其他引擎的关系
┌─────────────────────────────────────────────────────────┐
│ LangGraph State Graph │
│ analyze_node → generate_node → execute_node → report │
├─────────────────────────────────────────────────────────┤
│ RAGService 桥接 │
│ search() → MySQL/ChromaDB → 检索知识 → 优化上下文 │
├─────────────────────────────────────────────────────────┤
│ 三大引擎协同工作 │
│ RAG(知识增强) + MCP(工具调用) + Skills(技能扩展) │
└─────────────────────────────────────────────────────────┘
在测试流程中的应用:
analyze_node:调用 RAG 检索历史用例,优化上下文后注入state["rag_context"]generate_node:读取state["rag_context"],将知识融入测试步骤生成RAGEnhancedGenerator:独立使用,基于 RAG 生成问答/用例
8.3 下一步
下一篇将介绍 RAG 在测试流程中的应用,详细讲解分析节点和生成节点如何协作,将 RAG 知识真正融入测试用例生成流程。
附录:完整代码索引
| 文件 | 说明 |
|---|---|
backend/app/rag/__init__.py |
RAG 模块导出 |
backend/app/rag/service.py |
RAGService + RAGEnhancedGenerator |
backend/app/services_pkg/context_optimizer.py |
ContextOptimizer 上下文优化器 |
backend/app/graphs/test_flow.py |
analyze_node 中的 RAG 集成 + generate_node 中的知识增强 |
backend/app/models_fastapi.py |
KnowledgeBase 模型 |
init-db/01-schema.sql |
knowledge_base 表定义(ngram 分词器) |
init-db/migrations/V001__add_ngram_fulltext_index.sql |
ngram 索引迁移脚本 |
关注"测试员周周",14 年测试老兵,持续分享 AI测试实战经验。