向量检索评测实战:Recall@K 与 RAG 质量度量

向量检索评测是 RAG 系统上线前最容易被跳过、却最决定成败的一步。很多团队把精力全花在提示词和大模型选型上,结果答案答非所问,根因却是检索阶段就召回了错误片段。本文用 Recall@K 等核心指标,带你搭建一套可量化、可回归的向量检索评测体系。

一、为什么 RAG 要先做向量检索评测

RAG(检索增强生成)的效果由”检索”和”生成”两段组成,而检索是上游。检索召回错了,再强的大模型也只能基于错误上下文硬编。我们曾在大模型 RAG 进阶:混合检索与重排序中讨论过混合检索的思路,但”思路对不对”必须用数据回答。向量检索评测的价值,就是把”感觉召回还行”变成”Recall@5 从 0.62 提升到 0.81″这样可追踪的数字。

评测还能驱动其他关键决策:切分策略该用 512 还是 1024 字符?是否要上重排序模型?向量库选 Chroma 还是 pgvector?这些问题的答案,都应该来自评测集上的指标对比,而非拍脑袋。

举个真实例子:某知识库问答在测试时”看起来”能答,上线后用户却频繁投诉答非所问。复盘发现,根因是长文档被切成 2048 字符的大块,关键句子正好落在块边界,检索时总是差一口气没召回。把切分改到 512 并加上重叠切分后,Recall@5 从 0.58 升到 0.84,投诉立刻下降。这个改动如果没有评测集,根本无从验证——你甚至不会知道问题出在检索,而不是模型。

二、核心指标:Recall@K 与 MRR / NDCG

2.1 Recall@K:前 K 个结果里命中了多少

Recall@K 衡量”在返回的前 K 个文档中,是否包含了所有相关文档”。它是检索质量最直观的指标,K 通常取 3、5、10。对于问答型 RAG,只要正确片段落在前 5 名即可,因此 Recall@5 比召回全部更重要。

2.2 MRR 与 NDCG:看”排第几”

MRR(平均倒数排名)关注”第一个正确结果排在第几位”;NDCG 则对排序位置做加权,越靠前的相关项得分越高。当你的检索会接重排序(rerank)环节时,这两个指标能反映排序质量。

指标关注点适用场景
Recall@K前 K 是否含全部相关项问答召回是否漏片段
MRR首个正确项的位置单答案检索(FAQ)
NDCG排序整体质量带重排序的多结果列表
命中率 Hit@K前 K 是否至少含一个宽松召回兜底

三、用 Python 搭建最小评测集

评测集本质是一组”查询 → 预期相关文档 id”的配对。下面用最常见的 embedding 余弦相似度,手写一个最小评测脚本,计算 Recall@K 与 MRR:

评测集的构建本身有讲究:查询要尽量来自真实日志或业务方提供的 FAQ,相关文档需人工标注 1~3 篇;样本容量建议 100 条以上,并按业务域(如售前、运维、API 文档)分组,这样指标下降时能快速定位是哪个域出了问题。

import numpy as np

def recall_at_k(relevant, retrieved, k):
    top = retrieved[:k]
    hit = len(set(relevant) & set(top))
    return hit / len(relevant)

def mrr(relevant, retrieved):
    for i, doc_id in enumerate(retrieved, 1):
        if doc_id in relevant:
            return 1 / i
    return 0.0

# 一个查询:预期相关文档为 [12, 15],检索返回排序 [15, 8, 12, 3]
rel, ret = [12, 15], [15, 8, 12, 3]
print("Recall@3 =", round(recall_at_k(rel, ret, 3), 2))   # 1.0
print("MRR      =", round(mrr(rel, ret), 2))              # 1.0(首个命中在第1位)

# 汇总多查询
def evaluate(queries):
    r5, m = [], []    # queries: [(relevant_ids, retrieved_ids), ...]
    for rel, ret in queries:
        r5.append(recall_at_k(rel, ret, 5))
        m.append(mrr(rel, ret))
    return np.mean(r5), np.mean(m)

四、向量库选型对评测的影响

