RAG 检索效果量化测评落地:完整流程、指标实现与踩坑总结

文章来源声明: 原文作者:码农悟道; 来源站点:掘金; 原文链接:https://juejin.cn/post/7689219046839484454; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

把RAG调参从'肉眼看结果'升级为可量化闭环,踩坑排查手册尤其实用。适合正在搭建或迭代知识库问答、需要客观对比Embedding与检索策略的团队参考。

前言 --

在知识库问答(RAG)系统迭代过程中,最大的痛点往往不是检索功能不可用,而是无法量化检索效果好坏。

调切片大小、改向量/关键词权重、换 Embedding 模型后,我们基本只能“肉眼看结果”,非常主观,无法回答这些关键问题:

  • 检索结果到底有没有真正命中标准答案?
  • 标准切片排第几,是否满足业务要求?
  • 向量检索、关键词检索分别贡献多少得分?
  • 本次迭代到底是优化了还是变差了?
  • 测评指标全部为 0,是代码问题、标注问题、还是数据集不匹配?

为了解决上述问题,我在辞溯知识库项目二期落地了一套完整可闭环的 RAG 检索测评系统,实现:数据集准备 → 自动检索 → 指标计算 → 记录落库 → 可视化分析。

本文结合真实项目源码,完整拆解整套测评方案的设计、实现、踩坑经验与后续优化方向。

一、整体测评流程

整套评测链路完全标准化、可重复运行,流程如下:

准备评测数据集
        |
        v
选择知识库和检索策略
        |
        v
执行向量检索 + 关键词检索
        |
        v
候选集合合并、去重、排序
        |
        +--> 取最终 Top-K(用于指标计算)
        |
        +--> 保存完整候选明细(用于问题溯源)
        |
        v
对比召回切片ID & 人工标注切片ID
        |
        v
计算 Precision@K / Recall@K / MRR / nDCG@K
        |
        v
保存评测记录 & 管理端可视化展示

系统提供两种使用模式,适配不同研发场景:

  1. 命令行离线评测:本地快速跑批,输出 JSON 报告,适合调参对比。
  2. 管理端页面评测:持久化每次评测记录,支持大盘指标 + 单用例细节排查。

二、评测数据集标准化设计

评测是否靠谱,完全取决于数据集是否标准。我采用轻量 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 三张核心表

  1. retrieval_evaluation_datasets:评测数据集本体
  2. retrieval_evaluation_runs:单次评测任务汇总指标
  3. 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

功能:上传数据集、一键评测、查看大盘指标、逐题分析分数明细,支持历史版本对比。

效果展示:

image.png

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,优先按下面顺序排查:

  1. 切片ID填错:必须是 document_chunks.id
  2. 知识库不匹配:标准切片必须属于当前评测知识库
  3. 文档状态非法:必须是已发布、未删除、就绪状态
  4. PowerShell换行符问题:反引号后不能有空格
  5. 只命中文档、没命中切片:测评是切片级精准匹配

九、单元测试覆盖

<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 测评改造的核心价值:把主观感受,变成可量化、可复现、可对比的工程能力。

核心要点:

  1. 基于切片ID构建标准数据集,实验可复现
  2. 四大指标全面量化检索效果
  3. 全量候选快照落库,问题可精准溯源

后续迭代 Embedding、调权重、接入 Rerank、优化检索策略,都可以用这套体系做客观对比,极大提升 RAG 迭代效率与稳定性。