Головна / Статті

П’ять технік переранжування для 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. Модель ембеддингу оцінила її як помірно релевантну, але не серед найкращих 5, тому ШІ її не бачила. Це зовсім не рідкісний випадок. Це один із найпоширеніших способів збою в промислових системах 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] query [SEP] chunk [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. Cohere Rerank API

    Деякі команди не хочуть hostувати та налаштовувати модель для переранкінгу. Якщо ваш пріоритет — продукт, а не операції з моделями, то керований 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. Гібридний метод пошуку (dense плюс BM25) із фузією за допомогою RRF.
    2. Поновне ранжування за допомогою крос-енкодера серед перших 50 кандидатів.
    3. Модель LLM як альтернативний інструмент для розгляду запитів високої важливості.

    Це забезпечує швидкість у звичайних випадках та точність у критичних ситуаціях.

    Заключні міркування

    Поновна класифікація, ймовірно, є найбільш недооціненою частиною процесу створення RAG. Команди місяцями налаштовують розмір фрагментів та моделі вбудовування, тоді як найкращий контекст залишається „похованим“ на 23-й позиції, а хороший інструмент для поновної класифікації може це виправити з мінімальними технічними зусиллями.

    Техніки коливаються від безкоштовних та швидких (RRF) до дорогих та потужних (LLM-as-a-Judge), і поновна класифікація не обов’язково має бути або все, або нічого. Почніть з простого, поступово додавайте нові методи та підвищуйте складність лише тоді, коли це потребують ваші показники.

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