Не секрет, что RAG (Retrieval-Augmented Generation) сейчас является распространённой техникой использования Больших Языковых Моделей (LLM) в вопросно-ответных системах. Ну а где есть ML-модели, там есть и оценка качества. О том, как оценивать RAG-модели и автоматизировать этот процесс под свою задачу, вы прочитаете в данной статье.

Рассмотрим стандартный алгоритм RAG:

Схема процесса RAG
Схема процесса RAG

Чтобы получить достаточно универсальные метрики и алгоритмы оценки RAG-системы, выделим общие для всех вариаций алгоритма текстовые данные:

  1. вопросы, которые задаются RAG‑системе;

  2. контексты — фрагменты (чанки) из базы знаний, которые были выбраны для ответа на вопрос;

  3. ответы, которые дала RAG‑система на вопросы.

Классический и лучший с точки зрения качества вариант оценки RAG-систем — человеческая оценка. Но этот метод ресурсозатратный, он не всем подходит. Быстрой и доступной альтернативой могут служить автоматически рассчитываемые метрики, или аспекты оценивания.

Основываясь на выделенных текстовых данных, метрики RAG можно классифицировать по таким аспектам:

  • метрики качества контекста, или насколько правильный для вопроса контекст подобрала RAG система;

  • метрики качества ответа, или насколько верный ответ был дан;

  • метрики фактологической точности ответа для оценки количества галлюцинаций в процессе генерации ответа;

  • метрики ранжирования контекста, или оценка контекста через призму ранжирования фрагментов в RAG‑системе.

Виды аспектов для оценки RAG-системы. Зелёные стрелки означают зависимость компонента RAG
Виды аспектов для оценки RAG-системы. Зелёные стрелки означают зависимость компонента RAG

На место универсальной библиотеки для автоматической оценки RAG-систем претендует инструмент от маленького, но гордого стартапа exploidinggradients — RAGAS.

В свою очередь, RAGAS (Retrieval Augmented Generation Automated Scoring) — это фреймворк для автоматической оценки RAG-систем. Для вашей RAG-системы RAGAS предлагает широкий выбор метрик для оценки ответов и контекстов, полученных от вопросов к вашей базе знаний.При отсутствии вопросов, их можно сгенерировать вместе с эталонными ответами. Основным инструментом, использованным во всех алгоритмах RAGAS-а, являются большие языковые модели со специально подобранными под каждую задачу промптами.

Алгоритмы, которые предоставляет RAGAS, можно разделить на два вида:

  1. алгоритм для генерации синтетических вопросов и ответов по списку документов;

  2. алгоритмы для расчёта метрик по результатам работы RAG‑системы (и эталонных ответов в некоторых случаях).

Про расчёт метрик и алгоритм генерации синтетики можно прочитать в этом блоге, а также в моем выступлении на конференции Data Fest. Обратим внимание на технические аспекты реализации.

Базовые компоненты генерации и оценки RAGAS-а:

  • Generator LLM — большая языковая модель, отвечающая за генерации. С её помощью формируются вопросы для эталонных ответов, выделяются ключевых фразы в формате JSON, переписываются нерелевантные вопросы по обратной связи, делаются переводы.

  • Critic LLM — большая языковая модель, отвечающая за оценку. С её помощью оценивается генерация, формируется обратная связь в формате JSON, рассчитываются метрики.

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

Из этих компонентов складываются классы инструментов:

  • Docstore — хранилище базы знаний.

  • Filters — класс, реализующий оценку качества текстовых данных на разных этапах генерации синтетики;

  • Evolution — класс, реализующий алгоритм получения синтетических данных по фрагменту из базы знаний, в том числе генерация синтетических вопросов и ответов, их переписывание и оценка;

  • Generator — класс, реализующий алгоритм получения синтетических данных;

  • Metric — класс, реализующий метрики оценки RAG‑системы.

Алгоритм генерации синтетики

Алгоритм генерации предполагает, что RAGAS-у предоставляют базу документов в формате langchain или llama_docs, по которой требуется получить набор разносторонних вопросов к базе знаний; фрагменты, релевантные этим вопросам; и эталонные, относительно модели генерации, ответы(если это возможно).

