Image

Объяснение RAG: переоценка для лучших ответов

Как реранжирование улучшает генерацию результатов поиска, выводя на экран наиболее релевантные результаты

Делиться

57c595e78056b9e13c776dea0e6c28e2

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

К сожалению, высокие показатели сходства не всегда гарантируют идеальную релевантность. Другими словами, поисковая система может извлечь фрагмент текста с высоким показателем сходства, но на самом деле он не так уж и полезен — просто не то, что нужно для ответа на вопрос пользователя 🤷🏻‍♀️. Именно здесь и вводится повторное ранжирование , чтобы уточнить результаты перед отправкой их в LLM.

Как и в моих предыдущих постах, я снова буду использовать в качестве примера текст «Войны и мира», лицензированный как общественное достояние и легко доступный через Project Gutenberg.

🍨 DataCream — это новостная рассылка с историями и обучающими материалами по искусственному интеллекту, данным и технологиям. Если вам интересны эти темы, подпишитесь здесь.

• • •

А как насчет переоценки?

Текстовые фрагменты, извлеченные исключительно на основе метрики поиска (т. е. сырой поиск), могут оказаться не столь полезными по нескольким причинам:

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

Возвращаясь к моему любимому вопросу из примера с «Войной и миром», если мы спросим «Кто такая Анна Павловна?» и используем очень малое значение k (например, k = 2), извлеченные фрагменты могут содержать недостаточно информации для исчерпывающего ответа на этот вопрос. И наоборот, если мы допустим извлечение большого количества фрагментов k (например, k = 20), мы, скорее всего, также извлечём несколько нерелевантных фрагментов текста, где «Анна Павловна» просто упоминается, но не является темой фрагмента. Таким образом, значение некоторых из этих фрагментов будет не связано с запросом пользователя и бесполезно для ответа на него. Поэтому нам нужен способ выделить действительно релевантные извлеченные фрагменты текста среди всех извлеченных фрагментов.

Здесь стоит уточнить, что одним из простых решений этой проблемы было бы простое извлечение всех данных и передача их на этап генерации (в LLM). К сожалению, это невозможно по ряду причин, например, из-за того, что у LLM есть определённые контекстные окна, или из-за того, что производительность LLM снижается при переполнении информацией.

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

f4b2401f51e6f43ce9d5dd0ce01e0fee

Существуют различные методы для этого, например, кросс-кодировщики, использование LLM для переранжирования или эвристики. В конечном счёте, вводя этот дополнительный этап переранжирования, мы, по сути, реализуем так называемый двухэтапный поиск с переранжированием, что является стандартным подходом в отрасли. Это позволяет повысить релевантность извлекаемых фрагментов текста и, как следствие, качество генерируемых ответов.

Итак, давайте рассмотрим более подробно… 🔍

• • •

Переранжирование с помощью кросс-кодера

Кросс-энкодеры — это стандартные модели, используемые для переранжирования в фреймворке RAG. В отличие от функций извлечения, используемых на начальном этапе поиска, которые учитывают только оценки сходства различных текстовых фрагментов, кросс-энкодеры способны выполнять более глубокое сравнение каждого из полученных текстовых фрагментов с запросом пользователя. Более конкретно, кросс-энкодер совместно встраивает документ и запрос пользователя и вычисляет оценку сходства. С другой стороны, при поиске на основе косинусного сходства документ и запрос пользователя встраиваются отдельно друг от друга, а затем вычисляется их сходство. В результате часть информации об исходных текстах теряется при раздельном создании встраиваний, и часть информации сохраняется при совместном встраивании текстов. Следовательно, кросс-энкодер может лучше оценить релевантность между двумя текстовыми фрагментами (то есть запросом пользователя и документом).

db4c2a0cc331e81ce4fc06d769613bf0