不同向量库的默认索引(HNSW、IVF 等)和距离度量(余弦、内积、欧式)会直接影响召回率。我们在Chroma 向量数据库实战里用过它的相似度查询,其默认 HNSW 在召回率和延迟之间做了权衡。同样的 embedding,换库或换距离公式,Recall@K 可能差出 10 个百分点。尤其要注意距离度量的统一:同一个 embedding 模型,用余弦还是内积,在归一化后结果相近,但若混用未归一化的向量,召回排序会完全错位。评测时必须锁定 embedding、度量、索引三类变量,一次只改一个,才能干净地归因。

import chromadb
client = chromadb.PersistentClient(path="./chroma_db")
col = client.get_or_create_collection("docs")
# query 返回按相似度降序的 ids,直接喂给评测函数
res = col.query(query_texts=["如何重置 MySQL 密码"], n_results=10)
retrieved = res["ids"][0]
# 与评测集里该查询的 relevant 比对,计算 Recall@10
向量库默认索引评测时注意事项
ChromaHNSW确认距离度量与 embedding 一致
pgvectorIVFFlat / HNSWlists/probes 参数影响召回
MilvusHNSW / IVF索引构建耗时,需重评
FAISSFlat / IVFFlat 为精确基线对照

五、RAG 端到端评测:从检索到生成

检索指标只是上游,最终还要看”生成答案对不对”。业界常用 faithfulness(忠实度,是否与上下文一致)和 answer correctness(答案正确性)做端到端评估,工具如 Ragas 就是用 LLM 当评委打这两分项。faithfulness 衡量答案是否”无中生有”——所有论断是否都能在上下文找到依据;correctness 则对照标准答案,判断事实是否正确。两者侧重不同:前者防幻觉,后者防事实错误。可用本地量化模型跑打分,成本可控。下面是一段精简示意:

# 伪代码:用大模型对 RAG 答案打分(faithfulness / correctness)
def eval_answer(question, context, answer):
    prompt = f"""给定问题:{question}
检索上下文:{context}
生成答案:{answer}
请仅输出 JSON:{"faithfulness": 0-1, "correctness": 0-1}"""
    return llm_json(prompt)   # 复用本地量化模型

# 批量跑完所有 case,取均值,作为本次迭代的回归基线
scores = [eval_answer(q, c, a) for q, c, a in rag_cases]
print("faithfulness:", np.mean([s["faithfulness"] for s in scores]))

生成评估同样依赖模型能力,生产环境建议把评测用的打分模型与业务模型解耦,例如用 vLLM 部署实战 的独立服务承担评测流量,避免互相抢占;本地量化模型可参考 本地大模型量化部署

六、常见评测误区

  • 误区一:只在训练集上测。上线数据分布会变,必须保留 unseen 的评测集防止过拟合切分参数。
  • 误区二:只看 Recall@K 不看延迟。HNSW 参数调高召回,但查询变慢,需要权衡 P99 延迟。
  • 误区三:评测集太小。十几个 case 的波动很大,建议至少 100+ 条查询才有统计意义。
  • 误区四:忽略 chunk 边界。片段切在句子中间,会拉低检索与生成两端表现。

七、生产环境评测清单

  • 建立 100+ 条”查询—相关文档”评测集,按业务域分组。
  • 每次改 embedding、切分、向量库任一环节,先跑评测再上线。
  • 用 Flat/FAISS 精确基线对照,确认近似索引的召回损失可接受。
  • 检索指标(Recall@K)与生成指标(faithfulness)双轨并行。
  • 把评测脚本接入 GitHub Actions,每次 PR 自动回归。

向量检索评测不是一次性的上线动作,而是伴随 RAG 系统整个生命周期的”体检”。把 Recall@K、MRR 这些指标固化进 CI,每次改动都能看到数字变化,RAG 的质量提升才真正可控、可复盘。从今天起,别再凭感觉判断检索好不好了。

上一篇 MySQL 深度调优:核心参数与执行计划解读
下一篇 前端安全实战:XSS与CSRF防御全指南