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.
Ź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.
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:
- Pierwotne wyszukiwanie: szybkie, szerokie poszukiwanie zwraca od 50 do 100 kandydujących fragmentów.
- Reranking: inteligentniejszy model ocenia każdy kandydat pod kątem zapytania i przestawia listę.
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:
- Narzędzie do wyszukiwania zwraca 50 kandydujących fragmentów.
- Dla każdego fragmentu łączy się zapytanie z fragmentem w formie
[CLS] query [SEP] chunk [SEP].
cross-encoder/ms-marco-MiniLM-L-6-v2.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ę”.
- Offline: każdy dokument jest kodowany token po tokenie w kontekstowe embeddingi, które są obliczane z góry i przechowywane w indeksie.
- Online: gdy przychodzi zapytanie, jest ono traktowane w taki sam sposób na poziomie tokenów.
- 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:
- Hybrydowe wyszukiwanie (dense plus BM25) z fuzją RRF.
- Ponowne sortowanie przez cross-encoder wśród pierwszych 50 kandydatów.
- 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.
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ę.