一、RAG 上线后效果差,先查哪一环?
RAG 系统是个流水线:切分 → 索引 → 检索 → 重排 → 生成。当用户说"回答得不对"时,最要命的场景是------你根本不知道问题出在哪一环。
可能是检索没召回对文档,也可能是文档召回了但 LLM 没用好。如果两手一摊去调 Prompt,大概率是瞎忙:把生成端调得再好,检索给的是错的,答案还是错的。
所以工程上的第一原则是:
先量化检索,再谈优化生成。
本文是我在两个 RAG 项目(HeritageMind 非遗知识问答、LexRAG 法律条文问答)里做检索评测的完整方法:评测集怎么构造、怎么自动验证、比什么指标、怎么解读结果。全文数据和脚本均可复现。
二、第一步:构造评测集(Query 集)
评测集是地基。地基层次不够,后面所有数字都是自我安慰。
2.1 两条设计原则
原则 1:按难度分型,不要混在一起
只报一个"总体准确率"等于没报------它掩盖了系统在哪种问题上强、哪种问题上弱。我们把 query 分成两类:
| 类型 | 定义 | 例子 |
|---|---|---|
| 简单题 | 直问,问题里包含技艺名 | "景泰蓝的制作工艺是怎样的?" |
| 难题 | 间接提问,问题里不含技艺名 | "掐丝珐琅的另一个名称是什么?" |
为什么难题重要?因为真实用户的提问往往是难题------他们不知道"那个东西"叫什么,只能用特征描述。只测简单题,测的是"玩具场景"。
原则 2:query 必须覆盖全量语料主题
每个知识域至少 3~5 条 query,确保评测不是"只测了系统擅长的几篇文档"。
2.2 锚词自动验证:防住"无效 query"
这是我认为最值得讲的一个工程细节。
手工写评测集有个隐蔽的坑:你以为 query 的答案在文档里,实际上可能不在------或者答案在,但你记错了关键词。这种"无效 query"混进评测集,会把分数拉低,而你还以为是系统的问题。
解法:给每条 query 配一个锚词(anchor) ,写个脚本自动验证"锚词是否真的出现在期望文档里":
python
# (query, 期望文档, 锚词, 难度类型)
Q = [
("东阳木雕有哪些主要雕刻技法?", "东阳木雕", "镂空雕", "simple"),
("哪种木雕以平面浮雕见长?", "东阳木雕", "平面浮雕", "hard"),
# ... 100 条
]
valid, invalid = [], []
for q, exp, anchor, typ in Q:
with open(f"data/crafts/{exp}.txt", encoding="utf-8") as f:
txt = f.read()
if anchor in txt:
valid.append((q, exp, anchor, typ)) # 锚词在文档里 → query 有效
else:
invalid.append((q, exp, anchor, typ)) # 锚词不在 → query 无效,剔除
print(f"有效 query: {len(valid)} / {len(Q)}")
实测效果 :我们写了 100 条 query(40 简单 + 60 难题),自动验证后剔除了 6 条无效 query(锚词不在期望文档里),最终评测集 94 条。
如果不做这一步,这 6 条无效 query 会稳定地"失败",把真实 Hit@5 从 100% 拉低到 94%------你会误判系统有问题,然后开始一轮毫无意义的调参。
三、第二步:选指标,为什么是 Hit@5?
检索评测的常见指标:Hit@K、MRR、NDCG。我们选 Hit@5 作为主指标,理由很工程:
- 贴近实际用法:我们的 RAG 管线检索 Top-5 送入重排/生成,所以"正确答案是否出现在 Top-5"就是系统能不能工作的直接判据
- 判定简单、可自动:Hit@K 只要判断"期望文档是否在返回列表里",不需要人工打分,评测可以全自动跑
- 生成端的前置条件:如果 Top-5 里连正确答案都没有,后面重排和生成做得再好也白搭
判定规则:返回的 chunk 属于期望文档 即算命中。因为我们的语料按文档分文件、chunk 带 craft_id 元数据,判定是精确的、可复现的。
四、第三步:三路检索对比实验
评测的价值在对比。只测一路检索,你不知道自己的方案在同行里算什么水平;三路对比,你能看清每路检索的优缺点。
我们比了三路:
| 路线 | 原理 | 特点 |
|---|---|---|
| BM25 | 词频 + 逆文档频率(稀疏检索) | 快、可解释、依赖关键词重合 |
| 向量检索 | BGE-M3 编码后余弦相似度(稠密检索) | 语义理解强、需要 embedding 模型 |
| RRF 融合 | 对前两路排名做倒数排名融合 | 取长补短,工业界标配 |
RRF 融合的核心代码很简洁:
python
def reciprocal_rank_fusion(ranked_lists, k=60):
"""RRF: 每个文档在各路结果中的排名取倒数,求和排序"""
scores = {}
for lst in ranked_lists:
for rank, doc_id in enumerate(lst):
scores[doc_id] = scores.get(doc_id, 0) + 1.0 / (k + rank + 1)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
评测脚本结构(伪码):
ini
for query in eval_set:
bm25_top5 = bm25.retrieve(query) # 稀疏路
vec_top5 = vector_retrieve(query) # 稠密路(BGE-M3, 1024维)
rrf_top5 = rrf_fuse(bm25_top5, vec_top5) # 融合路
# 分别判断期望文档是否在 Top-5,按 simple/hard 分型统计
五、实测结果与解读
5.1 HeritageMind(非遗知识问答,94 条有效 query)
| 检索路线 | 简单题 (40) | 难题 (54) | 总体 |
|---|---|---|---|
| BM25 | 100% | 100% | 100% |
| 向量检索(BGE-M3) | 100% | 100% | 100% |
| RRF 融合 | 100% | 100% | 100% |
三路全部 100%,是不是显得很假?恰恰相反,这个 100% 是可以被解释的,解释比数字本身更重要:
- 语料小:非遗知识库 6 类技艺、几十个文档、500 字窗口切分后只有几百个 chunk------检索空间小,命中概率天然高
- 主题分离:每类技艺一个文档,文档间几乎没有语义重叠。问"景泰蓝"的 query 和"剪纸"文档的相似度极低,向量检索天然不会串
- 判定宽松:Hit@5 判定"在 Top-5 内即可",不需要第一名------正确文档只要进前五就算过
给面试官讲这套数字时,一定要主动说出这三个原因。 100% 不是"我强",而是"我的评测场景简单";主动解释反而显得你有方法论、不吹牛。
5.2 LexRAG(法律条文问答,15 条咨询 query)
换到法律场景,数字立刻出现分化:
| 检索路线 | 准确率 |
|---|---|
| BM25 | 87% |
| 向量检索(BGE-M3) | 100% |
| RRF 融合 | 100% |
BM25 掉到 87%,这是一个非常有价值的失败案例。原因也清晰:法律咨询里用户问题常常和法条原文用词不一致------用户问"合同签了但对方反悔怎么办",法条里写的是"当事人一方不履行合同义务",关键词零重合,BM25 的稀疏匹配就抓瞎了;而向量检索能理解语义等价。
这组对比直接证明了"为什么 RAG 必须上向量检索/混合检索" ------单靠 BM25 的关键词匹配,在语义鸿沟大的领域(法律、医疗)撑不住。
六、评测的边界:检索 100% 不等于问答 100%
必须诚实地说清楚评测的边界。Hit@5=100% 只证明了一件事:检索能把对的文档捞上来。它不证明:
- 生成端用得好不好:文档对了,LLM 可能还是答错/答不全------这是另一层评测(答案正确性、引用准确性)
- 覆盖面够不够:检索只能回答"知识库里有的问题"。知识库没有的内容,检索做得再好也是"优雅地失败"
针对第 2 点,我们做了补充验证------覆盖缺口 query:6 条特意问"知识库里没有的内容"的 query(如某非遗技艺知识库里没收录的细节)。结果符合预期:检索找不到对应文档。
这 6 条 query 没有白测,它们直接支撑了一个产品特性:gap_detector(知识缺口检测) ------当检索分数普遍偏低时,系统不硬答,而是诚实告知"这个问题超出了我的知识范围"。用评测数据反推产品设计,这是评测的最高价值。
七、方法学总结(可直接迁移)
把这套方法抽象成四个步骤,任何 RAG 项目都能用:
css
① 构造评测集:按难度分型 + 全主题覆盖
② 锚词自动验证:剔除无效 query(防自欺欺人)
③ 三路对比:BM25 vs 向量 vs RRF,分型统计 Hit@5
④ 结果解读:先解释"为什么是这个数字",再决定调什么
几个关键认知:
- 先量化检索,再优化生成:检索不达标,调 Prompt 是浪费生命
- 评测集要自动验证:手写 query 无效率可能高达 6%,不验证就是在跟幻觉数据搏斗
- 100% 也要能解释:语料小、主题分离、判定宽松------数字背后的原因才是面试和复盘的重点
- 失败案例比满分更有价值:BM25 在法律场景的 87%,是"必须上向量检索"的最强论据
- 评测要能反推产品:覆盖缺口 query 直接催生了 gap_detector 功能