0Pricing
AI Engineering Academy · Урок

Измерение влияния переранжирования

Проведите сравнительный тест до и после, сопоставив одноэтапный поиск с двухэтапным поиском и переранжированием, и измерьте NDCG, MRR и качество ответа от начала до конца.

«Измерение влияния переранжирования» — бесплатный урок AI Engineering Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения AI Engineering Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс AI Engineering Academy содержит 4 уроков всего.

Зачем измерять влияние повторного ранжирования

Повторное ранжирование увеличивает задержку и стоимость Вашей системы. Без измерений невозможно понять, оправдывает ли дополнительная сложность полученный результат. Сравнительное тестирование позволяет количественно оценить улучшение качества поиска и ответов по всему процессу, чтобы Вы могли принять обоснованное решение. Оно также показывает, какие типы запросов получают наибольшую пользу, позволяя применять повторное ранжирование выборочно, а не при каждом запросе.

Создание эталонного набора тестов

Для надёжного сравнительного тестирования нужен эталонный набор тестов: коллекция запросов, сопоставленных с идентификаторами документов, о которых заранее известно, что они релевантны. Создайте его, отобрав реальные запросы пользователей из журналов приложения, вручную или с помощью экспертов определив релевантные документы и организовав данные в структурированном формате. Для большинства задач оценки 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 (средний обратный ранг) показывает среднее обратное значение ранга первого релевантного документа. NDCG при K (нормализованный дисконтированный совокупный выигрыш) оценивает качество ранжирования, при этом более высоким позициям придаётся больший вес, чем более низким.

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}

Базовый вариант: одноэтапный плотный поиск

Прежде чем измерять влияние повторного ранжирования, задайте базовый вариант с помощью одноэтапного плотного поиска. Пропустите каждый запрос из набора тестов через поисковый модуль с двунаправленным кодировщиком, соберите идентификаторы документов в порядке ранжирования и вычислите средние значения 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,
    }

Проведение сравнительного тестирования до и после изменений

Запустите одну и ту же функцию оценки для одноэтапного поискового модуля и для двухэтапного поискового модуля с повторным ранжированием. Выведите результаты рядом, чтобы улучшение (или его отсутствие) сразу бросалось в глаза. Отслеживайте задержку для каждого запроса вместе с метриками качества: прирост качества поиска на 5 процентов может не оправдывать увеличение задержки на 400 мс — это зависит от требований Вашего приложения к SLA.

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, чтобы отображать токены по мере их генерации.

Часто задаваемые вопросы

Урок «Измерение влияния переранжирования» бесплатный?

Да — полный текст урока «Измерение влияния переранжирования» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс AI Engineering Academy, подпишись на CoddyKit PRO. Курс AI Engineering Academy содержит 4 уроков всего.

Чему я научусь в уроке «Измерение влияния переранжирования»?

Проведите сравнительный тест до и после, сопоставив одноэтапный поиск с двухэтапным поиском и переранжированием, и измерьте NDCG, MRR и качество ответа от начала до конца. Ты практикуешь AI Engineering Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать AI Engineering Academy?

Предыдущий опыт не требуется. AI Engineering Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Измерение влияния переранжирования»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке AI Engineering Academy?

Да. Каждый урок AI Engineering Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Почему двухэтапный поиск работает
  2. Переранжирование с помощью кросс-энкодера: Cohere и BGE
  3. Контекстное сжатие и фильтрация релевантности
  4. Измерение влияния переранжирования
← Назад к AI Engineering Academy