Привет, меня зовут Вова Ловцов. Я data science инженер, работаю в команде DS Core в Cloud.ru, где мы занимаемся разработкой агентов, RAG-систем и развитием AI-направления в целом.
Недавно мы запустили AI-помощника, который не только отвечает на вопросы по документации, разворачивает виртуальные машины и настраивает мониторинг за пользователей, но и помогает с SRE и FinOps. Под капотом это мультиагентная система, и один из ее ключевых компонентов — это RAG (Retrieval-Augmented Generation). Именно он отвечает за поиск информации и формирование понятных ответов.
Как понять, что RAG работает хорошо? Как его измерить, улучшить и выбрать лучшую конфигурацию? Обычные метрики вроде BLEU или ROUGE не всегда отражают качество ответа с точки зрения пользователя. Поэтому мы озадачились поиском автоматизированного и воспроизводимого решения и в итоге выбрали RAGAS — open-source инструмент для оценки RAG-систем. Но оказалось, что «из коробки» он работает далеко не идеально (а иногда и вообще не работает).
В этой части кратко расскажу про подходы к оценке RAG, наш выбор исходя из внутренних особенностей и опишу, как под капотом RAGAS представляет данные. А в продолжении — как RAGAS осуществляет непосредственную генерацию и оценку, какие проблемы встретили на пути применения этой библиотеки и что придумали, чтобы их решить.

Зачем и как оценивать RAG и что решили мы
RAG-системы сегодня повсюду — от чат-ботов до поисковых ассистентов. Но они не просто про вопрос-ответ. Качество работы и финальных ответов зависит от каждого из этапов, в базовом исполнении это:
Поиск релевантного контекста (retrieval),
Генерация ответа на основе этого контекста (generation).
И если где-то происходит сбой, например, система не может найти нужные документы или генерирует вымышленный ответ, пользователь быстро потеряет доверие. Поэтому определить, на каком из шагов произошла ошибка — важная часть поддержки сервиса.
Впрочем, современные RAG-системы зачастую содержат расширения, например:
Рерайтинг запроса пользователя.
Семантический поиск контекста (retrieval).
Полнотекстовый поиск контекста (retrieval).
Реранжирование фрагментов контекста (retrieval/reranking).
Генерация ответа на исходный запрос (generation).
Рефлексия и корректировка ответа (generation).
Так что при необходимости качество можно оценить после каждого шага поиска и генерации.
Вот несколько основных подходов к оценке RAG:
Классические метрики NLP (BLEU, ROUGE) и ранжирования (recall@k, ndcg). Эти метрики зачастую требуют Ground Truth. К тому же не всегда хорошо коррелируют с человеческой оценкой.
Ручная разметка — эксперты оценивают ответы. Точно, но медленно, дорого и не масштабируется.
LLM as a judge — оценка ответов с помощью другой LLM. Сравнительно быстро и автоматизируемо, но требует качественных промптов и метрик.
Помимо метрик, необходимо также понять, где мы будем брать данные для тестирования, есть следующие пути:
Найти готовые данные - скорее всего будут довольно общими и не подойдут под конкретную документацию.
Ручная генерация данных: берем эксперта/экспертов в вашей документации и ставим задачу придумать вопросы, найти к ним релевантный контекст и ответ.
Сгенерировать данные с помощью LLM: такой подход позволяет автоматизировать генерацию данных, при этом можно проводить множественные эксперименты в короткие сроки. Генерация может быть простой (LLM формулирует вопросы и ответы по каждому чанку, например) или с дополнительной логикой (моделируем связи между разными частями документации, разные типы вопросов и сценарии взаимодействия пользователя с документацией)
Безусловно, и в метриках, и в генерации данных мы выбрали автоматизацию, поскольку необходимо провести множественные эксперименты с большим набором данных и связок RAG. Поскольку документация часто обновляется, эксперименты должны быть повторяемыми и выполняться регулярно, из-за чего постоянное использование ручного труда становится затруднительным.
Тем не менее для проверки корректности результатов можно ограниченно осуществить ручную валидацию: провести руками оценку выборки результатов и сравнить тенденции автоматизированных и ручных метрик.
Почему мы выбрали RAGAS
После анализа альтернатив (включая DeepEval, TruLens, LangChain Evals) мы остановились на RAGAS — open-source библиотеке, которая позволяет как генерировать данные для тестирования, так и оценивать их с помощью LLM. К преимуществам RAGAS можно отнести:
самостоятельно разделяет документы на чанки;
формирует различные типы связей между документами и чанками;
использует разные стили, типы вопросов;
строит граф знаний и на его основе генерирует множество разнообразных сценариев вопросов.
А вся эта функциональность в совокупности помогла бы нам генерировать разнообразные вопросы с высоким покрытием документов базы знаний, причем в автоматизированном режиме.
Граф знаний: что такое и почему он для нас важен
Одна из ключевых концепций RAGAS — граф знаний, который мы получаем из входных документов, применяя к ним набор трансформаций. Весьма удобно, что, хоть пайплайн трансформаций и дефолтный, их можно переопределить и передать в метод построения графа, то есть процесс гибкий. При этом набор готовых классов для пайплайна также уже предоставлен, остается только настроить параметры.
Немного расскажу, как всё это устроено.

