Как генерировать synthetic датасеты для RAG evaluation?

Краткий тезис

Синтетические датасеты (synthetic datasets) — это искусственно созданные пары «запрос — релевантный контекст — ожидаемый ответ», которые позволяют оценивать RAG-систему без дорогой ручной разметки. Основной подход состоит из пяти шагов: взять корпус документов, с помощью LLM сгенерировать по ним вопросы, проверить качество вопросов (self-consistency, человеческая валидация), сгенерировать ground-truth ответы и обогатить датасет разнообразием (перефразирование, усложнение запросов). Инструменты вроде Ragas и Evol-Instruct автоматизируют этот процесс, а типичный размер качественного датасета — 500–2000 примеров.


1. Что такое synthetic датасеты и зачем они нужны

Synthetic датасет — это набор искусственно сгенерированных данных, имитирующий реальные пользовательские запросы вместе с эталонными ответами (ground truth). В контексте RAG он используется для оценки (evaluation) качества поиска и генерации, а также для fine-tuning компонентов системы.

Зачем нужен synthetic датасет

  • Скорость и масштаб: evaluation|ручная разметка 1000 примеров может занять недели, а generation|синтетическая генерация — часы.
  • Покрытие редких сценариев: LLM может сгенерировать сложные, многошаговые или редкие запросы, которые сложно собрать от пользователей.
  • Контролируемое разнообразие: можно менять стиль, сложность, язык, тип запроса (фактологический, сравнительный, аналитический).
  • Отсутствие приватности: данные генерируются из открытого корпуса, не содержат личной информации.

Ограничение: синтетические данные могут не полностью отражать распределение реальных запросов, поэтому их обычно дополняют небольшой ручной выборкой (human-in-the-loop).


2. Шаг 1: Подготовка корпуса документов

Исходные данные — это корпус документов (или чанков), которые RAG-система использует как базу знаний.

Требования к корпусу

  • Документы должны быть чистыми: удалены дубликаты, шум, лишние метаданные.
  • Желательно нормализовать формат: единый тип (текст, Markdown, HTML).
  • Если корпус большой, можно семплировать репрезентативную подвыборку (например, 10% случайных документов или кластеризовать по темам).

Пример подготовительного кода (Python):

import pandas as pd

# Предположим, у нас есть список чанков
chunks = pd.read_parquet("my_chunks.parquet")
# Фильтрация коротких чанков
chunks = chunks[chunks['text'].str.len() > 100].reset_index(drop=True)
print(f"Отобрано {len(chunks)} чанков")

3. Шаг 2: Генерация вопросов на основе документов

Основная идея: для каждого чанка (или группы чанков) LLM генерирует один или несколько вопросов, на которые можно ответить только из этого чанка.

Методы генерации вопросов

МетодОписаниеПлюсыМинусы
Один вопрос на чанкLLM просят: «Задайте один вопрос, ответ на который содержится в данном тексте»Простота, высокая покрываемостьМожет быть тривиальным
Несколько вопросов раундамиДля одного чанка генерируется 3–5 вопросов разной сложностиБольше разнообразияРиск повторений
На основе сценариевОпределяются типы вопросов (факт, сравнение, вывод) и генерируются целенаправленноЛучше покрывает разные навыкиСложнее в реализации

Пример промпта

Ты — эксперт по оценке RAG. По данному тексту сгенерируй вопрос, на который можно ответить, используя только этот текст. Вопрос должен быть чётким, недвусмысленным и не содержать ответа внутри себя. Ответь в формате: {"question": "...", "context_index": ...}.
Текст: {chunk}

Важно: в вопрос не должен вкладываться ответ. Иначе evaluation станет бессмысленным.


4. Шаг 3: Валидация качества вопросов (self-consistency и человек)

Не все сгенерированные вопросы будут хорошими. Нужен этап фильтрации.

Self-consistency — техника, при которой один и тот же вопрос подаётся LLM несколько раз (с разными температурами). Если ответы совпадают по сути (или модель отвечает правильно, используя тот же чанк), вопрос считается валидным. В противном случае — отбрасывается.