Процесс генерации синтетики выглядит так:

  1. Формирование базы знаний, в которой:

    1. документы разбиваются на фрагменты;

    2. для документов строятся эмбеддинги;

    3. для каждого фрагмента генерируется 3–5 ключевых фраз, которые характеризуют разные аспекты этого фрагмента.

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

    1. Charity (понятность);

    2. Depth (глубина контекста);

    3. Structure (структура повествования);

    4. Relevance (однородность текста относительно тематики повествования).

  3. По случайно выбранным фрагментам и их ключевым вопросам строится вопрос (Simple Question). Далее выполняется процедура, похожая на self‑refine: вопрос оценивается на адекватность (Question Filter), при неудачной генерации вопрос перегенерируется, при дальнейшей неудаче выполняется ресемплинг фрагмента из базы знаний.

  4. Вопросы (Simple Question) могут переписываться для усложнения тестовой выборки. Авторы называют это эволюцией (evolution), на данный момент доступны следующие варианты:

    1. Reasoning Evolution — переписывание вопроса таким образом, чтобы для ответа потребовалось больше рассуждений;

    2. Conditioning Evolution — добавление в вопрос условного элемента;

    3. Multi‑Context Evolution — к уже имеющемуся контексту добавляется ещё один, близкий по эмбеддингу фрагмент, и вопрос переписывается так, чтобы ответ потребовал также информацию и из нового, добавленного во фрагмент контекста;

    4. Conversational Evolution (in progress) — вопрос переписывается в более похожей на пользователя манере.

После каждой такой эволюции переписанный вопрос сравнивается с исходным по «глубине» и «ширине» (Evolution Filter). При необходимости перегенерируется эволюция или исходный вопрос. В ходе генерации вопроса может выполняться несколько эволюций, они образуют древовидную структуру (смотри картинку ниже). При разработке этого алгоритма авторы вдохновлялись Evol-instruct.

  1. Для удачно сгенерированных вопросов модель‑генератор формирует ответ (ground_truth). Конечно, он не обязательно будет «корректным» по своей сути, мы доверяем корректности ответа ровно настолько, насколько доверяем корректности модели‑генератора (Generator LLM).

Возможная вариация дерева эволюций для вопроса «Как застраховать Баню»
Возможная вариация дерева эволюций для вопроса «Как застраховать Баню»

Алгоритм расчёта метрик

Алгоритмы расчёта метрик довольно сильно отличаются друг от друга. Обратим внимание на некоторые метрики, которые могут пригодится каждому.

  • Context Relevancy — метрика релевантности контекста. С помощью промпта, подобранного разработчиками, из контекста выделяются только те предложения, которые были необходимы для ответа на вопрос. Результатом работы метрики является рациональная дробь — доля релевантных для возможного ответа предложений из контекста. Количество предложений в тексте считается с помощью библиотеки pysbt, которая, вообще говоря, не особо работает с русским языком. Вот один из ярких примеров её работы:

    В реализации RAGAS-а используется именно эта конфигурация и, как можно увидеть, с аббревиатурами и переносами строк на русском языке она работает некорректно. Несмотря на то, что именно в этой метрике pysbt используется только для подсчёта количества предложений в контексте, итоговое число получается неверным, что довольно критично. Для корректного использования метрик вы можете в файлах ragas/metrics/_context_relevancy.py (а лучше сразу во всех файлах метрик) заменить использование pysbt на библиотеку razdel, которая успешно справляется с разбивкой русскоязычного текста на предложения.

  • Faithfulness — метрика фактологической точности ответа относительно приведённого контекста. Здесь ответ разбивается на предложения (опять через pysbt), из которых строится промпт для выделения «утверждений» (claims) — некоторых атомарных, по мнению модели, фактов. Пример разбивки на такие факты приведён ниже:

    Далее полученные утверждения и контекст подаются другому промту, который для каждого утверждения проверяет его релевантность относительно контекста. Доля верных утверждений и является метрикой фактологической точности ответа.

  • Answer Correctness (не путать с Answer relevancy) — её название переводится как метрика корректности ответа, но возможно более верным будет назвать ее метрикой соответствия эталонному ответу. Так как эталонный ответ может не являться по факту верным, особенно если он сгенерирован большой языковой моделью, то и эта метрика не отражает то, насколько верный ответ получился. Ориентирование на такую метрику даёт дистилляцию ответов вашей RAG‑системы ответами Generator_llm, стоит помнить об этом. Для расчёта этой метрики из ответа и эталонного ответа мы выделяем утверждения, как и в метрике выше выделяем утверждения. Далее оба этих списка подаются на вход промпту, который должен из них сгенерировать три списка по этой классификации:

    • TP (true positive): утверждение из ответа подтверждается одним и более утверждением из эталонного ответа;

    • FP (false positive): утверждение из ответа напрямую не подтверждается ни одним из утверждений из эталонного ответа;

    • FN (false negative): утверждение находится в эталонном ответе, но не присутствует в ответе.

Далее из размеров этих список можно получить уже знакомые из табличных метрик числа TP, FP и FN, на основе которых можно вычислить F1-метрику. В некоторых случаях её может быть недостаточно, поэтому ещё одним из компонентов для расчёта является близость эмбеддингов ответа и эталонного ответа (простое косинусное расстояние). Взвешенная сумма(соотношение весов F1-метрики и метрики близости можно задать) этих чисел и является метрикой «верности» ответа.

