Advanced RAG Pipelines
Начиная писать этот материал я решил не вдаваться в подробности базовой архитектуры Retrieval-Augmented Generation (RAG), поскольку про нее и так много известно (но на крайний случай оставляю ссылки на краткое объяснение и полный гайд). Также хочу поделиться классным репозиторием от LangChain - в нескольких ноутбуках from scratch реализован RAG в нескольких вариантах для разных БД. Шпаргалка по работе RAG именно от туда:
Base RAG
Все знают, что LLM помогают в любой работе - писать код, статьи, проводить собесы и так далее. Однако у них есть несколько проблем - устаревшая информация и галюцинации. Например, вводя запрос "Какой сейчас курс доллара?", LLM, очевидно, не может воспользоваться той информацией, которую получала при обучении, потому что курс все время меняется. Спрашивая ее о конкретных фактах "Какое влияние оказали первые реформы Столыпина на экономику Российской Империи?", LLM может сгенерировать неточную или неверную информацию.
Retrieval-Augmented Generation помогает решить эти проблемы - он ищет по запросам пользователя релевантные документы в базе данных (БД) и прикрепляет их к промпту в LLM. Сами документы разбиты в БД на небольшие фрагменты текста (chunks), которые перекрывают друг друга - это необходимо для подачи в сеть последовательности фрагментов и сохранения контекстной взаимосвязи между ними.
Фрагменты текста хранятся в векторной БД (например QDrant, Chroma, FAISS), а их релевантность с запросом пользователя можно оценивать с помощью CosSim, Cross-Encoder, Bi-Encoder. Так LLM получает точные данные из БД вместе с запросом, что и помогает ей давать релевантные ответы.
Теперь давайте сфокусируемся на более сложных pipeline-ах RAG и попробуем их детально разобрать.
Document Hierarchies
Начнем с иерархии документов. Думаю по названию уже понятен основной смысл - мы можем создать что-то вроде оглавления к RAG чтобы структурировать и сегментировать наши данные. Закономерный вопрос - почему векторная БД не справиться с этой задачей и необходимо придумывать новую архитектуру хранения данных?
Попробуем ответить вопросом на вопросом - а что если в нашей системе миллиарды документов (например - бухгалтерская база экономики страны)?
1) Разделяя документ на чанки, мы получаем что каждый фрагмент содержит лишь минимальное представление содержимого исходного документа. Такое сокращение содержания приводит к потере контекста и потере важной информации во время поиска, поскольку каждому фрагменту не хватает полного понимания исходного документа.
2) Более того, с увеличением объема данных количество шума при каждом извлечении увеличивается, а значит система гораздо чаще находит неверные данные, которые просто оказались ближе друг к другу.
Иерархическая структура документов пытается решить эти проблемы - она извлекает и сегментирует семантические представления текста. С помощью многоуровневого распределения система итеративно уточняет наше пространство поиска, чтобы использовать только тот набор данных, который, как мы знаем, имеет семантически релевантное содержание для нашего первоначального запроса. В этом случае документ (родительская нода) выступает суммарным эмбеддингом дочерних фрагментов текста.
- Кластера или группы документов
- Отдельные документы по соответствующим темам
- Отдельные части релевантных документов
Еще одну реализацию иерархической структуры документов можно найти у LlamaIndex - HierarchicalNodeParser. В этом варианте иерархия строится на убывании размера чанков текста - от большего к меньшему.
Knowledge Graphs
Конечно же не обойтись без графов знаний - их основное преимущество перед иерархиями в отображении связей с использованием естесственного языка, а значит можно разработать интутивно понятные инструкции поиска информации для LLM.
Узлы таких графов представляют отдельные сущности, такие как люди, места, объекты или концепции. А ребра представляют взаимоотношения между этими узлами, указывая на то, как они связаны друг с другом.
Но давайте разберемся как это работает.
На самом деле LLM можно использовать еще на базовом уровне, генерируя запрос на языке запросов к графовой БД (например Cypher, GraphQL или Gremlin). Примерный промпт такой задачи Text2Cypher может выглядеть вот так:
You are a NebulaGraph Cypher expert. Based on the provided graph Schema and the question, please write the query statement. The schema is as follows: --- {schema} --- The question is: --- {question} --- Now, write down the query statement:
LLM отлично справляется с выделением ключевых сущностей в текстах, поэтому без проблем перестроит запрос на язык графовой БД и извлечет необходимые данные.
Но можно пойти дальше и построить GraphRAG. Есть 2 возможности - работать с обычным текстом или с его эмбеддингами. В первом случае ничего не меняется - у нас есть узлы и отношения между сущностями в виде ребер в графе и все это в текстовом формате. Во втором случае все интереснее - узлы представлены эмбеддингами, как и связи между ними. А это значит можно проходить по графу для поиска наиболее релевантных на запрос сущностей (привет CosSim).
В этом случае LLM выполняет 3 действия:
- Если работаем с эмбеддингами, то извлекаем топ-N семантически похожих узла из KG
- С помощью LLM делаем запрос в KG для релевантных узлов, связанных с извлеченными сущностями
- Формируем подграф контекста и формируем промпт для LLM вместе с запросом
Фреймворки для построения графовых БД - ontotext, NebulaGraph и Neo4j
Hypothetical Document Embeddings
Представьте, что вы задаете довольно общий, но простой вопрос LLM (например "Какие самые продаваемые бургеры?"). Проблема заключается в том, что найти контекст из БД документов для детализации этого вопроса иногда бывает сложно из-за его общности. К тому же по контексту этого запроса в БД слишком много релевантных фрагментов и становится струдной задачей определить какие именно являются релевантными.
Но можно пойти по другому. Если до этого момента мы пытались загрузить в сеть наш запрос вместе с релевантной информацией, взятой из документов БД, то исследователи в работе Precise Zero-Shot Dense Retrieval without Relevance Labels предположили обратное - что если просить сеть сгенерировать первичный гипотетический ответ?
Чтобы решить проблему "холодного старта", исследователи предложили брать первичный ответ из LLM на запрос пользователя, учитывая, что он может быть неточным. Но нам это и нужно - зная, что ответ содержит в себе лишь гипотетическую информацию (и возможно он не совсем верен), мы можем сравнить этот эмбеддинг с эмбеддингами правильных ответов из нашей БД и, благодаря похожему контексту, быстро найти точный ответ на наш запрос.
Улучшением такого подхода является генерация сразу нескольких гипотетических ответов и последующее их усреднение для нахождения более точного эмбеддинга из БД.
Необходимо уточнить, что это не работает, когда модели подается запрос, не являющийся wide-knowledge ("Объясни физический смысл лагранжиана квантовой хромодинамики") - в данном случае высока вероятность генерации галлюцинаций.
Contextual Compressors & Filters
Бывают ситуации, когда на ваш конкретный запрос находится много ответов в БД, а в итоге вам нужно лишь несколько фактов из разных фрагментов документов. Остальные детали вас не интересуют и вы не хотите, чтобы LLM видела их при формировании ответа. Эту проблему решает контекстное сжатие и фильтры.
Суть проста - у нас есть базовый retriever, который выдает набор релевантных документов, а compressor сжимает их в короткие ответы и оставляет только необходимую информацию (только то, что полезно для ответа на вопрос).
Фильтром может быть что угодно - например стороняя fine-tuned LLM на эмейлы пользователей (картинка ниже).
Вы даже можете построить собственный pipeline фильтра - например добавлять эмбеддинг важной информации (или формата ответа - JSON) в конец и только потом отправлять получившийся запрос в LLM.
Multi-Query Retrieval
Метод мульти-запросов тоже довольно прост в своей реализации. Между Retriever-ом и пользовательским запросом находится еще одна LLM, которая генерирует множетсво вариантов первичного запроса, рассматривая его с разных точек зрения.
Например на запрос "Расскажи о правлении Петра I" сеть может сгенерировать дополнительные вопросы "Экономическая стабильность во времена правления Петра I", "Войны и итоги во времена правления Петра I", "Градостроительство во времена Петра I" и так далее.
Все эти запросы подаются Retriver-у для нахождения фрагментов документов, соответствующих запросам, после чего отбираются релевантные и подаются в LLM вместе с основным запросом.
Эта архитектура поможет, когда пользователь ничего не знает о запрашиваемом объекте и составляет запрос общего характера. Так LLM сможет составить подробный ответ и собрать в нем максимальное количество информации.
RAG-Fusion
Один из самых новых pipeline-ов RAG, который вышел обычной публикацией на TowardsDataScience, предлагает устранить вечный разрыв между тем, что пользователи явно задают в запросе и тем, что они собираются спрашивать. Ведь иногда пользователь хочет узнать много фактов о какой-то теме и просто вводит ее в виде запроса. С этой проблемой также сталкиваются все поисковые системы.
Первое - мы генерируем multi-query на основании запроса пользователя. По каждому запросу (включая оригинальный пользовательский) находятся релевантные фрагменты текста из векторной БД. Далее все результаты ранжируются и объединяются с помощью Reciprocal Rank Fusion (RRF).
RRF (или взаимное слияние рангов) - это метод, который основан на объединении нескольких результатов поиска для получения единого унифицированного рейтинга. Один запрос не может охватить все аспекты запросов пользователей, и он может быть слишком узким для предоставления полных результатов. Вот почему при создании нескольких запросов необходимо учитывать все различные элементы и предоставлять тщательно подобранный ответ, с учетом ранга релевантности каждого фрагмента текста.
Ниже представлен пример. Четыре retrieval системы выдали отранжированные списки докумнтов. Далее для каждого документа в каждом случае вычисляется его обратный ранг. И после для каждого документа вычисляется финальное значение ранга - оно состоит из обратных сумм, где в знаменателе стоят два слагаемых - k и r(d). k - постоянный параметр, который обычно равняется 60. r(d) - вычисленный обратный ранг каждого документа в каждом случае. Далее суммы ранжируются по убыванию и мы получаем финальную выдачу документов по их релевантности. Необязательно подавать все фрагменты в контекст - можно брать top-K наиболее релевантных.
- Существует риск выдачи слишком подробных ответов - в таком случае можно использовать Contextual Compressors & Filters
- Нагрузка на контекстное окно модели
Multimodal RAG
Сегодняшние LLM способны работать с мультимодальными данными, а значит для таких данных тоже необходим свой RAG для уточнения запросов.
Классическим примером где нужен RAG такого типа может быть поломка какой-то бытовой техники. Вы присылаете ее фотографию в LLM (например диодов на пылесосе) и формируете запрос с проблемой и просьбой ее решить. По фотографии и описанию, LLM получит от RAG контекст таких фотографий, который поможет определить модель техники, а также похожие ее проблемы. Фрагменты документов инструкций помогут понять как решить эту проблему - возможно ли починить дома или стоит обратиться в сервисный центр.
Работает Multimodal RAG аналогично векторному поиску по эмбеддингам текста, только теперь добавляется пространство эмбеддингов фотографий. Вот пример с конференции Google поиска похожих фотографий в векторном пространстве.
Необходимо добавить, что эмбеддинги текстового описания картинки и самой картинки находятся рядом в векторном пространстве. Использование взаимосвязных эмбеддингов текста и картинки помогает LLM создавать более детальные генерации и уменьшает вероятность галлюцинаций, если БД картинок была тщательно отобрана и в ней не присутствуют асбтракции.
Классический pipeline мультимодального RAG представлен на рисунке ниже. Все как обычно - берем запрос, векторным поиском находим релевантные фрагменты данных (текст и картинки), формируем на основе этого контекста ответ модели.
Вот примеры генераций LLM на запрос "Imagining a day at the beach". Видим более детальное и насыщенное описание с использованием Multimodal RAG:
Generated without MM-RAG: I imagine a day at the beach would be very relaxing. I would sit in the sun and listen to the waves crash along the shore. Maybe I would go for a swim or build a sandcastle. It would be nice to get away from normal life for a while and enjoy the peaceful atmosphere.
Generated with MM-RAG: I imagine a day at the beach filled with golden sandy shores and the rhythmic crash of bright blue waves lapping gently at the coastline. My toes would sink into smooth sand as I breathed in the fresh and briny sea air under a bright sky, dotted with puffy clouds. I’d love to go snorkeling and glimpse the colorful fish swimming below or just nap on a towel, lulled into a rest by the glittering water and crying seagulls swooping overhead. Maybe later I could crack open a coconut or build an elaborate sandcastle with bridges and moats before taking a long walk at sunset, watching the glowing orange disk sink below the horizon.