Р. Аллахвердиев
RAGОценка качестваСерия «Оценка RAG» · часть 1

Golden-датасет для RAG: как собрать 60 вопросов, которые ловят регрессии

Схема датасета, восемь типов вопросов, откуда их брать, как размечать эталоны и согласие разметчиков, как версионировать и не загрязнить датасет. С примером CSV и проверкой покрытия на Python.

опубликовано · 9 мин71cb47d
Обложка: Golden-датасет для RAG: как собрать 60 вопросов, которые ловят регрессии
Содержание

Коротко

  • Golden-датасет — главный актив оценки RAG. Метрики и судья вторичны: без хороших вопросов они мерят не то.
  • Запись содержит вопрос, эталонный ответ, эталонные источники, тип вопроса и сложность. Эталонные источники нужны, чтобы считать метрики поиска.
  • Покрытие важнее объёма. 60 вопросов по восьми типам полезнее 300 одинаковых.
  • Берите реальные вопросы (логи, тикеты поддержки), синтетику используйте как дополнение и обязательно проверяйте человеком.
  • Датасет живёт в репозитории рядом с кодом, версионируется и пополняется реальными провалами из эксплуатации.

Зачем отдельный датасет

В разборе четырёх метрик RAG я советовал начинать с 50–100 реальных вопросов. Здесь описано, как их собрать так, чтобы они ловили именно те поломки, которые бывают у RAG. Датасет решает три задачи:

  1. Сравнивает версии: стало лучше или хуже после смены промпта, модели, чанкинга.
  2. Диагностирует: по срезам видно, где система проваливается.
  3. Защищает от регрессии: в CI каждый PR гоняется на одном и том же наборе.

Если вопросы случайные, вы получите случайные выводы. Поэтому датасет проектируют так же, как тест-дизайн: по классам, рискам и покрытию.

Схема записи

Минимальный набор полей:

Поле Зачем
id стабильный идентификатор для отчётов и сравнения версий
question формулировка, как её пишет пользователь
expected_answer эталонный ответ: короткий, проверяемый
expected_sources идентификаторы документов или фрагментов, где лежит ответ
type тип вопроса (см. ниже)
difficulty easy, medium, hard
notes комментарий разметчика, спорные места

Пример в CSV (содержимое вымышленное, иллюстративное):

id,question,expected_answer,expected_sources,type,difficulty,notes
q001,"Сколько дней на возврат товара?","30 дней при наличии чека","returns.md#1",simple,easy,
q014,"Можно ли вернуть товар без чека и сколько ждать возврата денег?","Без чека возврат невозможен; деньги возвращают в течение 10 дней","returns.md#1;payments.md#4",multi_hop,medium,"два документа"
q027,"Какая у вас гарантия на планеты Марса?","В базе знаний нет информации","",no_answer,easy,"ожидаем отказ"

Для вопросов без ответа в базе поле expected_sources пустое, а эталон описывает правильное поведение: отказ или уточнение.

Восемь типов вопросов

Тип Пример Что проверяет
simple «Срок возврата?» базовый поиск и ответ
multi_hop вопрос, ответ на который собирается из 2+ документов полноту поиска
no_answer вопрос вне базы умение отказать вместо выдумки
ambiguous «Сколько это стоит?» без уточнения запрос уточнения
typo «вазврат тавара» устойчивость поиска к опечаткам
cross_lingual вопрос на английском к русской базе (если актуально) мультиязычность
adversarial провокация, попытка вытянуть служебное границы поведения
long вопрос в несколько предложений с лишним контекстом работу с шумом

Не все типы нужны каждому проекту. Cross-lingual в чисто русскоязычном внутреннем боте можно убрать, но no_answer и adversarial нужны почти всегда. Распределение определяйте по рискам: в поддержке банка выше доля провокаций и вопросов про деньги.

Откуда брать вопросы

  1. Логи и тикеты поддержки. Лучший источник: реальные формулировки, опечатки, сленг. Перед использованием удалите персональные данные.
  2. Регламенты и документы. Для каждого раздела придумайте вопрос, ответ на который там лежит. Годится, чтобы проверить покрытие базы знаний.
  3. Интервью с экспертами. Спросите сотрудников, какие вопросы самые частые и самые опасные.
  4. Реальные провалы. Любой жалобный случай из эксплуатации превращается в запись датасета. Так он растёт осмысленно.
  5. Синтетика. Генераторы в DeepEval и RAGAS умеют создавать вопросы по документам. Это быстрый способ получить черновик, но не готовый датасет.

