Обзор техник RAG: Retrieval Augmented Generation
Рассмотрим техники построения и улучшения RAG систем: от нарезания текстов на куски, до продвинутых способов улучшения качества ответа.
Этим блогом можно пользоваться как шпаргалкой для проектирования своего RAG-а и/или для подготовки к собеседованиям.
Все полезные ссылки и материалы, на которые я опирался будут в конце.
Что такое RAG и зачем нужен
RAG - это фреймворк взаимодействия предобученной LLM с базой знаний. То есть при ответе LLM на запрос пользователя модель отвечает используя актуальный контекст из базы и свои pre-trained знания.
Обогащение запрос контекстом позволяет модели дать более точный ответ без необходимости дообучения на этих данных.
RAG очень часто можно использовать для формирования отчетов, создания корпоративных и специализированных чат-ботов. Причем так как не нужно дополнительного дообучения на доменных данных, то использование RAG-а часто более дешевый и быстрый вариант, а также безопасный и интерпретируемый по сравнению с fine-tuning-ом.
Базовый пайплайн подготовки системы RAG:
- Загрузить документы
- Нарезать на куски
- Построить базу данных
- Подготовить ретривер и, возможно, эмбеддер
- Развернуть LLM для инференса
Базовый пайплайн применения RAG:
- Аутентифицировать пользователя
- Обработать входной запрос
- Найти релеватные куски из базы данных
- Отранжировать контексты
- Собрать промпт из запроса и контекстов
- Запромптить LLM
- Получить от LLM ответ на вопрос
- Верифицировать и отдать пользователю
- Приверженность - то, как сильно ответ похож на контекст, поданный в модель. Чем ниже метрика, тем выше вероятность галлюцинаций.
- Полнота - то, насколько полный ответ для заданного вопроса и предоставленного контекста.
- Учёт контекстов - то, какая доля контекстов использовалась при ответе.
- Утилизация контекстов - то, какую долю контекста модель использовала при ответе на вопрос.
- токсичность ответа
- тональность ответа - например, по категориям эмоций
https://www.galileo.ai/blog/mastering-rag-improve-performance-with-4-powerful-metrics - больше про метрики можно почитать тут
Загрузка документов
Это первый этап построения RAG системы.
Для своей базы знаний можно использовать разные источники: видео на ютубе, конспекты ноушен, эксель таблицы и др.
Если в документе есть таблицы, или картинки, то из них можно извлечь полезную информацию в том числе - воспользоваться OCR и / или TableTransformer.
Библиотека, которая сама все делает за вас: https://unstructured-io.github.io/unstructured/introduction.html
Также важно помнить, что у каждого документа есть метаданные: название, дата, автор и др.
Нарезка документов
Mastering RAG: Advanced Chunking Techniques for LLM Applications
Урок по разделению с помощью LangChain
Нарезка документов на куски нужна для того, чтобы не переполнять контекст LLM ненужной и шумной информацией, а максимально информативной - это нужно как для более точного ответа, так и для ускорения работы LLM.
На что влияет нарезка на куски:
- качество контекстов, которые отдаем LLM
Чем меньше контекст, тем меньше там информации, которая может сбить с толку LLM, а также тем легче правильнее определить семантический смысл эмбеддеру при создании вектора представления - затраты на индекс кусков
Чем больше кусков, тем выше затраты, так как нужно хранить больше векторов - Скорость извлечения релевантных кусков из индекса
Чем больше кусков, тем дольше задержка - Скорость ответа LLM
Чем длиннее контекст, подаваемый в LLM, тем дольше она будет отвечать
Факторы, влияющие на разделения документа на куски:
- Структура текста
пунктуация, переносы строки, маркдаун верстка и др. - Контекстное окно LLM и эмбеддера
- Сложность и специфика запросов
Параметра функции разделения документа на куски:
- По символам / токенам без специфичного разделителя
Разделяем документ согласно длины контекста и размере пересечения.
Работает быстро, но глупо. - По символам / токенам с желаемыми разделителями
Пытаемся разделять на куски, например, по переносу строки, по точке или хотя бы по пробелу. Но все равно есть ограничение на длину контекста и длину наложения.
Чуть дольше, но намного умнее, потому что не обрывает слова или даже предложения. - По Маркдаун разметке
Примерно как предыдущий метод, только разделитель заголовки маркдауна
Получается более структурированное разделение, а также обновляет метадату кусков - добавляет поле названия. - Семантически
Разделяем текст на предложения.
Добавляем в кусок текста новое предложение, если оно похоже семантически на уже имеющийся кусок.
Ограничиваем количество предложений в куске.
Разделение довольно умное, но расчет более ресурсоемкий из-за модели схожести. - Переписываем текст как утверждение
С помощью специальной модели или через LLM переписываем исходные предложения так, чтобы каждой из них по отдельности имело смысл, было понятно, о чем идет речь и не могло больше разделится на более мелкое утверждение.
Потенциально очень умное разделение, которое сразу же помогает отвечать на вопрос. Но есть вероятность испортить хороший текст, а также требует дополнительных ресурсов на обработку документов.
- Мульти-векторная индексация
Иногда полезно для каждого документа иметь несколько векторов представления.
Например, поделить документ на куски и для каждого посчитать векторы, делать ретрив по векторам кусков, а возвращать сам документ, таким образом уменьшается влияние шума и разнообразие топиков в документе.
Также можно заменить или добавить к вектору документа вектор представление суммаризации этого документа.
Также частая практика это придумывать гипотетические вопросы, ответом на которые может быть документ, и складывать в индекс вектор представления этих вопросов.
https://python.langchain.com/docs/how_to/multi_vector/
Кстати, метадата документа должна наследоваться каждому его куску, а также иногда дополняться свойствами отдельного куска, например, заголовок маркдаун при соответствующем методе разделения.
Метрики оценки качества, на которые можно ориентироваться при выборе стратегии разбиения документов на куски:
- Приверженность - то, как сильно ответ похож на контекст, поданный в модель. Чем ниже метрика, тем выше вероятность галлюцинаций.
- Полнота - то, насколько полный ответ для заданного вопроса и предоставленного контекста.
- Учёт контекстов - то, какая доля контекстов использовалась при ответе.
- Утилизация контекстов - то, какую долю контекста модель использовала при ответе на вопрос.
https://www.galileo.ai/blog/mastering-rag-improve-performance-with-4-powerful-metrics - больше про метрики можно почитать тут
Как подбирать параметры для нарезки на куски:
База данных
База данных это система, которая хранит, индексирует и позволяет обрабатывать запросы для неструктурированных данных, таких как текст, изображения и подобное через числовое представление в виде вектора.
С помощью таких векторов можно делать поиск похожих объектов в базе данных. В нашем случае обычно поиск похожих кусков текста на пользовательский запрос.
Ключевые факторы выбора базы данных:
- Открытая ли база или закрытая
- Язык программирования, на котором можно создать клиента для использования базы данных
- Лицензия
- Фичи для организаций:
- Продуктовые фичи:
- точный поиск
- приближенный поиск - когда можно пожертвовать качеством для ускорения и масштабирования
- префильтрация - когда до векторного поиска нужно уменьшить количество кандидатов
- постфильтрация - дополнительный фильтр для улучшения точности результатов
- гибридный поиск
- поддержка разреженных векторов
- поддержка поиска напрямую по тексту, например, через bm25
- Возможность инференса моделей эмбеддингов.
Например, sentence transformers, Mixedbread, BGE, OpenAI. - Возможность инференса модели реранкера
- Скорость добавление новых объектов
- Скорость поиска
- Затраты на обслуживание
- Поддержка, мониторинг, бэкапы и тд
Выбор эмбеддера
Эмбеддинг - это векторное представление текста (или картинки, звука и тд) в пространстве, в котором похожие тексты отображаются в похожие векторы.
Как можно использовать эмбеддинги:
- кодировать вопросы и контексты эмбеддером, чтобы для вопроса находить самые релевантные куски информации
- находить few-shot примеры для in context learning (ICL)
- определять намерения пользователя, чтобы, например провести по какой-нибудь ветке заготовленного сценария общения, или вызвать какой-нибудь инструмент
На что смотреть при выборе эмбеддера:
- размерность векторного пространства
- размер модели
- перфоманс модели на доменных или общих бенчмарках
- открытая или закрытая модель
- стоимость
- поддержка языков
- гранулярность: на уровне слов, предложений, длинных документов
- dense векторы
классика - разряженные
Извлекают из текста только самую релевантную информацию, а в других размерностях значения просто 0.
Часто используется в задачах со специфической терминологией.
Работает примерно как bag-of-words, но обходит многие его недостатки.
https://arxiv.org/abs/2109.10086 - матрешка эмбеддинги
Позволяют выбирать размерность вектора на инференсе.
можно почитать тут лонгрид про них: https://teletype.in/@abletobetable/embeds_ops
- long-context эмбеддинги
Если мы можем эффективно и без потери качества кодировать более длинные куски текста, то будем уменьшать задержку при поиске и косты на хранение векторов, так как их будет меньше. - code эмбеддинги
специально натренированные модели для работы с кодом
Примеры как, на каких задачах измерить качество эмбеддингов:
Метрики рага, которые подходят и для метрик эмбеддера:
- Приверженность - то, как сильно ответ похож на контекст, поданный в модель. Чем ниже метрика, тем выше вероятность галлюцинаций.
- Полнота - то, насколько полный ответ для заданного вопроса и предоставленного контекста.
- Учёт контекстов - то, какая доля контекстов использовалась при ответе.
- Утилизация контекстов - то, какую долю контекста модель использовала при ответе на вопрос.
https://www.galileo.ai/blog/mastering-rag-improve-performance-with-4-powerful-metrics - больше про метрики можно почитать тут
Извлечение, поиск
Поиск - это процесс извлечения максимально релевантных кусков текста из базы данных, в которых потенциально находится информация необходимая для ответа на вопрос пользователя.
С одной стороны мы хотим найти как можно больше полезных кусков и предоставить максимально полную картину для LLM, поэтому мы хотим находить не только самые релевантные контексты, но и максимально разнообразные.
Но с другой стороны, чем больше текста вы извлекаем, тем более они зашумленные, менее релевантные, тем самым LLM будет проще галлюцинировать, поэтому важность этапа поиска нельзя недооценивать.
Техники для улучшения извлечения:
- Hypothetical document embeddings (HyDE) https://arxiv.org/abs/2212.10496
На вопрос пользователя генерируем с помощью LLM такой текст, в котором гипотетически мог бы содержаться ответ на вопрос.
Такой документ будет недостоверным в большинстве случаев, но зато текстовый энкодер сможет построить очень близкий эмбеддинг для реального контекста. - Maximal Marginal Relevance (MMR)
Техника для увеличения разнообразия в найденном множестве контекстов. То есть мы скорее отдадим предпочтение менее релевантному контексту, но еще незнакомому.
- Autocut
Смотрим на скоры похожести контекста и определяем так называемые “прыжки” в них, находя самую оптимальную границу между релевантными и менее релевантными контекстами. https://weaviate.io/developers/weaviate/api/graphql/additional-operators#autocut - Recursive retrieval
Нарезаем контекст на более мелкие куски, ищем релевантный, но отдаем более крупный контекст.
https://youtu.be/TRjq7t2Ms5I?si=D0z5sHKW4SMqMgSG&t=742
https://docs.llamaindex.ai/en/stable/examples/query_engine/pdf_tables/recursive_retriever.html
Похожая техника Sentence window retrieval, где мы возвращаем не кусок, а окно, которое включает наш кусок текста
https://docs.llamaindex.ai/en/latest/examples/node_postprocessor/MetadataReplacementDemo.html
- SelfQuery
Техника для таких вопросов, где полезно будет сделать фильтрацию по какому-нибудь атрибуту, например, по дате.
- Сжатие контекстов
После извлечения самый релевантных контекстов, мы их суммаризируем при условии вопроса юзера с помощью LLM. Таким образом финальная LLM получает на вход более плотную информацию, с минимумом шума, но, скорее всего, довольно полную. Хотя тут мы делаем дополнительные вызовы суммаризатора.
https://learn.deeplearning.ai/courses/langchain-chat-with-your-data/lesson/5/retrieval - урок по извлечению от LangChain
Question Answering / generation
На этапе генерации мы отдаем релевантные контексты вместе с вопросом пользователя в LLM, а от нее уже получаем ответ. Это центральный элемент RAG системы, поэтому тут также нужно аккуратно рассмотреть следующие пункты:
- Выбор LLM
При выборе LLM стоит опираться на то, на каких данных и задачах была обучена модель, с какими языками она хорошо работает. Желательно проверить несколько моделей самостоятельно перед выкаткой, также можно опираться на результаты бенчмарков. - Open-source или проприетарная модель
С одной стороны использование закрытых апи упрощает разработку системы, но с другой стороны возникает вопрос конфиденциальности данных, а также зависимости от внешней апи. - Размер модели
При большем размере растет качество, но растет задержка и падает пропускная способность. - Параметры генерации
Такие параметры как температура, top p, top k, могу сильно влиять на ответы моделей, их креативность и разнообразие. - Способ инференса
Этот вопрос может отпасть, если мы будем использовать закрытые апи, но в случае с моделями, которые мы сами хотим разворачивать и поддерживать, этот вопрос является очень существенным.
Так как разные фреймворки инференса поддерживают разные модели, способы оптимизации и ускорения инференса.
Основные решения: tensorrt-llm, vllm, tgi, deepspeed-mii.
Отдельно затронем техники промпт-инжиниринга для улучшения ответов LLM. Они могут помочь сделать ответ информативнее, более персонализированным для юзера, а также уменьшить вероятность галлюцинаций.
- Few-shot prompting
Показываем несколько примеров, как могут выглядеть ответы.
Можно улучшить выбор этих нескольких примеров через поиск ближайших соседей. - Chain-Of-Thoughts
Заставлять LLM генерировать цепочку мыслей и только после размышления давать финальный ответ: “Take a deep breath and let’s think step by step”. - Map reduce
делаем суммаризацию каждого контекста, и только потом по всем суммаризациям генерируем ответ на вопрос. - Map refine
Начинаем с 1го контекста, отвечаем на вопрос по нему, затем обновляем ответ с учетом 2го контекста и так далее. - Thread of Thought
Разбиваем длинные куски текста на контексты, модель извлекает из них релевантную информацию, затем просим модель суммаризировать и проанализировать информацию, а не просто прочитать и понять.
- Chain of Note
Примерно то же самое, что и ToT, но здесь для каждого извлеченного куска текста генерируем суммаризацию и оцениваем его релевантность касательно вопроса. И уже на основании таких заметок отвечаем.
- Chain of Verification(CoVe)
На запрос генеририруем бейзлайн, далее подбираем проверочные вопросы, отвечаем на них и редактируем ответ.
- Эмоциональное давление
Удивительно, но работает все: представь, что ты эксперт, я дам тебе 200 долларов за правильный ответ и др.)
Также для того, чтобы еще минимизировать риски галлюцинаций, можно ответ LLM проверять дополнительно на безопасность/адекватность той же LLM, или другими специализированными моделями, или просто регулярными выражениями.
Chat, user experience
Также для некоторых бизнес кейсов важно уметь помнить то, о чем шла речь в предыдущих сообщениях.
Это можно сделать, сохраняя предыдущие вопросы и ответы на них.
- Можно переписывать вопрос с учетом истории, чтобы поиск релевантных контекстов работал корректно.
- Можно вместе с вопросом и релевантным контекстом также предоставить доступ LLM к истории чата.
- Если чат разрастается, его можно суммаризировать: либо просто извлечь самое главное, либо суммаризировать при условии текущего вопроса, чтобы точно не потерять ничего важного из истории.
Так как RAG это в общение с пользователем, то для дальнейших улучшений, можно в сервис внедрить логику сбора обратной связи: через лайки/дизлайки или открытых форм.
В том числе для большей прозрачности работы сервиса можно давать пользователю доступ к извлеченным контекстам и цепочкам мыслей модели.
А для ускорения работы модели, можно делать кэширование и не нагружать модели по несколько раз.
А также использовать техники оптимизации инференса как эмбеддинг модели (например, вот лонгрид: https://teletype.in/@abletobetable/embeds_ops ), так и LLM (начиная от continuous batching до speculative decoding).
Заключение
Заключения не будет, я устал. Просто красивые схемы из статьи.
Полезные ссылки
https://teletype.in/@abletobetable
A Survey on Retrieval-Augmented Generation for Large Language Models:
https://arxiv.org/abs/2312.10997
RAG vs Fine-Tuning for LLMs: A Comprehensive Guide with Examples
Better RAG 2: Single-shot is not good enough
Better RAG 3: The text is your friend
How To Architect An Enterprise RAG System
RAG Vs Fine-Tuning Vs Both: A Guide For Optimizing LLM Performance
Advanced Chunking Techniques for LLM Applications
Improve RAG Performance With 4 Powerful RAG Metrics
LLM Prompting Techniques For Reducing Hallucinations
How to Select A Reranking Model
How to Select an Embedding Model
Choosing the Perfect Vector Database
Generate Synthetic Data for RAG in Just $10
Mastering RAG: 8 Scenarios To Evaluate Before Going To Production