0Pricing
AI Engineering Academy · 课时

衡量重排的影响

运行前后基准测试,对比单阶段检索与采用重排的两阶段检索,并衡量 NDCG、MRR 以及端到端答案质量。

衡量重排的影响 是 CoddyKit 上的免费 AI Engineering Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 AI Engineering Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 AI Engineering Academy 课程共包含 4 节课。

为什么要衡量重新排序的影响

重新排序会增加您的检索流程的延迟和成本。如果不进行衡量,您就无法判断增加的复杂性是否值得。基准测试可以量化检索质量和端到端答案质量的提升,帮助您做出明智的决策。它还可以揭示哪些查询类型受益最大,从而让您能够有选择地应用重新排序,而不是对每个请求都使用。

构建黄金测试集

可靠的基准测试需要一个黄金测试集:其中包含一组查询,以及与每个查询配对的、已知相关的文档 ID。您可以从应用的日志中抽取真实用户查询,然后手动或通过专家标注确定相关文档,并将其整理为结构化格式。对于大多数 RAG 评估场景,包含 50 到 200 个查询的测试集就足够了。

golden_test_set = [
    {
        'query': 'How does pgvector HNSW indexing improve search speed?',
        'relevant_doc_ids': ['doc_042', 'doc_107'],
    },
    {
        'query': 'What is the difference between BM25 and dense retrieval?',
        'relevant_doc_ids': ['doc_015'],
    },
    {
        'query': 'How to implement reciprocal rank fusion in Python?',
        'relevant_doc_ids': ['doc_093', 'doc_094'],
    },
    # ... 47 more entries
]

print(f'Test set size: {len(golden_test_set)} queries')
print(f'Avg relevant docs per query: {sum(len(e["relevant_doc_ids"]) for e in golden_test_set) / len(golden_test_set):.1f}')

检索指标:NDCG、MRR、命中率

请使用三个互补的指标来评估检索质量。K 值命中率用于衡量前 K 个结果中是否至少出现一个相关文档。MRR(平均倒数排名)用于衡量第一个相关文档排名的倒数的平均值。K 值 NDCG(归一化折损累积增益)用于衡量排序质量,其中排名靠前的位置获得的权重高于靠后的位置。

def compute_retrieval_metrics(results: list[str], relevant_ids: set, k: int = 5):
    results_at_k = results[:k]
    relevant_found = [r for r in results_at_k if r in relevant_ids]

    # Hit rate
    hit = 1 if relevant_found else 0

    # MRR
    rr = 0
    for i, doc_id in enumerate(results_at_k, start=1):
        if doc_id in relevant_ids:
            rr = 1.0 / i
            break

    # NDCG (binary relevance)
    import math
    dcg = sum(
        1.0 / math.log2(i + 1)
        for i, doc_id in enumerate(results_at_k, start=1)
        if doc_id in relevant_ids
    )
    ideal = sum(1.0 / math.log2(i + 1) for i in range(1, min(len(relevant_ids), k) + 1))
    ndcg = dcg / ideal if ideal > 0 else 0

    return {'hit': hit, 'rr': rr, 'ndcg': ndcg}

基线:单阶段密集检索

在衡量重新排序的影响之前,请使用单阶段密集检索建立一个基线。让测试集中的每个查询都通过双编码器检索器,收集排序后的文档 ID,并计算 NDCG、MRR 和命中率的平均值。这个基线可以告诉您当前的起点以及可提升的幅度——如果基线已经达到 0.95 NDCG@5,重新排序的提升空间就很小了。

def evaluate_pipeline(retriever_fn, test_set: list[dict], k: int = 5) -> dict:
    all_metrics = []

    for entry in test_set:
        query = entry['query']
        relevant = set(entry['relevant_doc_ids'])

        results = retriever_fn(query, top_k=k)
        result_ids = [r['id'] for r in results]

        metrics = compute_retrieval_metrics(result_ids, relevant, k)
        all_metrics.append(metrics)

    n = len(all_metrics)
    return {
        f'hit_rate@{k}': sum(m['hit'] for m in all_metrics) / n,
        f'mrr@{k}': sum(m['rr'] for m in all_metrics) / n,
        f'ndcg@{k}': sum(m['ndcg'] for m in all_metrics) / n,
    }

运行前后基准测试

对单阶段检索器,以及加入重新排序的两阶段检索器,运行相同的评估函数。将结果并排输出,让提升(或没有提升)一目了然。除了质量指标,还要跟踪每个查询的延迟——根据应用的 SLA 要求,5% 的检索质量提升可能不值得付出 400 毫秒的延迟增加。

import time

def evaluate_with_latency(retriever_fn, test_set, k=5):
    metrics_list = []
    latencies = []

    for entry in test_set:
        t0 = time.perf_counter()
        results = retriever_fn(entry['query'], top_k=k)
        latencies.append((time.perf_counter() - t0) * 1000)

        result_ids = [r['id'] for r in results]
        metrics_list.append(compute_retrieval_metrics(
            result_ids, set(entry['relevant_doc_ids']), k
        ))

    n = len(metrics_list)
    return {
        f'hit_rate@{k}': sum(m['hit'] for m in metrics_list) / n,
        f'ndcg@{k}': sum(m['ndcg'] for m in metrics_list) / n,
        'p50_latency_ms': sorted(latencies)[n // 2],
        'p99_latency_ms': sorted(latencies)[int(n * 0.99)],
    }

baseline = evaluate_with_latency(dense_retrieval_fn, golden_test_set)
two_stage = evaluate_with_latency(two_stage_fn, golden_test_set)
print('Baseline:', baseline)
print('Two-stage:', two_stage)

解读 NDCG 提升

在密集检索中加入交叉编码器重新排序后,NDCG@5 通常会绝对提升 0.05 到 0.15,也就是大约提升 5 到 15 个百分点。以下情况下,提升幅度通常更大:(1) 查询类型多样,且包含许多释义表达;(2) 语料库中有许多近似相关的分块;(3) 第一阶段检索器较弱。如果 NDCG 提升低于 0.02,所带来的收益可能不足以抵消增加的复杂性。

# Interpreting benchmark results

example_results = {
    'baseline': {'hit_rate@5': 0.78, 'ndcg@5': 0.64, 'p99_latency_ms': 35},
    'two_stage': {'hit_rate@5': 0.89, 'ndcg@5': 0.77, 'p99_latency_ms': 287},
}

delta_ndcg = example_results['two_stage']['ndcg@5'] - example_results['baseline']['ndcg@5']
delta_latency = example_results['two_stage']['p99_latency_ms'] - example_results['baseline']['p99_latency_ms']

print(f'NDCG improvement: +{delta_ndcg:.2f} (+{delta_ndcg/example_results["baseline"]["ndcg@5"]*100:.0f}%)')
print(f'Latency increase: +{delta_latency}ms')
# NDCG improvement: +0.13 (+20%) — clearly worth the 252ms latency cost

端到端答案质量衡量

检索指标衡量的是是否检索到了正确的文档,但最终的衡量标准是端到端答案质量。请使用 LLM 评判器评估:根据重新排序后的上下文生成的答案,是否比根据单阶段上下文生成的答案更加正确、更加忠实。请分别从正确性、忠实性和相关性三个方面按 1 到 5 分评分,然后计算测试集上的平均分。

from openai import OpenAI

client = OpenAI()

JUDGE_PROMPT = '''
Rate the following answer on a scale of 1-5 for correctness and faithfulness to the context.

Question: {question}
Context: {context}
Answer: {answer}
Ground truth: {ground_truth}

Return a JSON with fields: {"correctness": int, "faithfulness": int, "explanation": str}
'''

def judge_answer(question, context, answer, ground_truth):
    prompt = JUDGE_PROMPT.format(
        question=question, context=context,
        answer=answer, ground_truth=ground_truth,
    )
    response = client.chat.completions.create(
        model='gpt-4o',
        messages=[{'role': 'user', 'content': prompt}],
        response_format={'type': 'json_object'},
    )
    import json
    return json.loads(response.choices[0].message.content)

按查询类型进行分层分析

平均指标会掩盖不同查询类型之间的重要差异。请将测试集划分为以下类别,并分别计算每组的指标:事实查询(谁、什么、何时)、操作步骤查询(如何做)、概念查询(为什么、解释)以及技术查询(错误代码、API 名称)。在概念和操作步骤查询中,语义理解比关键词匹配更重要,因此重新排序通常对这两类查询帮助最大。

def stratified_eval(retriever_fn, test_set, k=5):
    groups = {'factual': [], 'procedural': [], 'conceptual': [], 'technical': []}

    for entry in test_set:
        q = entry['query'].lower()
        if any(w in q for w in ['how to', 'how do', 'steps to']):
            groups['procedural'].append(entry)
        elif any(w in q for w in ['why', 'explain', 'what is the reason']):
            groups['conceptual'].append(entry)
        elif any(c.isupper() for c in q.split()) or 'error' in q:
            groups['technical'].append(entry)
        else:
            groups['factual'].append(entry)

    for group_name, group_entries in groups.items():
        if group_entries:
            metrics = evaluate_pipeline(retriever_fn, group_entries, k)
            print(f'{group_name} ({len(group_entries)} queries): ndcg@{k}={metrics[f"ndcg@{k}"]:.3f}')

通过 CI 集成进行回归测试

将检索基准测试作为 CI 中的回归测试运行。为 NDCG@5、MRR 和命中率设置可接受的最低阈值。任何导致指标低于阈值的流程更改都会使 CI 构建失败,从而阻止检索质量回退的问题发布到生产环境中。在更改分块大小、嵌入模型或重新排序模型后,这一点尤其重要。

# pytest integration for retrieval quality gates
import pytest

MIN_NDCG_5 = 0.70
MIN_HIT_RATE_5 = 0.85

def test_retrieval_quality_meets_threshold():
    metrics = evaluate_pipeline(production_retriever_fn, golden_test_set, k=5)
    assert metrics['ndcg@5'] >= MIN_NDCG_5, (
        f'NDCG@5 {metrics["ndcg@5"]:.3f} below threshold {MIN_NDCG_5}'
    )
    assert metrics['hit_rate@5'] >= MIN_HIT_RATE_5, (
        f'Hit rate {metrics["hit_rate@5"]:.3f} below threshold {MIN_HIT_RATE_5}'
    )

# Run with: pytest tests/test_retrieval.py -v

可视化检索指标

在多个实验之间比较时,原始数字很难解读。请创建一个简单的对比表或柱状图,将基线、仅混合检索和混合检索加重新排序这几种流程的 NDCG、MRR、命中率和延迟并排列出。随着您不断改进,持续跟踪这些指标,可以建立检索改进历史,为今后的优化决策提供指导。

def print_comparison_table(results: dict[str, dict]):
    headers = ['Pipeline', 'NDCG@5', 'MRR@5', 'Hit@5', 'P99 ms']
    print('|'.join(f'{h:20}' for h in headers))
    print('-' * (len(headers) * 21))
    for pipeline_name, metrics in results.items():
        row = [
            pipeline_name,
            f'{metrics.get("ndcg@5", 0):.3f}',
            f'{metrics.get("mrr@5", 0):.3f}',
            f'{metrics.get("hit_rate@5", 0):.3f}',
            f'{metrics.get("p99_latency_ms", 0):.0f}',
        ]
        print('|'.join(f'{v:20}' for v in row))

results = {
    'Dense only': {'ndcg@5': 0.64, 'mrr@5': 0.68, 'hit_rate@5': 0.78, 'p99_latency_ms': 35},
    'Hybrid RRF': {'ndcg@5': 0.71, 'mrr@5': 0.74, 'hit_rate@5': 0.84, 'p99_latency_ms': 55},
    'Hybrid + Rerank': {'ndcg@5': 0.77, 'mrr@5': 0.81, 'hit_rate@5': 0.89, 'p99_latency_ms': 287},
}
print_comparison_table(results)

根据基准测试结果采取行动

完成基准测试后,请利用结果做出具体决策。如果重新排序带来的 NDCG 提升低于 0.03,请跳过重新排序,专注于改进第一阶段。如果命中率较低,说明第一阶段漏掉了相关文档——请增加候选集大小,或切换到混合检索。如果端到端答案质量显著提升,而检索指标的提升幅度不大,那么重新排序器可能找出了 LLM 能够有效利用的高度相关句子,即使它们的排名位置没有变化。

快速检查

测试您对本课中检索和重新排序影响衡量方法的理解。

课程回顾

本课中您学习了:在衡量检索质量之前,构建包含已知相关文档的黄金测试集至关重要;NDCG、MRR 和命中率是三个核心检索指标,结合起来可以全面衡量排序质量;使用 LLM 评判器衡量端到端答案质量,是评估流程改进效果的最终标准。请始终将检索基准测试作为 CI 中的回归测试运行。接下来,我们将探索 LLM 流式传输,以便在令牌生成时将其显示出来。

常见问题解答

「衡量重排的影响」课时是免费的吗?

是的 — 「衡量重排的影响」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 AI Engineering Academy 课程的其余内容,请升级到 CoddyKit PRO。 AI Engineering Academy 课程共包含 4 节课。

「衡量重排的影响」这节课中我会学到什么?

运行前后基准测试,对比单阶段检索与采用重排的两阶段检索,并衡量 NDCG、MRR 以及端到端答案质量。 你通过在浏览器中直接运行的动手代码来练习 AI Engineering Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 AI Engineering Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 AI Engineering Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「衡量重排的影响」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 AI Engineering Academy 课中编写并运行代码吗?

能。每节 AI Engineering Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 两阶段检索为何有效
  2. 使用 Cohere 和 BGE 进行交叉编码器重排
  3. 上下文压缩与相关性过滤
  4. 衡量重排的影响
← 返回 AI Engineering Academy