Так почему бы изначально не использовать кросс-энкодер? Дело в том, что кросс-энкодер очень медленный. Например, поиск по косинусному сходству примерно для 1000 отрывков занимает меньше миллисекунды. Напротив, использование только кросс-энкодера (например, ms-marco-MiniLM-L-6-v2) для поиска по тому же набору из 1000 отрывков и поиска совпадений по одному запросу будет на порядки медленнее!

Если задуматься, это вполне ожидаемо, поскольку использование кросс-кодировщика означает, что нам приходится сопоставлять каждый фрагмент базы знаний с запросом пользователя и встраивать их сразу же, для каждого нового запроса. В отличие от этого, при поиске на основе косинусного сходства мы создаём все вложения базы знаний заранее и всего один раз, а затем, как только пользователь отправляет запрос, нам остаётся только встроить запрос пользователя и вычислить парное косинусное сходство.

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

• • •

Возвращаемся к примеру «Войны и мира»

Давайте теперь посмотрим, как все это проявляется на примере «Войны и мира», ответив еще раз на мой любимый вопрос: «Кто такая Анна Павловна?».

Мой код на данный момент выглядит примерно так:

import os from langchain.chat_models import ChatOpenAI from langchain.document_loaders import TextLoader from langchain.embeddings import OpenAIEmbeddings from langchain.vectorstores import FAISS from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.docstore.document import Document import faiss api_key = «my_api_key» # инициализация LLM llm = ChatOpenAI(openai_api_key=api_key, model=»gpt-4o-mini», temperature=0.3) # инициализация встраиваний model embeddings = OpenAIEmbeddings(openai_api_key=api_key) # загрузка документов для использования в RAG text_folder = «RAG files» documents = [] for filename in os.listdir(text_folder): if filename.lower().endswith(«.txt»): file_path = os.path.join(text_folder, filename) loader = TextLoader(file_path) documents.extend(loader.load()) splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100) split_docs = [] для doc в documents: chunks = splitter.split_text(doc.page_content) для chunk в chunks: split_docs.append(Document(page_content=chunk)) documents = split_docs # нормализовать вложения базы знаний import numpy as np def normalize(vectors): vectors = np.array(vectors) norms = np.linalg.norm(vectors, axis=1, keepdims=True) return vectors / norms doc_texts = [doc.page_content for doc in documents] doc_embeddings = embeddings.embed_documents(doc_texts) doc_embeddings = normalize(doc_embeddings) # индекс faiss с импортом внутреннего произведения faiss dimension = doc_embeddings.shape[1] index = faiss.IndexFlatIP(dimension) # индекс внутреннего произведения index.add(doc_embeddings) # создание векторной базы данных w FAISS vector_store = FAISS(embedding_function=embeddings, index=index, docstore=None, index_to_docstore_id=None) vector_store.docstore = {i: doc for i, doc in enumerate(documents)} def main(): print(«Добро пожаловать в RAG Assistant. Введите 'exit' для выхода.n») while True: user_input = input(«Вы: «).strip() if user_input.lower() == «exit»: print(«Выход…») break # встраивание + нормализация запроса query_embedding = embeddings.embed_query(user_input) query_embedding = normalize([query_embedding]) # поиск индекса FAISS D, I = index.search(query_embedding, k=2) # получение релевантных документов relevant_docs = [vector_store.docstore[i] for i in I[0]] retrieved_context = «nn».join([doc.page_content for doc in relevant_docs]) # D содержит оценки внутреннего произведения == косинусное сходство (после нормализации) print(«nЛучшие фрагменты и их оценки косинусного сходства:n») for rank, (idx, score) in enumerate(zip(I[0], D[0]), start=1): print(f»Фрагмент {rank}:») print(f»Косинусное сходство: {score:.4f}») print(f»Content:n{vector_store.docstore[idx].page_content}n{'-'*40}») # системное приглашение system_prompt = ( «Вы полезный помощник. » «Используйте ТОЛЬКО следующий контекст базы знаний для ответа пользователю. » «Если ответа нет в контексте, скажите, что вы не знаете.nn» f»Context:n{retrieved_context}» ) # сообщения для LLM messages = [ {«role»: «system», «content»: system_prompt}, {«role»: «user», «content»: user_input} ] # сформировать ответ response = llm.invoke(messages) assistant_message = response.content.strip() print(f»nAssistant: {assistant_message}n») if __name__ == «__main__»: main()

