Главная / Статьи

Пять техник переранжирования для RAG: от быстрого поиска к точному контексту.

Почему первичный поиск не находит ответов и как это исправить: кросс-энкодеры, RRF, Cohere Rerank, ColBERT и LLM-as-a-judge, плюс слоистый подход для начала работы.

Издатель: REACTAPP.TOP12 мин чтения2769 слов

Источники: **5 Reranking Techniques in RAG: From Fast Retrieval to Accurate Context**

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

Если посмотреть на найденные фрагменты, часто оказывается, что тот, который содержит ответ, находится на 23-й позиции. Модель эмбеддингов оценила его как умеренно релевантный, но не в числе пяти самых релевантных, поэтому большая языковая модель его не увидела. Это вовсе не редкий крайний случай — это один из самых распространенных способов сбоев в производственных системах RAG.

Обычно вина не в механизме поиска, а в том, что происходит после его работы.

Узкое место после поиска

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

«Наиболее близкий» и «наиболее полезный для ответа на этот вопрос» — это разные понятия. Первоначальный этап поиска сталкивается с трудностями по четырем причинам:

  • Поверхностная оценка. Эмбеддинги измеряют сходство, а не полезность, поэтому фрагмент документа может находиться рядом с запросом, но при этом не содержать ничего полезного.
  • Би-кодеры кодируют запрос и документы отдельно, так что они никогда не рассматриваются вместе, что мешает выявлению тонких связей.
  • Незнание контекста. Система поиска не понимает, что нужно большой языковой модели, поэтому не может предсказать, приведёт ли тот или иной фрагмент к хорошему ответу.
  • Ложноположительные результаты. Общие фрагменты на определённую тему часто получают более высокие оценки, чем конкретные фактические, поскольку они являются более «безопасным» вариантом совпадения.
  • Решением является переоценка рангов.

    Что делает переоценка рангов

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

    Процесс состоит из трёх шагов:

    1. Первоначальный поиск: быстрый и широкий поиск возвращает от 50 до 100 кандидатских фрагментов.
    2. Переоценка рангов: более умная модель оценивает каждый кандидат по сравнению с запросом и переупорядочивает список.
  • Окончательный отбор: топ-3 до 5 переранжированных фрагментов передаются в LLM.
  • Вы получаете скорость приблизительного поиска вместе с точностью глубокой оценки. Механизм поиска не заменяется, а лишь дополняется.

    Поиск и переранжирование одновременно

    | Aspect              | Initial Retrieval        | Reranking                     |
    | ------------------- | ------------------------ | ----------------------------- |
    | Goal                | Find candidates fast     | Judge true relevance          |
    | Speed               | Milliseconds             | Tens to hundreds of milliseconds |
    | Input               | Query + index            | Query + top-k candidates      |
    | Scoring depth       | Shallow (embedding dot product) | Deep (cross-attention, token interaction) |
    | Cost                | Low (local compute)      | Higher (model inference)      |
    | When to use         | Every query              | On top-k candidates only      |
    

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

    Пять методов повторной оценки ранга

    Методы повторной оценки ранга варьируются от дешевых и быстрых до ресурсоемких и мощных. Выбор подходящего метода зависит от вашего бюджета на задержку, требований к точности и операционных ограничений. Эти пять методов охватывают весь диапазон возможностей.

    1. Повторная оценка ранга с использованием кросс-энкодеров

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

    Этапы:

    1. Метод поиска возвращает 50 кандидатских фрагментов.
    2. Для каждого фрагмента объединяются запрос и фрагмент в формате [CLS] запрос [SEP] фрагмент [SEP].
  • Подайте пару в модель кросс-энкодера, например cross-encoder/ms-marco-MiniLM-L-6-v2.
  • В результате получается оценка релевантности для каждой пары.
  • Отсортируйте по этой оценке и оставьте 5 лучших.
  • from sentence_transformers import CrossEncoder
    
    # Load a cross-encoder reranker
    # ms-marco-MiniLM-L-6-v2 is fast and accurate for general use
    cross_encoder = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
    
    def rerank_with_cross_encoder(query: str, retrieved_docs: list[str], top_k: int = 5):
        """
        Rerank retrieved documents using a cross-encoder.
    
        Args:
            query: The user question
            retrieved_docs: List of document chunks from initial retrieval
            top_k: Number of documents to return after reranking
    
        Returns:
            List of (document, score) tuples, sorted by relevance
        """
        # Create query-document pairs
        pairs = [[query, doc] for doc in retrieved_docs]
    
        # Get relevance scores
        scores = cross_encoder.predict(pairs)
    
        # Combine docs with scores and sort
        scored_docs = list(zip(retrieved_docs, scores))
        scored_docs.sort(key=lambda x: x[1], reverse=True)
    
        return scored_docs[:top_k]
    
    # Example usage
    query = "What are the side effects of amoxicillin?"
    retrieved = [
        "Amoxicillin is a penicillin antibiotic used to treat bacterial infections.",
        "Common side effects include nausea, vomiting, and diarrhea.",
        "The drug was first discovered in 1958 by researchers at Beecham.",
        "Patients with penicillin allergies should avoid amoxicillin.",
        "Side effects may include rash, itching, and in rare cases, anaphylaxis.",
    ]
    
    top_docs = rerank_with_cross_encoder(query, retrieved, top_k=3)
    for doc, score in top_docs:
        print(f"Score: {score:.4f} | {doc}")
    

    Выбирайте кросс-кодеры, когда важна точность, а дополнительные 50–200 мс считаются приемлемыми. Для большинства реальных систем RAG это лучшая отправная точка. Ограничением является то, что каждая пара запрос-документ обрабатывается отдельно, поэтому задержка растет пропорционально количеству кандидатов.

    2. Слияние рангов по принципу взаимности (RRF)

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

    RRF наилучшим образом подходит, когда уже используется гибридный поиск, например, плотный векторный поиск наряду с поиском по ключевым словам BM25. Каждый поисковый механизм предоставляет свой собственный рейтинг, а RRF объединяет их в один улучшенный список.

    Формула:

    RRF_score(d) = sum over retrievers of 1 / (k + rank_r(d))

    Здесь k — это константа, обычно равная 60, а rank_r(d) — позиция документа d в поисковой системе r. Документы, которые занимают высокие позиции в нескольких поисковых системах, поднимаются на вершину списка.

    def reciprocal_rank_fusion(rankings: list[list[str]], k: int = 60) -> list[tuple[str, float]]:
        """
        Merge multiple document rankings using Reciprocal Rank Fusion.
    
        Args:
            rankings: List of rankings, where each ranking is a list of document IDs
                      ordered from most to least relevant
            k: RRF constant (default 60, as recommended in the original paper)
    
        Returns:
            List of (document_id, rrf_score) tuples, sorted by fused score
        """
        scores = {}
    
        for ranking in rankings:
            for rank, doc_id in enumerate(ranking, start=1):
                if doc_id not in scores:
                    scores[doc_id] = 0.0
                # RRF formula: 1 / (k + rank)
                scores[doc_id] += 1.0 / (k + rank)
    
        # Sort by score descending
        return sorted(scores.items(), key=lambda x: x[1], reverse=True)
    
    # Example: merging BM25 and vector search results
    bm25_results = ["doc_5", "doc_2", "doc_8", "doc_1", "doc_9"]
    vector_results = ["doc_1", "doc_5", "doc_3", "doc_8", "doc_7"]
    
    fused = reciprocal_rank_fusion([bm25_results, vector_results])
    
    print("Fused ranking:")
    for doc_id, score in fused:
        print(f"  {doc_id}: {score:.4f}")
    
    # Notice: doc_5 and doc_1 appear in both retrievers and get boosted to the top
    

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

    3. API Cohere Rerank

    Некоторые команды не хотят развертывать и настраивать модель для переранжирования. Если ваш приоритет — продукт, а не работа с моделями, управляемый API для переранжирования кажется привлекательным вариантом, причем API от Cohere является самым распространенным в этой области.

    API принимает запрос вместе с списком документов и возвращает их в порядке релевантности. За его работой стоит современная модель переранжирования с оптимизациями, на разработку которых у вас ушли бы недели. Он обрабатывает многоязычный контент и длинные документы, а также масштабируется по мере необходимости.

    import cohere
    from dotenv import load_dotenv
    import os
    
    load_dotenv()
    
    # Initialize Cohere client
    co = cohere.Client(os.getenv("COHERE_API_KEY"))
    
    def rerank_with_cohere(query: str, documents: list[str], top_k: int = 5):
        """
        Rerank documents using Cohere's managed Rerank API.
    
        Args:
            query: The user question
            documents: List of document chunks from initial retrieval
            top_k: Number of documents to return
    
        Returns:
            List of (document, relevance_score) tuples
        """
        response = co.rerank(
            model="rerank-v3.5",
            query=query,
            documents=documents,
            top_n=top_k,
            return_documents=True
        )
    
        results = []
        for result in response.results:
            results.append((
                result.document.text,
                result.relevance_score
            ))
    
        return results
    
    # Example usage
    query = "How do I handle authentication in a FastAPI app?"
    docs = [
        "FastAPI is a modern web framework for building APIs with Python.",
        "To add authentication, use OAuth2PasswordBearer and JWT tokens.",
        "Pydantic models in FastAPI provide automatic request validation.",
        "The OAuth2PasswordBearer class expects a token URL endpoint.",
        "FastAPI was created by Sebastián Ramírez and released in 2018.",
    ]
    
    ranked = rerank_with_cohere(query, docs, top_k=3)
    for doc, score in ranked:
        print(f"Score: {score:.4f} | {doc}")
    

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

    4. ColBERT

    Кросс-кодеры обеспечивают высокую точность, но работают медленно, а би-кодеры — быстро, но имеют меньшую глубину обработки. ColBERT находит компромисс благодаря концепции «позднего взаимодействия».

    1. Оффлайн: каждый документ кодируется по токенам в контекстуальные эмбеддинги, которые затем предварительно вычисляются и хранятся в индексе.
    2. Онлайн: при поступлении запроса он обрабатывается аналогичным образом, по токенам.
    3. Оценка: каждый токен запроса сопоставляется с наиболее близким токеном документа (шаг MaxSim), после чего суммируются все максимальные значения сходства между токенами запроса для получения оценки релевантности.

    Поскольку вложения документов вычисляются заранее, это гораздо быстрее, чем использование кросс-энкодера. Благодаря сохранению взаимодействий на уровне токенов такой подход превосходит би-энкодеры по точности.

    from colbert import Searcher
    from colbert.infra import Run, RunConfig
    
    def setup_colbert_searcher(index_path: str, checkpoint: str):
        """
        Initialize a ColBERT searcher for late-interaction reranking.
    
        Args:
            index_path: Path to the pre-built ColBERT index
            checkpoint: Path to the ColBERT model checkpoint
    
        Returns:
            Configured Searcher instance
        """
        with Run().context(RunConfig(nranks=1, experiment="reranking")):
            searcher = Searcher(
                index=index_path,
                checkpoint=checkpoint
            )
        return searcher
    
    def rerank_with_colbert(searcher, query: str, doc_ids: list[str], top_k: int = 5):
        """
        Rerank documents using ColBERT's late interaction.
    
        Args:
            searcher: Initialized ColBERT Searcher
            query: The user question
            doc_ids: List of document IDs from initial retrieval
            top_k: Number of documents to return
    
        Returns:
            List of (doc_id, score) tuples
        """
        # Search within the candidate set
        results = searcher.search(
            query,
            k=top_k,
            filter_fn=lambda pid: pid in doc_ids  # Only rerank candidates
        )
    
        return list(zip(results[0], results[2]))  # doc_ids, scores
    
    # Note: ColBERT requires a pre-built index and model checkpoint.
    # For production use, build the index once and load it at startup.
    

    ColBERT подходит для крупномасштабных систем, которым требуется более высокая точность по сравнению с бай-энкодерами и меньшая задержка по сравнению с кросс-энкодерами. Недостаток заключается в необходимости создания и поддержки индекса. Если вы знакомы с векторными базами данных и хотите лучшее качество без задержек, растущих линейно с количеством кандидатов, это идеальный вариант.

    5. LLM как судья

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

    Идея заключается в том, чтобы попросить крупную модель (GPT-4, Claude или Gemini) оценить степень релевантности каждого кандидата к запросу. Благодаря обширным знаниям и сильным рассудочным способностям она может определять релевантность так, как не могут более мелкие модели для переранкинга. Типичный пример запроса:

    You are evaluating documents for a retrieval system.
    
    Query: {query}
    Document: {document}
    
    Rate how relevant this document is for answering the query.
    Respond with a single integer from 1 to 10, where 10 means perfectly relevant.
    
    Relevance score:
    

    Запустите его для каждого кандидата, проанализируйте оценки и верните топ-k.

    from openai import OpenAI
    import os
    from dotenv import load_dotenv
    
    load_dotenv()
    client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
    
    def score_document_with_llm(query: str, document: str) -> int:
        """
        Ask an LLM to score a document's relevance to a query.
    
        Args:
            query: The user question
            document: A candidate document chunk
    
        Returns:
            Integer relevance score from 1-10
        """
        prompt = f"""You are evaluating documents for a retrieval system.
    
    Query: {query}
    Document: {document}
    
    Rate how relevant this document is for answering the query.
    Respond with a single integer from 1 to 10, where 10 means perfectly relevant.
    Be strict: only give high scores to documents that directly help answer the query.
    
    Relevance score:"""
    
        response = client.chat.completions.create(
            model="gpt-4.1-mini",
            messages=[{"role": "user", "content": prompt}],
            temperature=0,
            max_tokens=5
        )
    
        try:
            score = int(response.choices[0].message.content.strip())
            return max(1, min(10, score))  # Clamp to 1-10
        except ValueError:
            return 5  # Default on parse failure
    
    def rerank_with_llm_judge(query: str, documents: list[str], top_k: int = 3):
        """
        Rerank documents using an LLM as a relevance judge.
    
        Args:
            query: The user question
            documents: List of candidate document chunks
            top_k: Number of documents to return
    
        Returns:
            List of (document, score) tuples, sorted by relevance
        """
        scored = []
        for doc in documents:
            score = score_document_with_llm(query, doc)
            scored.append((doc, score))
    
        scored.sort(key=lambda x: x[1], reverse=True)
        return scored[:top_k]
    
    # Example usage
    query = "What are the tax implications of RSU vesting for employees in California?"
    docs = [
        "RSUs are restricted stock units granted to employees as part of compensation.",
        "In California, RSU income is taxed as ordinary income at vesting, not at grant.",
        "Employers typically withhold federal and state taxes at vesting time.",
        "Stock options and RSUs have different tax treatments under IRS rules.",
        "California has one of the highest state income tax rates in the US.",
    ]
    
    ranked = rerank_with_llm_judge(query, docs, top_k=3)
    for doc, score in ranked:
        print(f"Score: {score}/10 | {doc}")
    

    Это самый точный метод среди приведенных здесь, но в то же время самый медленный и дорогой. Используйте его для сложных запросов с высокой ценностью, когда неверный ответ приводит к реальным финансовым потерям или серьезным рискам. Для большинства приложений кросс-кодеры или управляемые API обеспечивают примерно 90% точности при 10% затрат (это ориентировочные показатели, а не эталонные значения). Оставляйте методы оценки с помощью LLM для тех случаев, когда это действительно оправдано.

    Выбор между ними

    Ни один метод переранкинга не является идеальным во всех ситуациях. В этой таблице обобщены ограничения:

    | Technique             | Best For                                          | Latency      | Cost           |
    | --------------------- | ------------------------------------------------- | ------------ | -------------- |
    | Cross-Encoder         | Maximum quality on top-k candidates               | 50-200ms     | Local GPU/CPU  |
    | RRF                   | Hybrid retrieval without adding model inference   | ~0ms         | Free           |
    | Cohere Rerank API     | Speed without operational overhead                | 100-300ms    | Per API call   |
    | ColBERT               | Large-scale, low-latency use cases                | 20-100ms     | Index + GPU    |
    | LLM-as-a-Judge        | Complex, high-value queries (medical, legal)      | 1-5 seconds  | Per API call   |
    

    Разумным подходом будет начать с кросс-энкодера, поскольку он обеспечивает наибольшее улучшение точности при минимальной сложности реализации. Если задержка становится проблемой, рассмотрите использование ColBERT или управляемого API вроде Cohere. Добавьте RRF, если используется гибридный метод поиска, а модель LLM оставьте в качестве резерва для запросов, где максимальная точность оправдывает свою стоимость.

    Командам редко приходится выбирать только один вариант. Типичная структура производственного решения выглядит следующим образом:

    1. Гибридный метод поиска (плотное построение плюс BM25) с объединением результатов с помощью RRF.
    2. Переупорядочивание результатов с использованием кросс-энкодера среди 50 лучших кандидатов.
    3. Использование модели LLM в роли судьи для запросов высокой значимости, отмеченных как критичные.

    Это обеспечивает скорость в обычных случаях и высокую точность там, где это действительно важно.

    Заключение

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

    Существуют методы от бесплатных и быстрых (RRF) до дорогих и мощных (LLM-as-a-Judge), причём переупорядочивание не обязательно должно быть либо полным, либо отсутствующим. Начните с простого, постепенно добавляйте методы и увеличивайте сложность только тогда, когда это требуют ваши показатели.

    • На первом этапе поиска быстро собираются кандидаты; он не определяет окончательные ответы.
    • Переупорядочивание выполняет функцию контроля качества, являясь вторым этапом оценки истинной релевантности.
    • Кросс-кодеры представляют собой лучшую отправную точку благодаря высокой точности и умеренной сложности.
    • RRF позволяет добиться преимуществ без дополнительных затрат, если вы уже используете гибридный метод поиска.
  • Управляемые API экономят время инженеров, а Cohere Rerank работает отлично сразу после настройки.
  • ColBERT обеспечивает баланс между скоростью и точностью для крупных систем.
  • LLM-as-a-Judge — это более сложный вариант, когда точность важнее затрат.
  • Если вы используете RAG в производственных условиях без реранкинга, качество ответов остается недостаточным. Выберите один из методов, внедрите его на этой неделе и оцените разницу.