Метрики оценки RAG: retrieval, реранкинг, генерация

Автоматический перевод Эта статья была автоматически переведена с оригинальной английской версии.

Обновление статьи

Изначально опубликовано 10 мая 2026 года. Проверено и обновлено 6 сентября 2026 года. В обновлении добавлены более новые retrieval-бенчмарки и кандидаты для реранкинга, пересмотрены рекомендации по инструментам оценки и исправлены параметры API в примере с джаджем.

Система RAG со сломанными фильтрами релевантности может работать месяцами, не вызывая операционного алерта. Она по-прежнему возвращает ответы и укладывается в целевую латентность, но ответы опираются на неполные свидетельства. Recall@k относительно исходного полного gold set показывает эту потерю. Дашборды латентности и доступности — нет.

Для инженеров, которые эксплуатируют или оценивают многоэтапные RAG-системы, этот справочник сопоставляет сбои в парсинге документов, фильтрации, retrieval, реранкинге и генерации с метрикой, которая каждый такой сбой выявляет. Здесь также показано, какие проверки выполняются до релиза, а какие мониторят live-трафик.

Хотите сразу перейти к запуску кода?

Репозиторий slavadubrov/rag-evals-demo позволяет применить выбранные метрики к SciFact. Команда make eval запускает набор проверок, а make benchmark сравнивает конфигурации чанкинга, эмбеддингов и LLM. Ноутбуки 00–09 охватывают retrieval, фильтрацию, генерацию и примеры систем; они не реализуют каждую проверку из этого справочника. В демо используется встроенный Qdrant, поэтому Docker не требуется.

Сопутствующий репозиторий — это учебный харнесс. В ревизии, проверенной 6 сентября 2026 года, по-прежнему нужно исправить учёт отсутствующих запросов, парсинг результатов джаджа и gold data для авторизации. В его pairwise-джаде также отсутствует переданный контекст, необходимый для оценки поддержки утверждений. Исправленные контракты и inline-проверки ниже не патчат этот репозиторий; используйте его ноутбуки для изучения workflow и проверьте эти случаи, прежде чем принимать его оценки в качестве release criteria.

Коротко

  • Полезный стек оценки охватывает ingestion, retrieval, граундинг генерации, соответствие онтологии и системные сигналы. RAGAS, TruLens, DeepEval, Arize Phoenix и TREC 2024 RAG Track предлагают библиотеки или публичные протоколы оценки. Но они не выбирают метрики за вас.
  • Для RAG, основанного на метаданных и онтологии, неправильный тег или хрупкий hard predicate могут обнулить recall. Стандартный Recall@k обнаруживает потерю, если сохраняет исходный полный gold set релевантных документов. Метрика false-exclusion rate для фильтра выявляет причину. Faithfulness всё ещё может оценивать утверждения по неполному контексту, но не диагностирует причину сбоя фильтра или retrieval. Пустой отказ может не содержать утверждений и дать NaN — это зависит от реализации.

Таблица решений для оценки RAG

Используйте эту таблицу как отправную точку перед выбором фреймворка. Подходящая метрика зависит от failure mode, который вы хотите поймать, а не от названия инструмента.

ВопросСемейство метрикКогда использоватьНа что обратить внимание
Сохранил ли парсинг исходный документ?Полнота извлечения, покрытие таблиц/рисунковВ корпус попадают PDF, слайды, сканы и HTML-страницыТекст может выглядеть чистым, но при этом терять подписи, сноски или структуру таблиц
Нашёл ли retrieval нужные свидетельства?Recall@k, nDCG@k, MRR, precision/recall контекстаМожно разметить релевантные чанки или документыЖёсткий фильтр метаданных может удалить правильный документ до начала ранжирования
Улучшил ли реранкинг shortlist?Uplift реранкера, Precision@1, delta nDCGПосле retrieval работают cross-encoder или LLM-rankerИзмеряйте латентность и стоимость вместе с приростом качества
Использовал ли ответ свидетельства?Faithfulness, groundedness, поддержка цитатОтвет цитирует документы или извлекает факты из контекстаFaithfulness не диагностирует плохой парсинг или плохой retrieval
Стабильна ли система в продакшене?Drift, регенерация, fallback, p95 latency, стоимость ответаТрафик меняется после запускаProduction-телеметрии нужна выборочная проверка людьми для калибровки

Для более короткого сравнения инструментов см. Лучшие инструменты оценки RAG: Ragas, DeepEval и TruLens.

Часть 1: Определите критерии успеха до архитектуры

Составьте eval set до архитектурной диаграммы. Он задаст измеримую цель для каждого последующего выбора компонента.

Нельзя выбрать между BM25 и dense retrieval, recursive и semantic chunking или Cohere Rerank и BGE, пока не определено, что именно вы оптимизируете. «Лучшие ответы» — не метрика. Иллюстративное требование для релиза: «faithfulness ≥ 0.85 на golden set из 200 запросов, покрывающем три главных интента, при p95 latency < 1,5 с и filter false-exclusion rate < 2%». Числа здесь условны; важно, чтобы для качества, покрытия, латентности и фильтрации были заданы явные пороги.

Определите харнесс до написания кода retrieval. Первый харнесс окажется неправильным, и вы его пересмотрите. Пересмотреть метрику гораздо дешевле, чем пересматривать уже выпущенную систему.

Три слоя пайплайна и два режима запуска

В production-оценке есть три слоя пайплайна. Оценка ingestion проверяет, сохраняют ли корпус и индекс исходные данные. Оценка во время запроса проверяет, нашли ли rewriting, фильтрация, retrieval, реранкинг и сборка контекста нужные свидетельства. Оценка ответа и production проверяет, использовал ли ответ эти свидетельства и сохраняется ли качество на live-трафике. Если свести слои к одной оценке, баг нормализации может затеряться внутри приемлемого score ответа.

Три места, где RAG-система может потерять свидетельства: корпус и индекс, путь retrieval, а также ответ и live-трафикТри места, где RAG-система может потерять свидетельства: корпус и индекс, путь retrieval, а также ответ и live-трафик

Эти слои описывают, где происходит сбой. Offline и online описывают, когда и относительно каких данных выполняется проверка. Offline-оценка использует фиксированный датасет с известным ground truth; она воспроизводима и нужна для выбора компонентов, A/B-сравнений и CI-проверок, способных блокировать изменение. Online-оценка оценивает выбранку live-трафика и фиксирует регенерации, dwell time, явный feedback и реальный query drift. Она более шумная и сложнее для инструментирования.

Используйте оба режима там, где они полезны: фиксированные корпуса и наборы запросов делают регрессии воспроизводимыми, а выбранные live-трейсы выявляют проблемы свежести и drift.

Оценка компонентов и end-to-end

Есть две распространённые ошибки. Оценка только end-to-end сообщает, что система сломана, но не показывает где. Оценка только компонентов может показать, что каждый компонент проходит проверки, хотя вся система всё ещё ломается. Решение — несколько ключевых end-to-end-метрик для go/no-go-решений плюс компонентные метрики для диагностики. Метрики retrieval ловят регрессии retriever. Метрики генерации ловят регрессии generator. End-to-end-корректность ответа выявляет интеграционные сбои.

Референсные фреймворки: субъективный обзор

ФреймворкЛучше всего подходит дляГде возникают проблемы
RAGASЕдиного словаря для faithfulness, релевантности ответа и precision/recall контекста (метрики)Стоимость LLM-джаджа; непрозрачные компоненты score при отладке; изменения версий
ARESTask-specific classifier judge, если обучение и разметка оправдывают стоимость (статья); заявленная precision привязана к бенчмаркуБолее тяжёлый setup; модели действительно нужно обучить
TruLensFeedback-функции, связанные с трейcами, и интеграция с OpenTelemetry (проект)Меньше готовых RAG-метрик, чем в RAGAS
DeepEvalИнтеграция с test runner и пользовательские метрики (проект)Активное использование LLM-джаджа приводит к скачкам стоимости
Arize PhoenixТрейсинг, эксперименты с датасетами и готовые или пользовательские evaluators для RAG/agent (документация по оценке)Доменные rubric и пороги джаджа всё равно требуют локальной калибровки
TREC 2024 RAG TrackПубличный бенчмарк для оценки nuggets (AutoNuggetizer), поддержки и fluency на MS MARCO Segment v2.1Это не runtime-инструмент, а бенчмарк для калибровки

Мой стек по умолчанию: RAGAS для словаря метрик, DeepEval для CI-проверок, Phoenix для production-трейсинга и собственный код для метрик, специфичных для онтологии. Выбирайте фреймворк, в котором удобно добавлять пользовательские метрики.

При выборе бенчмарка сначала сопоставьте задачу, а уже потом смотрите leaderboard. BEIR, MTEB и MIRACL остаются полезными базовыми ориентирами для retrieval. Добавьте тесты для возможностей, которых они не подтверждают:

  • Актуальный end-to-end RAG: TREC 2026 RAG Track использует narrative queries и ClimbMix-400b вместо MS MARCO v2.1 и ссылается на evaluation toolkit RAGDoll. На 6 сентября страница track не объявила дату возврата результатов и judgments. Выпущенные topics доступны для экспериментов; это ещё не завершённый judged leaderboard 2026 года. Протоколы 2024 и 2025 годов сохраняйте привязанными к их собственным корпусам и judgments.
  • Технические вопросы по развивающемуся коду: FreshStack объединяет вопросы со Stack Overflow, заданные людьми, корпуса репозиториев и nugget judgments. Выпущенный snapshot и механизм сборки новых корпусов — разные вещи; зафиксируйте revision репозитория и дату вопроса.
  • Изображения, содержащие часть вопроса или свидетельств: MM-BRIGHT разделяет retrieval text-to-text, multimodal-to-text, multimodal-to-image и multimodal-to-multimodal. Оценивайте нужную задачу отдельно. Один только текст OCR может потерять информацию, которую дают график или скриншот.

Эти бенчмарки расширяют покрытие, но не заменяют versioned query set приложения с учётом eligibility.


Часть 2: Сопоставьте точки оценки

Пайплайн RAG, сгруппированный по ingestion, этапу запроса и пути ответа, с диагностическими метриками рядом с каждым этапомПайплайн RAG, сгруппированный по ingestion, этапу запроса и пути ответа, с диагностическими метриками рядом с каждым этапом

Используйте диаграмму, чтобы сопоставить симптом с первой диагностической метрикой. Потери на ранних этапах ограничивают downstream quality: плохой парсинг ограничивает retrieval, а плохой retrieval — реранкинг и генерацию. Faithfulness измеряет ответ, но не upstream-причину.


Часть 3: Оценка ingestion

Многие production-сбои RAG начинаются на ingestion. Система работает на чистых тестовых документах, а затем ломается на реальных PDF, сканах, таблицах и неаккуратных страницах корпуса.

Получение и парсинг документов

