RAG 效果评估(召回率、准确率、幻觉检测)+ RAG混合检索与Reranker 重排优化
承接前面本地 Embedding+ChromaDB + 本地大模型 RAG 实例。 RAG 评估分为两大块:检索阶段评估(评估向量库召回好不好) 和 生成阶段评估(评估大模型回答质量、幻觉)
链路回顾:文档入库 → 向量检索召回 TopN 片段 → Prompt 组装 → LLM 生成答案
- 检索模块:负责找到相关参考资料,核心指标:召回率 Recall、精确率 Precision
- 生成模块:负责基于资料回答,核心评估:答案准确率、幻觉检测
1 检索阶段评估(向量召回评估:召回率和准确率)
检索评估目标:用户提问,向量库能不能把真正相关的知识库片段给找回来。 这一步不看大模型回答,只看召回出来的文档片段是否是和问题相关的。
1.1基础概念
- 召回率 Recall(重点,RAG 检索最关注)
(Recall = {检索出来的相关文档数量}/{知识库中全部相关文档总数})
含义:所有真正相关的文档,有多少被成功召回。
- Recall 越高:漏检越少,重要参考资料不会丢失;
- Recall 低:知识库明明有答案,但是向量检索没捞出来,大模型缺少素材,大概率回答错误。
RAG 检索场景优先保证召回率,宁可多召回几条无关片段(引入噪声),也不要漏掉关键文档。
- 精确率 Precision(准确率)
(Precision = {检索出来的相关文档数量}/{本次一共召回的文档数量})
含义:本次召回返回的 TopN 条里面,真正相关的占多少。
- Precision 高:召回回来的内容基本都是有用的,噪声少;
- Precision 低:召回一堆无关片段,占用上下文窗口,误导大模型。
举例: 知识库中针对问题【动态召回流程】一共有 3 条真正相关片段。 设置
n_results=3,向量检索返回 3 条,其中 2 条是真相关。
- Recall = 2 / 3 ≈ 66.7%
- Precision = 2 / 3 ≈ 66.7%
1.2 检索评估数据集构建方式
需要构造测试集(Question → 标准相关文档列表) 手工标注一批样本:
| query 问题 | 标准答案文档 ID 列表 |
|---|---|
| 动态召回流程是什么? | doc0,doc1 |
| ChromaDB distance 代表什么? | doc2 |
1.3 代码示例:计算 Recall & Precision(基于前面 Chroma 实例)
def calc_recall_precision(retrieved_docs, ground_truth_docs): """ retrieved_docs:本次向量检索召回得到的文档id列表 ground_truth_docs:标注好的真实相关文档id列表 """ # 检索出来且属于真实相关的文档 hit = set(retrieved_docs) & set(ground_truth_docs) if len(ground_truth_docs) == 0: recall = 1.0 else: recall = len(hit) / len(ground_truth_docs) if len(retrieved_docs) == 0: precision = 0.0 else: precision = len(hit) / len(retrieved_docs) return recall, precision # ===== 测试样例 ===== user_query = "动态召回流程" # 1.向量检索(复用前面chroma查询代码) results = collection.query(query_texts=[user_query], n_results=3) retrieved_docs = results['ids'][0] # 召回文档id列表 # 人工标注:该问题真实相关文档id ground_truth_docs = ["doc0", "doc1"] recall, precision = calc_recall_precision(retrieved_docs, ground_truth_docs) print(f"召回率Recall: {recall:.2f}") print(f"精确率Precision: {precision:.2f}")
1.4 检索调优手段(提升 Recall/Precision)
- Recall 偏低:增大
n_results;更换更好的 Embedding 模型;优化文本切分策略;开启混合检索(向量 + BM25 关键词检索) - Precision 偏低:增加 Reranker 重排模型;增加距离阈值过滤,丢弃距离过大的召回结果;优化 chunk 切分,减少碎文档
2 生成阶段评估 + 幻觉检测
检索拿到了正确的上下文,但是大模型回答依然可能出错,编造不存在内容,也就是幻觉。
幻觉分为两类:
- 内部幻觉:参考资料里存在信息,但是模型回答错误,和原文冲突;
- 外部幻觉:参考资料完全没有提到,模型凭空编造信息。
2.1幻觉检测思路
思路:把【用户问题】+【召回参考上下文】+【模型生成答案】送入检测模型,校验两件事:
- 答案里所有陈述,是否都能在参考资料中找到依据(事实一致性 Factuality);
- 是否存在资料不存在的编造内容。
方案 1:Prompt 式幻觉检测(轻量,直接用本地大模型)
# 幻觉检测模板 hallucination_check_prompt = """ 你是一个RAG幻觉检测器。任务:判断【模型回答】是否存在幻觉。 规则: 1. 仅允许使用【参考资料】中的信息; 2. 如果模型回答的内容,参考资料不存在,或者和资料矛盾 → 判定存在幻觉; 3. 如果回答全部内容都可以在参考资料找到依据 → 判定无幻觉。 【参考资料】 {context_content} 【用户问题】 {user_query} 【模型回答】 {llm_answer} 输出格式: 幻觉判定:有/无 理由:简短说明 """ # 填充参数 check_prompt = hallucination_check_prompt.format( context_content=context_content, user_query=user_query, llm_answer=answer ) # 调用本地LLM执行检测 check_result = llm.create_completion(prompt=check_prompt, max_tokens=256, temperature=0) print(check_result["choices"][0]["text"])
方案 2:使用专门事实评估模型(例如 ragas)
Ragas 是 RAG 评估常用开源库,可以自动计算:
context_recall:上下文召回率(检索指标)context_precision:上下文精确率answer_faithfulness:答案忠实度(幻觉指标,越低幻觉越严重)answer_relevancy:答案相关性,判断回答是否贴合用户提问
ragas 示例代码(适配我们本地 RAG)
from ragas import evaluate from ragas.metrics import context_recall, context_precision, faithfulness, answer_relevancy from datasets import Dataset # 构造评估数据集,多条测试样本 data = { "question": ["动态召回流程是什么?"], "answer": [answer], "contexts": [[context_content]], "ground_truth": ["动态召回流程是用户提问,向量检索召回相关文档片段,送入大模型生成答案"] } dataset = Dataset.from_dict(data) # 执行评估 result = evaluate( dataset=dataset, metrics=[context_recall, context_precision, faithfulness, answer_relevancy] ) print(result)
注意:Ragas 默认使用 OpenAI 接口;如果需要完全本地私有化评估,可以替换成本地 Embedding / 本地 LLM。
3 RAG 评估整体流程总结
- 准备测试集:批量构造问题 + 人工标注标准答案 + 标注相关文档;
- 检索评估:跑向量检索,计算 Recall、Precision;
- 生成评估:用召回的上下文,调用本地大模型生成答案;
- 幻觉评估:校验答案忠实度,检测是否编造;
- 迭代调优:根据评估结果,调整 chunk 大小、Embedding 模型、TopN、Reranker 等参数。
4 常见评估结论 & 调优方向
| 现象 | 问题定位 | 优化方案 |
|---|---|---|
| Recall 低,Precision 高 | 检索漏掉关键文档 | 增大 TopN;换 Embedding;BM25 混合检索 |
| Recall 高,Precision 低 | 召回很多无关噪声文档 | 增加 Reranker 重排;设置距离阈值过滤 |
| 检索指标很好,但大模型回答幻觉多 | 上下文噪声、Prompt 约束不足 | 优化 Prompt,降低 temperature;Reranker 精选上下文 |
| 回答和问题不相关 | Prompt 引导弱 | 优化系统提示词,要求回答必须贴合用户问题 |
5. RAG 进阶优化:Reranker 重排 + 混合检索
承接前面 RAG 评估章节:向量检索擅长语义匹配,但容易召回语义相近但事实无关的文本;单纯关键词检索擅长字面匹配,无法理解同义转述。
优化后的完整链路: 用户问题 → 混合检索(向量检索 + BM25 关键词检索) → 合并候选结果 → Reranker 重排模型打分、二次筛选 → 精选 TopK 上下文 → 组装 Prompt → LLM 生成答案
5.1 为什么需要这两个优化?
-
向量检索的短板
向量匹配是语义层面,会出现 “语义相似,但内容无关” 的假阳性召回。
例子:查询 “动态召回流程”,向量库可能召回一段 “动态扩容流程”,向量距离很近,但内容完全不是我们想要的。
-
BM25 关键词检索的短板
只匹配字面关键词,不理解同义词、转述。
例子:知识库写的是 “动态召回流程”,用户提问 “动态获取知识片段的步骤”,关键词完全不重合,BM25 会漏检。
-
Reranker 重排模型作用
向量 / BM25 只做粗召回(比如先召回 Top10),Reranker 做精细打分,对粗召回的所有候选文档和用户问题做相关性排序,过滤掉假阳性,只保留相关性最高的 Top3 送入大模型。
Reranker 输入:
(问题,单条文档)→ 输出 0~1 相关性分数,分数越高越相关。Reranker 理解细粒度语义,比 Embedding 向量更擅长判断 “这条文档是不是真的能回答这个问题”。
5.2 混合检索实现(向量检索 + BM25)
思路:
- 向量检索:从 Chroma 召回 TopN(例如 Top5),捕捉语义相似内容;
- BM25 关键词检索:对知识库原始文本块做关键词检索,召回 Top5,捕捉字面关键词匹配;
- 合并两组候选文档,去重,得到候选文档池;
- 送入 Reranker 进行二次打分重排。
环境准备:安装 BM25 库
pip install rank_bm25 jieba
混合检索代码示例(基于前面的知识库)
BM25Okapi 的输入要求是词语列表,不是原始文本,中文必须分词;英文直接空格分割,中文需要 jieba 做分词。
from rank_bm25 import BM25Okapi # ---------- 准备:提前缓存所有文档块文本,用于BM25检索 ---------- # all_chunks:入库Chroma时所有切分好的文本块列表 all_chunks = [ "文档块1:动态召回流程包含问题向量化、向量检索、上下文组装、大模型生成", "文档块2:ChromaDB的distance代表向量距离,数值越小相似度越高", "文档块3:动态扩容流程用于服务器资源弹性伸缩", "文档块4:RAG评估指标包含召回率、精确率、幻觉检测" ] # BM25需要分词(中文可以用jieba) import jieba tokenized_corpus = [jieba.lcut(chunk) for chunk in all_chunks] bm25 = BM25Okapi(tokenized_corpus) user_query = "动态召回流程" # 1.向量检索(Chroma粗召回Top5) vector_results = collection.query(query_texts=[user_query], n_results=5) vector_retrieved_docs = vector_results['documents'][0] # 2.BM25关键词检索粗召回Top5 query_tokens = jieba.lcut(user_query) bm25_scores = bm25.get_scores(query_tokens) bm25_top_indexes = sorted(range(len(bm25_scores)), key=lambda i: bm25_scores[i], reverse=True)[:5] bm25_retrieved_docs = [all_chunks[idx] for idx in bm25_top_indexes] # 3.合并候选文档,并去重 candidate_docs = list(set(vector_retrieved_docs + bm25_retrieved_docs)) print(f"混合检索粗召回候选文档总数:{len(candidate_docs)}")
运行结果如下:

