RAG 效果评估(召回率、准确率、幻觉检测)+ RAG混合检索与Reranker 重排优化

承接前面本地 Embedding+ChromaDB + 本地大模型 RAG 实例。 RAG 评估分为两大块:检索阶段评估(评估向量库召回好不好) 和 生成阶段评估(评估大模型回答质量、幻觉)

链路回顾:文档入库 → 向量检索召回 TopN 片段 → Prompt 组装 → LLM 生成答案

  • 检索模块:负责找到相关参考资料,核心指标:召回率 Recall、精确率 Precision
  • 生成模块:负责基于资料回答,核心评估:答案准确率、幻觉检测

1 检索阶段评估(向量召回评估:召回率和准确率)

检索评估目标:用户提问,向量库能不能把真正相关的知识库片段给找回来。 这一步不看大模型回答,只看召回出来的文档片段是否是和问题相关的。

1.1基础概念

  1. 召回率 Recall(重点,RAG 检索最关注)

(Recall = {检索出来的相关文档数量}/{知识库中全部相关文档总数})

含义:所有真正相关的文档,有多少被成功召回。

  • Recall 越高:漏检越少,重要参考资料不会丢失;
  • Recall 低:知识库明明有答案,但是向量检索没捞出来,大模型缺少素材,大概率回答错误。

RAG 检索场景优先保证召回率,宁可多召回几条无关片段(引入噪声),也不要漏掉关键文档。

  1. 精确率 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}")
View Code

1.4 检索调优手段(提升 Recall/Precision)

  • Recall 偏低:增大n_results;更换更好的 Embedding 模型;优化文本切分策略;开启混合检索(向量 + BM25 关键词检索)
  • Precision 偏低:增加 Reranker 重排模型;增加距离阈值过滤,丢弃距离过大的召回结果;优化 chunk 切分,减少碎文档

2 生成阶段评估 + 幻觉检测

检索拿到了正确的上下文,但是大模型回答依然可能出错,编造不存在内容,也就是幻觉。

幻觉分为两类:

  1. 内部幻觉:参考资料里存在信息,但是模型回答错误,和原文冲突;
  2. 外部幻觉:参考资料完全没有提到,模型凭空编造信息。

2.1幻觉检测思路

思路:把【用户问题】+【召回参考上下文】+【模型生成答案】送入检测模型,校验两件事:

  1. 答案里所有陈述,是否都能在参考资料中找到依据(事实一致性 Factuality);
  2. 是否存在资料不存在的编造内容。

方案 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"])
View Code

方案 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)
View Code

注意:Ragas 默认使用 OpenAI 接口;如果需要完全本地私有化评估,可以替换成本地 Embedding / 本地 LLM。

3 RAG 评估整体流程总结

  1. 准备测试集:批量构造问题 + 人工标注标准答案 + 标注相关文档;
  2. 检索评估:跑向量检索,计算 Recall、Precision;
  3. 生成评估:用召回的上下文,调用本地大模型生成答案;
  4. 幻觉评估:校验答案忠实度,检测是否编造;
  5. 迭代调优:根据评估结果,调整 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)

思路:

  1. 向量检索:从 Chroma 召回 TopN(例如 Top5),捕捉语义相似内容;
  2. BM25 关键词检索:对知识库原始文本块做关键词检索,召回 Top5,捕捉字面关键词匹配;
  3. 合并两组候选文档,去重,得到候选文档池;
  4. 送入 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)}")
View Code

运行结果如下:

image

 

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)
View Code

运行结果如下图:

image

承接 本地 Embedding+ChromaDB + 本地大模型 RAG 实例的后续步骤,拼接上下文 ,调用LLM 生成答案

运行结果的完整提示词信息:

image

 模型返回结果如下:

image

 

上面的那个用户问题,针对混合检索+重排优化后的效果不是很明显,特意准备了几个可以看出有明显优化效果的用户问题

具体代码:

# ===================== 对比实验:不同问法下「粗召回 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}")

运行结果:

image

image

image

image

5.4 优化后完整 RAG 链路对比

旧链路(仅向量检索)

用户问题 → Embedding 向量检索 Top3 → 直接拼接上下文 → LLM 生成

缺点:容易召回语义相近但无关噪声片段

优化后链路(混合检索+Reranker)

用户问题

  1. 向量检索 Top5(语义召回)
  2. BM25 关键词检索 Top5(字面关键词召回)
  3. 合并去重得到候选池
  4. Reranker 交叉编码器模型,对候选文档精细打分重排
  5. 选取重排后 Top3 高相关文档
  6. 组装 Prompt,送入本地大模型回答

5.5 参数调优经验

  1. 粗召回数量:混合检索每一路召回 Top5~Top10,候选池不要太大,避免 Reranker 推理变慢;
  2. Reranker 筛选 TopK:送入大模型的上下文一般 2~4 条 chunk,兼顾信息量和上下文窗口;
  3. 分数阈值过滤:可以增加判断,相关性分数低于阈值(如 0.01)直接丢弃文档,进一步减少噪声;

5.6 评估对比(优化前后)

方案Recall 召回率Precision 精确率幻觉概率
仅向量检索 中等 较低 较高
Hybrid 混合检索 ↑提升(兼顾语义 + 关键词,减少漏检) 中等 中等
Hybrid + Reranker ↑提升 ↑显著提升 ↓显著降低

核心收益:Reranker 极大提升 Precision,过滤假阳性噪声,减少送入大模型的无关上下文,从而降低幻觉。

posted @ 2026-09-14 14:13  sunshine_coast  阅读(34)  评论(0)    收藏  举报