MAATRIX / Блог / Как оценить качество RAG-ответов без ручной проверки

Как оценить качество RAG-ответов без ручной проверки

MAATRIX

Вы поменяли модель эмбеддингов, подправили параметры чанкинга или добавили реранкер — и теперь нужно понять, стало лучше или хуже. Открывать 50 диалогов руками и читать каждый ответ можно один раз, но не после каждого коммита в пайплайн. Дальше расскажу, как разложить оценку RAG на три отдельные измеримые метрики и прогонять их автоматически на небольшом наборе тестовых вопросов — без человека в цикле на каждой итерации.

Почему ручная проверка RAG не масштабируется

RAG-система — это конвейер из двух разных шагов: поиск релевантных фрагментов и генерация ответа на их основе. Каждый шаг ломается по-своему, но при ручной проверке вы видите только финальный текст и оцениваете его целиком — «нравится» или «не нравится». Это создаёт три проблемы.

Во-первых, это не масштабируется по времени. Если у вас 100 тестовых вопросов и вы меняете параметры чанкинга раз в неделю, час-два на прочтение ответов после каждого изменения быстро съедает бюджет времени на улучшения — и через месяц-два вы просто перестаёте тестировать после мелких правок, что и приводит к тихой деградации.

Во-вторых, целостная оценка «нравится/не нравится» не говорит, что именно сломалось. Ответ вышел плохим — потому что поиск не нашёл нужный фрагмент, модель проигнорировала контекст и придумала факт от себя, или текст технически верный, но не отвечает на заданный вопрос? Без разделения на компоненты вы чините наугад: сегодня меняете промпт, завтра — модель эмбеддингов, послезавтра реранкер, и непонятно, что из этого реально помогло.

В-третьих, ручная оценка субъективна и непостоянна: один и тот же ответ два разных человека оценят по-разному. Без числовой метрики невозможно объективно сравнить версию A и версию B пайплайна — только «вроде получше».

Решение — не искать один универсальный «скор качества ответа», а измерять три ортогональных, каждую отдельно и максимально механически.

Три слоя качества RAG, которые стоит мерить отдельно

Разберём RAG-ответ на составляющие, каждую можно проверить независимо от остальных:

  1. Качество поиска (retrieval) — нашёл ли поисковый движок тот фрагмент документа, где реально лежит ответ на вопрос. Это вопрос к векторной базе, модели эмбеддингов, параметрам чанкинга и реранкеру — генерация текста тут вообще не участвует.
  2. Фактическая обоснованность (grounding / faithfulness) — действительно ли утверждения в сгенерированном ответе подтверждаются найденными фрагментами, или модель что-то добавила от себя. Это вопрос к промпту генерации и к самой LLM — независимо от того, насколько хорош был поиск.
  3. Релевантность ответа вопросу — отвечает ли текст на то, что реально спросили, или уходит в сторону, приводит общие рассуждения вокруг темы, отвечает на смежный, но другой вопрос.

Важно: это независимые оси. Можно найти идеальный фрагмент и сгенерировать ответ, полностью на него опирающийся, — и всё равно не ответить на заданный вопрос (низкая релевантность при высоком grounding). А можно ответить точно в тему, но добавить одно выдуманное число, которого не было в источнике (высокая релевантность, но провал по grounding). Разделение экономит часы отладки: retrieval-метрика упала — чинить эмбеддинги и чанкинг, grounding упал при хорошем retrieval — чинить промпт генерации, упала только релевантность — смотреть, как модель интерпретирует вопрос. Подробнее о том, что происходит на каждом шаге между вопросом и ответом — в статье что происходит внутри RAG между вопросом и ответом.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть ИИ на сервере

Метрика поиска: попадание в топ-N (recall@k) — без единого вызова LLM

Это самая дешёвая и надёжная из трёх метрик, потому что она вообще не оценивает текст — только совпадение идентификаторов.

Идея: у вас есть заранее подготовленный набор вопросов, для каждого из которых вы ЗАРАНЕЕ знаете, какой конкретный фрагмент (chunk_id или document_id) документа содержит правильный ответ. Прогоняете вопрос через поисковый движок, смотрите — попал ли известный правильный фрагмент в топ-N результатов поиска. Никакой LLM для этого не нужно — чистая механическая проверка совпадения ID.

Пример golden-набора для retrieval-теста (JSONL, одна строка — один вопрос):