Что измерять:

  • Sanity check длины извлечения: extracted_chars / expected_chars по классу документа выявляет подозрительные изменения длины, но дублированный или неправильный текст всё ещё может получить 1.0. Сравнивайте выровненный текст с вручную очищенным reference-текстом, чтобы находить пропуски и замены, а затем отдельно проверяйте сноски, подписи, содержимое таблиц и порядок чтения.

  • Точность OCR: CER (Character Error Rate) и WER (Word Error Rate) — стандартные метрики для речи и OCR:

    CER=S+D+IN,WER=Sw+Dw+IwNw\text{CER} = \frac{S + D + I}{N}, \qquad \text{WER} = \frac{S_w + D_w + I_w}{N_w}

    где SS, DD, II — замены, удаления и вставки на уровне символов, а NN — число символов в reference-тексте (для версии со словами используется subscript ww). Не применяйте один порог CER ко всему корпусу. Калибруйте его по классу документа и downstream-потерям в ответах. У печатного текста, рукописей и мультиязычных материалов разные профили ошибок. Считайте с помощью jiwer (jiwer.cer(refs, hyps), jiwer.wer(refs, hyps)) или HuggingFace evaluate. Для evaluation corpora доступны публичные бенчмарки FUNSD и SROIE.

    from jiwer import cer, wer
    
    refs = ["Mars has two moons, Phobos and Deimos."]
    hyps = ["Mars has two m00ns, Phobos and Deirnos."]
    
    print(f"CER = {cer(refs, hyps):.3f}")  # CER = 0.105
    print(f"WER = {wer(refs, hyps):.3f}")  # WER = 0.286
  • Fidelity извлечения таблиц: TEDS (Tree-Edit-Distance-based Similarity) измеряет, насколько предсказанное HTML-дерево таблицы близко к reference, с нормализацией по размеру большего дерева. Из Zhong et al., 2020 (PubTabNet):

    TEDS(Ta,Tb)=1EditDist(Ta,Tb)max(Ta,Tb)\text{TEDS}(T_a, T_b) = 1 - \frac{\text{EditDist}(T_a, T_b)}{\max(|T_a|, |T_b|)}

    TEDS учитывает и структуру (строки, столбцы, span), и содержимое ячеек. TEDS-S удаляет содержимое и оценивает только структуру. Reference implementation: teds.py в PubTabNet (внутри использует apted). Для evaluation corpora см. PubTabNet, FinTabNet и SciTSR. Наивные парсеры часто ломаются на таблицах. Проведите бенчмарк до того, как начнёте им доверять.

  • Сохранение layout / структуры: порядок заголовков, целостность списков, порядок чтения в многочастичных PDF. Для размеченного бенчмарка используйте DocLayNet. Готовое сравнение может включать element parser, например unstructured, PDF-библиотеку, например pymupdf, и выбранный Docling pipeline. Docling предлагает standard- и VLM-пути; фиксируйте, какой именно вы тестируете.

Сравнивайте разные семейства парсеров, например baseline на Tesseract, OCR-модель на базе VLM и кандидата от вендора. Используйте стратифицированную выборку реальных классов документов при фиксированном DPI: чистые сканы, фотографии, таблицы, мультиязычный текст, математика и рукописный текст. Отчитывайтесь о CER или WER для каждого класса и TEDS для страниц с таблицами.

Очистка и нормализация

  • Точность удаления boilerplate: precision/recall относительно вручную размеченных boilerplate spans. Агрессивное удаление убирает релевантное содержимое, а слишком мягкое загрязняет эмбеддинги. Инструменты для сравнения: trafilatura, jusText, Resiliparse. В Barbaresi (2021) Trafilatura сравнивается с baseline, включая jusText; Resiliparse — отдельный кандидат, а не система, оценённая в этой статье.

  • Нормализация Unicode: процент документов, дающих идентичные результаты NFC и NFKC (вычисляется с помощью стандартной библиотеки unicodedata.normalize), — полезный сигнал drift в compatibility form. Он не обнаруживает невидимые/default-ignorable code points или похожие символы из разных скриптов: первые нужно искать явно, а для вторых применять политику или детектор Unicode confusables, если они входят в scope.

  • Точность определения языка: F1 на размеченной мультиязычной выборке. Это критично для multilingual indexes. Используйте fasttext-langdetect (Facebook’s lid.176), lingua-py или cld3. FLORES-200 предоставляет evaluation text для 200 языков, но тестовый срез должен определяться языковым составом вашего production-трафика.

  • Эффективность дедупликации (MinHash / LSH): precision/recall near-duplicate detector относительно вручную размеченного набора. Идея состоит в том, чтобы оценивать сходство Жаккара J(A,B)=ABABJ(A, B) = \frac{|A \cap B|}{|A \cup B|} между множествами document shingles с помощью kk хэшей случайных перестановок (Broder, 1997) и группировать near-duplicates с помощью LSH banding (Indyk & Motwani, 1998). Переберите число хэшей и порог Jaccard на вашем корпусе. Отдельно отслеживайте false-merge rate (портит ответы) и missed-merge rate (тратит место в индексе). datasketch содержит реализацию, использованную ниже; её параметры иллюстративны:

    from datasketch import MinHash, MinHashLSH
    
    def shingles(text: str, k: int = 5) -> set[str]:
        text = text.lower()
        return {text[i:i + k] for i in range(len(text) - k + 1)}
    
    def to_minhash(text: str, num_perm: int = 128) -> MinHash:
        m = MinHash(num_perm=num_perm)
        for s in shingles(text):
            m.update(s.encode("utf-8"))
        return m
    
    docs = {
        "d1": "Mars has two moons, Phobos and Deimos.",
        "d2": "Mars has two moons, Phobos and Deimos!",   # near-dup
        "d3": "Curiosity rover landed on Mars in 2012.",
    }
    
    lsh = MinHashLSH(threshold=0.8, num_perm=128)
    for did, text in docs.items():
        lsh.insert(did, to_minhash(text))
    
    print(sorted(lsh.query(to_minhash(docs["d1"]))))  # ['d1', 'd2']
  • Удаление PII: precision и recall, рассчитанные отдельно для каждого типа сущностей (email, SSN, имена, адреса). Ошибки recall создают compliance-риск, ошибки precision ухудшают качество ответов. Рабочую точку задавайте вместе с legal team. Среди кандидатов — Microsoft Presidio, scrubadub или файн-тюненная NER-модель на размеченном наборе.

Чанкинг определяет качество retrieval

Чанкинг меняет то, какие свидетельства доходят до ответа. В vendor-бенчмарке NVIDIA 2025 года чанкинг по страницам дал наивысшую среднюю end-to-end-точность ответа в протестированной конфигурации, где таблицы и графики сохранялись как целые единицы. Это измеряет точность ответа, а не recall retrieval. Считайте этот результат свидетельством для протестированного корпуса, а не универсальным победителем.

Semantic chunking группирует соседние предложения по сходству эмбеддингов и разрезает текст на несхожих границах. SemanticChunker в LangChain и SemanticSplitterNodeParser в LlamaIndex реализуют эту стратегию. Она может повысить recall по сравнению с фиксированными окнами, если важны тематические границы.

RecursiveCharacterTextSplitter в LangChain по умолчанию пробует двойные переводы строк, одиночные переводы строк, пробелы, а затем отдельные символы: ["\n\n", "\n", " ", ""]. Границы предложений она не определяет, если не настроить подходящие separators или не использовать sentence-aware splitter. Выбирайте размер окна и overlap с учётом структуры документов, а затем сравнивайте варианты на golden set.

Метрики для отслеживания:

  • Когерентность чанков: coherence=cos(si,sj)withincos(si,sj)across boundary\text{coherence} = \overline{\cos(s_i, s_j)}_{\text{within}} - \overline{\cos(s_i, s_j)}_{\text{across boundary}}, где sis_i — эмбеддинги предложений. Здоровые чанки имеют внутреннее сходство и низкое сходство на границе. Считайте с помощью sentence-transformers и cosine_similarity из scikit-learn.
  • Качество границ: ручная разметка «разумно ли здесь разделять?» на выборке плюс структурная проверка того, что чанки не разрезают таблицы, списки или нумерованные разделы.
  • Оптимальный размер чанка: переберите размеры в токенах (128, 256, 512, 1024) и постройте график Recall@k в зависимости от размера на golden set. Выберите точку перегиба. Не берите значение из туториала.
  • Эффективность overlap: проведите ablation для нескольких долей overlap и измерьте Recall@k. Прекращайте увеличивать overlap, когда локальная кривая recall выравнивается или стоимость дублирования начинает превышать прирост.
  • Fidelity атрибуции чанков: процент чанков, сохраняющих проверяемый указатель на источник (номер страницы, section anchor, doc ID). Это необходимо для auditability.
  • Late vs. early chunking: late chunking (Günther et al., 2024) сначала эмбеддит весь документ, а затем сегментирует его, сохраняя глобальный контекст (reference implementation в jina-embeddings-v3). Contextual Retrieval (Anthropic, 2024) добавляет к каждому чанку контекст, сгенерированный LLM. Оба подхода увеличивают стоимость. Проведите бенчмарк на своём корпусе до внедрения.

Моё мнение: structural chunking (разделение по заголовкам, таблицам и секциям — с помощью парсеров вроде unstructured.io или обхода AST, который уже создал ваш парсер) используется недостаточно. Если в документах есть структура, используйте её до добавления similarity-эвристик. Recursive character splitting — baseline; semantic chunking оправдывает overhead главным образом для неструктурированной прозы.

Извлечение и обогащение метаданных

  • NER precision/recall/F1: для каждого типа сущностей на размеченном подмножестве. Стандарт CoNLL/MUC-style. Считайте с помощью seqeval (from seqeval.metrics import f1_score) для версии с учётом BIO/IOB-тегов или scikit-learn для сравнения span sets. CoNLL-2003 и OntoNotes 5.0 — канонические reference corpora.
  • F1 извлечения отношений: для ontology-grounded систем это ещё важнее. Разметьте набор, стратифицированный по типу отношения и классу документа. Публичные бенчмарки — TACRED и DocRED; среди реализаций-кандидатов — relation pipelines opennre и spaCy.
  • Точность извлечения title / heading: exact match плюс нормализованное сходство Левенштейна (1edit_dist(a,b)max(a,b)1 - \frac{\text{edit\_dist}(a, b)}{\max(|a|, |b|)}) с ground truth — python-Levenshtein или rapidfuzz дают оба значения одним вызовом.
  • Сохранение иерархических метаданных: процент чанков, корректно сохраняющих родительскую секцию, родительский документ и путь предков. Именно эта метрика определяет, способен ли ваш RAG отвечать на вопросы вроде «что говорит child политики X?».

