RAG (Retrieval Augmented Generation) — простое и понятное объяснение
Меня все время спрашивают, что такое RAG (в контексте больших языковых моделей) и я все время хочу дать ссылку на статью на habr, где бы простыми словами, но тем не менее детально была бы изложена концепция RAG (Retrieval Augmented Generation), включая инструкцию, как этого «зверя» реализовать. Но такой статьи все нет и нет, в итоге решил не дожидаться и написать самостоятельно.
Если в статье что‑то непонятно или есть какие‑то аспекты RAG, которые нужно раскрыть, не стесняйтесь, пишите комментарии, все разжую и дополню при необходимости. Ну все, поехали…
RAG (Retrieval Augmented Generation) — это метод работы с большими языковыми моделями, когда пользователь пишет свой вопросы, а вы программно к этому вопросу «подмешиваете» дополнительную информацию из каких‑то внешних источников и подаете все целиком на вход языковой модели. Другими словами вы добавляете в контекст запроса к языковой модели дополнительную информацию, на основе которой языковая модель может дать пользователю более полный и точный ответ.
Самый простейший пример:
Пользователь спрашивает модель: «какой сейчас курс доллара?»
Очевидно, что языковая модель понятия не имеет какой СЕЙЧАС курс доллара, эту информацию она должна откуда‑то получить, чтобы ответить на вопрос.
Что нужно сделать? Правильно, открыть первую ссылку в Google по запросу «курс доллара к рублю» и содержимое страницы добавить к вопросу пользователя, чтобы LLM могла правильно ответить на вопрос используя эту информацию.
Чуть более сложный пример:
Допустим вы делаете автоматического оператора технической поддержки на основе LLM. У вас есть база знаний вопросов и ответов или просто подробное описание функциональности.
Самое плохое, что может прийти вам в голову, это дообучить модель на вашу базу знаний вопрос‑ответ. Почему это плохо? Потому что база знаний будет постоянно меняться и постоянно дообучать модель это дорого и не эффективно.
RAG вам в помощь. В зависимости от того, какой вопрос задал пользователь ваша RAG система должна найти соответствующую статью в базе знаний, и подать на вход LLM не только вопрос пользователя, но и релевантную запросу часть содержимого базы знаний, чтобы LLM могла сформировать правильный ответ.
Таким образом фраза Retrieval Augmented Generation как нельзя более точно описывает суть происходящего:
Retrieval — поиск и извлечение релевантной информации. Часть системы, которая отвечает за поиск и извлечение информации, так и называют — ретривер (retriever).
Retrieval Augmented — дополнение запроса пользователя найденной релевантной информацией.
Retrieval Augmented Generation — генерация ответа пользователю с учетом дополнительно найденной релевантной информации.
А теперь самый главный вопрос, который мне задают, когда я вот на таком примитивном уровне объясняю концепцию RAG: ну и что тут сложного? Ищешь кусок данных и добавляешь его в контекст, получаешь PROFIT! Но дьявол как всегда в деталях и самые первые, лежащие на поверхности «детали» с которыми сталкивается человек, вознамерившийся «запилить RAG за один день» следующие:
Нечеткий поиск — просто взять запрос пользователя и найти по точному соответствию все куски из базы знаний не получится. Каким алгоритмом реализовать поиск, чтобы он искал релевантные и только релевантные «куски» текста?
Размер статей из базы знаний — какого размера «куски» текста надо давать LLM, чтобы она по ним формировала ответ?
А если в базе знаний нашлось несколько статей? А если они большие? Как их «обрезать», как комбинировать, может быть сжать?
Это самые базовые вопросы, с которыми сталкивается любой, начинающий создатель RAG. К счастью, на данный момент есть общепринятый подход, с которого нужно начинать создание RAG и далее, как в анекдоте, полученный «паровоз» допиливать напильником, чтобы получился нужный вам «истребитель».
Итак из чего состоит изначальный, базовый «паровоз» под названием RAG. Основной алгоритм его работы следующий:
Вся база знаний «нарезается» на небольшие куски текста, так называемые chunks (чанки по‑русски). Размер этих chunks может варьироваться от нескольких строк, до нескольких абзацев, т. е. примерно 100 до 1000 слов.
Далее эти чанки оцифровываются с помощью «эмбеддера» и превращаются в эмбеддинги или другимими словами в вектора, некоторые наборы чисел. Считается, что в этих числах зашит скрытый смысл всего чанка, и именно по этому смыслу и можно производить поиск.
Далее все эти полученные вектора складываются в специальную базу данных, где лежат и ждут пока над ними и начнут производить ту самую операцию поиска (наиболее релевантных, т. е. близких по смыслу чанков поисковому запросу).
Когда пользователь отправляет свой вопрос в LLM, то текст его запроса точно по такому же алгоритму (как правило тем же самым эмбеддером), кодируется также в еще один эмбеддинг и далее над базой данных содержащей наши эмбеддинги чанков производится поиск наиболее близких «по смыслу» эмбеддингов (векторов). В реальности, как правило, считается косинусная близость вектора запроса и вектора каждого чанка и далее выбираются топ N векторов наиболее близких к запросу.
Далее текст чанков, соответствующий этим найденным векторам, вместе с запросом пользователя объединяется в единый контекст и подается на вход языковой модели. т. е. модель «думает», что пользователь написал ей не только вопрос, но еще и предоставил данные на основе которых нужно ответить на поставленный вопрос.
Как видно из описания ничего сложного: разбили текст на куски (чанки), каждый кусок оцифровали, нашли наиболее близкие по смыслу куски и подали текст этих кусков на вход большой языковой модели вместе с запросом от пользователя. Более того, весь этот процесс как правило уже реализован в так называемом пайплайне и все, что вам нужно, это собственно запустить пайплайн (из какой‑нибудь готовой библиотеки).
Написав первую версию и подав на вход несколько документов можно убедится в том, что полученный алгоритм в 100% случаев работает не так, как от него ожидалось и далее начинается долгий и сложный процесс допиливания RAG напильником, чтобы работало так как надо.
Ниже я опишу основные идеи и принципы допиливания RAG напильником, а во второй статье раскрою каждый из принципов в отдельности (возможно это будет серия статей, чтобы не получилась одна большая, бесконечная простыня).
Размер чанков и их количество. Настоятельно рекомендую начать эксперименты с изменения размера чанков и их количества. Если вы подаете на вход LLM слишком много ненужной информации или наоборот, слишком мало, то вы просто не оставляете шансов модели ответить правильно:
Чем меньше чанк по размеру, тем точнее будет буквальный поиск, чем больше размер чанка тем больше поиск приближается к смысловому.
Разные запросы пользователя могут содержать разное количество чанков, которое необходимо добавлять в контекст. Необходимо опытным путем подобрать тот самый коэффициент, ниже которого чанк смысла не имеет и будет лишь замусоривать ваш контекст.
Чанки должны перекрывать друг друга, чтобы у вас был шанс подать на вход последовательность чанков, которые следуют друг за другом вместе, а не просто вырванные из контекста куски.
Начало и конец чанка должны быть осмысленными, в идеале должны совпадать с началом и концом предложения, а лучше абзаца, чтобы вся мысль была в чанке целиком.
Добавление других методов поиска. Очень часто поиск «по смыслу» через эмбеддинги не дает нужного результата. Особенно если речь идет о каких‑то специфических терминах или определениях. Как правило к поиску через эмбеддинги подключают также TF‑IDF поиск и объединяют результаты поиска в пропорции, подобранной экспериментальным путем. Также очень часто помогает ранжирование найденных результатов например алгоритмом BM25.
Мультиплицирование запроса. Как правило, запрос от пользователя имеет смысл несколько раз перефразировать (с помощью LLM) и осуществлять поиск чанков по всем вариантам запроса. На практике сделают от 3-х до 5-ти вариаций запросов и потом результаты поиска объединяют в один.
Суммаризация чанков. Если по запросу пользователя найдено очень много информации и вся эта информация не помещается в контекст, то ее можно точно также «упростить» с помощью LLM и подать на вход в виде контекста (в добавление к вопросу пользователя) нечто вроде заархивированного знания, чтобы LLM могла использовать суть (выжимку из базы знаний) при формировании ответа.
Системный промпт и дообучение модели на формат RAG. Чтобы модель лучше понимала, что от нее требуется возможно также дообучить LLM на правильный формат взаимодействия с ней. В подходе RAG контекст, всегда состоит из двух частей (мы пока не рассматриваем вариант диалога в формате RAG): вопроса пользователя и найденного контекста. Соответственно модель можно дообучить понимать именно вот такой формат: тут вопрос, тут информация для ответа, выдай ответ, к вопросу. На первоначальном этапе эту проблему можно также попытаться решить через системный промпт, объяснив модели, что «вопрос тут, инфа тут, не перепутай!».
В заключение хочу отметить самый важную вещь, которую необходимо реализовать при работе с RAG. С первой же минуты, когда вы возьмете в руки напильник и начнете подпиливать RAG у вас встанет вопрос оценки качество того, что у вас получается. Пилить вслепую не вариант, каждый раз проверять качество руками тоже не вариант, потому что проверять желательно не один и не два, а десятки, а лучше сотни вопросов/ответов, даже после небольшого, единичного изменения. Я уже не говорю, что при смене библиотеки эмбеддера или генеративной LLM нужно запустить как минимум несколько десятков тестов и оценить качество результата. Как правило для оценки качества RAG модели используют следующие подходы:
Вопросы для проверки — должны быть написаны человеками. Тут никуда нельзя деться, только вы (или ваш заказчик) знает какие вопросы будут задаваться системе и ни кто, кроме вас проверочные вопросы не напишет.
Референсные (золотые) ответы — тоже в идеале должны быть написаны людьми, и желательно разными и в двух (а то и трех) экземплярах, чтобы избавиться от зависимости от человеческого фактора. Но тут, что называется, есть варианты. Например Ассистенты от OpenAI на модели GPT Turbo вполне себе не плохо справляются с этой задачей, особенно если в них не грузить большие (более 100 страниц) документы. Для получения референсных ответов, можно написать скрипт, который обратиться к ассистенту (в который загружены релевантные документы) и задаст ему все ваши вопросы для проверки. Причем несколько раз один и тот же вопрос.
Близость ответов LLM к референсным — измеряется несколькими метриками, такими как BERTScore, BLEURT, METEOR и даже простым ROUGE. Как правило опытным путем подбирается средневзвешенная метрика, являющаяся суммой вышеперечисленных (с соответствующими коэффициентами) которая наиболее точно отражает реальную близость ответов вашей LLM к золотым ответам.
Референсные чанки и близость найденных чанков к референсным. Как правило начинающий создатель RAG в своем желании побыстрее получить хорошие ответы от генератора напрочь забывает про то, что найденные чанки, которые подаются на вход этому генератору обеспечивают 80% качества ответа. Поэтому при разработке RAG первостепенное внимание необходимо уделить тем самым данным, которые нашел ретривер. В первую очередь необходимо создать базу данных из вопросов и соответствующим им чанков и каждый раз допиливании RAG напильником проверать насколько точно ретривер нашел чанки, соответствующие запросу от пользователя.
Жду ваши вопросы и замечания!