向量检索评测是 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
| 向量库 | 默认索引 | 评测时注意事项 |
|---|---|---|
| Chroma | HNSW | 确认距离度量与 embedding 一致 |
| pgvector | IVFFlat / HNSW | lists/probes 参数影响召回 |
| Milvus | HNSW / IVF | 索引构建耗时,需重评 |
| FAISS | Flat / IVF | Flat 为精确基线对照 |
五、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 的质量提升才真正可控、可复盘。从今天起,别再凭感觉判断检索好不好了。




