在知识库问答(RAG)系统迭代过程中,最大的痛点往往不是检索功能不可用,而是无法量化检索效果好坏。
调切片大小、改向量/关键词权重、换 Embedding 模型后,我们基本只能“肉眼看结果”,非常主观,无法回答这些关键问题:
- 检索结果到底有没有真正命中标准答案?
- 标准切片排第几,是否满足业务要求?
- 向量检索、关键词检索分别贡献多少得分?
- 本次迭代到底是优化了还是变差了?
- 测评指标全部为 0,是代码问题、标注问题、还是数据集不匹配?
为了解决上述问题,我在辞溯知识库项目二期落地了一套完整可闭环的 RAG 检索测评系统,实现:数据集准备 → 自动检索 → 指标计算 → 记录落库 → 可视化分析。
本文结合真实项目源码,完整拆解整套测评方案的设计、实现、踩坑经验与后续优化方向。
一、整体测评流程
整套评测链路完全标准化、可重复运行,流程如下:
准备评测数据集
|
v
选择知识库和检索策略
|
v
执行向量检索 + 关键词检索
|
v
候选集合合并、去重、排序
|
+--> 取最终 Top-K(用于指标计算)
|
+--> 保存完整候选明细(用于问题溯源)
|
v
对比召回切片ID & 人工标注切片ID
|
v
计算 Precision@K / Recall@K / MRR / nDCG@K
|
v
保存评测记录 & 管理端可视化展示
系统提供两种使用模式,适配不同研发场景:
- 命令行离线评测:本地快速跑批,输出 JSON 报告,适合调参对比。
- 管理端页面评测:持久化每次评测记录,支持大盘指标 + 单用例细节排查。
二、评测数据集标准化设计
评测是否靠谱,完全取决于数据集是否标准。我采用轻量 JSON 数组格式,每条数据对应一条检索测试用例。
2.1 数据集示例
<span>[</span>
<span>{</span>
<span>"case_id"</span><span>:</span> <span>"monorepo-advantages-001"</span><span>,</span>
<span>"question"</span><span>:</span> <span>"Monorepo架构有什么优势"</span><span>,</span>
<span>"relevant_chunk_ids"</span><span>:</span> <span>[</span>
<span>"30465673-88ea-41f0-a5a0-da496592f3df"</span>
<span>]</span><span>,</span>
<span>"k"</span><span>:</span> <span>6</span>
<span>}</span>
<span>]</span>
2.2 字段说明
| 字段 | 说明 |
|---|---|
| case\_id | 测试用例唯一ID,用于关联指标与候选明细 |
| question | 用户检索问题 |
| relevant\_chunk\_ids | **核心**:人工标注的正确切片ID(document\_chunks.id) |
| k | 参与指标计算的 Top-K 数量 |
避坑重点:
relevant_chunk_ids不能填文档ID、知识库ID、自定义ID。只要 ID 不匹配数据库真实切片 ID,无论内容多相似,指标直接为 0。
三、混合检索核心实现(向量+关键词)
项目采用 向量语义检索 + Postgres 全文关键词检索 双路混合方案,兼顾语义泛化与关键词精准匹配。
3.1 混合检索流程
向量候选 Top10
+
关键词候选 Top10
|
v
按 chunk_id 合并去重
|
v
最终分 = 向量分 * 0.7 + 关键词分 * 0.3
|
v
按最终分降序排序
|
v
输出最终 Top-K 结果
3.2 核心代码
<span>async</span> <span>def</span> <span>hybrid_search_with_trace</span>(<span>
session: AsyncSession,
*,
knowledge_base_id: UUID,
query: <span>str</span>,
limit: <span>int</span> | <span>None</span> = <span>None</span>,
</span>) -> RetrievalTrace:
<span># 1. 问题向量化,用于语义检索</span>
query_embedding = <span>await</span> embed_query(query)
<span># 2. 只检索有效、已发布、未删除的文档切片</span>
base_filters = (
DocumentChunk.knowledge_base_id == knowledge_base_id,
Document.status == DocumentStatus.READY,
Document.publication_status == DocumentPublicationStatus.PUBLISHED,
Document.is_deleted.is_(<span>False</span>),
)
<span># 3. 向量检索召回 Top10</span>
vector_rows = (
<span>await</span> session.execute(
select(DocumentChunk, Document.original_name, distance.label(<span>"distance"</span>))
.join(Document, Document.<span>id</span> == DocumentChunk.document_id)
.where(*base_filters)
.order_by(distance)
.limit(settings.vector_candidate_k)
)
).<span>all</span>()
<span># 4. 关键词全文检索召回 Top10</span>
keyword_rows = (
<span>await</span> session.execute(
select(DocumentChunk, Document.original_name, rank.label(<span>"rank"</span>))
.join(Document, Document.<span>id</span> == DocumentChunk.document_id)
.where(*base_filters, DocumentChunk.content_tsvector.op(<span>"@@"</span>)(ts_query))
.order_by(rank.desc())
.limit(settings.keyword_candidate_k)
)
).<span>all</span>()
<span># 5. 合并去重、加权打分、排序</span>
candidates = <span>sorted</span>(merged.values(), key=<span>lambda</span> item: item.score, reverse=<span>True</span>)
final_limit = limit <span>or</span> settings.final_top_k
<span># 6. 返回完整候选池 + 最终TopK,实现无侵入测评</span>
<span>return</span> RetrievalTrace(
candidates=candidates,
selected=candidates[:final_limit],
)
设计亮点:正常问答业务只使用 selected,测评使用完整 candidates,完全不影响原有业务逻辑。
四、全量候选池快照:问题溯源核心
如果只存最终 Top-K 结果,我们只能知道“有没有命中”,但无法回答:
- 标准切片到底有没有进候选池?
- 为什么没进 TopK?向量分低还是关键词分低?
- 和上榜结果差距多少分?
因此我新增 retrieval_evaluation_candidates 表,保存每一条候选切片的完整打分快照(不存正文,避免数据冗余与敏感信息)。
4.1 核心存储字段
| 字段 | 说明 |
|---|---|
| evaluation\_run\_id | 所属评测任务ID |
| case\_id | 测试用例ID |
| chunk\_id / document\_id | 切片ID、文档ID |
| rank | 最终排序名次 |
| vector\_score / keyword\_score / final\_score | 分项得分 + 混合得分 |
| is\_relevant | 是否为标准答案切片 |
| is\_selected | 是否进入最终 Top-K |
| strategy\_version | 检索策略版本,用于版本对比 |
4.2 候选落库核心代码
trace = <span>await</span> hybrid_search_with_trace(
session,
knowledge_base_id=knowledge_base_id,
query=<span>case</span>.question,
limit=<span>case</span>.k,
)
selected_chunks = trace.selected
retrieved_ids = [<span>str</span>(chunk.chunk_id) <span>for</span> chunk <span>in</span> selected_chunks]
relevant_ids = <span>set</span>(<span>case</span>.relevant_chunk_ids)
<span># 全量候选落库,用于事后分析</span>
<span>for</span> rank, chunk <span>in</span> <span>enumerate</span>(trace.candidates, start=<span>1</span>):
session.add(
RetrievalEvaluationCandidate(
evaluation_run_id=run.<span>id</span>,
case_id=<span>case</span>.case_id,
chunk_id=chunk.chunk_id,
document_id=chunk.document_id,
document_name=chunk.document_name,
rank=rank,
vector_score=chunk.vector_score,
keyword_score=chunk.keyword_score,
rerank_score=chunk.rerank_score,
final_score=chunk.score,
is_relevant=<span>str</span>(chunk.chunk_id) <span>in</span> relevant_ids,
is_selected=rank <= <span>len</span>(selected_chunks),
strategy_version=chunk.strategy_version,
)
)
五、四大检索指标原理与代码实现
系统采用工业界常用的四个检索指标:Precision@K、Recall@K、MRR、nDCG@K,分别衡量精准度、召回完整性、靠前程度、整体排序质量。
5.1 Precision@K 精确率
Top-K 结果中,有效相关切片的占比。
Precision@K = TopK 相关数量 / TopK 总数量
5.2 Recall@K 召回率
所有标准切片中,有多少被 Top-K 成功召回。
Recall@K = TopK 相关数量 / 全部标准切片数量
5.3 MRR 平均倒数排名
关注第一个正确结果的位置,越靠前分数越高。
MRR = 1 / 第一个相关切片排名
5.4 nDCG@K 归一化折损累计增益
综合相关性 + 排序位置,位置越靠前,收益越高,是最全面的排序指标。
<span>def</span> <span>ndcg_at_k</span>(<span>
retrieved_ids: <span>Sequence</span>[<span>str</span>],
relevant_ids: <span>set</span>[<span>str</span>],
k: <span>int</span>,
</span>) -> <span>float</span>:
selected = _unique_prefix(retrieved_ids, k)
<span>if</span> <span>not</span> relevant_ids:
<span>return</span> <span>0.0</span>
<span># 计算实际DCG</span>
dcg = <span>sum</span>(
<span>1.0</span> / log2(rank + <span>1</span>)
<span>for</span> rank, chunk_id <span>in</span> <span>enumerate</span>(selected, start=<span>1</span>)
<span>if</span> chunk_id <span>in</span> relevant_ids
)
<span># 理想最大DCG</span>
ideal_length = <span>min</span>(<span>len</span>(relevant_ids), k)
ideal_dcg = <span>sum</span>(
<span>1.0</span> / log2(rank + <span>1</span>)
<span>for</span> rank <span>in</span> <span>range</span>(<span>1</span>, ideal_length + <span>1</span>)
)
<span>return</span> dcg / ideal_dcg <span>if</span> ideal_dcg <span>else</span> <span>0.0</span>
5.5 全局指标平均
<span>def</span> <span>average_metrics</span>(<span>metrics: <span>Sequence</span>[RetrievalMetrics]</span>) -> RetrievalMetrics:
<span>if</span> <span>not</span> metrics:
<span>return</span> RetrievalMetrics(<span>0.0</span>, <span>0.0</span>, <span>0.0</span>, <span>0.0</span>)
count = <span>len</span>(metrics)
<span>return</span> RetrievalMetrics(
precision_at_k=<span>sum</span>(item.precision_at_k <span>for</span> item <span>in</span> metrics) / count,
recall_at_k=<span>sum</span>(item.recall_at_k <span>for</span> item <span>in</span> metrics) / count,
reciprocal_rank=<span>sum</span>(item.reciprocal_rank <span>for</span> item <span>in</span> metrics) / count,
ndcg_at_k=<span>sum</span>(item.ndcg_at_k <span>for</span> item <span>in</span> metrics) / count,
)
六、数据表与接口设计
6.1 三张核心表
retrieval_evaluation_datasets:评测数据集本体retrieval_evaluation_runs:单次评测任务汇总指标retrieval_evaluation_candidates:逐题候选明细(用于问题定位)
6.2 核心接口
| 方法 | 接口 | 作用 |
|---|---|---|
| POST | `/api/v1/knowledge-bases/{id}/retrieval/evaluate` | 执行一次评测 |
| GET | `/api/v1/knowledge-bases/{id}/retrieval/runs` | 查询历史评测列表 |
| GET | `/api/v1/retrieval/evaluations/{id}` | 查看评测详情与候选明细 |
6.3 详情响应示例
<span>{</span>
<span>"case_id"</span><span>:</span> <span>"monorepo-advantages-001"</span><span>,</span>
<span>"retrieved_chunk_ids"</span><span>:</span> <span>[</span>
<span>"6f6bd250-b484-4506-842d-0e53073320b8"</span>
<span>]</span><span>,</span>
<span>"relevant_chunk_ids"</span><span>:</span> <span>[</span>
<span>"30465673-88ea-41f0-a5a0-da496592f3df"</span>
<span>]</span><span>,</span>
<span>"metrics"</span><span>:</span> <span>{</span>
<span>"precision_at_k"</span><span>:</span> <span>0.0</span><span>,</span>
<span>"recall_at_k"</span><span>:</span> <span>0.0</span><span>,</span>
<span>"reciprocal_rank"</span><span>:</span> <span>0.0</span><span>,</span>
<span>"ndcg_at_k"</span><span>:</span> <span>0.0</span>
<span>}</span><span>,</span>
<span>"candidates"</span><span>:</span> <span>[</span>
<span>{</span>
<span>"chunk_id"</span><span>:</span> <span>"6f6bd250-b484-4506-842d-0e53073320b8"</span><span>,</span>
<span>"rank"</span><span>:</span> <span>1</span><span>,</span>
<span>"vector_score"</span><span>:</span> <span>0.8123</span><span>,</span>
<span>"keyword_score"</span><span>:</span> <span>0.5000</span><span>,</span>
<span>"final_score"</span><span>:</span> <span>0.7186</span><span>,</span>
<span>"is_relevant"</span><span>:</span> <span><span>false</span></span><span>,</span>
<span>"is_selected"</span><span>:</span> <span><span>true</span></span><span>,</span>
<span>"strategy_version"</span><span>:</span> <span>"hybrid-v1"</span>
<span>}</span>
<span>]</span>
<span>}</span>
七、两种评测使用方式
7.1 管理端页面评测
访问路径:/admin/retrieval-evaluations
功能:上传数据集、一键评测、查看大盘指标、逐题分析分数明细,支持历史版本对比。
效果展示:
7.2 命令行离线评测
脚本路径:scripts/evaluate_retrieval.py
<span>cd</span> cisu-knowledge-api
uv run python -m scripts.evaluate_retrieval \
--knowledge-base-id xxx \
--dataset ./eval.json \
--output ./report.json
八、测评指标全0 快速排查手册
指标归零99%不是代码bug,优先按下面顺序排查:
- 切片ID填错:必须是
document_chunks.id - 知识库不匹配:标准切片必须属于当前评测知识库
- 文档状态非法:必须是已发布、未删除、就绪状态
- PowerShell换行符问题:反引号后不能有空格
- 只命中文档、没命中切片:测评是切片级精准匹配
九、单元测试覆盖
<span>def</span> <span>test_retrieval_evaluation_metrics</span>() -> <span>None</span>:
<span># c1、c2为标准答案,c1排在第2位</span>
metrics = evaluate_retrieval(
[<span>"c3"</span>, <span>"c1"</span>, <span>"c2"</span>],
{<span>"c1"</span>, <span>"c2"</span>},
<span>3</span>,
)
<span>assert</span> metrics.precision_at_k == pytest.approx(<span>2</span> / <span>3</span>)
<span>assert</span> metrics.recall_at_k == <span>1.0</span>
<span>assert</span> metrics.reciprocal_rank == pytest.approx(<span>1</span> / <span>2</span>)
十、当前局限与后续迭代方向
- 预留
rerank_score,待接入重排模型测评 - 中文分词基于 Postgres simple,可优化分词能力
- 命令行报告可补充候选明细
- 同步任务需改造为异步队列,支持大数据集评测
- 目前只测检索,未来可拓展回答质量、引用准确性、拒答测评
十一、总结
本次 RAG 测评改造的核心价值:把主观感受,变成可量化、可复现、可对比的工程能力。
核心要点:
- 基于切片ID构建标准数据集,实验可复现
- 四大指标全面量化检索效果
- 全量候选快照落库,问题可精准溯源
后续迭代 Embedding、调权重、接入 Rerank、优化检索策略,都可以用这套体系做客观对比,极大提升 RAG 迭代效率与稳定性。
把RAG调参从'肉眼看结果'升级为可量化闭环,踩坑排查手册尤其实用。适合正在搭建或迭代知识库问答、需要客观对比Embedding与检索策略的团队参考。