5.3 Reranker 重排模型实现(本地模型,私有化)
使用bge-reranker,国内常用开源中文重排模型,可以本地加载,不上传数据。
安装依赖
pip install sentence-transformers torch
from sentence_transformers import CrossEncoder # 加载本地Reranker重排模型(bge-reranker-base) reranker = CrossEncoder('BAAI/bge-reranker-base') # user_query 用户问题, candidate_docs 混合检索得到的候选文档池 pairs = [[user_query, doc] for doc in candidate_docs] # 模型批量打分 scores = reranker.predict(pairs) # 将【文档+分数】打包,按相关性分数降序排序 doc_score_pairs = list(zip(candidate_docs, scores)) doc_score_pairs.sort(key=lambda x:x[1], reverse=True) # 筛选重排后Top3,送入Prompt组装阶段(替换原来向量检索的结果) top_k = 3 selected_docs = [item[0] for item in doc_score_pairs[:top_k]] print("==== Reranker重排后精选文档 ====") for idx, (doc, score) in enumerate(doc_score_pairs[:top_k]): print(f"序号:{idx}, 相关性分数:{score:.3f}") print(doc,"\n") # 拼接精选上下文,继续后面Prompt组装 context_content = "\n\n".join(selected_docs)
运行结果如下图:

承接 本地 Embedding+ChromaDB + 本地大模型 RAG 实例的后续步骤,拼接上下文 ,调用LLM 生成答案
运行结果的完整提示词信息:

模型返回结果如下:

上面的那个用户问题,针对混合检索+重排优化后的效果不是很明显,特意准备了几个可以看出有明显优化效果的用户问题
具体代码:
# ===================== 对比实验:不同问法下「粗召回 vs 重排」 ===================== def hybrid_retrieve(user_query): """一次完整混合检索+重排。返回 (纯向量顺序, BM25顺序, 重排后 [(doc, score)...])""" # 1.纯向量粗召回 q_emb = embedding_model.encode([user_query]).tolist() vector_docs = collection.query(query_embeddings=q_emb, n_results=5)['documents'][0] # 2.BM25关键词粗召回 q_tokens = jieba_cut(user_query) bm25_scores = bm25.get_scores(q_tokens) bm25_idx = sorted(range(len(bm25_scores)), key=lambda i: bm25_scores[i], reverse=True)[:5] bm25_docs = [all_chunks[i] for i in bm25_idx] # 3.合并去重(保序) candidate_docs = list(dict.fromkeys(vector_docs + bm25_docs)) # 4.Reranker精排 scores = reranker.predict([[user_query, doc] for doc in candidate_docs]) ranked = sorted(zip(candidate_docs, scores), key=lambda x: x[1], reverse=True) return vector_docs, bm25_docs, ranked test_queries = [ "服务器资源不够,需要扩容时走什么流程?", # 考验Reranker:chunk1「动态召回」vs chunk3「动态扩容」 "容量不足要做弹性伸缩,有什么流程?", # 考验Reranker:chunk3 和 chunk1 的分数差会被拉开 "ChromaDB 的 distance 越小代表什么?", # 考验BM25:专有名词,向量易漏 "两条向量离得近,说明它们相似度高还是低?", # 考验向量语义:BM25关键词失效 ] for q in test_queries: vector_docs, bm25_docs, ranked = hybrid_retrieve(q) print("\n" + "=" * 64) print(f"问题{test_queries.index(q)+1}:{q}") print("-" * 64) print("① 纯向量 Top3:") for i, d in enumerate(vector_docs[:3], 1): print(f" {i}. {d}") print("② BM25 Top3:") for i, d in enumerate(bm25_docs[:3], 1): print(f" {i}. {d}") print("③ 混合 + Reranker 精排 Top3:") for i, (d, s) in enumerate(ranked[:3], 1): print(f" {i}. [{s:.3f}] {d}")
运行结果:




5.4 优化后完整 RAG 链路对比
旧链路(仅向量检索)
用户问题 → Embedding 向量检索 Top3 → 直接拼接上下文 → LLM 生成
缺点:容易召回语义相近但无关噪声片段
优化后链路(混合检索+Reranker)
用户问题
- 向量检索 Top5(语义召回)
- BM25 关键词检索 Top5(字面关键词召回)
- 合并去重得到候选池
- Reranker 交叉编码器模型,对候选文档精细打分重排
- 选取重排后 Top3 高相关文档
- 组装 Prompt,送入本地大模型回答
5.5 参数调优经验
- 粗召回数量:混合检索每一路召回 Top5~Top10,候选池不要太大,避免 Reranker 推理变慢;
- Reranker 筛选 TopK:送入大模型的上下文一般 2~4 条 chunk,兼顾信息量和上下文窗口;
- 分数阈值过滤:可以增加判断,相关性分数低于阈值(如 0.01)直接丢弃文档,进一步减少噪声;
5.6 评估对比(优化前后)
| 方案 | Recall 召回率 | Precision 精确率 | 幻觉概率 |
|---|---|---|---|
| 仅向量检索 | 中等 | 较低 | 较高 |
| Hybrid 混合检索 | ↑提升(兼顾语义 + 关键词,减少漏检) | 中等 | 中等 |
| Hybrid + Reranker | ↑提升 | ↑显著提升 | ↓显著降低 |
核心收益:Reranker 极大提升 Precision,过滤假阳性噪声,减少送入大模型的无关上下文,从而降低幻觉。

浙公网安备 33010602011771号