Человеческая валидация: небольшая выборка (50–100 вопросов) проверяется аннотатором. Можно настроить классификатор для автоматической отбраковки явно некачественных (например, слишком общих вопросов или вопросов, ответ на которые не в чанке).

Критерии отбраковки

  • Вопрос не имеет однозначного ответа в чанке.
  • Вопрос содержит ответ или подсказку.
  • Явная грамматическая/стилистическая ошибка делает вопрос непонятным.
  • Вопрос дублирует уже имеющийся (для этого используют эмбеддинг-сходство, например, cosine > 0.95).

5. Шаг 4: Генерация ground-truth ответов

После того, как отобраны качественные вопросы, для каждого вопроса нужно создать эталонный ответ (ground truth).

Способы

  1. Дословный фрагмент из документа: из чанка извлекается предложение, содержащее ответ. Подходит для фактологических вопросов.
  2. Ответ сгенерированный LLM: LLM формулирует ответ на основе чанка, но без использования внешних знаний. Важно указать промпт: «Ответь строго на основе данного текста».
  3. Аннотация человеком: для критически важных сценариев (например, медицинские или юридические) человек пишет эталон.

Пример генерации с помощью LLM

from openai import OpenAI

client = OpenAI()
def generate_ground_truth(question, chunk):
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": "Ответь на вопрос, используя только предоставленный текст. Не добавляй знания извне."},
            {"role": "user", "content": f"Текст: {chunk}\nВопрос: {question}"}
        ]
    )
    return response.choices[0].message.content

Важно: ground truth должен быть полным и корректным относительно документа. Если ответ состоит из нескольких фактов, нужно их все включить.


6. Шаг 5: Добавление разнообразия (Evol-Instruct, парафраз и сложные запросы)

Базовый датасет, где каждому вопросу соответствует ровно один чанк, слишком прост. Реальные пользовательские запросы бывают:

  • многошаговые (требуют объединения нескольких чанков);
  • с неявной информацией (нужна перефразировка);
  • с негативными формулировками («почему не работает?»).

Evol-Instruct — техника автоматического усложнения инструкций с помощью LLM. Изначально был предложен для instruction tuning, но отлично работает для RAG.

Пример применения к RAG

  1. Возьмите базовый вопрос (простой фактологический).
  2. Попросите LLM переписать его так, чтобы ответ требовал анализа двух документов или логического вывода.
  3. Отфильтруйте вопросы, которые уже не имеют ответа в корпусе (проверка через retrieval).

Парафразирование: сгенерировать 3–5 вариантов одного вопроса с разными формулировками (синонимы, инверсия подлежащего, вопросительные слова).


7. Инструменты: Ragas и другие

Основные инструменты для создания synthetic датасетов:

ИнструментОписаниеОсобенности
Ragas (библиотека Python)Содержит модуль testset_generatorПоддерживает генерацию вопросов, ground truth, метрики качества
Evol-Instruct (от Microsoft)Алгоритм усложнения через LLMИспользуется в WizardLM, адаптируется под RAG
LangChain + LLMКастомная цепочка промптовГибкость, можно комбинировать с собственными правилами
GPT-4 / Claude APIПрямая генерация через APIСамый простой способ, но дорогой

Пример использования Ragas:

from ragas.testset.generator import TestsetGenerator
from ragas.testset.evolutions import simple, reasoning, multi_context
from langchain_openai import ChatOpenAI, OpenAIEmbeddings

generator = TestsetGenerator(
    llm=ChatOpenAI(model="gpt-4"),
    embedding_model=OpenAIEmbeddings(),
    knowledge_set=documents  # список LangChain Document объектов
)

testset = generator.generate(
    test_size=500,
    distributions={simple: 0.5, reasoning: 0.3, multi_context: 0.2}
)

# testset — это список TestSample: question, contexts, ground_truth

8. Размер датасета и баланс

Оптимальный размер synthetic датасета для RAG evaluation — 500–2000 примеров. Меньше 500 — недостаточно статистической значимости, больше 2000 — дорого и избыточно при разумном доверительном интервале.

Баланс распределения:

  • Простые вопросы (50%) — фактологические, ответ в одном чанке.
  • Вопросы на рассуждение (30%) — требуют объединения двух-трёх чанков.
  • Сложные вопросы (20%) — многошаговые, с неявной информацией, или requiring multi-hop reasoning.