При k = 2 мы получаем следующие верхние фрагменты.

c527d6cbf67a7215a14bc0c4f9660d26

Но если мы установим k = 6, то получим следующие извлеченные фрагменты и более информативный ответ, содержащий дополнительные данные по нашему вопросу, например, тот факт, что она «фрейлина и фаворитка императрицы Марии Федоровны».

9fc5ac3f10739ed9c8e4ec6d91c7c3d6

Теперь давайте скорректируем наш код, чтобы переранжировать эти 6 фрагментов и проверить, остались ли первые два прежними. Для этого мы будем использовать модель кросс-кодирования для переранжирования первых k извлеченных документов перед их передачей вашему LLM. В частности, я буду использовать кросс-кодирование cross-encoder/ms-marco-TinyBERT-L2 — простую предобученную модель кросс-кодирования, работающую на базе PyTorch. Для этого нам также потребуется импортировать библиотеки Torch и Transformers.

импортировать горелку из трансформаторов импортировать CrossEncoder

Затем мы можем инициализировать кросс-кодер и определить функцию для переранжирования первых k фрагментов, полученных из векторного поиска:

# инициализация модели кросс-кодировщика cross_encoder = CrossEncoder('cross-encoder/ms-marco-TinyBERT-L-2', device='cuda' if torch.cuda.is_available() else 'cpu') def rerank_with_cross_encoder(query, relevant_docs): pairs = [(query, doc.page_content) for doc in relevant_docs] # пары (query, document) for cross-encoder scores = cross_encoder.predict(pairs) # оценки релевантности из модели кросс-кодировщика ranked_indices = np.argsort(scores)[::-1] # сортировка документов на основе оценки кросс-кодировщика (чем выше, тем лучше) ranked_docs = [relevant_docs[i] for i in ranked_indices] ranked_scores = [scores[i] for i in ranked_indices] вернуть ranked_docs, ranked_scores

… а также настроить функцию следующим образом:

… # поиск по индексу FAISS D, I = index.search(query_embedding, k=6) # получение релевантных документов relevant_docs = [vector_store.docstore[i] for i in I[0]] # повторное ранжирование с помощью нашей функции reranked_docs, reranked_scores = rerank_with_cross_encoder(user_input, relevant_docs) # получение верхних повторно ранжированных фрагментов retrieved_context = «nn».join([doc.page_content for doc in reranked_docs[:2]]) # D содержит оценки внутреннего произведения == косинусные сходства (после нормализации) print(«n6 верхних извлеченных фрагментов:n») for rank, (idx, score) in enumerate(zip(I[0], D[0]), start=1): print(f»Фрагмент {rank}:») print(f»Сходство: {score:.4f}») print(f»Содержимое:n{vector_store.docstore[idx].page_content}n{'-'*40}») # отобразить верхние переоцененные фрагменты print(«nЛучшие 2 переоцененных фрагмента:n») for rank, (doc, score) in enumerate(zip(reranked_docs[:2], reranked_scores[:2]), start=1): print(f»Ранг {rank}:») print(f»Оценка переоценки: {score:.4f}») print(f»Содержимое:n{doc.page_content}n{'-'*40}») …

… и, наконец, вот два верхних фрагмента и соответствующий ответ, который мы получаем после повторного ранжирования с помощью кросс-кодера:

bb1a7db89412aaeaf3c7355e85cf504c

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