{"question": "Как продлить лицензию SSL-сертификата на сервере?", "expected_chunk_id": "docs/ssl-setup.md#chunk-14"}
{"question": "Какой порт использует Postgres по умолчанию?", "expected_chunk_id": "docs/db-setup.md#chunk-3"}
{"question": "Как включить автопродление баланса VPS?", "expected_chunk_id": "docs/billing.md#chunk-7"}

Скрипт оценки (упрощённо, на Python, векторная база любая — Qdrant, pgvector, Meilisearch):

import json

def recall_at_k(golden_path, retriever, k=5):
    total = 0
    hits = 0
    with open(golden_path) as f:
        for line in f:
            item = json.loads(line)
            total += 1
            results = retriever.search(item["question"], top_k=k)
            returned_ids = [r.chunk_id for r in results]
            if item["expected_chunk_id"] in returned_ids:
                hits += 1
    return hits / total

score = recall_at_k("golden_retrieval.jsonl", my_retriever, k=5)
print(f"recall@5 = {score:.2%}")

Запускаете эту функцию после каждого изменения — смены модели эмбеддингов, других параметров чанкинга, добавления реранкера — и сравниваете число с предыдущим прогоном. Это секунды на десятки вопросов, без затрат на LLM-вызовы.

Есть грабля: если вы меняете параметры чанкинга (размер чанка, overlap), chunk_id в индексе тоже меняются — старый golden-набор с привязкой к конкретным ID перестаёт быть валидным напрямую. Практичнее привязывать expected не к id чанка, а к диапазону строк документа или к document_id + подстроке, которая обязана присутствовать в найденном фрагменте — тогда переиндексация не ломает тестовый набор. Про то, как чанкинг влияет на итоговое качество ответов: RAG выдавал чушь — проблема была в чанкинге.

LLM-судья №1: обоснованность ответа найденными фрагментами (grounding)

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

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

Пример промпта для судьи (используйте модель, отличную от той, что генерирует ответы — снижает риск, что судья и генератор одинаково ошибаются в одну сторону):

Ты — строгий проверяющий фактической обоснованности ответов.

ВОПРОС ПОЛЬЗОВАТЕЛЯ:
{question}

НАЙДЕННЫЕ ФРАГМЕНТЫ-ИСТОЧНИКИ:
{retrieved_chunks}

СГЕНЕРИРОВАННЫЙ ОТВЕТ:
{generated_answer}

Разбей ответ на отдельные фактические утверждения. Для каждого утверждения укажи:
- SUPPORTED — прямо подтверждается текстом фрагментов
- CONTRADICTED — противоречит фрагментам
- UNSUPPORTED — фрагменты не содержат информации для проверки этого утверждения

Выведи JSON-массив объектов {"claim": "...", "verdict": "..."}.
Не оценивай фактическую правильность самого утверждения вне контекста фрагментов — только соответствие приведённому тексту источников.

Итоговая метрика grounding-score для одного ответа — доля утверждений со статусом SUPPORTED. Усредняете по golden-набору — получаете число, сравнимое между прогонами. Если после смены промпта генерации доля UNSUPPORTED резко выросла — модель начала чаще додумывать, надо вернуть более строгую инструкцию в духе «отвечай только на основе приведённых фрагментов, если информации недостаточно — так и скажи». Механику того, почему языковые модели склонны придумывать факты: почему LLM придумывает факты: механика галлюцинаций.

Держите температуру судьи на 0 (или близко к 0) — это не убирает вариативность полностью, но заметно снижает случайный разброс между одинаковыми прогонами на одном входе.

Golden-набор и регулярный прогон при изменениях пайплайна

Практика внедрения простая, но требует дисциплины.

Соберите golden-набор. Это 30–100 вопросов, отражающих реальные запросы пользователей вашей системы — не абстрактные, а формулировки, которые люди реально пишут (можно взять из логов, плюс добавить руками несколько «сложных» и «пограничных» кейсов: неоднозначные вопросы, вопросы про то, чего в базе знаний нет вообще, вопросы с опечатками). Для каждого зафиксируйте ожидаемый источник (chunk/document) и, если есть время, эталонный ответ или ключевые факты, которые в ответе обязаны быть.

Формат — плоский JSONL или CSV, чтобы его легко было прогонять скриптом и хранить в git рядом с кодом пайплайна:

{"id": "q001", "question": "...", "expected_source": "...", "reference_answer": "..."}