Важно: распределение должно имитировать реальное использование, иначе метрики будут смещены.


9. Оценка качества самого synthetic датасета

Прежде чем использовать датасет для RAG evaluation, нужно убедиться, что он качественный. Метрики:

МетрикаОписаниеКак измерять
Answer RelevancyСоответствует ли вопрос ответу (ground truth)С помощью LLM-as-judge (например, Ragas answer_relevancy)
FaithfulnessНасколько ответ точен относительно контекстаПроверка фактов из контекста
CoverageКакая доля чанков корпуса упомянута хотя бы в одном вопросеПодсчёт уникальных context_index
DiversityЛексическое разнообразие вопросов (type-token ratio, тематическая кластеризация)Использовать TF-IDF, BERTopic

Если датасет содержит много тривиальных вопросов, его надо дообогатить (вернуться к шагу 5).


10. Плюсы и минусы synthetic датасетов

ПлюсыМинусы
Быстрая итерация (цикл «генерируй — оценивай — улучшай»)Возможен дрейф распределения (distribuition shift) от реальных запросов
Низкая стоимость (по сравнению с ручной разметкой)Зависимость от LLM-модели: слабая модель генерирует плохие вопросы
Легко масштабировать до тысяч примеровТрудности с оценкой субъективных ответов (мнения, рекомендации)
Можно контролировать сценарии (негативные, adversarial)Риск переобучения на синтетические паттерны

Рекомендация: использовать synthetic датасет как основной, но дополнительно собрать 100–200 реальных запросов от пользователей и добавить их в evaluation set.


11. Пример полного пайплайна (Ragas + Evol-Instruct)

Ниже приведён псевдокод, объединяющий описанные шаги:

from ragas.testset.generator import TestsetGenerator
from ragas.testset.evolutions import simple, reasoning, multi_context
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 1. Загрузка и чанкинг документов
loader = TextLoader("docs.txt")
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
chunks = splitter.split_documents(documents)

# 2. Настройка генератора
generator = TestsetGenerator(
    llm=ChatOpenAI(model="gpt-4", temperature=0.7),
    embedding_model=OpenAIEmbeddings(),
    knowledge_set=chunks
)

# 3. Генерация 1000 примеров с распределением эволюций
testset = generator.generate(
    test_size=1000,
    distributions={simple: 0.4, reasoning: 0.4, multi_context: 0.2}
)

# 4. Фильтрация по self-consistency (упрощённо)
#   (в Ragas этот этап встроен в generate)

# 5. Сохранение
import json
with open("rag_synthetic_testset.json", "w") as f:
    json.dump([sample.dict() for sample in testset], f, indent=2)

Пет-проект для закрепления

Задача: разработать synthetic датасет для оценки RAG-системы, которая отвечает на вопросы по документации вымышленного продукта.

Инструменты: Python, Ragas, OpenAI API, набор Markdown-файлов (10–20 страниц).

Шаги:

  1. Собрать тексты (или использовать датасет ms_marco как готовый корпус).
  2. Разбить на чанки (500–1000 символов).
  3. Сгенерировать 300 вопросов с помощью Ragas (простые + многошаговые).
  4. Вручную проверить 30 случайных вопросов на корректность.
  5. Создать ground truth для каждого вопроса (используя LLM или извлечение).
  6. Оценить полученный датасет метрикой answer_relevancy и faithfulness через Ragas.
  7. Сравнить распределение вопросов с десятью реальными вопросами от коллег.

Ожидаемый результат: готовый JSON-файл с полями question, contexts, ground_truth, а также отчёт о качестве датасета (таблица метрик). Вы научитесь видеть, какие типы вопросов покрыты, а какие — нет, и сможете улучшать датасет итеративно.


Связь с другими вопросами

ВопросТема
5Как оценивать качество retrieval в RAG?
6Как оценивать качество генерации (faithfulness и др.)?
10Что такое Self-RAG и когда его использовать?
12Как организовать Human-in-the-loop для RAG?
15Как готовить данные для fine-tuning RAG-компонентов?
20Data augmentation для LLM: методы и ограничения

Навигация