Таким образом, важность этапа переранжирования становится очевидной. Мы используем векторный поиск, чтобы сузить круг потенциально релевантных фрагментов из всех доступных документов в базе знаний, а затем применяем этап переранжирования для точного определения наиболее релевантных фрагментов.

f90647300c4d2af35ada29b473841986

Двухэтапный поиск можно представить в виде воронки: на первом этапе извлекается широкий набор фрагментов-кандидатов, а на этапе переранжирования отфильтровываются нерелевантные. Остаётся наиболее полезный контекст, что позволяет получать более чёткие и точные ответы.

• • •

У меня на уме

Итак, становится очевидным, что это важный шаг для построения надёжного конвейера RAG. По сути, это позволяет нам преодолеть разрыв между быстрым, но не таким точным векторным поиском и контекстно-зависимыми ответами. Выполняя двухэтапный поиск, где векторный поиск является первым шагом, а вторым — переранжированием, мы получаем лучшее из обоих миров: эффективность при масштабировании и более качественные ответы. На практике именно этот двухэтапный подход делает современные конвейеры RAG практичными и мощными.

• • •

Понравился этот пост? Давайте дружить! Присоединяйтесь ко мне:

📰 Substack 💌 Medium 💼 LinkedInКупите мне кофе!

• • •

Источник: towardsdatascience.com

✅ Найденные теги: новости, Объяснение
Каталог бесплатных опенсорс-решений, которые можно развернуть локально и забыть о подписках

галерея

Фото сгенерированных лиц: исследование показывает, что люди не могут отличить настоящие лица от сгенерированных
Нейросети построили капитализм за трое суток: 100 агентов Claude заперли…
Скетч: цифровой осьминог и виртуальный мир внутри компьютера с человечком.
Сцена с жестами пальцами, где один жест символизирует "VPN", а другой "KHP".
‼️Paramount купила Warner Bros. Discovery — сумма сделки составила безумные…
Скриншот репозитория GitHub "Claude Scientific Skills" AI для научных исследований.
Структура эффективного запроса Claude с элементами задачи, контекста и референса.
Эскиз и готовая веб-страница платформы для AI-дизайна в современном темном режиме.
ideipro logotyp
Image Not Found
Звёздное небо с галактиками и туманностями, космос, Вселенная, астрофотография.

Система оповещения обсерватории Рубина отправила 800 000 сигналов в первую ночь наблюдений.

Астрономы будут получать оповещения о небесных явлениях в течение нескольких минут после их обнаружения. Теренс О'Брайен, редактор раздела «Выходные». Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной…

Мар 2, 2026
Женщина с длинными тёмными волосами в синем свете, нейтральный фон.

Расследование в отношении 61-фунтовой машины, которая «пожирает» пластик и выплевывает кирпичи.

Обзор компактного пресса для мягкого пластика Clear Drop — и что будет дальше. Шон Холлистер, старший редактор Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной странице вашего…

Мар 2, 2026
Черный углеродное волокно с текстурой плетения, отражающий свет.

Материал будущего: как работает «бессмертный» композит

Учёные из Университета штата Северная Каролина представили композит нового поколения, способный самостоятельно восстанавливаться после серьёзных повреждений.  Речь идёт о модифицированном армированном волокном полимере (FRP), который не просто сохраняет прочность при малом весе, но и способен «залечивать» внутренние…

Мар 2, 2026
Круглый экран с изображением замка и горы, рядом электронная плата.

Круглый дисплей Waveshare для креативных проектов

Круглый 7-дюймовый сенсорный дисплей от Waveshare создан для разработчиков и дизайнеров, которым нужен нестандартный экран.  Это IPS-панель с разрешением 1 080×1 080 пикселей, поддержкой 10-точечного ёмкостного сенсора, оптической склейкой и защитным закалённым стеклом, выполненная в круглом форм-факторе.…

Мар 2, 2026

Впишите свой почтовый адрес и мы будем присылать вам на почту самые свежие новости в числе самых первых