Прогоняйте оценку при каждом значимом изменении: смена модели эмбеддингов, изменение размера чанка или overlap, добавление/замена реранкера, правка системного промпта генерации, смена самой LLM-генератора. Скрипт последовательно считает recall@k, затем для каждого ответа вызывает судью grounding и судью релевантности, усредняет по набору и выводит три числа плюс дельту к предыдущему сохранённому прогону:

recall@5:     0.91  (было 0.86, +0.05)
grounding:    0.88  (было 0.90, -0.02)
relevance:    3.6/4 (было 3.4/4, +0.2)

Храните историю прогонов — простой CSV с датой, версией пайплайна и тремя числами уже даёт график тренда во времени и позволяет увидеть, какое изменение подвинуло метрику не туда. Golden-набор держите небольшим и представительным, а не исчерпывающим — каждый вызов судьи стоит денег и времени, а на 300+ вопросах прогон растягивается на десятки минут и тормозит саму итерацию, ради которой всё затевалось. Полный проход по устройству такого пайплайна с нуля — в статье RAG-пайплайн с нуля: практический пример.

Ограничения LLM-судьи: почему не стоит доверять ему слепо

Здесь важна честность: модель-судья — это тоже языковая модель со всеми её слабостями, а не безупречный эталон истины.

Судья может ошибаться систематически: известная особенность LLM-as-judge — склонность выше оценивать более длинные и «уверенно» написанные ответы независимо от фактической точности, чувствительность к формулировке промпта (небольшая правка инструкции судье может заметно сдвинуть итоговые числа) и непостоянство между запусками даже при температуре 0. Кроме того, судья хорошо ловит явные противоречия и грубые добавления «из головы», но может пропускать тонкие неточности, для распознавания которых нужна глубокая экспертиза именно в вашей предметной области.

Практический вывод: автоматическую оценку нужно периодически калибровать выборочной ручной проверкой. Не после каждого прогона — это снова сведёт всё к ручному труду, — а раз в несколько циклов или после крупного изменения (смена модели-судьи, переписывание промпта оценки): берёте случайную выборку из 15–30 ответов, читаете сами и сверяете свой вердикт с вердиктом судьи. Согласие высокое (условно, 85%+ по grounding-вердиктам) — можно доверять метрике как основному индикатору между калибровками. Согласие низкое или расходится в одну сторону — правьте промпт судьи или критерии, а не игнорируйте расхождение.

Автоматическая оценка LLM-as-judge — это не абсолютный сертификат качества, а компас для относительного сравнения: «после этого изменения стало лучше или хуже», а не «это идеальный ответ». Именно в этой роли — быстрой, регулярной, объективной проверки после каждой правки пайплайна — она и приносит основную пользу.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть ИИ на сервере

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Нужна ли для судьи отдельная, более мощная модель, чем та, что генерирует ответы?

Не обязательно более мощная, но желательно другая — если судья и генератор одна и та же модель с теми же слабостями, она может систематически не замечать собственные ошибки. Разные модели снижают риск такой корреляции, хотя и не убирают его полностью.

Сколько вопросов должно быть в golden-наборе?

Для старта достаточно 30–50 репрезентативных вопросов, покрывающих типичные и пограничные случаи вашей предметной области. Больше 100–150 обычно уже избыточно для регулярных прогонов — рост числа вопросов увеличивает время и стоимость каждого прогона быстрее, чем растёт точность метрики.

Можно ли использовать готовые библиотеки вроде RAGAS вместо самодельных промптов для судьи?

Да, такие open-source инструменты для оценки RAG существуют и реализуют похожие метрики (faithfulness, answer relevancy, context precision/recall) с готовыми промптами и агрегацией — это разумная отправная точка, но всё равно стоит проверить их промпты на своём домене и откалибровать вручную так же, как и самодельные.

Что делать, если recall@k высокий, а сгенерированные ответы всё равно выглядят плохо?

Значит, проблема не в поиске — фокус отладки нужно смещать на grounding (не додумывает ли модель) и релевантность (отвечает ли она на заданный вопрос), а также перепроверить сам промпт генерации: возможно, модели даётся слишком много нерелевантного контекста, из-за чего она теряется в фрагментах.

Как часто вообще нужно прогонять эту оценку?

После любого изменения, которое теоретически может повлиять на качество: смена модели эмбеддингов или генератора, правка параметров чанкинга, добавление или замена реранкера, изменение системного промпта. На практике это обычно совпадает с ритмом релизов пайплайна, а не с фиксированным расписанием.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →