Strona główna / Artykuły

Pięć technik ponownego rankowania w RAG: od szybkiego wyszukiwania do dokładnego kontekstu

Dlaczego pierwszy etap wyszukiwania pomija odpowiedzi i jak to naprawić: cross-encoders, RRF, Cohere Rerank, ColBERT oraz LLM-as-a-judge, wraz z warstwowym procesem do rozpoczęcia pracy.

Wydawca: REACTAPP.TOP12 min czytania2769 słów

Źródła: **5 Reranking Techniques in RAG: From Fast Retrieval to Accurate Context**

Załóżmy, że pipeline RAG został prawidłowo zbudowany: dokumenty podzielone na fragmenty, wybrany model embeddingowy oraz połączona baza wektorowa. Nadchodzi pytanie, narzędzie do wyszukiwania zwraca 50 fragmentów, a 5 najlepszych trafia do promptu. Odpowiedź jest przyzwoita, ale nie doskonała. Czasami w ogóle nie znajduje rozwiązania.

Gdy spojrzymy na to, co zostało znalezione, często okazuje się, że fragment zawierający odpowiedź znajduje się na pozycji 23. Model embeddingowy ocenił go jako umiarkowanie istotny, ale nie należący do 5 najważniejszych, więc model językowy go nie zauważył. To wcale nie jest rzadki przypadek specjalny – to jeden z najczęstszych sposobów awarii w praktycznym zastosowaniu RAG.

Zazwyczaj winowajcą nie jest narzędzie do wyszukiwania, lecz to, co dzieje się po jego działaniu.

Wąskie gardło po wyszukiwaniu

Pierwszy etap wyszukiwania jest zoptymalizowany pod kątem szybkości. Gęste wektory, rzadkie BM25 lub ich połączenie – celem jest zmniejszenie miliona dokumentów do zdatnego do pracy zbioru w ciągu kilku milisekund. Kosztem jest szybka, powierzchowna ocena istotności: model analizuje zapytanie i każdy dokument osobno, ocenia je i przekazuje ten najbliższy.