Генерация эмбеддингов

  • Бенчмарки для выбора модели: используйте результаты MTEB по retrieval-задачам, BEIR для zero-shot generalization и MIRACL для multilingual retrieval в качестве точек сравнения. В retrieval-задачах MTEB обычно публикуется nDCG@10; в других семействах задач используются другие метрики. Python-пакет MTEB запускает бенчмарки локально. Перенос результата с English MTEB на low-resource language считайте гипотезой, которую нужно проверить на размеченном наборе этого языка.
  • Оценка для домена: не принимайте место в общем бенчмарке за результат в домене. Размер domain golden set определяйте по матрице покрытия и допустимой неопределённости решения. Затем переранжируйте модели-кандидаты на нём с помощью ranx или pytrec_eval. Domain set может изменить порядок leaderboard, поэтому публикуйте с результатом срез датасета, retrieval protocol и confidence interval.
  • Детектирование drift эмбеддингов: сравнивайте фиксированное reference window со скользящими эмбеддингами с помощью MMD или валидированного classifier reference-versus-current. KL требует явного estimator распределения вероятностей и не может напрямую применяться к сырым координатам эмбеддингов. Также измеряйте стабильность ближайших соседей для фиксированного набора probes. evidently и alibi-detect реализуют model-based и statistical detectors. Сравнительное исследование от Evidently — это одна vendor evaluation; сравнивайте методы на известных сдвигах собственных эмбеддингов.
  • Multi-vector vs. single-vector: late interaction сохраняет представления на уровне токенов, вместо того чтобы сворачивать каждый документ в один вектор; ColBERT — канонический дизайн, а reference implementations доступны в RAGatouille и PyLate. Более богатое представление увеличивает стоимость индекса и retrieval. Перед внедрением сравните качество, storage и latency с single-vector baseline на одном и том же domain set.

Построение индекса

  • Recall@k при approximation: сравнивайте ANN-индекс с точным brute-force baseline при одном и том же k — в FAISS это IndexHNSWFlat (или IndexIVFFlat) против IndexFlatIP/IndexFlatL2. Допустимую потерю recall задавайте из бюджета downstream quality. Проект ann-benchmarks отслеживает Pareto-кривые recall–QPS для разных библиотек.
  • Настройка HNSW: HNSW (Hierarchical Navigable Small World) — многоуровневый граф близости; см. Malkov & Yashunin, 2018. Он реализован в hnswlib, IndexHNSWFlat из FAISS и большинстве vector DB. У HNSW три параметра: M (fan-out графа), efConstruction (ширина кандидатов при построении) и efSearch (ширина кандидатов при запросе). Начните с документированных default values библиотеки, затем перебирайте параметры, пока кривая recall–latency не будет соответствовать требованиям evaluation set.
  • Настройка IVF: IVF (Inverted File index — разбиение векторов с помощью k-means на nlist ячеек и сканирование во время запроса nprobe ближайших ячеек; см. IndexIVFFlat и IndexIVFPQ из FAISS). Перебирайте nlist и nprobe относительно recall и latency exact search. Filtered queries бенчмаркайте отдельно, поскольку разные семейства индексов и vector DB по-разному реализуют обход с фильтрами.
  • Lag свежести обновлений: время от commit документа до его доступности для retrieval. Отслеживайте p50 и p99. Для систем с regulatory requirements также отслеживайте процент запросов, обслуженных устаревшими индексами.

Часть 4: Оценка во время запроса

В query-time lane находятся метрики, диагностирующие путь retrieval. Одного Recall@k недостаточно, чтобы понять, стали ли причиной сбоя rewriting, фильтрация, реранкинг или сборка контекста.

Понимание и rewriting запроса

  • Качество расширения запроса: uplift Recall@k на golden set для expanded query по сравнению с raw query. До тестирования задайте минимальный полезный прирост и его неопределённость. Если расширение не достигает этого требования, его latency и стоимость не оправданы. Классические baseline PRF (pseudo-relevance feedback), такие как RM3 и Bo1, по-прежнему полезны как sanity check; LLM-based expansion должна их превзойти.
  • Оценка HyDE: HyDE (Gao et al., 2022) генерирует с помощью LLM гипотетический ответ, эмбеддит его и выполняет retrieval по нему. Это добавляет latency генерации и новую поверхность сбоев. Отдельно измеряйте Recall@10 на in-domain, out-of-domain и low-confidence срезах, затем решите, должен ли HyDE быть default path, fallback или не использоваться.
  • Генерация multi-query: union Recall@k для N rewrites против одного запроса. Переберите N и выберите точку на frontier recall–latency. Реализации: MultiQueryRetriever в LangChain и QueryFusionRetriever в LlamaIndex.
  • Точность классификации интента: стандартные precision/recall/F1 для каждого интента (считайте с помощью sklearn.metrics.classification_report), но операционная метрика — routing correctness: был ли вызван правильный downstream pipeline?
  • Адаптивный routing: Adaptive-RAG (Jeong et al., NAACL 2024) показывает, что каждому запросу не нужна одна и та же стратегия retrieval. Отслеживайте точность router как задачу классификации на размеченном наборе «retrieval не нужен / one-shot / iterative».

Итеративный поиск требует end-to-end-теста с бюджетом

Агент может выполнить поиск, изучить результат и искать снова вместо одного фиксированного списка top-k. Текущий tau3 knowledge domain предоставляет настраиваемые RAG и agentic shell-based search, превращая это в конкретный путь оценки, а не только архитектурный эскиз. Для локального сравнения дайте one-shot и iterative retrieval один и тот же eligible corpus и явные лимиты по времени, токенам модели и tool calls. Записывайте каждый запрос и увиденные на каждом шаге свидетельства; оценивайте итоговое покрытие свидетельств, корректность ответа, поддержку цитат и случаи исчерпания бюджета. Дополнительные вызовы поиска полезны только тогда, когда добавленные свидетельства улучшают ответ в рамках заданных ограничений.

Метрики retrieval

Это baseline-метрики. Если вы их не отслеживаете, вы не сможете понять, улучшается ли retrieval.

МетрикаЧто измеряетКогда использовать
Recall@kдолю релевантных документов запроса, возвращённых в top kкогда важно не пропустить ни одну часть релевантного набора
Precision@kпроцент релевантных документов среди top-kкогда bottleneck — context window
MRRсреднее значение 1/rank первого релевантного документакогда пользователи смотрят только top-1 или top-3
nDCG@kgain со скидкой по позиции, взвешенный степенями релевантностистандартная retrieval-метрика для graded relevance
MAPсреднее по запросам от average precisionкогда важен весь ранжированный список
Hit Rate@kналичие хотя бы одного релевантного документа в top kусредняйте бинарный результат по запросам для быстрой sanity metric
Coverageпроцент golden docs, когда-либо извлечённых по всем запросамвыявляет систематические пробелы в индексе

Формулы для справки (binary relevance с релевантным множеством RqR_q для запроса qq и reli=1\text{rel}_i = 1, если ii-й извлечённый документ входит в RqR_q):

Recall@k=Rq{d1,,dk}Rq,Precision@k=Rq{d1,,dk}k\text{Recall@k} = \frac{|R_q \cap \{d_1, \dots, d_k\}|}{|R_q|}, \quad \text{Precision@k} = \frac{|R_q \cap \{d_1, \dots, d_k\}|}{k} RRq=1rank of first relevant doc,MRR=1QqQRRq\text{RR}_q = \frac{1}{\text{rank of first relevant doc}}, \quad \text{MRR} = \frac{1}{|Q|} \sum_{q \in Q} \text{RR}_q DCG@k=i=1k2reli1log2(i+1),nDCG@k=DCG@kIDCG@k\text{DCG@k} = \sum_{i=1}^{k} \frac{2^{\text{rel}_i} - 1}{\log_2(i + 1)}, \quad \text{nDCG@k} = \frac{\text{DCG@k}}{\text{IDCG@k}}

Для graded relevance, reli{0,1,2,}\text{rel}_i \in \{0, 1, 2, \dots\}; binary nDCG — частный случай, используемый в коде ниже. MAP — среднее по запросам от APq=1Rqi:reli=1Precision@i\text{AP}_q = \frac{1}{|R_q|}\sum_{i: \text{rel}_i = 1} \text{Precision@}i. Выводы формул см. в Manning, Raghavan, Schütze, Introduction to Information Retrieval, chapter 8.

Для production code используйте ranx, pytrec_eval или ir_measures — они реализуют всё семейство TREC-метрик и корректно обрабатывают graded relevance. Задавайте release targets на реалистичном golden set, downstream quality ответа и стоимости пропуска. Не наследуйте пороги из туториала.

Здесь k считает уникальные document IDs. Дубликаты нужно отклонять, а не давать повторному документу дополнительный gain. Для экспериментов с чанкингом явно укажите, считает ли k чанки или deduplicated parent documents, а также сравнивайте evidence, переданное в рамках фиксированного token budget. Один и тот же document recall может скрывать очень разное качество контекста.

Харнесс для этих метрик короткий. Его можно запустить из ноутбука ещё до выбора vector database.

from math import log2
from statistics import mean

# synthetic gold set: query_id -> set of relevant doc ids
gold = {
    "q1": {"d3"},
    "q2": {"d7", "d2"},
    "q3": {"d11"},
    "q4": {"d5"},
}

# ranked retrieval results: query_id -> ranked list of doc ids (top-10)
runs = {
    "q1": ["d8", "d3", "d1", "d4", "d2", "d9", "d6", "d10", "d12", "d13"],
    "q2": ["d2", "d6", "d4", "d7", "d1", "d3", "d8", "d11", "d5", "d9"],
    "q3": ["d11", "d2", "d3", "d4", "d1", "d6", "d7", "d8", "d10", "d12"],
    "q4": ["d1", "d2", "d3", "d6", "d8", "d9", "d10", "d12", "d13", "d14"],
}

def recall_at_k(ranked, gold_set, k):
    if k <= 0 or len(ranked) != len(set(ranked)):
        raise ValueError("require positive k and unique document IDs")
    if not gold_set:
        return 0.0
    hit = sum(1 for d in ranked[:k] if d in gold_set)
    return hit / len(gold_set)

def reciprocal_rank(ranked, gold_set):
    if len(ranked) != len(set(ranked)):
        raise ValueError("require unique document IDs")
    # MRR contribution per query: 1/rank of the first relevant doc.
    for rank, d in enumerate(ranked, start=1):
        if d in gold_set:
            return 1.0 / rank
    return 0.0

def ndcg_at_k(ranked, gold_set, k):
    if k <= 0 or len(ranked) != len(set(ranked)):
        raise ValueError("require positive k and unique document IDs")
    # binary relevance: rel ∈ {0, 1}
    gains = [1.0 if d in gold_set else 0.0 for d in ranked[:k]]
    dcg = sum(g / log2(i + 2) for i, g in enumerate(gains))
    # ideal DCG: all gold docs ranked first, capped by k
    n_gold_in_topk = min(k, len(gold_set))
    idcg = sum(1.0 / log2(i + 2) for i in range(n_gold_in_topk))
    return dcg / idcg if idcg else 0.0

