Привет, Хабр! Все мы наблюдаем стремительное развитие больших языковых моделей (LLM), которые находят широкое применение для решения различных NLP задач, включая создание вопрос-ответных систем и чат-ботов.
В компании АльфаСтрахование мы начали активно использовать LLM для создания умных консультантов и в этой статье хотим поделиться нашим практическим опытом в одном из ключевых аспектов — оценке качества ответов чат-бота.
Несмотря на то, что современные LLM имеют миллиарды параметров, их использование в чистом виде в QA системах может быть неэффективным. Одна из причин — модель изначально обучалась на общедоступных данных и не имела доступа к специфическим или внутренним корпоративным данным компаний. Еще одна из причин – данные обрезаны годом выпуска модели. Также распространенная проблема LLM – галлюцинации: модель что-то «плохо выучила» или вообще не знает.
Чтобы получить надежного AI-помощника для бизнеса, надо бы сначала помочь ему самому генерировать адекватные и корректные ответы: можно подать LLM контекст и просить ее отвечать на основе этого контекста. Таким образом ответ модели будет построен уже не только на базе обученных весов, но и на основе актуальных данных.
Итак, мы хотим, чтобы модель отвечала по информации из нашей базы знаний. Почему бы не подать всю документацию в контекст? Ответ очевиден – размер контекстного окна модели ограничен.
Следовательно, для того чтобы модель генерировала качественный ответ, ей необходим помощник, роль которого может выполнять система, основанная на подходе Retrieval-Augmented Generation (RAG). Про RAG выпущено довольно много статей, поэтому только кратко остановимся на принципе его работы.
Принцип работы RAG

