Reranking: Relevanz verbessern
Auf dieser Seite
Lernziele
- Verstehen, warum Reranking die Ergebnisse verbessert
- Implementierung eines Rerankers mittels eines Cross-Encoders
- Integration von Reranking in eine RAG-Pipeline
Das Problem des Bi-Encoders
Alle bisher erstellte Suchfunktionen basieren auf einem Bi-Encoder: Anfrage und Dokumente werden separat kodiert und anschließend über das Skalarprodukt verglichen. Dies ermöglicht eine sofortige Suche über eine Million Segmente hinweg, da die Korpusvektoren einmalig berechnet wurden. Gleichzeitig ist dies die Ursache für Ungenauigkeiten, da das Modell Anfrage und Dokument nie gemeinsam sieht: Es muss jeden Text in einen Vektor zusammenfassen, ohne zu wissen, was später danach gefragt wird.
Ein Cross-Encoder, auch als Reranker bezeichnet, nimmt das (Anfrage, Dokument)-Paar als Eingabe und erzeugt eine deutlich feinere Relevanzbewertung, da es beide Texte gemeinsam liest und erkennen kann, dass ein Dokument ein Thema behandelt, jedoch in einem gegenteiligen Sinne. Dieser Gewinn hat einen Preis: Das Modell muss jedes Dokument einzeln verarbeiten, was den Einsatz am gesamten Korpus unmöglich macht. Die Lösung besteht darin, den Einsatz dort vorzusehen, wo er kosteneffizient ist, nämlich auf einer kleinen Menge bereits vorqualifizierter Kandidaten.
Daher besteht die zweistufige Pipeline aus zwei Modellen, die jeweils ihre Stärken ausspielen: Der schnelle Eingrenzt den Kreis, der präzise Modell trifft die endgültige Auswahl.
Anfrage → Bi-Encoder → Top 50 Kandidaten → Cross-Encoder → Top 5 Ergebnisse
Reranking mit Cohere
Cohere stellt einen Hochleistungs-Reranker über die API bereit, wodurch das Self-Hosting eines Modells entfällt. Die folgende Funktion verknüpft beide Schritte: Zuerst werden über Vektorähnlichkeit zwanzig Kandidaten abgerufen, danach wird rerank-v3.5 angewendet, um nur fünf zu behalten. Beachten Sie das Feld index_original im Ergebnis: Der Reranker gibt Positionen relativ zur Kandidatenliste zurück, und Sie benötigen dies, um die Zuordnung zum tatsächlichen Dokument im Korpus wiederherzustellen.
import cohere
from openai import OpenAI
co = cohere.Client(api_key="your-cohere-key")
openai_client = OpenAI()
def search_and_rerank(
question: str,
documents: list[str],
embeddings_corpus,
k_retrieval: int = 20,
k_final: int = 5
) -> list[dict]:
"""Semantische Suche + Reranking."""
# Schritt 1: Schnelle Abrufung (Bi-Encoder)
query_emb = openai_client.embeddings.create(
input=question,
model="text-embedding-3-large"
).data[0].embedding
import numpy as np
scores = embeddings_corpus @ np.array(query_emb)
top_indices = np.argsort(scores)[-k_retrieval:][::-1]
candidates = [documents[i] for i in top_indices]
# Schritt 2: Reranking (Cross-Encoder)
rerank_response = co.rerank(
query=question,
documents=candidates,
top_n=k_final,
model="rerank-v3.5"
)
return [
{
"text": candidates[r.index],
"rerank_score": r.relevance_score,
"original_index": int(top_indices[r.index])
}
for r in rerank_response.results
]
Reranking mit einem LLM
Falls Sie aus vertraglichen Gründen, Datenlokalisierungsanforderungen oder reinen Kostengründen kein weiteres Provider-System in Ihre Architektur integrieren möchten, kann ein LLM, das Sie bereits nutzen, diese Rolle übernehmen. Sie übermitteln die Frage und ein Dokument, bitten um eine Bewertung von 0 bis 10 und sortieren die Ergebnisse. Eine Temperatur von 0 und eine Reduzierung von max_tokens auf fünf stellen sicher, dass die Ausgabe deterministisch und kostengünstig ist. Der try/except-Block behandelt Fälle, in denen das Modell auf etwas anderes als eine Zahl antwortet.
from openai import OpenAI
client = OpenAI()
def llm_reranker(
question: str,
candidates: list[str],
k: int = 5
) -> list[dict]:
"""Verwendet ein LLM zur Bewertung der Relevanz."""
scores = []
for i, doc in enumerate(candidates):
response = client.chat.completions.create(
model="gpt-5.6-terra",
messages=[{
"role": "user",
"content": (
f"Beurteilen Sie auf einer Skala von 0 bis 10, wie relevant dieses Dokument "
f"ist, um die Frage zu beantworten.\n\n"
f"Frage: {question}\n\n"
f"Dokument: {doc}\n\n"
f"Antworten Sie nur mit einer Zahl zwischen 0 und 10."
)
}],
temperature=0,
max_tokens=5
)
try:
score = float(response.choices[0].message.content.strip())
except ValueError:
score = 0.0
scores.append({"index": i, "text": doc, "score": score})
scores.sort(key=lambda x: x["score"], reverse=True)
return scores[:k]
Diese Variante kostet einen Aufruf pro Kandidat, was zwanzig Aufrufe und eine entsprechende kumulierte Latenz für eine einzelne Benutzeranfrage bedeutet. In der Praxis ist dies für eine interaktive Schnittstelle unzumutbar. Stattdessen bitten wir das Modell, alle Dokumente auf einmal zu bewerten, indem wir jeden Kandidaten auf 200 Zeichen gekürzt und nummeriert präsentieren, und erwarten eine Liste von Indizes als Antwort.
def llm_batch_reranker(
question: str,
candidates: list[str],
k: int = 5
) -> list[int]:
"""Reranking in einem einzigen LLM-Aufruf."""
formatted_docs = "\n".join(
f"[{i}] {doc[:200]}" for i, doc in enumerate(candidates)
)
response = client.chat.completions.create(
model="gpt-5.6-terra",
messages=[{
"role": "user",
"content": (
f"Sortieren Sie diese Dokumente nach Relevanz zur Frage. "
f"Geben Sie die Indizes der {k} relevantesten "
f"Dokumente, getrennt durch Kommas, von der "
f"höchsten zur niedrigsten Relevanz zurück.\n\n"
f"Frage: {question}\n\n"
f"Dokumente:\n{formatted_docs}\n\n"
f"Indizes (z. B. 3,1,7,0,5):"
)
}],
temperature=0,
max_tokens=50
)
indices_str = response.choices[0].message.content.strip()
return [int(i.strip()) for i in indices_str.split(",")][:k]
Integration in die RAG-Pipeline
In einer vollständigen Pipeline passt Reranking zwischen Abruf und Generierung ein, ohne dass andere Komponenten geändert werden müssen. Der entscheidende Punkt in der Klasse unten ist die Schwelle von 0,3 für relevance_score: Sie erlaubt es, weniger als fünf Kontexte zurückzugeben, falls kein Kandidat wirklich relevant ist, anstatt dem Generator Rauschen zuzuführen. Oft eliminiert dieser Filter mehr als das Reranking selbst halluzinierte Antworten.
class RAGPipelineWithReranking:
def __init__(self):
self.client = OpenAI()
self.co = cohere.Client()
def respond(self, question: str, chunks, embeddings_corpus):
# 1. Grober Abruf
candidates = self.search(question, embeddings_corpus, k=20)
# 2. Reranking
candidate_docs = [chunks[i]["text"] for i in candidates]
reranked = self.co.rerank(
query=question,
documents=candidate_docs,
top_n=5,
model="rerank-v3.5"
)
# 3. Auswahl der besten Ergebnisse
contexts = [
chunks[candidates[r.index]]
for r in reranked.results
if r.relevance_score > 0.3 # Relevanz-Schwelle
]
# 4. Generierung der Antwort
return self.generate(question, contexts)
Auswirkungen des Rerankings
| Metrik | Ohne Reranking | Mit Reranking | Gewinn |
|---|---|---|---|
| Recall@5 | 0,72 | 0,72 | = |
| Precision@5 | 0,58 | 0,78 | +34% |
| MRR | 0,65 | 0,82 | +26% |
Diese Zahlen zeigen genau, was ein Reranker leistet. Der Recall bleibt unverändert, und das ist logisch: Die Kandidaten sind dieselben, und der Cross-Encoder kann ein Dokument nicht retten, das der Bi-Encoder nicht auf die Liste gesetzt hat. Präzision und MRR verbessern sich jedoch signifikant, da das richtige Dokument von der sechsten auf die erste Position aufsteigt. Praktische Folgerung: Wenn Ihr Recall schlecht ist, wird Reranking Sie nicht retten; Sie müssen den Abruf zunächst erweitern oder das Chunking überdenken.
Zusammenfassung
- Reranking sortiert Kandidaten mit einem präziseren Modell neu
- Typische Pipeline: Bi-Encoder (Top 20–50) → Cross-Encoder (Top 5)
- Cohere rerank-v3.5 ist eine Komplettlösung
- Ein LLM kann als Reranker dienen, wenn Sie eine Abhängigkeit vermeiden möchten
- Der Gewinn an Präzision ist signifikant (typischerweise 20–35%)