K = 5
print(f"Recall@{K}: {mean(recall_at_k(runs.get(q, []), gold[q], K) for q in gold):.3f}")
print(f"MRR:       {mean(reciprocal_rank(runs.get(q, []), gold[q]) for q in gold):.3f}")
print(f"nDCG@{K}:  {mean(ndcg_at_k(runs.get(q, []), gold[q], K) for q in gold):.3f}")
# Recall@5: 0.750
# MRR:       0.625
# nDCG@5:    0.627
assert recall_at_k([], {"d1"}, 5) == 0.0
for score in (lambda ids: recall_at_k(ids, {"d1"}, 2),
              lambda ids: reciprocal_rank(ids, {"d1"}),
              lambda ids: ndcg_at_k(ids, {"d1"}, 2)):
    try:
        score(["d1", "d1"])
    except ValueError:
        pass
    else:
        raise AssertionError("duplicate IDs must fail")

Агрегатор проходит по всем ожидаемым запросам, считая отсутствующий run пустым. В этом примере пустым gold-запросам назначается ноль; в реальном suite помечайте их как отдельный answerability slice со своим знаменателем, а не трактуйте ноль как измеренный recall. Отчитывайтесь также о таймаутах и отсутствующих run, а не только о score.

Запускайте быстрый coverage-driven subset на каждом PR, а полный golden set — перед релизом. Блокируйте merge, когда preregistered metric пересекает допустимый regression budget.

Сопутствующий репозиторий фиксирует точные числа выше (Recall@5 = 0.750, MRR = 0.625, nDCG@5 = 0.627) как unit test в tests/test_retrieval_metrics.py; ноутбук 01 перебирает Recall@k / MRR / nDCG на реальном SciFact index, а production-shaped харнесс находится в evaluation/retrieval.py.

Hybrid retrieval и reciprocal rank fusion

BM25 — sparse lexical scorer, объединяющий matching по точным термам, взвешивание термов и нормализацию длины. Он доступен в rank_bm25, Elasticsearch, OpenSearch и большинстве search engines.

Reciprocal Rank Fusion (Cormack, Clarke и Buettcher, SIGIR 2009) объединяет BM25 и dense rankings по позиции. Исходная настройка k=60 — полезный baseline. RRF не зависит от score, поэтому не требует cross-lane normalization, необходимой при линейной интерполяции. Если размеченный набор достаточно велик для устойчивой оценки delta, также протестируйте convex combination и настройте α.

Моя гипотеза: hybrid retrieval вместе с cross-encoder reranker может помочь техническим корпусам, логам и коду. На сильно semantic corpora прирост может быть небольшим. Сравнивайте его с dense-only и sparse-only lanes, поскольку неудачная конфигурация fusion может работать хуже любого из входов. Сопутствующий SciFact notebook — ограниченный тест, а не общий результат.

Реализация занимает несколько строк.

from collections import defaultdict

# two retrieval lanes: dense embeddings and BM25.
dense  = ["d3", "d7", "d1", "d4", "d2", "d9", "d10"]
sparse = ["d2", "d3", "d8", "d1", "d11", "d4", "d6"]

def rrf(rankings: list[list[str]], k: int = 60) -> list[tuple[str, float]]:
    """Reciprocal Rank Fusion (Cormack et al., SIGIR 2009).

    score(d) = sum over rankings of 1 / (k + rank(d))
    Score-agnostic: only rank position matters. k=60 is the canonical default.
    """
    scores: dict[str, float] = defaultdict(float)
    if k <= 0:
        raise ValueError("k must be positive")
    for ranking in rankings:
        if len(ranking) != len(set(ranking)):
            raise ValueError("each ranking must contain unique document IDs")
        for rank, doc in enumerate(ranking, start=1):
            scores[doc] += 1.0 / (k + rank)
    return sorted(scores.items(), key=lambda kv: kv[1], reverse=True)

fused = rrf([dense, sparse], k=60)
for doc, score in fused[:5]:
    print(f"{doc}  score={score:.5f}")
# d3  score=0.03252   <- rank 1 dense, rank 2 sparse
# d2  score=0.03178   <- rank 5 dense, rank 1 sparse
# d1  score=0.03150
assert [doc for doc, _ in fused[:3]] == ["d3", "d2", "d1"]
try:
    rrf([["d1", "d1"]])
except ValueError:
    pass
else:
    raise AssertionError("one lane must not vote twice for a document")

Обратите внимание, чего RRF не делает: он никогда не смотрит на raw similarity scores. Dense retriever с cosine 0.98 и BM25 lane со score 17.4 напрямую несопоставимы. Z-score и min-max normalization устраняют affine scale differences, но ни один из методов не калибрует score как relevance. На нормализованные значения и результат fusion по-прежнему влияют выбросы, форма распределения и набор кандидатов. Любую score-based combination проверяйте на размеченных запросах.

RRF использует только rank. Если retriever ставит документ на позицию 2, его вклад равен 1 / (60 + 2) независимо от raw score, который к этому привёл.

Hybrid + RRF на SciFact: notebook 02 сравнивает dense, BM25 и RRF с delta по каждому запросу. Production-shaped fuser находится в retrieval/hybrid_rrf.py; tests/test_rrf.py фиксирует канонический порядок d3 / d2 / d1 на позиции k=60.

Реранкинг

  • ΔnDCG / ΔMRR: uplift по сравнению с режимом без реранкинга на golden set и на глубине, которую реально использует приложение. Запускайте retrieval metrics с реранкером и без него на идентичных наборах кандидатов.
  • Cross-encoder vs. bi-encoder: bi-encoder независимо эмбеддит query и doc (по одному вектору на сторону) и считает score через dot product; cross-encoder конкатенирует query+doc и выполняет один forward pass, совместно применяя attention к обеим частям. Cross-encoder меняет отдельный forward pass для каждого кандидата на более богатое query–document interaction. Reference implementation: sentence-transformers CrossEncoder. Бенчмаркайте relevance и latency на конкретном hardware, batch size и candidate depth; не переносите результат одной модели или managed service в другую среду.
  • Listwise vs. pointwise: pointwise независимо оценивает каждую пару (query, doc); listwise совместно оценивает весь список кандидатов, чтобы модель могла сравнить кандидатов. Оценивайте оба подхода на одинаковых наборах кандидатов. Калибруйте любой score threshold для каждой модели и корпуса, а не считайте опубликованный пример переносимым.
from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")  # Reproducible baseline, not a latest-model ranking

query = "How do I rotate database credentials in production?"
candidates = [
    "Production database credentials are rotated via Vault every 30 days.",
    "The new logo was unveiled at the all-hands meeting.",
    "To rotate prod DB creds, run the `rotate-secrets` GitHub Action.",
]

scores = reranker.predict([(query, c) for c in candidates])
ranked = sorted(zip(candidates, scores), key=lambda x: -x[1])
for doc, score in ranked:
    print(f"{score:+.3f}  {doc}")

Код BGE выше — небольшой baseline. Для актуального сравнения добавьте Cohere Rerank 4.0 fast/pro для managed multilingual text ranking или Qwen3-VL-Reranker 2B/8B, если запросы или документы содержат изображения, скриншоты или видео. Явно фиксируйте modality задач, candidate depth, input limits, инструкции и hardware. General Qwen3.8 generation checkpoint — не та же модель, что специализированный Qwen3-VL reranker.

Реранкер часто помогает базовому RAG-пайплайну, но прирост не гарантирован. Измерьте его ΔPrecision@1 и ΔnDCG на golden set и оставляйте его только в том случае, если прирост укладывается в бюджет latency и cost. Перед выбором следующей оптимизации сравните этот измеренный прирост с более небольшими изменениями retrieval.

ΔnDCG и ΔPrecision@1 для cross-encoder на SciFact: notebook 03; модуль: retrieval/reranker.py.

Сборка контекста и lost-in-the-middle

Многие случаи «хороший retrieval, плохой ответ» начинаются на этапе сборки контекста.

  • Релевантность контекста: Ragas ContextRelevance оценивает список переданного контекста с помощью двух промптов и нормализованных оценок; это не per-chunk score. Для диагностики чанков оценивайте каждую пару query–chunk явно, например cross-encoder, и публикуйте распределение относительно локально откалиброванного порога.
  • Покрытие цитат переданного контекста: число уникальных переданных чанков, на которые ссылаются, делённое на число уникальных переданных чанков. Случаи пустого контекста отчитывайте отдельно. Этот наблюдаемый proxy показывает, какие чанки получили citations, но не какие из них модель использовала внутренне и поддерживают ли citations её claims. Отдельно проверяйте citation support и сравнивайте coverage с качеством ответа и token cost.
  • Детектирование lost-in-the-middle: synthetic eval, где gold chunk помещается на позиции {first, middle, last} длинного контекста и измеряется корректность ответа. В цитируемом исследовании Liu et al. (TACL 2024) сообщается U-shaped degradation в их условиях long context. Тот же паттерн в актуальной модели считайте гипотезой для проверки. Mitigations: сначала переранжировать, затем перестроить top-k так, чтобы chunk с самым высоким score оказался первым или последним (именно это делает LongContextReorder в LangChain), либо агрессивно сжать средние чанки. Измеряйте через position-stratified eval, а не только aggregate score. Готовая position-stratified eval находится в notebook 06 (модуль: evaluation/lost_in_middle.py).
  • Сжатие контекста: публикуйте compression ratio (input tokens / output tokens) вместе с корректностью ответа. Среди инструментов — ContextualCompressionRetriever в LangChain и LongLLMLingua. Заранее определите максимальную допустимую потерю корректности исходя из риска приложения и token budget, а затем отклоняйте конфигурации, пересекающие этот порог.

Часть 5: Filter false-exclusion rate

Этой метрике посвящён отдельный раздел, потому что aggregate retrieval scores не могут показать, что пропуск вызван relevance filter. Проверяйте eligibility filters отдельно относительно caller entitlements; документ вне этого множества должен оставаться исключённым.

Жёсткий relevance predicate, например product = Y AND locale = en-US, может обнулить effective recall среди документов, которые caller имеет право видеть. Корректно реализованный Recall@k обнаруживает потерю, поскольку его знаменатель остаётся исходным множеством eligible relevant documents. Он не сообщает, вызвана ли потеря фильтром, retriever или ranker. Faithfulness оценивает claims относительно retrieved context. Она всё ещё может оценить claims, поддержанные неполным контекстом, но не диагностирует причину в фильтре или retrieval. Пустой отказ может не содержать statements и дать NaN — это зависит от реализации; не считайте это свидетельством того, что faithfulness одобрила отказ.

Выделенная ветка — типичный сбой: нужный документ существует, но фильтр удаляет его до retrieval. Recall@k фиксирует падение; только exclusion rate связывает его с predicate.

Тихие сбои RAG, сопоставленные с путём от исходного корпуса через фильтрацию, ранжирование и генерацию к метрике, выявляющей каждый источникТихие сбои RAG, сопоставленные с путём от исходного корпуса через фильтрацию, ранжирование и генерацию к метрике, выявляющей каждый источник

Метрика