1. Пользовательский ввод
Пользователь вводит вопрос, который обрабатывается системой.
2. Преобразование вопроса в эмбеддинги
Вопрос преобразуется в векторное пространство (эмбеддинги) для последующего семантического анализа или, в случае текстового или шаблонного поиска, обрабатывается на уровне ключевых слов, правил или регулярных выражений. Метод обработки запроса может различаться в зависимости от типа движка (векторный, дословное совпадение, гибридный, графовый, иерархический или другой подход).
3. Поиск релевантной информации
В зависимости от типа движка выполняется:
-
Семантический поиск — на основе эмбеддингов, что позволяет находить релевантную информацию по смысловой близости.
-
Поиск по ключевым словам или шаблонам — для точного совпадения с конкретными словами или регулярными выражениями.
-
Графовый поиск — через граф знаний, который ищет информацию, учитывая связи между сущностями.
-
Иерархический поиск — структурируя информацию по уровням в больших документах, таких как разделы или главы.
-
Иерархический поиск — структурируя информацию по уровням в больших документах, таких как разделы или главы.
4. Извлечение релевантных документов
RAG извлекает куски текста (чанки) из документов, релевантность которых определяется выбранным методом — косинусной близостью между эмбеддингами, точным совпадением по ключевым словам, структурной иерархией или связями в графе знаний.
5. Передача в LLM
Извлеченные фрагменты документов вместе с исходным вопросом передаются в большую языковую модель (LLM), которая генерирует ответ.
6. Генерация ответа
Языковая модель создаёт ответ на основе предоставленной информации и своих собственных знаниях.
7. Ответ выводится пользователю
Возникает вопрос: как оценить качество работы получившейся вопрос-ответной системы?
Традиционные метрики
Можно использовать традиционные метрики для сравнения эталонного и сгенерированного текстов: BLEU, ROUGE, METEOR, BERTscore.
BLEU (Bilingual Evaluation Understudy):
Измеряет точность (precision) n-грамм слов между сгенерированными и эталонными текстами.
где Count — количество n-грамм в переведенном тексте, а max_count_Ref — максимальное количество этих n-грамм в эталонном тексте.
BP – штраф за кратность, c — длина сгенерированного текста, а r — длина эталонного текста по размеру
Итоговая формула:
где w — веса для каждого измерения точности n-грамм, которые часто равны 1/N, предполагая, что каждое измерение равнозначно.
ROUGE (Recall-Oriented Understudy for Gisting Evaluation)
Измеряет полноту (recall) n-грамм слов и наибольшие общие последовательности.
ROUGE-N измеряет количество совпадающих n-грамм между системным выводом и эталонным (или эталонными) текстами. Формула для расчета ROUGE-N представляет собой отношение количества совпадающих n-грамм к общему количеству n-грамм в эталонном тексте (Recall):
где Countmatch — это количество n-грамм, которые появляются и в эталоне, и в предсказании.
ROUGE-L основан на наибольшей общей подпоследовательности (LCS — Longest Common Subsequence). Эта метрика измеряет наибольшую последовательность слов, которая встречается как в выходном тексте модели (системном выводе), так и в эталонном тексте, позволяя словам быть в любом порядке.
где LCS — длина наибольшей общей подпоследовательности между эталонным текстом и текстом, который выдала модель, а len(S) — длина эталонного текста S.
METEOR (Metric for Evaluation of Translation with Explicit ORdering)
Включает в себя полноту, точность и дополнительное семантическое сопоставление на основе стемминга и перефразирования.
Precision (P): Доля слов (униграмм) из сгенерированного текста, которые правильно соответствуют словам в эталонном тексте, с учетом синонимов и совпадений
Recall (R): Доля слов (униграмм) из эталонного текста, которые есть в сгенерированном, с учетом синонимов и точных совпадений.
Penalty for Fragmentation: Штраф, который применяется, если слова в сгенерированном тексте расположены в порядке, отличном от эталонного.
Точность и полнота комбинируются, используя формулу гармонического среднего, в которой вес полноты в 9 раз больше веса точности
BERTScore
Данная метрика использует контекстуализированные эмбеддинги токенов предобученной модели BERT. Она вычисляет семантическую близость двух предложений, суммируя косинусную близость между эмбеддингами их токенов. Далее вычисляется F1 мера по следующим формулам:
где x — это эмбеддинги предсказания, а x̂ — эмбеддинги эталонного текста. Чем больше метрика, тем лучше качество.
Стоит отметить, что ROUGE и BLEU учитывают только совпадающие n-граммы между эталонным и сгенерированным текстом. Соответственно, они не способны учитывать семантическое сходство или использование синонимов. А Метрика METEOR, хотя и частично решает эту проблему, вводя поддержку синонимов и учёт морфологических преобразований, также не всегда адекватно отражает смысловые вариации текста.
Наиболее приближённой к человеческой оценке на сегодняшний день является BERTScore. Она использует эмбеддинги одноименной модели, что позволяет лучше учитывать семантическое содержание, а не только поверхностное совпадение слов. Отсюда можно предположить, что метрики на базе LLM имеют еще больший потенциал для оценки семантической близости текстов.
Свои предположения мы проверили экспериментально. Для этого собрали датасет, содержащий вопросы на тему страхования, эталонные и сгенерированные LLM в сочетании с RAG ответы. Затем мы провели ручную оценку (Human evaluation) сходства эталонных и сгенерированных ответов, далее сравнили распределение получившихся оценок с распределением вышеперечисленных метрик.


Из графиков распределения и корреляции видно, что BERTScore наиболее близок к человеческой оценке, так как использует векторные представления слов для оценки семантической схожести. Это подтверждает гипотезу о том, что нейросетевые метрики лучше соответствуют человеческим оценкам, а значит, существует потенциал использования других LLM для оценки семантической близости текстов.
Далее в качестве эксперимента были взяты три большие языковые модели: популярные GPT-3.5, GPT-4, а также Mistral 7B. Попросили эти модели оценить семантическое сходство текстов с одинаковым промтом. На рисунках ниже представлены результаты распределения и корреляции метрик.