Синтетические вопросы обычно слишком «чистые»: они повторяют лексику документа, поэтому retriever находит их легко. Реальные пользователи формулируют иначе. Если оценивать только на синтетике, метрики поиска получатся завышенными.

Разметка эталонов

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

Инструкция разметчику, которая у меня работает как отправная точка:

  1. Читай только документы базы, не используй общие знания.
  2. Если ответа в базе нет, поставь тип no_answer и опиши желаемое поведение.
  3. Укажи все источники, без которых ответ неполон.
  4. Не знаешь, как оценивать, пиши в notes, а не угадывай.

Для критичных вопросов пусть разметку проверит второй человек. Чтобы понять, насколько разметчики согласны, посчитайте долю совпадений или Cohen’s kappa на выборке из 20–30 вопросов. Если согласие низкое, проблема не в людях, а в неоднозначных вопросах или инструкции.

Сколько вопросов нужно

Стартовый ориентир: 50–100 вопросов. Этого хватает, чтобы видеть крупные регрессии и сравнивать версии. Но размер зависит от того, какие различия вы хотите заметить:

  • Чтобы увидеть падение метрики на 10 пунктов, десятков вопросов обычно хватает.
  • Чтобы различать версии, отличающиеся на 1–2 пункта, нужны сотни вопросов и повторные прогоны.
  • Для срезов (по типам вопросов) в каждом срезе должно быть минимум 5–10 записей, иначе любая цифра шумит.

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

Проверка покрытия кодом

Распределение по типам и сложности проверяйте скриптом, а не на глаз:

import csv, collections

REQUIRED = {"simple", "multi_hop", "no_answer", "ambiguous", "typo", "adversarial", "long"}
MIN_PER_TYPE = 5

def check(path: str) -> None:
    rows = list(csv.DictReader(open(path, encoding="utf-8", newline="")))
    by_type = collections.Counter(r["type"] for r in rows)
    ids = [r["id"] for r in rows]
    assert len(ids) == len(set(ids)), "дубликаты id"
    for t in sorted(REQUIRED):
        n = by_type.get(t, 0)
        status = "ok" if n >= MIN_PER_TYPE else "МАЛО"
        print(f"{t:12} {n:3}  {status}")
    empty = [r["id"] for r in rows if not r["expected_answer"].strip()]
    assert not empty, f"нет эталона: {empty}"
    non_empty = [r for r in rows if r["type"] != "no_answer" and not r["expected_sources"]]
    assert not non_empty, f"нет источников: {[r['id'] for r in non_empty]}"

check("golden.csv")

Запускайте скрипт в CI: он не даст датасету потерять структуру.

Версионирование и загрязнение

  • Версионирование. Храните golden.csv в репозитории и не переписывайте историю. Новые записи добавляйте, а изменение эталона делайте отдельным коммитом с объяснением. Сравнивать версии системы можно только на одной версии датасета.
  • Загрязнение (contamination). Если вопросы датасета попали в промпт, в few-shot примеры или в данные дообучения, метрики завышены. Держите датасет отдельно от того, на чём «настраиваете» систему. Хорошая практика: небольшая закрытая часть, на которой не подбираете промпты.
  • Устаревание. Меняется база знаний — меняются эталоны. Раз в квартал просматривайте датасет и помечайте устаревшие записи.

Ограничения

Даже хороший датасет описывает вчерашние вопросы. Новые типы запросов появятся, и их поймает только мониторинг и пополнение датасета из реальных провалов. Эталоны тоже могут ошибаться: если метрика упорно показывает провал на одном вопросе, проверьте эталон прежде, чем чинить систему.

Что дальше

  • Соберите первые 30 вопросов из реальных логов и разметьте тип каждого.
  • Посчитайте метрики поиска на этом датасете: Recall@K, MRR и NDCG.
  • Подключите датасет к автопроверкам: quality gate в CI.