Первая часть. Часть вторая про Advanced RAG тут.
Если, открывая холодильник, вы еще не слышали из него про RAG - то наверняка скоро услышите. Однако, в сети на удивление мало полных гайдов, учитывающих все тонкости (оценка релевантности, борьба с галлюцинациями и т.д.) а не обрывочных кусков. Базируясь на опыте нашей работы, я составил гайд который покрывает эту тему наиболее полно.
Итак, зачем нужен RAG?
Вы может использовать LLM модели типа ChatGPT для составления гороскопов (что он вполне успешно делает), а можете для чего-то полезного (например, работы). Однако тут возникает проблема: у компании как правило есть куча документов, правил, нормативов и так далее, о чем ChatGPT ничего, конечно, не знает.
Что делать?
Тут два варианта: доучивать модель вашими данными или использовать RAG.
Доучивать долго, дорого и скорее всего у вас все равно не выйдет (не переживайте, дело не в том что вы плохой родитель, просто это мало кто может и умеет делать).
Второй вариант — это Retrieval-Augmented Generation (известный так же под позывным RAG). По сути, идея простая как пять копеек - взять существующую хорошую модель (тот же OpenAI), и прикрутить ей сбоку поиск по информации компании. Модель все еще мало что про вашу компанию знает, но теперь у нее есть где поглядеть. Это не так эффективно, как если бы она знала, но достаточно для большинства задач.
Базово RAG выглядит следующим образом:
Поисковик (он же Retreiver) - часть системы, которая ищет информацию, релевантную вашему запросу (аналогично тому, как вы бы искали ее в своем wiki или документах компании или в Гугле). Обычно в этом качестве используется векторная база данных типа Qdrant, где лежат все проиндексированные документы компании, но в принципе может использоваться что угодно.
Генератор - получает от поисковика найденные данные, и использует их (комбинирует, сокращает, извлекает только важное) для того чтобы дать ответ пользователю. Эта часть обычно делается с помощью LLM типа OpenAI. Ей просто дается вся (или часть) найденной информации с просьбой разобраться и дать ответ.
Вот пример простейшей реализации RAG на python и langchain
import os
import wget
from langchain.vectorstores import Qdrant
from langchain.embeddings import OpenAIEmbeddings
from langchain import OpenAI
from langchain_community.document_loaders import BSHTMLLoader
from langchain.chains import RetrievalQA
#download War and Peace by Tolstoy
wget.download("http://az.lib.ru/t/tolstoj_lew_nikolaewich/text_0073.shtml")
#load text from html
loader = BSHTMLLoader("text_0073.shtml", open_encoding='ISO-8859-1')
war_and_peace = loader.load()
#init Vector DB
embeddings = OpenAIEmbeddings()
doc_store = Qdrant.from_documents(
war_and_peace,
embeddings,
location=":memory:",
collection_name="docs",
)
llm = OpenAI()
# ask questions
while True:
question = input('Ваш вопрос: ')
qa = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=doc_store.as_retriever(),
return_source_documents=False,
)
result = qa(question)
print(f"Answer: {result}")
Выглядит просто но есть один нюанс:
Поскольку знания в модели не зашиты, качество ответов ну очень сильно зависят от того, что найдет Retriver, и в какой форме. Задача не тривиальная, так как в типичном бардаке документов компании обычно и люди разбираются с трудом. Документы и знания как правило хранятся в плохо структурированном виде, в разных местах, иногда в виде изображений, графиков и записок от руку и т.п. Часто информация в одном месте противоречит информации в другом, и во всем этом зоопарке надо как-то разбираться.
Часть информация просто не имеет смысла без контекста, как например сокращения, аббревиатуры принятые в компании, имена и фамилии.
Что делать?
Вот тут в ход идут разного рода оптимизации поиска (aka костыли). Они применяются на разных стадиях поиска. Условно поиск можно разделить на:
Первоначальную обработку и очистку вопроса пользователя
Поиск данных в хранилищах
Ранжирование полученных результатов из хранилища
Обработка и комбинирование результатов в ответ
Оценка ответа
Применение форматирования, стилистики и тона
Рассмотрим детально каждый этап:
Первоначальная обработка вопроса пользователя
Вы не поверите, чего пишут пользователи в качестве вопросов. На адекватность тут рассчитывать не приходится - вопрос может быть сформулирован как: требование, утверждение, жалоба, угроза, просто одна буква или ЦЕЛОЕ сочинение размером с “Война и Мир”. Например:
Или
Нужно обработать вход, превратив его в запрос, которые можно использовать для поиска информации. Для решения проблемы нам нужен переводчик с языка юзера на человеческий. Кто бы это мог сделать? Конечно же LLM. Базово это может выглядеть так:
Это самый простой вариант - попросить LLM переформулировать запрос пользователя. Но, в зависимости вашей аудитории, этого может оказаться не достаточно!!!!!1111
Тогда в ход идет техника чуть посложнее - RAG Fusion.
Rag Fusion
Идея в том, чтобы попросить LLM дать несколько вариантов вопроса пользователя, сделать по ним поиск, а затем объединить результаты, предварительно отранжировав их с помощью какого-либо хитрого алгоритма, например Cross Encoder. Cross Encoder работает довольно медленно, но дает более релевантные результаты, поэтому для поиска информации его использовать не практично - однако для ранжирования списка найденных результатов - вполне.
Отступление о Cross и Bi encoders
Векторные базы используют Bi-encoder модели чтобы вычислить похожесть двух понятий в векторном пространстве. Эти модели обучаются представлять данные в виде векторов и, соответственно, при поиске запрос пользователя тоже превращается в вектор, и при поиске возвращаются вектора ближайшие к запросу. Но такая близость не гарантирует что это наилучший ответ.
Cross Encoder работает по другому. Он принимает два объекта (текста, изображения и т.п.) и возвращает их релевантность (similarity) относительно друг друга. Его точность как правило лучше чем у Bi Encoder. Обычно, из векторной базы возвращают больше результатов, чем нужно (на всякий случай, допустим 30) и потом ранжируют их, используя Cross Encoder или подобные техники, и возвращают первые 3.
К пре-обработке запроса пользователя так-же относится его классификация. Например, запросы могут подразделяться на вопросы, жалобы, просьбы и так далее. Можно далее классифицировать запросы на срочные, не срочные, спам, фрод. Можно классифицировать по подразделениям (например бухгалтерия, производство, HR) и так далее. Это все позволяет сузить круг поиска информации и соответственно повышает скорость и качество ответа.
Для классификации может использоваться, опять же, LLM модель или специально обученная нейронная сеть-классификатор.
Поиск данных в хранилищах
За поиск отвечает так называемый retreiver (первая буква в RAG).
В качестве хранилища обычно выступает векторная база данных, куда проиндексированы данные компании из разных источников (хранилищ документов, баз данных, wiki, CRM и т.п.). Но в целом, это не обязательно и может использоваться что угодно, например elasticsearch или даже поиск в гугле.
Поиск вне векторных баз я тут разбирать не буду, принцип везде один и тот же.
Отступление о векторных базах
Векторная база данных (или векторное хранилище, я использую их как взаимозаменяемые понятия, хотя технически это не одно и тоже) — это тип хранилища данных, оптимизированный для хранения и обработки векторов (которые, по сути, массивы чисел). Эти векторы используются для представления сложных объектов, таких как изображения, тексты или звуки в виде векторов векторных пространств в задачах машинного обучения и анализа данных. В векторной базе (а точнее в векторном пространстве) понятия схожие по смыслу находятся близко, вне зависимости от их представления. Например, слова “собака” и “бульдог” будут близко, а слова “замок” и “замок” - далеко. Соответственно, векторные базы хорошо подходят для поиска данных по смыслу.
Самые популярные векторные базы (на текущий момент):
QDrant - база с открытым исходным кодом (интересна тем что сделана нашими ребятами, основатель Андрей Васнецов учился в Бауманке)
Pinecone - cloud-native (читай - сдерут 3 шкуры) база
Chroma - еще одна база с открытым исходным кодом (Apache-2.0 license)
Weaviate - открыта по BSD-3-Clause license
Milvus - открыта под Apache-2.0 license
FAISS - отдельный зверь, не база а фрейворк от одной известной экстремисткой организации
Так же, некоторые популярные не-векторные базы стали предлагать векторные возможности:
Pgvector для postgres
Atlas для Mongo
Тут можно посмотреть сравнение всех возможных векторных баз.
Для улучшения результатов используется несколько основных приемов:
Ансамбль ретриверов и/или источников данных
Простая но эффективная идея, которая заключается в том что если спросить несколько экспертов один и тот же вопрос - а затем как то агрегировать их ответ (ну хоть усреднить) - результат в среднем получается лучше. В каком-то смысле это аналог “Спросить мнение зала”.
Как пример - использование нескольких типов ретриверов из langchain. Ансамблинг особенно полезен при обьединении sparse (даже не знаю как это перевести, поэтому пусть будет так) ретриверов(например BM25) ретриверов и dense ретриверов(работающих на основе embdedding similarity, например те же векторные базы) потому что они хорошо дополняют друг друга.
Dense Retriever - использует обычно трансформеры, например BERT, для кодирования как запросов, так и документов в векторы в многомерном пространстве. Сходство между запросом и документом измеряется близостью их векторов в этом пространстве, часто используется косинусное сходство (cosine similarity) для оценки их близости. Это то на чем построены векторные базы данных. Такая модель лучше понимает семантическое (смысловое) значение запросов и документов, что приводит к более точным и релевантным результатам, особенно для сложных запросов. В силу того, что модель оперирует на уровне смысла (семантики), она хорошо справляется с перефразированием и семантическими сходствами.
Sparse Retriever - использует традиционные методы информационного поиска, такие как TF-IDF (Частотность Термина) или BM25. Эти методы создают разреженные векторы, где каждое измерение соответствует определенному термину из предопределенного словаря. Релевантность документа к запросу пользователя рассчитывается на основе присутствия и частоты терминов (ну слов, скажем) запроса в документе. Эффективно для запросов, основанных на ключевых словах, и когда ожидается, что термины запроса будут прямо присутствовать в релевантных документах. Работают не всегда так точно как dense, но зато быстрее и требует меньше ресурсов на поиск и обучение.
EnsembleRetriever затем ранжирует и объединяет результаты используя, например, Reciprocal Rank Fusion:
Пример ансамбля
!pip install rank_bm25
from langchain.retrievers import BM25Retriever, EnsembleRetriever
from langchain.vectorstores import Chroma
embedding = OpenAIEmbeddings()
documents = "/all_tolstoy_novels.txt"
bm25_retriever = BM25Retriever.from_texts(doc_list)
bm25_retriever.k = 2
vectorstore = Chroma.from_texts(doc_list, embedding)
vectorstore_retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
# initialize the ensemble retriever
ensemble_retriever = EnsembleRetriever(retrievers=[bm25_retriever, vectorstore_retriever ], weights=[0.4, 0.6])
docs = ensemble_retriever.get_relevant_documents("Война и Мир")
Как выбрать правильную стратегию из всего этого зоопарка? Экспериментировать. Или воспользоваться фреймворком, например https://github.com/Marker-Inc-Korea/AutoRAG.
Ансамблировать, кстати, можно и несколько LLM, это тоже улучшает результат. См. More agents is all you need.
RELP
Это еще один способ поиска данных, Retrieval Augmented Language Model based Prediction. Отличается тут шаг поиска - после того как мы находим информацию в векторном хранилище, в том числе используя техники выше, - мы используем ее не для генерации ответа с помощью LLM а для генерации примеров ответов (с помощью few-shot prompting) для LLM, и на основе этих примеров LLM как бы учиться, и отвечает на основе этого мини-обучения на заданный вопрос. Эта техника является формой динамического обучения, что намного менее затратно чем до-обучение модели стандартными методами.
Отступление о few-shot (learning) prompting
Есть две похожие техники промптинга LLM: zero shot и few-shot. Zero-shot это когда вы спрашиваете LLM свой вопрос не приводя никаких примеров. Например:
Few-shot — это когда сначала LLM дается несколько примеров, на которых она обучается. Это значительно повышает вероятность получить релевантный ответ, в релевантной форме. Например:
Ранжирование, объединение и оценка полученных результатов
Мы частично уже затронули эту тему как часть RAG Fusion и ансамблирование ретриверов. Когда мы вытаскиваем результат из (векторного) хранилища, прежде чем посылать эти данные в LLM для генерации ответа - надо результаты отранжировать, и, возможно, отбросить не релевантные. То, в каком порядке вы дадите результаты поиска LLM для формирования ответа играет значение. То что LLM увидит раньше - сильнее повлияет на итоговый результат (подробнее тут).
Для ренкинга (ранжирования) используются разные подходы. Наиболее частые:
Использование Cross Encoder (описано выше) для ре-ранжирования полученных результатов и отбрасывания наименее релевантных (например достаем топ 30 результатов из векторной базы (top k), ранжируем Cross Encoder’ом, берем первые 10).
Есть уже готовые решения для этих целей, например от Cohere.
Reciprocal Rank Fusion. Основная идея RRF заключается в том, чтобы придать большее значение элементам, занимающим более высокие позиции в каждом наборе результатов поиска. В RRF оценка каждого элемента рассчитывается на основе его места в индивидуальных результатах поиска. Обычно это делается с использованием формулы 1/(k + ранг), где "ранг" - это позиция элемента в определенном наборе результатов поиска, а "k" - это константа (часто устанавливаемая на уровне около 60). Эта формула обеспечивает высокую оценку для элементов с более высоким рангом.
Оценки для каждого элемента в разных наборах результатов затем суммируются для получения итоговой оценки. Элементы сортируются по этим итоговым оценкам для формирования комбинированного списка результатов.
RRF особенно полезен, потому что он не зависит от абсолютных оценок, присвоенных отдельными поисковыми системами, которые могут значительно различаться по своей шкале и распределению. RRF эффективно объединяет результаты из разных систем таким образом, что подчеркивает наиболее последовательно высоко ранжированные элементы.
LLM based ренкинг и оценка: можно не напрягаться и просто попросить LLM отранжировать и оценить результат 🙂. Последние версии OpenAI довольно хорошо с этим справляются, однако их использование для этого накладно.
Оценка результатов поиска в Vector Store
Допустим вы сделали реранжирование или еще какие-то изменения - как то понять, а вообще оно чего то дало? Повысилась ли релевантность или нет? А в целом насколько система хорошо работает? Это метрика качества найденной информации. Она используется для понимания насколько релевантную информации ваша система находит, и принимать решения об ее дальнейшей доработке.
Оценить, насколько результаты релевантны запросу можно используя следующие метрики: P@K, MAP@K, NDCG@K (и подобные). Обычно они возвращают число от 0 до 1, где 1 - это самая высокая точность. Они похожи по смыслу, отличия в деталях:
P@K означает precision at K, то есть точность на k элементах. Допустим на запрос о зайцах система нашла 4 документа:
[ “Зайцы в дикой природе”, “Как ездить зайцем в автобусе”, “Трактат о Моркови”, “Зайка моя: мемуары by Киркоров”]
Так как жизнеописание Киркорова или автобусы к зайцам отношения не имеют, в этих позициях стоит 0, и общая точность получается:
P@K при К=4, иди P@4 = 2 релевантных / (2 релевантных + 2 нерелевантных) = ½ = 0.5
Однако, тут не учитывается порядок. Что если возвращаемый список выглядит так:
[ “Как ездить зайцем в автобусе”, “Зайка моя: мемуары by Киркоров”, “Зайцы в дикой природе”, “Трактат о Моркови”]
P@K все еще равен 0.5, но как мы знаем порядок релевантный и нерелевантных результатов имеет значение! (как для людей так и для LLM, которая будет их использовать).
Поэтому мы берем AP@K или average precision at K. Идея простая, надо модифицировать формулу так чтобы порядок учитывался, и релевантные результаты в конце не увеличивали общую оценку меньше, чем те что вначале списка:
Или для нашего примера выше:
AP@4 = (0 * 0 + 0 *½ + 1 * ⅓ + 1 + 1 * 2/4) .2 = (⅓ + 2/4) / 2 = 0.41
Тут возникает пару вопросов: а как мы оценили релевантность индивидуальных элементов, чтобы посчитать эти метрики. Это детектив, очень хороший вопрос.
В контексте RAG мы чаще всего просим LLM или другую модель сделать оценку. То есть мы спрашиваем LLM по каждому элементу - этот документ что мы нашли в векторном хранилище - оно вообще релевантно вот этому запросу?
А теперь второй вопрос - а достаточно ли спросить именно так? Ответ - недостаточно. Нужны более конкретные вопросы к LLM, которые просят ее оценить релевантность по определенным параметрам. Например, для выборки выше это могут быть вопросы:
Этот документ имеют отношение к типу животного заяц?
Заяц в этом документе реальный или аллегорический?
И т.д. Вопросов может быть много (от двух до сотен), и они зависят от того как вы оцениваете релевантность. Это нужно агрегировать, и на сцену выходит:
MAP@K (Mean Average Precision at K) — это среднее от суммы AP@K для всех вопросов.
NDCG@K означает normalized discounted cumulative gain at K, даже не буду переводить 🙂. Описание посмотрите в интернете сами.
Оценка результатов ответа LLM
Не все знают, но LLM (включая Llama и OpenAI) можно попросить вернуть не токены (текст) а логиты (logits). Т.е. по факту можно попросить ее вернуть распределение токенов с их вероятностью, и поглядеть - а насколько вообще модель уверенна в том, чего она набредила (посчитав token level uncertainty). Если вероятности в распределении низкие (что считать низким зависит от задачи), то скорее всего модель начала выдумывать (галлюцинировать) и совсем не уверенна в своем ответе. Это может использоваться для оценки ответа, и возвращения юзеру честного “Я не знаю”.
Применение форматирования, стилистики и тона
Самый простой пункт 🙂. Достаточно попросить LLM отформатировать ответ определенным образом и использовать определенный тон. Лучше дать модели пример, так она лучше следует инструкциям. Например, тон можно задать так:
Форматирование и стилистика могут быть заданы програмно, в последнем шаге RAG - запросе к LLM на генерацию финального ответа, например:
question = input('Ваш вопрос: ')
style = 'Пользователи совсем обнаглели. Отвечай агрессивно, как бандит из 90х.'
qa = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=doc_store.as_retriever()
)
result = qa(style + “ Вопрос пользователя: ” + question)
print(f"Answer: {result}")
Fine-tuning моделей
Иногда вам таки может потребоваться дотренировка. Да, в начале я говорил, что скорее всего у вас ничего не выйдет, однако есть случаи когда это оправданно. Если у вас в компании используются аббревиатуры, имена/фамилии и термины, о которых модель не знает и знать не может - RAG может работать плохо. Например, он может плохо искать данные по русским фамилиям, а особенно по их склонениям. Тут может помочь легкий файнтюнинг модели через LORA, для того чтобы обучить модель понимать подобные узкие случаи. Можно использовать фреймворки типа https://github.com/bclavie/RAGatouille.
Подобный файнтюнинг лежит за пределами данной статьи, если будет интерес - я опишу это отдельно.
Системы, базирующиеся на RAG
Есть несколько более продвинутых вариантов, которые базируются на RAG. На самом деле новые варианты появляются чуть ли не каждый день, их авторы утверждают, что они стали все более лучше работать...
Тем не менее, одна вариация выделяется - это FLARE (Forward Looking Active REtrieval Augmented Generation).
Очень интересная идея, базирующаяся на том что RAG надо использовать не как попало, а только когда LLM сама того хочет. Если LLM уверенно отвечает без RAG - ну пожалуйста. Если же она начинает сомневаться - вот тогда надо искать больше контекстных данных. И делать это нужно не один раз, а столько сколько потребуется. Когда в процессе ответа LLM считает, что ей нужно больше данных - она делает RAG поиск.
В каком то смысле это похоже на то как работают люди. Мы зачастую не знаем что мы не знаем, и понимаем это только во время самого процесса поиска.
Я не буду тут вдаваться в детали, это тема для отдельной статьи.
Если есть вопросы и предложения - пишите в комментариях.
Часть вторая про Advanced RAG тут.
Я со-основатель компании Рафт. Если вам не интересно о чём я пишу – не подписывайтесь на мой личный блог, пожалуйста!
Всем добра и позитивного настроения!