Golden-датасет для RAG: как собрать 60 вопросов, которые ловят регрессии
Схема датасета, восемь типов вопросов, откуда их брать, как размечать эталоны и согласие разметчиков, как версионировать и не загрязнить датасет. С примером CSV и проверкой покрытия на Python.
71cb47d
Содержание
Коротко
- Golden-датасет — главный актив оценки RAG. Метрики и судья вторичны: без хороших вопросов они мерят не то.
- Запись содержит вопрос, эталонный ответ, эталонные источники, тип вопроса и сложность. Эталонные источники нужны, чтобы считать метрики поиска.
- Покрытие важнее объёма. 60 вопросов по восьми типам полезнее 300 одинаковых.
- Берите реальные вопросы (логи, тикеты поддержки), синтетику используйте как дополнение и обязательно проверяйте человеком.
- Датасет живёт в репозитории рядом с кодом, версионируется и пополняется реальными провалами из эксплуатации.
Зачем отдельный датасет
В разборе четырёх метрик RAG я советовал начинать с 50–100 реальных вопросов. Здесь описано, как их собрать так, чтобы они ловили именно те поломки, которые бывают у RAG. Датасет решает три задачи:
- Сравнивает версии: стало лучше или хуже после смены промпта, модели, чанкинга.
- Диагностирует: по срезам видно, где система проваливается.
- Защищает от регрессии: в 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 нужны почти всегда. Распределение определяйте по рискам: в поддержке банка выше доля провокаций и вопросов про деньги.
Откуда брать вопросы
- Логи и тикеты поддержки. Лучший источник: реальные формулировки, опечатки, сленг. Перед использованием удалите персональные данные.
- Регламенты и документы. Для каждого раздела придумайте вопрос, ответ на который там лежит. Годится, чтобы проверить покрытие базы знаний.
- Интервью с экспертами. Спросите сотрудников, какие вопросы самые частые и самые опасные.
- Реальные провалы. Любой жалобный случай из эксплуатации превращается в запись датасета. Так он растёт осмысленно.
- Синтетика. Генераторы в DeepEval и RAGAS умеют создавать вопросы по документам. Это быстрый способ получить черновик, но не готовый датасет.
Синтетические вопросы обычно слишком «чистые»: они повторяют лексику документа, поэтому retriever находит их легко. Реальные пользователи формулируют иначе. Если оценивать только на синтетике, метрики поиска получатся завышенными.
Разметка эталонов
Эталонный ответ должен быть коротким и проверяемым: не «хороший развёрнутый ответ», а факт или набор фактов. Чем короче и конкретнее эталон, тем стабильнее работают метрики, которые сравнивают с ним ответ.
Инструкция разметчику, которая у меня работает как отправная точка:
- Читай только документы базы, не используй общие знания.
- Если ответа в базе нет, поставь тип
no_answerи опиши желаемое поведение. - Укажи все источники, без которых ответ неполон.
- Не знаешь, как оценивать, пиши в
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.