Как запустить RAGAS

Не каждая большая языковая модель способна качественно выполнить все эти поставленные перед ней задачи. Изначально разработчики рассчитывали, что эта библиотека будет надстройкой над моделями компании OpenAI, поэтому весь промпт-инжиниринг и замеры качества был выполнен на этой модели. Сейчас команда активно развивает возможности настройки пользовательских моделей и других отдельных компонентов библиотеки. К сожалению, не все возможные варианты можно найти в документации, особенно на момент написания этой статьи. Здесь будут приведены варианты настройки моделей, которые помогут вам запустить RAGAS на вашей любимой LLM.

На что стоит обратить внимание при запуске RAGAS любым способом:

  • Технические параметры запускаемых моделей регулируются конфигурацией RunConfig(), по умолчанию указаны следующие параметры:

    • timeout: int = 60

    • max_retries: int = 10

    • max_wait: int = 60

    • max_workers: int = 16

    • thread_timeout: float = 80.0

    Соответственно, если ваши модели технически не соответствуют заявленным параметрам, то алгоритм будет вылетать с ошибкой, чаще всего связанной с асинхронностью. Чтобы не передавать свою конфигурацию в каждую функцию и модель, можно один раз объявить конфигурацию (объект класса RunConfig) один раз с нужными вам параметрами.

  • Если ваш параметр max_retries не маленький, а модель‑генератор не очень сильная, то вам предстоит сделать много перегенераций, ну очень много. Берегите бюджет, следите за количеством запросов к моделям.

Langchain-совместимая модель

Если модель, которую вы хотите использовать, доступна в библиотеке Langchain, то вам крупно повезло: вы можете запустить её из коробки. Здесь будет приведена реализация на примере GigaChat и GigaEmbeddings. Также зададим параметр distribution, который отвечает за долю тех или иных эволюций, происходящих в генерируемых вопросах.

В этой реализации Gigachat с разными температурами выполняет как роль генератора, так и роль критика.

from langchain.chat_models.gigachat import GigaChat
from langchain_community.embeddings.gigachat import GigaChatEmbeddings
from ragas.testset.generator import TestsetGenerator
import os
from langchain.document_loaders import TextLoader
import pandas as pd
from ragas.llms import LangchainLLMWrapper
from ragas.embeddings import LangchainEmbeddingsWrapper
from datasets import Dataset

embeddings = LangchainEmbeddingsWrapper( GigaChatEmbeddings(base_url='https://gigachat.devices.sberbank.ru/api/v1',
        auth_url='https://ngw.devices.sberbank.ru:9443/api/v2/oauth',
        credentials=os.environ['YOUR_CREDS'],
        scope='GIGACHAT_API_PERS',
        verify_ssl_certs=False))

generator_llm =LangchainLLMWrapper(GigaChat(
        base_url='https://gigachat.devices.sberbank.ru/api/v1',
        auth_url='https://ngw.devices.sberbank.ru:9443/api/v2/oauth',
        credentials=os.environ['YOUR_CREDS'],
        scope='GIGACHAT_API_PERS',
        model="GigaChat-Pro",
        timeout=60.0,
        verbose=True,
        verify_ssl_certs=False,
        temperature=1.05,
        top_p=0.36,
        profanity=False,
        max_tokens=200,
    ))
critic_llm=  LangchainLLMWrapper(GigaChat(
        base_url='https://gigachat.devices.sberbank.ru/api/v1',
        auth_url='https://ngw.devices.sberbank.ru:9443/api/v2/oauth',
        credentials=os.environ['YOUR_CREDS'],
        scope='GIGACHAT_API_PERS',
        model="GigaChat-Pro",
        timeout=60.0,
        verbose=True,
        verify_ssl_certs=False,
        temperature=1e-8,
        profanity=False,
        max_tokens=200,
    ))

dataframe = pd.read_csv(os.environ['PATH TO YOUR DATAFRAME'])
dataframe['contexts'] = [x for x in dataframe['document']]
loader = DataFrameLoader(dataframe, page_content_column="YOUR CONTEXTD COLUMN")
df = loader.load()

generator = TestsetGenerator.from_langchain(
   generator_llm, 
    critic_llm,
    embeddings,
    docstore
)

testset = generator.generate_with_langchain_docs(df, TEST_SIZE, raise_exceptions=False, with_debugging_logs=True, is_async=False)
testset.to_pandas

Hugging face via VLLM

Весьма насущным вопросом является вопрос запуска RAGAS-a на моделях из Hugging face. Здесь будет предложен метод запуска на примере модели Command-R. Она запускается через VLLM (список доступных моделек), а используется через ChatOpenAI, обернутая в обёртку из Langchain. Звучит мудрёно, но реализация весьма изящная:

Команда для запуска VLLM:

export HF_TOKEN='*****************'
python -m vllm.entrypoints.openai.api_server --model CohereForAI/c4ai-command-r-v01 --tensor-parallel-size 1 --gpu-memory-utilization 1

Токен Hugging Face может, и не понадобится, это зависит от используемой модели.

Стоит отметить, что VLLM работает исключительно под ОС Linux и Python 3.8-3.11. Кроме того, на момент написания этой статьи в ней не реализованы некоторые библиотеки для квантизации типа bitsandbytes — квантизованную Command-R таким способом на данный момент запустить не представляется возможным

from langchain_openai import ChatOpenAI
from ragas.llms import  LangchainLLMWrapper

inference_server_url = "http://localhost:8000/v1"

# create vLLM Langchain instance
chat = ChatOpenAI(
    model="CohereForAI/c4ai-command-r-v01",
    openai_api_key="no-key",
    openai_api_base=inference_server_url,
    max_tokens=1024,
    temperature=0.3,
)
# use the Ragas LangchainLLM wrapper to create a RagasLLM instance
vllm =  LangchainLLMWrapper(chat)

Далее можно пробросить API через ChatOpenAI и обернуть в RAGAS-овский Langchain-совместимый класс.

При использовании пользовательских не Langchain-моделей требуется явно определить модели для промежуточных структур: хранилищ базы знаний (здесь важно объявить модель для выделения ключевых фраз) и структуры эволюций вопросов (и всех входящих в них фильтров). При желании можно переопределить промпт для фильтров или иных инструментов.

from ragas.testset.generator import TestsetGenerator
from ragas.testset.evolutions import simple, reasoning, multi_context, conditional
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
import os
from ragas.testset.extractor import KeyphraseExtractor
from langchain.text_splitter import TokenTextSplitter
from langchain.document_loaders import TextLoader
from ragas.testset.evolutions import ComplexEvolution
import pandas as pd
from ragas.run_config import RunConfig

from ragas.testset.filters import QuestionFilter, EvolutionFilter, NodeFilter
from ragas.llms import LangchainLLMWrapper
from ragas.testset.docstore import InMemoryDocumentStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
from ragas.embeddings import LangchainEmbeddingsWrapper
from ragas.testset.prompts import (
    context_scoring_prompt,
    evolution_elimination_prompt,
    filter_question_prompt,
)

generator_llm =  vllm
critic_llm =  vllm
embeddings= LangchainEmbeddingsWrapper(vllm_embeddings)

qa_filter = QuestionFilter(critic_llm, filter_question_prompt)
node_filter = NodeFilter(critic_llm, context_scoring_prompt=context_scoring_prompt)
evolution_filter = EvolutionFilter(critic_llm, evolution_elimination_prompt)
distributions = {
    simple: 0.5,
    reasoning: 0.25,
    conditional: 0.25
}
splitter = RecursiveCharacterTextSplitter(chunk_size=250, chunk_overlap=20)
keyphrase_extractor = KeyphraseExtractor(llm=generator_llm)

docstore = InMemoryDocumentStore(
    splitter=splitter,
    embeddings=embeddings,
    extractor=keyphrase_extractor
)

generator = TestsetGenerator.from_langchain(
   generator_llm, 
    critic_llm,
    embeddings,
    docstore
)
for evolution in distributions:
    evolution.generator_llm = generator_llm
    evolution.question_filter = qa_filter
    evolution.node_filter = node_filter
    evolution.docstore = docstore
    evolution.evolution_filter = evolution_filter

testset = generator.generate_with_langchain_docs(df, TEST_SIZE, raise_exceptions=False, with_debugging_logs=True, is_async=False)

Теперь можно пользоваться моделью из Hugging Face.

BaseRagasLLM

Самый болезненный, скудно описанный и самый открытый для манёвров способ вписать свою модель в RAGAS. Он подразумевает создание класса, наследующегося от абстрактного класса BaseRagasLLM и реализующего асинхронный и синхронные методы генерации результата по промпту:

  • agenerate_prompt — асинхронная генерация по списку промптов;

  • generate_prompt — синхронная генерация по списку промптов;

  • generate_text — генерация по одному промпту;

  • agenerate_text — асинхронная генерация по одному промпту.

Сущности, подающиеся на вход (PromptValue), и возвращаемые функцией значения (LLMResult) — Langchain-совместимые. Из-за отсутствия документации, внятного описания, а также тестирования, на пути использования этого подхода может быть много подводных камней, поэтому его сложно рекомендовать.

Эпилог

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

Если вы хотите присоединиться на нашей команде и решать интересные задачи в машинном обучении, пишите сюда.

Комментарии (1)


  1. AlexandreFrolov
    26.07.2024 08:25

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