filter_false_exclusion_rate =
    (# queries where all gold docs were excluded by metadata filter) /
    (# queries with at least one entitlement-eligible gold doc)

Это определение на уровне запроса считает катастрофические исключения: ни один eligible relevant document не прошёл фильтр. Перед оценкой пересекайте gold set каждого запроса с entitlement set caller; недоступный документ не является false exclusion. Для multi-gold queries стандартный Recall@k всё равно показывает частичную потерю; если эта граница важна, добавьте per-document exclusion rate. Для расчёта любой из ставок нужны (a) ground-truth doc IDs для каждого eval query и (b) instrumentation, логирующая применённые filter predicates, а не только финальные результаты. Целевой уровень задавайте из стоимости исключения корректного ответа и confidence interval production sample.

Ниже — рабочая реализация. Она сравнивает корректный стандартный recall с некорректным evaluator, который переопределяет relevance после фильтрации.

# A small worked example where relevance predicates remove eligible documents.
docs = [
    {"id": "d1", "tenant": "acme",   "locale": "en-US"},
    {"id": "d2", "tenant": "acme",   "locale": "en-GB"},
    {"id": "d3", "tenant": "globex", "locale": "en-US"},
    {"id": "d4", "tenant": "acme",   "locale": "en-US"},
    {"id": "d5", "tenant": "acme",   "locale": "de-DE"},
    {"id": "d6", "tenant": "acme",   "locale": "fr-FR"},
]

# Tenant isolation is an eligibility invariant, not a relevance experiment.
# Verify it independently before evaluating relevance preferences.
eligible_docs = [d for d in docs if d["tenant"] == "acme"]
assert {d["id"] for d in eligible_docs} == {"d1", "d2", "d4", "d5", "d6"}

queries = [
    # the gold doc lives in en-GB but the dynamic filter forced en-US
    {"qid": "q1", "gold": {"d2"}, "filter": lambda d: d["locale"] == "en-US"},
    # the gold doc is correctly within the tenant filter
    {"qid": "q2", "gold": {"d4"}, "filter": lambda d: d["tenant"] == "acme"},
    # the gold doc lives in fr-FR but the dynamic filter forced en-US
    {"qid": "q3", "gold": {"d6"}, "filter": lambda d: d["locale"] == "en-US"},
    # the gold doc passes the filter (de-DE locale match)
    {"qid": "q4", "gold": {"d5"}, "filter": lambda d: d["locale"] == "de-DE"},
]

def filter_false_exclusion_rate(queries, eligible_docs):
    n_with_gold, n_excluded = 0, 0
    eligible_ids = {d["id"] for d in eligible_docs}
    for q in queries:
        eligible_gold = q["gold"] & eligible_ids
        if not eligible_gold:
            continue
        n_with_gold += 1
        survivors = {d["id"] for d in eligible_docs if q["filter"](d)}
        if not (eligible_gold & survivors):
            n_excluded += 1
    if not n_with_gold:
        raise ValueError("false-exclusion rate needs queries with eligible gold")
    return n_excluded / n_with_gold, n_with_gold

rate, eligible_query_count = filter_false_exclusion_rate(queries, eligible_docs)
print(f"filter_false_exclusion_rate = {rate:.2%}")
# filter_false_exclusion_rate = 50.00%
print(f"eligible queries = {eligible_query_count} / {len(queries)}")
# eligible queries = 4 / 4

# Correct Recall@k keeps the original eligible gold set as its denominator.
def standard_recall_at_k(queries, eligible_docs, k=10):
    recalls = []
    eligible_ids = {d["id"] for d in eligible_docs}
    for q in queries:
        eligible_gold = q["gold"] & eligible_ids
        if not eligible_gold:
            continue
        # demo only: the survivor set stands in for a ranked run.
        # A real harness ranks the survivors first, then slices to k.
        survivors = [d for d in eligible_docs if q["filter"](d)][:k]
        survivor_ids = {d["id"] for d in survivors}
        recalls.append(len(eligible_gold & survivor_ids) / len(eligible_gold))
    return sum(recalls) / len(recalls) if recalls else 0.0

print(f"standard recall@10 = {standard_recall_at_k(queries, eligible_docs):.2%}")
# standard recall@10 = 50.00%

# INVALID: rebuilding the gold set after filtering changes the question.
# It drops queries whose relevant documents did not survive, then scores 100%.
def invalid_recall_over_filtered_gold(queries, eligible_docs, k=10):
    recalls = []
    all_doc_ids = {d["id"] for d in eligible_docs}
    for q in queries:
        all_survivors = {d["id"] for d in eligible_docs if q["filter"](d)}
        filtered_gold = q["gold"] & all_doc_ids & all_survivors
        if not filtered_gold:
            continue
        top_k_ids = set(list(all_survivors)[:k])
        recalls.append(len(filtered_gold & top_k_ids) / len(filtered_gold))
    return sum(recalls) / len(recalls) if recalls else 0.0

invalid = invalid_recall_over_filtered_gold(queries, eligible_docs)
print(f"INVALID recall (filtered gold) = {invalid:.2%}")
# INVALID recall (filtered gold) = 100.00%

assert rate == 0.5
assert standard_recall_at_k(queries, eligible_docs) == 0.5
assert invalid == 1.0

# An unauthorized-only gold document belongs in an entitlement test, not this metric.
unauthorized_only = [
    {"qid": "q5", "gold": {"d3"}, "filter": lambda d: d["locale"] == "en-US"},
]
assert eligible_query_count == 4
assert filter_false_exclusion_rate(queries + unauthorized_only, eligible_docs) == (rate, 4)
for unscorable in ([], unauthorized_only):
    try:
        filter_false_exclusion_rate(unscorable, eligible_docs)
    except ValueError:
        pass
    else:
        raise AssertionError("an unmeasured exclusion rate must not pass as zero")

Функция возвращает rate и число запросов с eligible gold. Если знаменатель пуст, она выбрасывает исключение вместо успокаивающего нуля; обязательная release check должна трактовать это как отсутствие свидетельств. Запросы, в которых весь gold неавторизован, остаются отдельным entitlement tests.

Половина запросов теряет gold document из-за фильтра, поэтому корректный Recall@10 падает до 50%. Этот score обнаруживает симптом, но не показывает причину. False-exclusion rate показывает, что predicate удалил два ответа до запуска retriever. Преднамеренно некорректный evaluator сообщает 100% только потому, что исключает эти failure cases из gold set. Ни одна модель не может восстановить документ, отфильтрованный заранее.

Rate 50% выше воспроизводится как unit test в сопутствующем репозитории: tests/test_filter_exclusion.py::test_50_percent_exclusion_rate. Notebook 04 запускает тест на SciFact с synthetic metadata, чтобы можно было увидеть, как реальный фильтр обнуляет recall; runtime metric с companion metrics для precision/recall predicate находится в evaluation/filter_exclusion.py.

Сопутствующая метрика: precision и recall predicate

Если фильтрация динамическая (например, LLM извлекает filter predicates из запроса), рассматривайте extractor predicate как classifier model и оценивайте его соответственно. Измеряйте predicate precision и recall на размеченном наборе пар (query, correct predicate). Error rate predicate не переводится напрямую в такую же потерю retrieval recall; измеряйте, как часто эти ошибки исключают gold document. После удаления gold document жёстким фильтром никакой реранкинг уже не поможет.

Eligibility filters и relevance preferences

Авторизация, tenant isolation, legal jurisdiction и publication state определяют, может ли документ попасть в candidate set. Оставляйте их hard filters и проверяйте независимо; Recall@k, false-exclusion rate и relevance precision не дают права ослаблять эти ограничения.

Для relevance preference, например locale, recency или version, сравнивайте hard predicate и soft boost на одной и той же held-out выборке eligible queries:

For each relevance preference F:
  hard_recall_F  = retrieval_recall@k with F as a hard filter
  soft_recall_F  = retrieval_recall@k with F as a +0.X rerank boost
  hard_precision = relevant_in_top_k / k under hard filter
  soft_precision = relevant_in_top_k / k under soft boost
  precision_gain = hard_precision - soft_precision
  precision_gain_CI = paired-query uncertainty interval for precision_gain
  exclusion_rate = % of queries where the gold doc was filtered out (hard)

Pre-register Δ_min, ε, and recall_loss_max for this workflow.
Use a hard predicate only if:
  lower_bound(precision_gain_CI) >= Δ_min
  AND exclusion_rate <= ε
  AND hard_recall - soft_recall >= -recall_loss_max.
Otherwise prefer a soft boost.

Минимальный полезный прирост precision, uncertainty interval, ε и bound потери recall выбирайте исходя из вреда от исключения иначе eligible ответа, пользы дополнительной precision и размера held-out sample. Это локальные release criteria, а не универсальные пороги. Отдельный подробный материал об этом trade-off запланирован; follow-ups перечислены в конце.


Часть 6: Оценка генерации

Метрики retrieval показывают, что система могла бы ответить правильно. Они не показывают, что она ответила правильно. Метрики генерации закрывают этот пробел.

Faithfulness и groundedness

Faithfulness в RAGAS разбирает ответ на атомарные claims (короткие самостоятельные factual statements), а затем проверяет каждый относительно retrieved context с помощью LLM judge:

faithfulness=claims supported by contexttotal claims\text{faithfulness} = \frac{|\text{claims supported by context}|}{|\text{total claims}|}

Faithfulness проверяет поддержку в переданных свидетельствах; она не доказывает, что свидетельства корректны или достаточны для ответа на вопрос. Ответы без claims записывайте отдельно и публикуйте их количество, не присваивая пустому ответу идеальную faithfulness.

Текущая документация Ragas рекомендует приведённый ниже collections API. В uv-проекте установите ragas и openai с помощью uv add ragas openai, задайте OPENAI_API_KEY и сохраните это как script для запуска через uv run. Скрипт выполняет provider calls и расходует их budget; score — результат judge, а не детерминированная ожидаемая константа. Зафиксируйте resolved dependencies в lockfile.

import asyncio

from openai import AsyncOpenAI, omit
from ragas.llms import llm_factory
from ragas.metrics.collections import Faithfulness

async def main():
    async with AsyncOpenAI() as client:
        llm = llm_factory(
            "gpt-5.6-terra", client=client, reasoning_effort="none",
            max_tokens=omit, max_completion_tokens=1024,
            temperature=omit, top_p=omit,
        )
        scorer = Faithfulness(llm=llm)
        result = await scorer.ascore(
            user_input="How many moons does Mars have?",
            response="Mars has two moons, Phobos and Deimos.",
            retrieved_contexts=["Mars has two moons named Phobos and Deimos."],
        )
        print(result.value)

if __name__ == "__main__":
    asyncio.run(main())

В этом примере используется GPT-5.6 Terra как актуальный кандидат со structured outputs; Ragas factory передаёт model arguments. Ragas 0.4.3 не распознаёт dotted GPT generation names в token-limit mapper. В примере явно исключены legacy max_tokens и sampling defaults, а также передан max_completion_tokens; при обновлении adapter проверяйте исходящий request. Отключение reasoning делает конфигурацию явной, но не валидированной. Перед заменой более дешёвого откалиброванного judge сравните false passes, false failures и стоимость с human labels.

Ниже тот же цикл развёрнут с детерминированным stand-in judge, чтобы показать end-to-end-структуру.

def extract_claims(answer: str) -> list[str]:
    # Production: an LLM call that decomposes the answer.
    # Demo: split on sentence-final punctuation.
    return [c.strip() for c in answer.replace("?", ".").replace("!", ".").split(".") if c.strip()]

def verify_claim(claim: str, context: str) -> bool:
    # Production: an NLI (natural-language inference) model or LLM judge.
    # Demo: a deterministic stand-in so the example runs offline.
    entailed_pairs = {
        "Mars has two moons": True,
        "Phobos and Deimos orbit Mars": True,
        "Mars has a thick atmosphere": False,  # unsupported by context
        "Curiosity landed in 2012": True,
    }
    for k, v in entailed_pairs.items():
        if k.lower() in claim.lower() or claim.lower() in k.lower():
            return v
    words = [w.lower() for w in claim.split() if len(w) > 3]
    return all(w in context.lower() for w in words) if words else False

context = (
    "Mars has two moons, Phobos and Deimos. NASA's Curiosity rover "
    "landed on Mars in 2012."
)
answer = (
    "Mars has two moons. Phobos and Deimos orbit Mars. "
    "Mars has a thick atmosphere. Curiosity landed in 2012."
)

claims = extract_claims(answer)
verdicts = [(c, verify_claim(c, context)) for c in claims]
faithfulness = sum(1 for _, ok in verdicts if ok) / len(verdicts) if verdicts else None
for c, ok in verdicts:
    print(f"  [{'✓' if ok else '✗'}] {c}")
print(f"faithfulness = {faithfulness:.2f}" if faithfulness is not None else "no_claims")
# faithfulness = 0.75   (one unsupported claim about the atmosphere)
assert faithfulness == 0.75
assert extract_claims("") == []

Важна структура. В production verify_claim превращается в NLI model или LLM call. Сохраняйте extract–verify–aggregate structure, но отдельно валидируйте extraction и entailment, а также записывайте no-claim или failed judgments. Приведённый offline stand-in жёстко зашит под эти примеры и не является factuality detector.

End-to-end claim extraction + verification для сгенерированных ответов SciFact: notebook 05; модуль: evaluation/faithfulness.py. Репозиторий запускает тот же цикл через два семейства judges — собственную модель generator и cross-family judge (RAG_EVALS_JUDGE_MODEL) — плюс deterministic lexical baseline, чтобы показать, где семейства расходятся.

Специализированная альтернатива LLM-as-judge — HHEM-2.1-Open (Hughes Hallucination Evaluation Model, Vectara), classifier, fine-tuned для детектирования галлюцинаций. В model card описаны checkpoint, выдаваемый raw score 0–1 и balanced-accuracy results на AggreFact и RAGTruth. Default decision boundary не публикуется, поэтому выбирать его нужно вам. Рассматривайте эти данные как evidence из model card, а не гарантию для вашего корпуса: откалибруйте threshold на локальных labels и сравните его с выбранным judge до деплоя.

Оценка атомарных фактов

FActScore (Min et al., EMNLP 2023) разбирает long-form generations на atomic facts, извлекает evidence для каждого факта, помечает каждый как supported / not-supported и вычисляет долю поддержанных фактов:

FActScore=supported atomic factstotal atomic facts\text{FActScore} = \frac{|\text{supported atomic facts}|}{|\text{total atomic facts}|}

Reference implementation: shmsw25/FActScore. Метод хорошо работает для биографий, summary и других long-form outputs. Будьте внимательны: повторяющиеся тривиальные факты могут завысить score, а по отдельности истинные statements могут сформировать вводящий в заблуждение ответ. MontageLie (EMNLP 2025) проверяет эту слабость через deceptive relationships и порядок истинных statements. VeriScore работает с claims, содержащими необходимые modifiers; фильтр Core помогает предотвращать fact-padding.

Точность цитирования

Отслеживайте citation precision (действительно ли процитированные spans поддерживают claim) и citation recall (процитированы ли claims, которые должны быть процитированы):

cite_precision=cited spans that support a claimcited spans,cite_recall=claims with at least one supporting cited spanclaims that should be cited\text{cite\_precision} = \frac{|\text{cited spans that support a claim}|}{|\text{cited spans}|}, \quad \text{cite\_recall} = \frac{|\text{claims with at least one supporting cited span}|}{|\text{claims that should be cited}|}

TREC 2024 RAG Track определяет воспроизводимый протокол support evaluation. Thakur et al. (SIGIR 2025) сообщают примерно о 56% agreement с human judgments, сделанными с нуля, и 72% в другой конфигурации, где люди постредактировали predictions LLM. Второй показатель относится к assisted annotation, а не к независимому свидетельству улучшения точности judge. Всегда указывайте условие аннотации рядом с числом. Для автоматизированного приближения ALCE (Gao et al., EMNLP 2023) реализует citation precision/recall с NLI-based verification.

Корректность, полнота ответа и отказ

  • Корректность ответа относительно reference: exact match или token-F1 подходят для коротких ответов. Для длинных ответов проверяйте factual relations, сущности, величины, отрицания и обязательную информацию относительно проверенных reference. BERTScore и embedding cosine измеряют similarity; неправильное число или отрицание могут сохранить высокий score. AnswerCorrectness в Ragas объединяет factual comparison с similarity, но не отождествляет их.
  • Полнота через nuggets: nugget — релевантная информационная единица, при этом vital nuggets отличаются от optional useful. Вопрос о дате основания может требовать год; имя основателя не становится обязательным автоматически. AutoNuggetizer строит и уточняет nuggets из judged document pools, а затем проверяет их наличие в сгенерированных ответах. В его initial TREC 2024 report были охвачены 21 topic и 45 runs. TREC 2025 overview, опубликованный в марте 2026 года, расширяет протокол на narrative queries и оценивает retrieval relevance, полноту ответа и attribution. Это публичные протоколы оценки, а не свидетельство того, что каждому production RAG нужна такая же nugget rubric.
  • Поведение при отказе: размечайте, допускают ли переданные свидетельства ответ, затем измеряйте correct refusals среди всех отказов и refusals среди случаев, где система должна воздержаться. NoMIRACL (Findings of EMNLP 2024) проверяет устойчивость на релевантных и нерелевантных переданных passages; он не доказывает, что во всём корпусе нет ответа. В своём suite разделяйте retrieval misses и действительно out-of-scope queries.

Проверка после генерации

Самые дешёвые улучшения надёжности часто дают детерминированные post-checks, а не более крупные модели.

  • Флаг unseen entity: записывайте named-entity strings в ответе, отсутствующие в нормализованной строке контекста (например, spaCy’s ents плюс exact matching). Это дешёвый domain-specific flag для unseen entity strings, а не grounding check: он не устанавливает identity, relation, negation, time или provenance. До использования для approval release измерьте precision и recall на локальных labels, а для verification сохраняйте claim-level entailment или human review.

    def unseen_entity_flag(answer: str, context: str, entities: list[str]) -> bool:
        return any(entity.lower() not in context.lower() for entity in entities)
    
    context = "Paris was not founded in 1994."
    answer = "Paris was founded in 1994."
    assert not unseen_entity_flag(answer, context, ["Paris"])
    claim_supported = False  # human label or an NLI verdict, not the lexical flag
    assert not claim_supported
  • Проверка claims: извлекайте claims, запускайте NLI относительно контекста и fail или flag всё, что ниже threshold. NLI-as-faithfulness models: cross-encoder/nli-deberta-v3-large, MoritzLaurer/DeBERTa-v3-large-mnli-fever-anli-ling-wanli. Это добавляет latency и оправдано для high-stakes domains.

  • Self-consistency (Wang et al., ICLR 2023): сэмплируйте несколько генераций при temperature > 0; публикуйте agreement rate (например, долю генераций, совпадающих с modal answer, или pairwise BERTScore); число samples выбирайте по stability–cost curve, а ответы с низким agreement отправляйте на human review.

  • Калибровка confidence: собирайте verbalized confidence («Насколько вы уверены, 0–1?») и сравнивайте его с фактической корректностью на eval set. Стройте calibration curve и публикуйте Expected Calibration Error: ECE=m=1MBmnacc(Bm)conf(Bm)\text{ECE} = \sum_{m=1}^{M} \frac{|B_m|}{n} |\text{acc}(B_m) - \text{conf}(B_m)|, где BmB_m — confidence bins. Реализации: netcal, torchmetrics.CalibrationError. Модель с confidence 0.9 должна быть корректной примерно в 90% сопоставимых случаев; измеряйте разрыв, а не предполагайте calibration.


Часть 7: Оценка ontology-grounded RAG

Стандартные метрики выше покрывают open-corpus RAG. Если RAG выполняет retrieval по structured ontology, taxonomy или knowledge graph, этих метрик необходимо, но недостаточно. Примеры — товары в каталоге, состояния в SNOMED, компоненты в BOM и security techniques в MITRE ATT&CK. Нужно также измерять ontology layer.

Точность entity linking

Первая задача — сопоставить mention в запросе с ontology entity («Aspirin» → wikidata:Q18216, «the 737» → aircraft:Boeing_737).

  • Precision/recall/F1 на уровне mention: стандартные метрики относительно gold mention spans (считайте с помощью seqeval или span-set comparator).
  • Точность disambiguation: среди корректно обнаруженных mentions какая доля сопоставлена правильному entity ID? Публичные reference — ReFinED, REL и GENRE; бенчмарки вроде AIDA-CoNLL и BELB показывают, что результаты зависят от системы и домена.
  • Обработка NIL: precision/recall для «entity отсутствует в ontology». Отдельно измеряйте over-linking к близким, но неправильным entities и корректный abstention.

Оценка с учётом иерархии

Обычная accuracy трактует «предсказан Sedan вместо Hatchback» так же, как «предсказан Sedan вместо Submarine». Эти ошибки неравноценны.

  • Hierarchical precision/recall/F1 (Kosmopoulos et al., 2015): учитывайте общих предков в ontology DAG. Пусть P^q\hat{P}_q — predicted node и все его ancestors, а TqT_q — true node и все его ancestors:

    hP=qP^qTqqP^q,hR=qP^qTqqTq,hF1=2hPhRhP+hRhP = \frac{\sum_q |\hat{P}_q \cap T_q|}{\sum_q |\hat{P}_q|}, \quad hR = \frac{\sum_q |\hat{P}_q \cap T_q|}{\sum_q |T_q|}, \quad hF1 = \frac{2 \cdot hP \cdot hR}{hP + hR}

    Реализуйте через networkx на ontology graph: дополните каждое prediction и label его ancestors, затем вычислите приведённые пересечения множеств.

  • Сходство Wu–Palmer между predicted и gold entity в taxonomy (Wu & Palmer, 1994):

    WuP(c1,c2)=2depth(LCA(c1,c2))depth(c1)+depth(c2)\text{WuP}(c_1, c_2) = \frac{2 \cdot \text{depth}(\text{LCA}(c_1, c_2))}{\text{depth}(c_1) + \text{depth}(c_2)}

    где LCA — lowest common ancestor в taxonomy. Готовая реализация есть в NLTK для WordNet (from nltk.corpus import wordnet as wn; wn.synset("car.n.01").wup_similarity(wn.synset("truck.n.01"))); для custom taxonomies вычисляйте LCA с помощью networkx.

  • Частота ошибок sibling/parent: отдельно отслеживайте ошибки сопоставления с siblings, parents и children — count_sibling / total_errors, count_parent / total_errors, count_descendant / total_errors. На проверенных примерах выясняйте, вызваны ли ошибки siblings неоднозначными mentions, а ошибки parents — чрезмерным обобщением.

Filter false-exclusion rate: повторение, теперь критичное

В ontology-grounded системах hard filters часто исходят из самой онтологии («извлекать документы только с категорией X»). Метрика exclusion rate (определённая в части 5) становится основным сигналом корректности. Неправильное предсказание категории может обнулить recall; exclusion rate связывает эту потерю с фильтром.

Constrained generation conformance

Если output должен соответствовать ontology (каждое entity name в ответе должно быть допустимым членом ontology; каждый predicate должен входить в closed vocabulary), измеряйте:

  • Schema validity rate: процент outputs, которые парсятся и валидируются относительно ontology schema. Валидируйте с помощью jsonschema или pydantic. JSONSchemaBench — публичный бенчмарк для general structured output; для ontology-specific schemas создайте собственный validator.
  • Vocabulary conformance: процент named entities в output, являющихся допустимыми ontology IDs — это однострочная set-membership check относительно closed vocabulary.
  • Semantic conformance: syntactically valid output всё ещё может выбрать неправильную, но допустимую entity. Сопоставляйте conformance с downstream answer correctness.

Фреймворки constrained decoding (Outlines, XGrammar, Guidance, OpenAI Structured Outputs) предназначены для enforcement schema validity. JSONSchemaBench сравнивает efficiency, coverage и quality разных implementations. Повторно запустите его cases, соответствующие вашим schemas и serving backend, потому что coverage и latency зависят от обоих факторов.

Аудируемость

Для ontology-grounded систем, ответы которых проходят review:

  • Citation completeness: процент factual claims, имеющих хотя бы одну проверяемую citation.
  • Provenance depth: процент citations, которые разрешаются до исходного документа со стабильным ID, а не только до chunk hash.
  • Reproducibility rate: повторный запуск того же запроса на фиксированном snapshot возвращает тот же ответ. Зафиксируйте model version, runtime, decoding configuration и seed, а required repeat rate задайте исходя из auditability needs workflow. Temperature zero сам по себе не гарантирует determinism. Источник miss может находиться в генерации, serving runtime или на любом upstream-этапе.

Часть 8: Оценка на уровне системы

Холистическое качество ответа

  • LLM-as-judge (Zheng et al., NeurIPS 2023): масштабируемый подход к model-based evaluation. G-Eval (Liu et al., EMNLP 2023) генерирует evaluation steps из задачи и criteria, а затем взвешивает уровни оценки их token probabilities: score=ip(si)si\text{score} = \sum_i p(s_i)\,s_i. Это probabilities, а не log probabilities. Agreement зависит от judge, задачи, промпта и calibration set.
  • Pairwise preference: предъявляйте judge answer A и answer B и записывайте preference. Это заменяет absolute rating сравнительным решением, но всё равно требует калибровки относительно human preferences. MT-Bench сообщал об agreement GPT-4 judge выше 80% с human preferences и human–human agreement в условиях своего бенчмарка; не переносите этот показатель в другой домен без calibration.

У LLM-as-judge есть реальные biases:

  • Position bias: измеряйте чувствительность к порядку на human-labeled cases для выбранного judge и задачи. Рандомизация или агрегация swapped-order может помочь некоторым сочетаниям model/task, но сохраняйте её только при улучшении локального human agreement; controlled study 2026 года обнаружило, что swapping positions ухудшал результаты на его adversarial cases.
  • Verbosity bias: judges могут путать длину с качеством. В цитируемом controlled study 2026 года, version 2 обнаружилось heterogeneous behavior на expansion pairs: три judge предпочитали более длинные ответы, Claude — краткие, а GPT-4o был примерно нейтрален. Все пять хорошо работали на truncation controls. Эти результаты привязаны к бенчмарку, поэтому задайте judge, как трактовать полноту и filler, а затем публикуйте length-controlled performance по собственной rubric.
  • Риск self-preference: Zheng et al. наблюдали на 10% более высокий self-win rate GPT-4 и на 25% более высокий self-win rate Claude-v1 в своих данных, но заключили, что ограниченность данных и небольшие различия не позволяют установить self-enhancement bias. Сравнивайте same-family и cross-family judges с локальными human labels; выбирайте лучше откалиброванный judge, а не считайте какую-либо конфигурацию безопасной по умолчанию.

Практический рецепт: выбирайте judge на human-labeled calibration data, маскируйте identity моделей, измеряйте order sensitivity и явно задавайте length policy в rubric. Повторяйте cases только тогда, когда дополнительные samples существенно снижают неопределённость. Для high-stakes evaluations сравнивайте same-family и cross-family judges и анализируйте расхождения относительно human labels.

Schema-Guided Reasoning для judges

Свободный output — один из источников вариативности в запусках judge. Два запуска для одного и того же ответа могут по-разному организовать rubric и выдать разные scores. Schema-Guided Reasoning (SGR) делает rubric явной: задайте evaluation stages как Pydantic schema, а затем используйте constrained output через Outlines, XGrammar, vLLM structured outputs или OpenAI response_format, чтобы enforcement supported schema constraints. Порядок полей может помогать reviewer проверять запись, но не доказывает, что модель выполняла reasoning stages именно в таком порядке.

Для RAG eval schema декомпозирует judgment на явные audit-friendly fields, вместо того чтобы позволять модели сразу переходить к числу:

from pydantic import BaseModel, Field, ValidationError, model_validator
from typing import Literal

class FaithfulnessJudgment(BaseModel):
    extracted_claims: list[str] = Field(
        description="Atomic factual claims in the answer, one per item."
    )
    supported_claims: list[str] = Field(
        description="Subset of extracted_claims that are entailed by the context."
    )
    unsupported_claims: list[str] = Field(
        description="Subset that is NOT entailed by the context."
    )
    outcome: Literal["scored", "no_claims"]
    failure_mode: Literal[
        "none", "fabrication", "overgeneralization", "wrong_entity", "stale_fact"
    ]
    rationale: str

    @model_validator(mode="after")
    def require_claim_partition(self):
        for claims in (self.extracted_claims, self.supported_claims, self.unsupported_claims):
            if any(not claim.strip() for claim in claims):
                raise ValueError("claims must contain non-whitespace text")
        extracted = set(self.extracted_claims)
        supported = set(self.supported_claims)
        unsupported = set(self.unsupported_claims)
        if not extracted:
            if supported or unsupported or self.outcome != "no_claims":
                raise ValueError("an empty extraction must be a no_claims outcome")
            return self
        if (
            len(extracted) != len(self.extracted_claims)
            or len(supported) != len(self.supported_claims)
            or len(unsupported) != len(self.unsupported_claims)
            or supported & unsupported
            or supported | unsupported != extracted
            or self.outcome != "scored"
        ):
            raise ValueError("verdict lists must be a complete, disjoint claim partition")
        return self

    @property
    def score(self) -> float | None:
        if not self.extracted_claims:
            return None
        return len(self.supported_claims) / len(self.extracted_claims)

valid = FaithfulnessJudgment(
    extracted_claims=["Mars has two moons", "Mars has a thick atmosphere"],
    supported_claims=["Mars has two moons"],
    unsupported_claims=["Mars has a thick atmosphere"],
    outcome="scored",
    failure_mode="fabrication",
    rationale="The atmosphere claim lacks support.",
)
assert valid.score == 0.5
empty = FaithfulnessJudgment(
    extracted_claims=[],
    supported_claims=[],
    unsupported_claims=[],
    outcome="no_claims",
    failure_mode="none",
    rationale="Score refusal correctness separately.",
)
assert empty.score is None
try:
    FaithfulnessJudgment(
        extracted_claims=["Mars has two moons"],
        supported_claims=["Mars has two moons"],
        unsupported_claims=["Mars has two moons"],
        outcome="scored",
        failure_mode="none",
        rationale="Contradictory verdict.",
    )
except ValidationError:
    pass
else:
    raise AssertionError("overlapping verdicts must fail validation")

for blank in ("", " ", "\t\n"):
    for verdict_field in ("supported_claims", "unsupported_claims"):
        fields = {"extracted_claims": [blank], "supported_claims": [], "unsupported_claims": []}
        fields[verdict_field] = [blank]
        try:
            FaithfulnessJudgment(
                **fields, outcome="scored", failure_mode="none", rationale="Blank claim."
            )
        except ValidationError:
            pass
        else:
            raise AssertionError("blank claims must fail validation")

Этот иллюстративный контракт вычисляет score только после того, как формы verdict образуют полное непересекающееся разбиение extracted claims. Outcome без claims не получает faithfulness score и должен исключаться из faithfulness aggregate; публикуйте его количество и отдельно score корректности отказов, чтобы abstentions не могли незаметно улучшать среднее. Сохраняйте эти semantic checks в production; constrained output гарантирует форму, но не unbiased verdict. Pydantic model также делает изменение rubric видимым как code diff, а human calibration проверяет сам judgment.

Это работает для любой rubric-based judge, не только для faithfulness. Pairwise preference, citation support и refusal correctness также выигрывают от такого подхода.

Упрощённый rubric judge и примеры pairwise, position bias и cross-family находятся в notebook 07; модуль: evaluation/llm_judge.py. В проверенной revision функция с именем g_eval запрашивает одно integer rating; она не генерирует evaluation steps и не вычисляет probability-weighted scores, поэтому не воспроизводит G-Eval protocol. Benchmark sweep (make benchmark в репозитории) подключает три модели (gpt-5-mini, claude-haiku-4-5, gemini-2.5-flash) к rotating-judge pairwise A/B: каждый judge оценивает ответы двух других model families. Такая topology поддерживает cross-family pairwise results; для измерения self-preference также нужны same-family и cross-family conditions в сравнении с локальными human labels.

Латентность и стоимость

  • p50, p95, p99 на каждом этапе пайплайна. SLO percentile и alert threshold выбирайте по user journey, traffic volume и error budget.
  • Time-to-first-token и общее время генерации. Для streaming UX пользователям важен TTFT.
  • Stage breakdown: retrieval, реранкинг, генерация, post-processing. Используйте trace, чтобы найти tail, а не предполагайте, какой этап его вызвал; при сравнении запусков фиксируйте устройство реранкера и batch size.
  • Total $/query = embedding + retrieval + rerank + generation + storage amortized. Отслеживайте p50 и p99; именно long tail поглощает бюджет.
  • Cache hit rates на уровнях embedding cache, retrieval cache и KV-cache. Отдельные target values задавайте исходя из наблюдаемой повторяемости, политики invalidation и стоимости, сэкономленной на каждом уровне.

Per-stage p50/p95/p99 со stage breakdown встроены в notebook 08 и runner в evaluation/latency.py; benchmark report объединяет latency и faithfulness в одной matrix, которую можно повторно запустить с помощью make benchmark.

A/B-тестирование

  • Unit of randomization: выбирайте единицу исходя из estimand, carryover и interference. Используйте assignment по user или session, если повторный exposure может изменить поведение или создать непоследовательный UX. Assignment по query оправдан только при незначительных эффектах и если анализ моделирует repeated observations.
  • Primary, guardrail и exploratory metrics: preregister их. Primary measure выбирайте по product outcome; satisfaction proxies включают thumbs, regenerations и dwell. Latency и cost рассматривайте как guardrails, когда они ограничивают experience.
  • Sample size: выполните power analysis до запуска, исходя из минимального обнаруживаемого эффекта, baseline variance, assignment unit и stopping rule.

Часть 9: Построение test set

Метрика хороша лишь настолько, насколько хорош test set, на котором она запускается. Если golden set покрывает три интента, а production traffic — двенадцать, Recall@10 измеряет только эти три интента. Хуже того, test set, переобученный на простых вопросах («Какова политика возврата компании?»), может одобрить систему, которая ломается на сложных («Право на возврат при частичной отмене по EU Digital Services Act 2023 года, счёт в EUR, страна происхождения — Ирландия?»). Aggregate score растёт, а система по-прежнему не справляется с важной частью production traffic.

Неполные relevance labels могут искажать recall в обе стороны. Если истинное релевантное множество — {a, b}, а labels содержат только {a}, извлечение {a} получает 1.0 вместо 0.5; извлечение {b} получает ноль вместо 0.5. Версионируйте judgments и проверяйте вновь извлечённые, ещё не оценённые evidence перед интерпретацией delta.

Сначала стройте test set вокруг реального распределения и сложности запросов. Затем выбирайте метрики, реагирующие на целевые failure modes, и настраивайте по ним систему.

Генерация synthetic queries

Используйте LLM для генерации вопросов по корпусу:

  • Per-chunk: «Generate 3 questions a user might ask that this chunk answers».
  • Multi-hop: выберите два чанка и сгенерируйте вопрос, требующий оба.
  • Adversarial: генерируйте вопросы с distractor entities, near-duplicate phrasing и ambiguous mentions.

Ragas test generation использует graph-based scenarios с single-hop и multi-hop queries и specific или abstract information needs. DataMorgana генерирует настраиваемые synthetic benchmarks по категориям пользователей и вопросов. Synthetic data полезна для cold start и проверки покрытия. Она не заменяет реальные user queries.

Построение golden dataset

Human-curated data закрепляет golden set.

  1. Выберите реальные user queries (или simulated queries до запуска), стратифицированные по интенту.
  2. Попросите SMEs ответить на каждый вопрос и указать документы, содержащие ответ.
  3. Определяйте размер набора по coverage matrix и confidence interval, необходимому для release decisions; покрытие важнее заимствованного числа запросов.
  4. Выполняйте re-curation, когда это оправдано cadence релизов, сигналами drift, domain risk и capacity разметки.

Разделяйте development queries, judge calibration, held-out release measurement и monitoring samples. Перед split группируйте общие source documents и sessions. Как только query или label начинает направлять tuning, это development data. Оставляйте untouched cases для проверки выбранной конфигурации и judge.

Записывайте corpus, query, версии relevance labels и policy, unit of k, supplied-context budget, версии scorer и judge и правила aggregation. Сравнивайте paired per-query deltas на одной и той же population, с uncertainty и slice counts. Включайте каждый ожидаемый query: timeout, missing result или unscorable judge response должны оставаться видимыми в completion/error accounting. Не позволяйте release score улучшаться за счёт молчаливого удаления failures. ARES предлагает research-подход с automated judges, human validation и prediction-powered inference для system estimates при дефиците annotation; проверенный локальный suite можно начать проще.

Adversarial test sets

  • Counterfactuals: заменяйте ключевые сущности в query. Извлекает ли система правильные чанки для изменённого запроса?
  • Distractors: запросы, для которых в corpus есть правдоподобный, но неправильный ответ, который не должен извлекаться. Именно это stress-тестирует RGB (Chen et al., AAAI 2024): noise robustness, negative rejection, information integration и counterfactual robustness.
  • Negation и quantifiers: запросы с «not», «except» и «only». Dense retrievers часто испытывают с ними проблемы.
  • Out-of-scope: запросы без ответа в corpus. Система должна сказать «Я не знаю», а не галлюцинировать. NoMIRACL предоставляет passage-level tests для relevance/answerability; добавьте отдельные corpus-level out-of-scope labels. Явно оценивайте abstention на типах запросов из production.

Coverage и continuous evaluation

  • Постройте coverage matrix: query intent × document type × ontology branch. Один query на cell — начало inventory покрытия, но недостаточно statistical power для release decision. Пустые cells показывают пропуски покрытия; размер заполненных slices задавайте по допустимой неопределённости.
  • Запускайте bounded fast regression subset на каждом PR, а полный suite — по более медленному расписанию.
  • Планируйте полный golden-set eval с учётом release cadence и evaluation cost; запускайте его на release candidates.
  • Планируйте drift evaluation по traffic volume, ожидаемым изменениям и риску. Используйте rolling production sample и стратифицируйте по feedback, а не меняйте target distribution молча.

Часть 10: Production monitoring

Eval suite, который вы выпускаете, описывает систему в момент запуска. После этого production traffic меняется.

Неявный и явный feedback

  • Считайте implicit events кандидатами на signals, а не positive или negative quality KPIs, пока они не покажут корреляцию с blinded review или explicit feedback на локальной выборке.
  • Click-through / open rate на процитированных источниках (если UI их показывает).
  • Dwell time на ответе.
  • Regeneration rate: процент ответов, которые пользователь переспрашивает или просит переделать. Считайте это одним из сигналов dissatisfaction и калибруйте относительно reviewed conversations.
  • Copy / share / export rates: candidate implicit signals, которые могут означать usefulness, verification, handoff или dissatisfaction. До назначения направления измерьте их association и confidence interval.
  • Follow-up patterns: используйте «Are you sure?» или «But what about X?» как strata для review, затем размечайте их связь с distrust или unresolved need.
  • Thumbs up/down с optional reason categories (wrong, incomplete, off-topic, harmful, slow). Inline edits могут сохранять больше диагностического контекста; оценивайте эту ценность на reviewed samples.

Детектирование drift

  • Query drift: сравнивайте query embeddings с reference window с помощью MMD или валидированного classifier reference-versus-current. KL требует заданного probability estimator, например выбранных histograms; raw embedding coordinates не являются probabilities. Калибруйте alarms на известных сдвигах, затем изучайте затронутые slices.
  • Embedding drift: зафиксируйте representation и probe set, затем измеряйте neighbor stability и retrieval quality. Разные версии модели не обязаны иметь одинаковые dimensions или coordinate basis, поэтому cross-version cosine может быть бессмысленным. Мигрируйте query и document encoders вместе, оцените новый индекс и сохраняйте versioned snapshots для rollback.
  • Performance drift: отслеживайте production-equivalent metrics (regeneration rate по intent) во времени. Резкие и постепенные изменения подсказывают разные гипотезы, но их форма не устанавливает причину; проверяйте data, traffic, provider, policy и deployment changes.

Shadow evaluation и human-in-the-loop

Запускайте candidate system параллельно с production, сравнивайте outputs offline и не показывайте их пользователям. Это может выявить регрессии до запуска. Shadow inference всё равно потребляет capacity и может вызывать tools: изолируйте resources, подавляйте writes и убедитесь, что сравнение не ухудшает production latency.

Для human-in-the-loop (HITL) review:

  • Направляйте low-confidence outputs в review queue.
  • Добавляйте random sample production traffic для blind review; rate задавайте по traffic volume, risk и reviewer capacity.
  • Oversample thumbs-down outputs для review вместе с random sample.
  • Используйте reviewed outputs для расширения golden set.

Минимальный набор guardrails

Приоритеты и пороги алертов выбирайте по user harm, SLO и validated detector performance. Кандидатные сигналы:

  1. Faithfulness/HHEM score ниже threshold на rolling production sample.
  2. p95 latency выше SLO.
  3. Filter false-exclusion rate выше threshold (sample-based).
  4. Regeneration rate вне locally calibrated control band, учитывающей window size, traffic, seasonality и false-alert budget.
  5. Cost/query выше budget.

Используйте timing релиза как подсказку для диагностики, а затем проверяйте её по traces и затронутым slices. Deployment может совпасть с traffic drift, а изменение provider или data может произойти без application release. Alerts — это evidence для расследования; их lead time относительно user reports нужно измерять.


Оговорки

  • Пороги локальны, а не универсальны. Любое число, названное в этом руководстве иллюстративным, — пример конфигурации или воспроизведённый результат, а не release threshold. Калибруйте пороги под домен, stakes, uncertainty evaluation set и ожидания пользователей.
  • Ландшафт фреймворков быстро меняется. Версии HHEM, названия метрик RAGAS, model cards и порядок leaderboard могут измениться после публикации. Повторно проверяйте источники по ссылкам и выполняйте бенчмарк перед фиксацией решений.
  • У чисел agreement для LLM-as-judge есть оговорки. Показатель 80% для GPT-4 и людей получен в условиях MT-Bench / Chatbot Arena. Он не устанавливает agreement для нишевого домена или adversarial slice. Используйте judges как force multiplier, а не замену spot-checking.
  • Vendor benchmark uplifts часто нельзя независимо воспроизвести. Воспроизводите результаты на собственных данных, особенно для новых rerankers и OCR-систем.
  • Ни одна метрика не заменяет просмотр outputs. Планируйте blind review случайной production sample с учётом traffic, risk и reviewer capacity. Метрики масштабируют эту практику, но не заменяют её.

Что дальше в этой серии

Это был индекс. Я планирую следующие материалы:

  • Soft Boosts vs. Hard Filters: подробный разбор filter false-exclusion rate с кодом, реальными production examples и decision framework.
  • Chunking Is the Hidden Variable: controlled experiment для recursive, semantic, late и structural chunking на трёх корпусах.
  • Reranker Selection in 2026: BGE vs. Cohere vs. ZeRank vs. актуальные cross-encoder models, head-to-head по cost, latency и uplift.
  • Ontology-Grounded RAG: An End-to-End Walkthrough: построение полного evaluation harness для entity-grounded retrieval system.
  • LLM-as-Judge Without the Self-Preference Trap: практические рецепты unbiased automated evaluation.
  • Online Evaluation in Production: instrumentation patterns, alerting policies и дашборды, которые ловят реальные регрессии.

References

Frameworks и benchmarks

Retrieval и ranking

Generation, faithfulness, judges

Drift и production

Сопутствующий код

  • slavadubrov/rag-evals-demo — runnable harness для выбранных метрик этой статьи на корпусе SciFact, а также benchmark sweep chunking × embedding × LLM. Ноутбуки 00–09, unit tests, фиксирующие приведённые выше примеры, и embedded-Qdrant index позволяют запускать всё без Docker.