Среди трансформаций у нас есть экстракторы, которые извлекают данные — по сути выполняют задачу feature extraction. Из наших текстов извлекаются заголовки, эмбеддинги, сущности, темы и т. д.
Полный список экстракторов:
HeadlinesExtractor(LLMBasedExtractor)
EmbeddingExtractor(Extractor)
NERExtractor(LLMBasedExtractor)
ThemesExtractor(LLMBasedExtractor)
KeyphrasesExtractor(LLMBasedExtractor)
SummaryExtractor(LLMBasedExtractor)
TitleExtractor(LLMBasedExtractor)
Также есть один сплиттер — HeadlineSplitter. Как можно догадаться по названию, он не будет работать, если на предыдущем шаге не определили HeadlinesExtractor, ведь его задача — побить документ на чанки, основываясь на заголовках, которые были найдены. Также есть метод ограничения длины самого чанка количеством токенов.
Построение связей. У нас есть 4 (на самом деле 3, потому что два из них есть одно и то же) класса для построения связей: косинусная связь (для суммаризации), перекрытие сущностей и jaccard similarity.
На каждом этапе можно применять трансформацию не ко всем элементам нашего графа, а к определенным подвыборкам. Есть и соответствующие фильтры: только к чанкам, только к документам и к документам с определенной длиной. А еще — CustomNodeFilter, о котором чуть позже.
Пример нашего пайплайна
Как выглядит дефолтный pipeline? Первый шаг — HeadlineExtractor, который применяется к документам длиной не меньше 500 токенов. Потом мы разбиваем эти документы на чанки с помощью HeadlineSplitter, берем summary и применяем CustomNodeFilter. Затем берем эмбеддинги от summary, извлекаем темы из chunk, извлекаем сущности из chunk, строим косинусные связи между документами и строим связи по перекрытию сущностей.
Разберем на примере. У нас есть четыре абстрактных документа, один из которых меньше, чем 500 токенов.
Шаг первый: мы получили заголовки (headlines). Первый слева документ меньше 500 токенов, к нему данная трансформация не применяется (он слишком короткий)

Шаг второй: по этим headlines мы побили документы на чанки. Допустим, что у третьего слева документа headlines не найдены (пустой список) и он не превышает максимально ограничение по длине, поэтому чанков у него нет.

Шаг третий: мы берем summary у документов. Вот здесь как раз начинает работать CustomNodeFilter. В чем его идея? Он смотрит на summary родительского документа и на текущий chunk, а затем проверяет — есть ли какая-то связь или нет. Если нет — чанк удаляется. Такой фильтр позволяет удалить из рассмотрения, например, стандартные элементы навигации (меню, footer).

Шаг четвертый: извлекаем themes и NER из чанков, а также получаем эмбеддинги summary документов.

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

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