„Najbliższy” i „najbardziej przydatny do odpowiedzi na to pytanie” to różne kryteria. Początkowe wyszukiwanie napotyka trudności z czterech powodów:

  • Powierzchowna ocena. Embeddingi mierzą podobieństwo, a nie przydatność, więc fragment może znajdować się blisko zapytania i nadal nie zawierać nic przydatnego.
  • Bi-encodery kodują zapytanie i dokumenty oddzielnie i nigdy nie widzą ich razem, co ukrywa subtelne związki.
  • Ślepota na kontekst. System nie wie, czego potrzebuje model językowy, więc nie może przewidzieć, czy dany fragment doprowadzi do dobrego odpowiedzi.
  • Fałszywie pozytywne wyniki. Ogólne fragmenty na dany temat często uzyskują wyższe oceny niż konkretne, faktyczne fragmenty, ponieważ stanowią „bezpieczniejsze” dopasowanie.
  • Reranking to rozwiązanie tego problemu.

    Czym jest reranking

    Reranking to druga faza przetwarzania. Po szybkim, szerokim wyszukiwaniu uzyskuje się listę kandydatów, a silniejszy model przestawia je według tego, jak dobrze pasują do konkretnej zapytania. Można to porównać do kontroli jakości przed dostarczeniem kontekstu do modelu językowego.

    Proces składa się z trzech kroków:

    1. Pierwotne wyszukiwanie: szybkie, szerokie poszukiwanie zwraca od 50 do 100 kandydujących fragmentów.
    2. Reranking: inteligentniejszy model ocenia każdy kandydat pod kątem zapytania i przestawia listę.
  • Ostateczna selekcja: 3 do 5 najlepiej ponownie sklasyfikowanych fragmentów trafia do modelu LLM.
  • Otrzymujesz szybkość wyszukiwania przybliżonego w połączeniu z dokładnością zaawansowanego oceniania. Narzędzie do wyodrębniania wyników nie jest zastępowane, a jedynie uzupełniane.

    Wyodrębnianie wyników i ponowna klasyfikacja obok siebie

    | 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      |
    

    Celem jest uniknięcie intensywnego obliczania dla miliona dokumentów i skupienie zasobów na 50 najbardziej obiecujących.

    Pięć technik ponownego rankowania

    Narzędzia do ponownego rankowania wariują od tych tanich i szybkich po te wymagające dużych zasobów obliczeniowych. Wybór odpowiedniego narzędzia zależy od budżetu na opóźnienia, wymagań dotyczących dokładności oraz ograniczeń operacyjnych. Te pięć rozwiązań obejmuje cały ten zakres.

    1. Ponowne rankowanie za pomocą cross-encoderów

    Cross-encodery stanowią standard dokładności. Bi-encoder embeduje zapytanie i dokument oddzielnie, natomiast cross-encoder odczytuje je razem jako jeden wejście. Dzięki temu może uwzględnić interakcje na poziomie tokenów, takie jak rozwiązywanie problemów z zaimkami, negacje oraz implikowane relacje, których zupełnie nie dostrzegają zwykłe narzędzia do wyszukiwania.

    Kroki:

    1. Narzędzie do wyszukiwania zwraca 50 kandydujących fragmentów.
    2. Dla każdego fragmentu łączy się zapytanie z fragmentem w formie [CLS] query [SEP] chunk [SEP].
  • Podaj tę parę do modelu cross-encodera, na przykład cross-encoder/ms-marco-MiniLM-L-6-v2.
  • Wynikiem jest ocena trafności dla każdej pary.
  • Ułóż wyniki według oceny i zachowaj 5 najlepszych.
  • 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}")
    

    Należy wybrać kodery krzyżowe, gdy najważniejsza jest dokładność, a dodatkowe 50 do 200 ms jest akceptowalne. Dla większości zastosowań RAG w produkcji jest to najlepszy punkt wyjścia. Wadą jest to, że każda para zapytanie-dokument jest przetwarzana oddzielnie, więc opóźnienie rośnie wraz z liczbą kandydatów.

    2. Reciprocal Rank Fusion (RRF)

    Załóżmy, że wolałbyś w ogóle nie używać modelu neuronalnego. Reciprocal Rank Fusion łączy rankingi z kilku systemów wyszukiwania za pomocą deterministycznej formuły: bez szkolenia, bez inferencji, tylko obliczenia arytmetyczne.

    RRF sprawdza się najlepiej, gdy już istnieje hybrydowe wyszukiwanie, np. gęste wyszukiwanie wektorowe w połączeniu z wyszukiwaniem według słów kluczowych BM25. Każdy system wyszukiwania dostarcza swój własny ranking, a RRF łączy je w jeden lepszy listę.

    Formuła:

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

    Tutaj k jest stałą, zwykle równą 60, a rank_r(d) oznacza pozycję dokumentu d w wyszukiwarki r. Dokumenty, które uzyskują wysokie ranki w kilku wyszukiwarkach, znajdują się na górze listy.

    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 jest tutaj najszybszą opcją, z praktycznie zerową dodatkową opóźnieniem. Użyj go, gdy już korzystasz z hybrydowego wyszukiwania i chcesz taniego ulepszenia bez konieczności inferyencji modelu. Nie przewyższy on cross-encodera pod względem dokładności, ale działa zaskakująco dobrze mimo braku elementów sztucznej inteligencji.

    3. Cohere Rerank API

    Część zespołów nie chce zarządzać ani dostosowywać modelu do ponownego rankowania. Jeśli twoim priorytetem jest produkt, a nie operacje modelu, zarządzany API do ponownego rankowania jest atrakcyjny, a rozwiązanie Cohere jest najbardziej ugruntowane w tej dziedzinie.

    API przyjmuje zapytanie wraz z listą dokumentów i zwraca je w kolejności ich istotności. W tle działa nowoczesny model do ponownego rankowania z optymalizacjami, których samodzielne stworzenie zajęłoby tygodnie. Obsługuje treści wielojęzyczne i długie dokumenty oraz skaluje się w zależności od potrzeb.

    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}")
    

    Wybierz go, gdy potrzebujesz wysokiej dokładności bez dodatkowego obciążenia operacyjnego. Kompromisem są koszt za każdą próbę wywołania oraz zależność od zewnętrznych elementów, a przy bardzo dużym ruchu opóźnienie połączenia sieciowego może mieć znaczenie. Dla większości zespołów oszczędzone czas na hostowaniu i optymalizacji przewyższają te koszty.

    4. ColBERT

    Kodery krzyżowe są dokładne, ale wolne, natomiast kodery dwukierunkowe działają szybko, lecz są mniej zaawansowane. ColBERT znajduje środkową drogę poprzez „późną interakcję”.

    1. Offline: każdy dokument jest kodowany token po tokenie w kontekstowe embeddingi, które są obliczane z góry i przechowywane w indeksie.
    2. Online: gdy przychodzi zapytanie, jest ono traktowane w taki sam sposób na poziomie tokenów.
    3. Ocena: każdy token zapytania jest dopasowywany do najbliższego tokenu dokumentu (krok MaxSim), a następnie te maksymalne wartości są sumowane, aby uzyskać wynik oceny powiązania.

    Ponieważ embeddingi dokumentów są obliczane z góry, jest to znacznie szybsze niż użycie cross-encodera. Dzięki zachowaniu interakcji na poziomie tokenów przewyższa bi-encodery pod względem dokładności.

    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 nadaje się do systemów na dużą skalę, które wymagają wyższej dokładności niż bi-encodery oraz mniejszego opóźnienia niż cross-encodery. Kosztem jest praca związana z tworzeniem i utrzymywaniem indeksu. Jeśli znasz się na bazach danych wektorowych i chcesz lepszej jakości bez opóźnień rosnących liniowo wraz z liczbą kandydatów, to jest idealne rozwiązanie.

    5. LLM jako sędzia

    W najważniejszych dziedzinach, takich jak diagnostyka medyczna, badania prawnicze i zgodność finansowa, nawet cross-encodery mogą okazać się niewystarczające. Zapytania są złożone, kontekst subtelny, a błędna odpowiedź ma poważne konsekwencje. W takich przypadkach najdokładniejszym narzędziem do ponownego rankowania może być sam LLM.

    Idea polega na skierowaniu dużego modelu (GPT-4, Claude lub Gemini) do oceny stopnia istotności każdego kandydata w odniesieniu do zapytania. Dzięki szerokiej wiedzy o świecie i silnym umiejętnościom rozumowania może on oceniać istotność w sposób, jakiemu nie są w stanie sprostać mniejsze narzędzia do ponownego rankowania. Przykładowy prompt:

    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:
    

    Zrób to dla każdego kandydata, przetwarzaj wyniki i zwróć najlepsze k-k wyników.

    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}")
    

    To najdokładniejsza technika w tym przypadku, ale jednocześnie najwolniejsza i najdroższa. Należy ją stosować przy złożonych, wysokowartościowych zapytaniach, gdzie błędna odpowiedź może kosztować prawdziwe pieniądze lub niesie rzeczywiste ryzyko. W większości zastosowań cross-encodery lub zarządzane API zapewniają około 90% dokładności przy 10% kosztów (to wartość orientacyjna, a nie standard mierzenia). Zachowaj oceny LLM dla przypadków, które rzeczywiście wymagają ich użycia.

    Wybór pomiędzy nimi

    Żaden z narzędzi do ponownego rankowania nie jest najlepszy we wszystkim. Ta tabela podsumowuje ograniczenia:

    | 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   |
    

    Rozsądnym planem jest rozpoczęcie od cross-encodera, ponieważ zapewnia on największy wzrost dokładności przy minimalnej złożoności. Jeśli opóźnienie stanie się problemem, rozważ użycie ColBERT lub zarządzanej API takiej jak Cohere. Dodaj RRF, jeśli używasz hybrydowego systemu wyszukiwania, a LLM-as-a-Judge zachowaj do zapytań, w których maksymalna dokładność usprawiedliwia koszt.

    Zespoły rzadko muszą wybierać tylko jeden z tych rozwiązań. Typowa struktura warstw produkcyjnych wygląda następująco:

    1. Hybrydowe wyszukiwanie (dense plus BM25) z fuzją RRF.
    2. Ponowne sortowanie przez cross-encoder wśród pierwszych 50 kandydatów.
    3. Rozwiązanie awaryjne w postaci LLM-as-a-Judge dla zapytań o wysokim znaczeniu, które zostały oznaczone.

    To zapewnia szybkość w zwykłych przypadkach oraz dokładność, gdy jest to kluczowe.

    Podsumowanie

    Reranking to prawdopodobnie najmniej doceniana część systemów RAG do produkcji. Zespoły spędzają miesiące na dostosowywaniu rozmiaru fragmentów i modeli embeddingowych, podczas gdy ich najlepszy kontekst pozostaje pogrzebany na pozycji 23, a dobry narzędzie do rerankingu naprawia to przy niewielkim wysiłku inżynieryjnym.

    Techniki te wariują od darmowych i szybkich (RRF) po drogie i potężne (LLM-as-a-Judge), a reranking nie musi być albo wszystko, albo nic. Zacznij od prostych rozwiązań, stopniowo dodawaj kolejne metody i zwiększaj złożoność tylko wtedy, gdy tego wymagają Twoje wskaźniki.

    • Faza pierwsza pozyskiwania danych szybko zbiera kandydatów; nie decyduje jeszcze o odpowiedziach.
    • Reranking pełni rolę kontroli jakości – to druga próba oceny rzeczywistej istotności informacji.
    • Cross-encoders stanowią najlepszy punkt wyjścia, oferując wysoką dokładność przy umiarkowanej złożoności.
    • RRF zapewnia korzyści bez dodatkowych kosztów, jeśli już używasz hybrydowego sposobu pozyskiwania danych.
  • Zarządzane API oszczędzają czas inżynierów, a Cohere Rerank działa doskonale od razu po uruchomieniu.
  • ColBERT zapewnia równowagę między szybkością a dokładnością w dużych systemach.
  • LLM-as-a-Judge to bardziej zaawansowane rozwiązanie, gdy dokładność ma większe znaczenie niż koszt.
  • Jeśli używasz RAG w produkcji bez ponownego sortowania wyników, jakość odpowiedzi jest niska. Wybierz jedną z technik, wdroż ją w tym tygodniu i sprawdź różnicę.