Видно, что все метрики имеют высокую корреляцию с человеческой оценкой. В дальнейшем при оценке ответов LLM и RAG+LLM мы использовали метрику на базе GPT-4o mini.
Эксперимент
Итак, давайте попробуем создать ассистента, способного отвечать на вопросы из вселенной Гарри Поттера. Для начала давайте прогоним вопросы, которые находятся в файле questions.xlsx, через разные LLM: Llama 3.2 3B, Qwen 2 7B и GPT 4o mini, чтобы понять, насколько хорошо модели знакомы с сюжетом книги. В статье будет приведен пример кода только для GPT-4o mini, для других моделей код выложен здесь.
import os import openai def make_req(req_text): openai.api_key = "sk-***" model_list = ["gpt-4","gpt-3.5-turbo","gpt-4-1106-preview","gpt-4o","gpt-4o-mini"] model_name = model_list[4] system_prompt = "Ты эксперт вселенной Гарри Поттера из романа Джоан Роулинг 'Гарри Поттер и философский камень'. Отвечай строго по контексту, не придумывай." messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": req_text} # запрос пользователя ] completion = openai.chat.completions.create( model=model_name, messages=messages ) return completion.choices[0].message.content # Чтение Excel-файла df = pd.read_excel('questions.xlsx') df['gpt4-o-mini'] = '' # Прогон вопросов до RAG for index, row in df.iterrows(): question = row['Вопрос'] answer = make_req(question) df.at[index, 'gpt-4o-mini'] = answer # Сохранение DataFrame обратно в Excel df.to_excel('before_rag.xlsx', index=False)
Стоит отметить, что LLama и Qwen запросто могут вставить в предложение английское слово или его часть.

Далее получим бинарную метрику для сравнения эталонного ответа и сгенерированного и посмотрим на точность ответов моделей.
def make_req(req_text): openai.api_key = "sk-***" model_list = ["gpt-4","gpt-3.5-turbo","gpt-4-1106-preview","gpt-4o", "gpt-4o-mini"] model_name = model_list[4] completion = openai.chat.completions.create(model=model_name, messages=[{"role": "user", "content": req_text}]) return completion.choices[0].message.content def gpt_metrics(df): for index, row in df.iterrows(): df.loc[index, 'metric_llama']= make_req(f"Сравни эталонный ответ 1-{row['Ответ']} и 2- {row['gpt-4o-mini']}. Поставь 1, если семантическая близость ответов >0.7 и 0 в остальных случаях. В ответе укажи только число. Не давай никаких пояснений") df.to_excel('metrics.xlsx', index=False) return df

GPT-4o mini неплохо разбирается во вселенной Гарри Поттера, в отличие от LLama и Qwen.
Теперь давайте попробуем построить простейший RAG при помощи фреймворка llamaindex. В качестве базы знаний будем использовать русскоязычный текст книги Джоан Роулинг «Гарри Поттер и философский камень».
# Глобальные переменные для хранения индекса и query_engine global_index = None query_engine = None # Функция для создания шаблона для запросов Q&A def create_text_qa_template(): # Настройка сообщений для шаблона запроса в формате Q&A chat_text_qa_msgs = [ ChatMessage( role=MessageRole.SYSTEM, content=( "Ты эксперт вселенной Гарри Поттера из романа Джоан Роулинг 'Гарри Поттер и философский камень'. " "Отвечай строго по контексту, не придумывай." ), ), ChatMessage( role=MessageRole.USER, content=( "Контекстная информация приведена ниже.\n" "---------------------\n" "{context_str}\n" "---------------------\n" "Теперь ответь на вопрос с учетом контекста: {query_str}\n" ), ), ] # Создание объекта шаблона с подготовленными сообщениями text_qa_template = ChatPromptTemplate(chat_text_qa_msgs) return text_qa_template # Функция для инициализации модели и настроек def initialize_model(): # Объявляем глобальные переменные для хранения индекса и query_engine global global_index, query_engine # Загрузка модели эмбеддингов из Hugging Face embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large") # Устанавливаем параметры для окна контекста и максимального количества токенов в ответе context_window = 5500 num_output = 256 # Инициализация модели от OpenAI с заданными параметрами from llama_index.llms.openai import OpenAI llm = OpenAI(temperature=0, model="gpt-4o-mini", max_tokens=512) # Настройка параметров для разбиения текста на чанки chunk_size = 900 chunk_overlap = 256 node_parser = SimpleNodeParser.from_defaults(chunk_size=chunk_size, chunk_overlap=chunk_overlap) # Устанавливаем параметры модели и разбиения текста в глобальные настройки Settings.llm = llm Settings.embed_model = embed_model Settings.context_window = context_window Settings.num_output = num_output Settings.node_parser = node_parser # Загружаем документы для RAG из указанной директории documents = SimpleDirectoryReader('./RAG_data').load_data() # Создаем векторный индекс для поиска по документам global_index = VectorStoreIndex.from_documents(documents, embed_model=embed_model) # Создаем шаблон для Q&A text_qa_template = create_text_qa_template() # Настраиваем query_engine с использованием созданного индекса и шаблона для Q&A query_engine = global_index.as_query_engine( similarity_top_k=5, # Задает количество похожих документов для поиска text_qa_template=text_qa_template, return_source_documents=True # Добавляет возможность возвращать контекст документов ) # Запуск инициализации модели initialize_model() def make_rag(question): global query_engine response = query_engine.query(question) answer = response.response return answer
Теперь прогоним наши вопросы через RAG, и затем снова оценим точность.
df = pd.read_excel('before_rag.xlsx') df['RAG_gpt'] = '' # Прогон вопросов через RAG и запись ответов и контекста в DataFrame for index, row in df.iterrows(): question = row['Вопрос'] rag_answer = make_rag(question) df.at[index, 'RAG_gpt] = rag_answer df.to_excel('with_rag.xlsx', index=False)

Видно, что даже простейший RAG улучшил точность ответов на 30 п.п.
При более глубоком анализе выясняется, что RAG демонстрирует более высокую точность при ответах на вопросы, требующие конкретных и детализированных фактов. Это обусловлено тем, что модель эффективно извлекает релевантные данные из базы знаний и использует их для формирования точных ответов на узкие вопросы.



Однако с обобщающей способностью у RAG наблюдаются проблемы, так как LLM в RAG в первую очередь использует данные, полученные из контекста. Поэтому вопросы, требующие агрегирования данных из нескольких источников, могут оказаться непростой задачей для RAG.


Кроме того, в контексте могут оказаться чанки с противоречивой информацией, что так же затрудняет обобщение. К примеру, на вопрос «Как зовут собаку Хагрида» модели дают разные ответы, т.к. в контекст попадает как информация про Клыка, так и про Пушка, который формально тоже является «собакой» Хагрида.

Теперь добавим в нашу базу знаний краткое описание сюжета книги и главных героев. Построим новый RAG и оценим его качество (RAG New).

Такая простейшая доработка повысила качество получившейся системы на 5 п.п.
Выводы
В заключение хотелось бы отметить, что выбор LLM для RAG — дело вкуса. С одной стороны, модели GPT отлично подходят для RAG-систем благодаря своей точности и качеству генерации ответов на русском языке. С другой, их существенными недостатками являются высокая цена и отсутствие возможности локального развертывания, что требует передачи конфиденциальных данных компании.
Альтернативой может быть использование опенсорсных моделей с дообучением на собственных данных для повышения точности и адаптации системы под конкретные задачи.
Также для повышения качества вопрос-ответной системы можно использовать более сложные конструкции RAG. К примеру, ансамбли поисковых движков.
Исходный код и материалы для RAG выложили здесь, чтобы вы могли попробовать сами.
Будем рады комментариям, а также если поделитесь своим практическим опытом в построении и оценки RAG систем.
Авторы статьи @aaniretake@leadsci
ссылка на оригинал статьи https://habr.com/ru/articles/889042/
Добавить комментарий