RAG produktiv betreiben: Chunking, Hybrid-Search, Reranking und Antwortqualität messen
Wie Sie eine RAG-Pipeline produktionsreif machen: hierarchisches Chunking, Tabellenextraktion, Hybrid-Search mit Qdrant und RRF, Cross-Encoder-Reranking und Evaluation mit RAGAS und DeepEval, mit aktuellen Codebeispielen.
Geprüft am 30.09.2026 · für ragas 0.4.3, qdrant-client 1.19.1, cohere 7.2.0
WerbelinksMit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Retrieval-Augmented Generation klingt in der Theorie einfach: Dokumente einlesen, Embeddings erzeugen, bei einer Nutzeranfrage die passenden Chunks finden und dem LLM mitgeben. In der Praxis scheitern RAG-Systeme selten am Embedding-Modell, sondern an Chunking-Strategie, Suchansatz und fehlender Qualitätsmessung. Diese Anleitung zeigt, wie Sie eine belastbare Produktiv-Pipeline aufbauen: vom richtigen Chunking über Hybrid-Search mit Reciprocal Rank Fusion bis zu Cross-Encoder-Reranking und messbarer Eval mit RAGAS und DeepEval.
Voraussetzungen
- Python 3.10 oder neuer in einer venv (getestete Paketstände September 2026: qdrant-client 1.19, ragas 0.4, deepeval 4.2, cohere 7.2, llama-index 0.14)
- Qdrant-Instanz lokal via Docker (
docker run -d -p 127.0.0.1:6333:6333 -v qdrant_data:/qdrant/storage qdrant/qdrant:v1.19.1, nur lokal erreichbar) oder Qdrant Cloud; mindestens Version 1.10 für serverseitige RRF-Fusion - API-Key für ein Embedding-Modell (OpenAI
text-embedding-3-small/-large, Cohereembed-v4oder self-hostedsentence-transformers/all-MiniLM-L6-v2) - API-Key für Cross-Encoder-Reranking: Cohere Rerank 3.5 oder Voyage rerank-2.5 (alternativ lokales
ms-marco-MiniLM-L-6-v2via sentence-transformers) - API-Key für ein Judge-LLM (z. B. OpenAI
gpt-4o) für RAGAS und DeepEval - Dokumentkorpus bereinigt: Duplikate entfernt, Boilerplate gefiltert, Tabellen strukturell extrahiert
- Mindestens 50 handgelabelte Query-Answer-Paare für die Eval-Baseline (oder RAGAS-Testset-Generator nutzen)
- Python-Pakete:
pip install qdrant-client ragas deepeval llama-index cohere voyageai sentence-transformers pdfplumber camelot-py langchain-openai langchain-community tabulate(tabulatebrauchtto_markdown()in Schritt 2)
Schritt 1: Chunking-Strategie richtig wählen
Das größte Missverständnis im RAG-Aufbau: semantisches Chunking klingt fortschrittlich, erzeugt aber im Schnitt nur 43 Tokens pro Chunk – zu wenig Kontext, damit das LLM korrekte Antworten formulieren kann. Eine auf der NAACL 2025 vorgestellte Auswertung zeigte, dass einfache Chunks fester Größe (200 Wörter) gleichwertige oder bessere Retrieval-Ergebnisse liefern als semantisches Chunking, bei deutlich geringeren Kosten.
Bewährt ist hierarchisches Chunking (auch „Small-chunk-retrieve, large-chunk-generate“ genannt): Kleine Leaf-Chunks (128–256 Tokens) werden für das Retrieval verwendet, weil sie präzise Treffer liefern. Die zugehörigen Parent-Chunks (512–2048 Tokens) werden dem LLM übergeben, weil es dort den nötigen Kontext für eine korrekte Antwort findet.
| Ansatz | Chunk-Größe (Tokens) | Stärke | Schwäche |
|---|---|---|---|
| Fixed-Size | 512–1024 + 10 % Overlap | Einfach, reproduzierbar | Kein semantischer Zusammenhang |
| Semantisch (Sentence-Transformer) | Ø 43 (zu klein) | Natürliche Grenzen | Zu wenig LLM-Kontext |
| Hierarchisch (empfohlen) | 128 Leaf / 512–2048 Parent | Präzises Retrieval + voller Kontext | Komplexeres Setup, mehr Speicher |
| Satzweise (strukturierte Docs) | 1–3 Sätze | Gut für API-Docs, Gesetze | Zu wenig Kontext bei langen Sätzen |
Hierarchisches Chunking mit LlamaIndex lässt sich schnell einrichten – der HierarchicalNodeParser übernimmt die gesamte Logik:
from llama_index.core.node_parser import HierarchicalNodeParser
from llama_index.core import VectorStoreIndex
parser = HierarchicalNodeParser.from_defaults(
chunk_sizes=[2048, 512, 128] # Parent -> Middle -> Leaf
)
nodes = parser.get_nodes_from_documents(documents)
# Leaf-Nodes (128 Token) fuer Retrieval,
# Parent-Nodes (512/2048 Token) fuer LLM-Antwort
# Parent-Nodes in einen Docstore legen und per AutoMergingRetriever
# beim Abruf zusammenfuehren
Verifizieren: from llama_index.core.node_parser import get_leaf_nodes; print(len(nodes), len(get_leaf_nodes(nodes))) zeigt die Gesamtzahl der Nodes und die Zahl der Leaf-Nodes; Letztere muss kleiner, aber deutlich größer als die Zahl der Dokumente sein. Indexieren Sie nur die Leaf-Nodes.
Schritt 2: PDF-Tabellen korrekt extrahieren
Tabellen sind eine häufige Ursache fehlerhafter RAG-Antworten. Ein Roh-Text-Parser linearisiert Tabellenzeilen, das LLM liest dann Zahlen im falschen Kontext. Abhilfe schafft eine zweistufige Extraktion:
import camelot
import pdfplumber
def extract_tables(pdf_path: str) -> list[str]:
tables_md = []
# Stufe 1: Camelot lattice mode fuer bordered Tabellen
try:
tables = camelot.read_pdf(pdf_path, flavor="lattice")
for t in tables:
tables_md.append(t.df.to_markdown(index=False))
except Exception:
pass
# Stufe 2: pdfplumber als robuster Fallback
if not tables_md:
with pdfplumber.open(pdf_path) as pdf:
for page in pdf.pages:
for table in page.extract_tables():
tables_md.append(
"\n".join(" | ".join(str(c) for c in row)
for row in table if row)
)
return tables_md
# Markdown-Tabellen vor dem Chunking im Dokument einfuegen
Verifizieren: Geben Sie für ein Test-PDF print(extract_tables("test.pdf")[0]) aus; Spalten und Zeilen müssen wie im Original zugeordnet sein. Camelot benötigt Ghostscript (apt install ghostscript).
Schritt 3: Qdrant-Collection für Hybrid-Search einrichten
Für eine vollständige Hybrid-Pipeline brauchen Sie in Qdrant drei Vektortypen: Dense-Vektoren (Cosine-Similarity für semantische Suche), Sparse-Vektoren (BM25 für Exact-Match-Retrieval) und optional Late-Interaction-Vektoren (ColBERT für Reranking). Ab Qdrant 1.10 ist RRF-Fusion serverseitig verfügbar. Für BM25-Sparse-Vektoren ist der IDF-Modifier Pflicht, sonst fehlt die Dokumenthäufigkeit in der Gewichtung:
from qdrant_client import QdrantClient
from qdrant_client.models import (
VectorParams, Distance, SparseVectorParams, SparseIndexParams,
MultiVectorConfig, MultiVectorComparator, Modifier
)
client = QdrantClient(url="http://localhost:6333")
client.create_collection(
collection_name="hybrid_rag",
vectors_config={
"dense": VectorParams(size=384, distance=Distance.COSINE),
"colbert": VectorParams(size=96, distance=Distance.COSINE,
multivector_config=MultiVectorConfig(
comparator=MultiVectorComparator.MAX_SIM))
},
sparse_vectors_config={
"sparse": SparseVectorParams(
index=SparseIndexParams(on_disk=False),
modifier=Modifier.IDF
)
}
)
# dense = sentence-transformers/all-MiniLM-L6-v2 (384d)
# sparse = qdrant/bm25
# colbert = answerdotai/answerai-colbert-small-v1 (96d)
Verifizieren: curl -s http://127.0.0.1:6333/collections/hybrid_rag liefert "status":"green" und listet dense, colbert und sparse.
Schritt 4: Hybrid-Search mit RRF-Fusion abfragen
Der entscheidende Fehler bei naivem Hybrid-Search: BM25 gibt unbegrenzt positive ganze Zahlen zurück, Cosine-Similarity liegt in [−1, 1]. Addieren Sie beide Scores direkt, dominiert BM25. Die Lösung ist Reciprocal Rank Fusion (RRF) – ein rankbasiertes Verfahren, das ausschließlich auf Positionen, nicht auf Rohwerten arbeitet.
Die RRF-Formel lautet: score(d) = Σ 1 / (k + rank(d)) mit Standard-k=60 (Elasticsearch-Default). Ein Dokument auf Rang 1 trägt 0,0164 bei, auf Rang 100 nur 0,00625. In Qdrant nutzen Sie die Query-API:
from qdrant_client.models import Prefetch, FusionQuery, Fusion
results = client.query_points(
collection_name="hybrid_rag",
prefetch=[
Prefetch(query=dense_vector, using="dense", limit=50),
Prefetch(query=sparse_vector, using="sparse", limit=50),
],
query=FusionQuery(fusion=Fusion.RRF), # k=60 Standard
limit=10,
with_payload=True
)
for p in results.points:
print(p.id, round(p.score, 4))
Verifizieren: Die Ausgabe listet bis zu 10 Treffer mit RRF-Scores unter 0,04. Suchen Sie testweise nach einer exakten Produktnummer: Sie muss oben erscheinen, auch wenn die reine Vektorsuche sie nicht findet.
Zum manuellen Nachvollziehen von RRF genügen wenige Zeilen Python:
def rrf_score(rank: int, k: int = 60) -> float:
return 1.0 / (k + rank)
def rrf_fusion(result_lists: list[list[str]]) -> dict[str, float]:
scores: dict[str, float] = {}
for result_list in result_lists:
for rank, doc_id in enumerate(result_list, start=1):
scores[doc_id] = scores.get(doc_id, 0) + rrf_score(rank)
return dict(sorted(scores.items(), key=lambda x: x[1], reverse=True))
Wichtig für Weaviate-Nutzer: Ab Version 1.24 ist der Fusion-Standard von RRF auf „Relative Score Fusion“ gewechselt. Bestehender Code ohne explizites fusionType-Feld ändert das Verhalten beim Upgrade still – ohne Fehlermeldung. Immer explizit setzen.
Schritt 5: Cross-Encoder-Reranking hinzufügen
Ein Cross-Encoder bewertet Anfrage und Chunk gemeinsam und sortiert die Kandidaten aus der Hybrid-Suche deutlich präziser. Veröffentlichte Benchmarks zeigen Zuwächse im zweistelligen Prozentpunktbereich bei MRR und Recall; messen Sie den Effekt mit Ihren eigenen Daten (Schritt 6).
| Modell | Sprachen | Kontext-Fenster | Besonderheiten |
|---|---|---|---|
| Cohere Rerank 3.5 | 100+ | Standard | Tabellen, JSON, Code |
| Voyage rerank-2.5 (Aug. 2025) | Mehrsprachig | 32.000 Tokens | bis 1.000 Dokumente je Aufruf |
| ms-marco-MiniLM-L-6-v2 | Englisch | 512 Tokens | lokal betreibbar, Open Source |
import cohere
co = cohere.ClientV2() # liest CO_API_KEY aus der Umgebung
results = co.rerank(
model="rerank-v3.5",
query=user_query,
documents=[chunk.text for chunk in hybrid_candidates], # top-50 bis 200
top_n=5,
)
for hit in results.results:
print(hit.index, round(hit.relevance_score, 3))
# Voyage rerank-2.5 (32K Token Kontextfenster):
# import voyageai
# vo = voyageai.Client()
# vo.rerank(query, docs, model="rerank-2.5", top_k=5)
Verifizieren: Die Ausgabe listet fünf Indizes mit absteigendem relevance_score zwischen 0 und 1. Stellen Sie eine Testfrage, deren Antwort Sie kennen: Der passende Chunk muss unter den Top 5 stehen.
Empfehlung für Top-K: Hybrid-Retrieval mit K=50, dann Cross-Encoder auf Top-5 reduzieren. Zu niedriges K (3) verfehlt die richtige Antwort; zu hohes K (50+) ohne Reranking füllt das Context-Window mit irrelevantem Text und erhöht Halluzinationen. Einen soliden Einstieg in die zugrunde liegende Vektordatenbank liefert die Anleitung Qdrant lokal mit Embeddings einrichten.
Schritt 6: Evaluation mit RAGAS und DeepEval einrichten
Ohne Messung wissen Sie nicht, ob Ihre Optimierungen helfen. BLEU und ROUGE taugen nicht für RAG: Sie erkennen weder faktische Fehler noch Retrieval-Schwächen. Der empfohlene Workflow: RAGAS für die Generierung eines synthetischen Testsets (Golden Dataset), DeepEval für laufende CI/CD-Integration und Quality-Gates.
RAGAS: synthetisches Testset erzeugen (API ab ragas 0.2; die früheren Module ragas.testset.generator und ragas.testset.evolutions gibt es nicht mehr):
import openai
from langchain_openai import ChatOpenAI
from ragas.llms import LangchainLLMWrapper
from ragas.embeddings import OpenAIEmbeddings
from ragas.testset import TestsetGenerator
generator = TestsetGenerator(
llm=LangchainLLMWrapper(ChatOpenAI(model="gpt-4o")),
embedding_model=OpenAIEmbeddings(client=openai.OpenAI()),
)
# documents = LangChain-Dokumente, z. B. aus DirectoryLoader
testset = generator.generate_with_langchain_docs(documents, testset_size=50)
testset.to_pandas().to_csv("golden_testset.csv", index=False)
DeepEval: fünf RAG-Metriken in einem Aufruf
from deepeval import evaluate
from deepeval.test_case import LLMTestCase
from deepeval.metrics import (
FaithfulnessMetric, AnswerRelevancyMetric,
ContextualRelevancyMetric, ContextualRecallMetric,
ContextualPrecisionMetric
)
metrics = [
FaithfulnessMetric(threshold=0.7, model="gpt-4o", include_reason=True),
AnswerRelevancyMetric(threshold=0.7, model="gpt-4o"),
ContextualRelevancyMetric(threshold=0.7, model="gpt-4o"),
ContextualRecallMetric(threshold=0.7, model="gpt-4o"),
ContextualPrecisionMetric(threshold=0.7, model="gpt-4o"),
]
test_cases = []
for user_input in test_inputs:
actual_output, retrieval_context = your_rag_pipeline(user_input)
test_cases.append(LLMTestCase(
input=user_input,
actual_output=actual_output,
expected_output=expected_outputs[user_input],
retrieval_context=retrieval_context
))
evaluate(test_cases=test_cases, metrics=metrics)
Verifizieren: Die Ausgabe zeigt je Testfall und Metrik einen Score mit Begründung und am Ende die Erfolgsquote. Liegt Faithfulness unter 0,70, prüfen Sie zuerst das Retrieval (Context Recall), dann den Prompt.
Für CI/CD bietet DeepEval ein pytest-Plugin; Tests unter dem Schwellwert lassen den Build fehlschlagen:
# test_rag.py
import pytest
from deepeval import assert_test
from deepeval.test_case import LLMTestCase
from deepeval.metrics import FaithfulnessMetric
@pytest.mark.parametrize("test_case", test_cases)
def test_faithfulness(test_case):
assert_test(test_case, [FaithfulnessMetric(threshold=0.7)])
# Ausfuehren:
# deepeval test run test_rag.py
Richtwerte für Ihre Metriken:
| Metrik | Produktionsziel | Nicht ship-fähig unter | Kritische Anwendungen |
|---|---|---|---|
| Faithfulness | ≥ 0,75 | 0,70 | ≥ 0,90 (DeepEval) |
| Answer Relevancy | ≥ 0,80 | – | – |
| Context Precision | ≥ 0,70 | – | – |
| Context Recall | ≥ 0,80 | – | – |
Das Monitoring der laufenden Produktionsdaten lässt sich gut in einen bestehenden Observability-Stack integrieren, siehe Grafana-Dashboards bauen und Datenquellen einbinden.
Troubleshooting / Typische Fehler
- Retrieval-Qualität bricht nach Embedding-Modell-Update ein: Alte und neue Embeddings liegen in verschiedenen Vektorräumen und sind nicht vergleichbar. Lösung: Embeddings versionieren, bei Modellwechsel kompletten Re-Index anstoßen, vorher A/B-Test durchführen.
- BM25 dominiert beim Score-Mischen: Rohscores nie addieren, RRF verwenden (Schritt 4).
- Kontextfenster läuft über: Informationen am Ende werden abgeschnitten; nur reranked Top-Chunks übergeben und Tokenbudget für System-Prompt und Frage reservieren.
- Kein Permission-Filtering: Mitarbeiter erhalten Kontext aus vertraulichen Dokumenten, weil der Vektorspeicher Zugriffsrechte ignoriert. Metadata-Filter mit User-Rolle/Tenant-ID bei jeder Query zwingend anwenden.
- Duplikate und Boilerplate im Index: Kopf-, Fußzeilen und Disclaimer verdrängen relevante Chunks; vor dem Indexieren deduplizieren und filtern.
- Evaluation nur mit BLEU/ROUGE: erkennt keine faktischen Fehler; Faithfulness und Contextual Recall messen (Schritt 6).
- Falsches Top-K: Hybrid-Retrieval mit K=50, Cross-Encoder auf Top-5 (Schritt 5).
Häufige Fragen
Welche Chunk-Größe ist ein guter Startpunkt?
Für die meisten Text-Korpora: 512 Tokens für Retrieval-Chunks mit 10 % Overlap (ca. 50 Tokens), eingebettet in Parent-Chunks von 1024–2048 Tokens für die LLM-Antwort. Overlap bringt nicht in jedem Setup einen messbaren Vorteil; testen Sie beide Varianten. Bei stark strukturierten Dokumenten wie API-Docs oder Gesetzestexten satzweise Segmentierung testen.
Muss ich einen Cross-Encoder-Reranker selbst hosten?
Nein. Cohere Rerank 3.5 und Voyage rerank-2.5 sind als API verfügbar, unterstützen Tabellen, JSON und Code, und verursachen bei 50 Kandidaten meist nur eine geringe Zusatzlatenz. Bei strengen Datenschutzanforderungen oder hohem Volumen eignet sich lokal ms-marco-MiniLM-L-6-v2 (nur Englisch) oder ein mehrsprachiger Cross-Encoder via sentence-transformers.
Was ist der Unterschied zwischen RAGAS und DeepEval?
RAGAS (Open Source) bietet Metriken und einen Testset-Generator, der aus Ihren eigenen Dokumenten Query-Answer-Paare erzeugt. DeepEval (Open Source, optional Cloud) integriert sich per pytest in CI/CD. Empfohlener Workflow: RAGAS für die Golden-Dataset-Generierung, DeepEval für laufende Pipeline-Tests.
Wie oft sollte der Vektorindex aktualisiert werden?
Bei täglich geänderten Quellen inkrementell mit Zeitstempel-Metadaten indexieren, bei stabilen Korpora wöchentlich neu indexieren. Bei einem Embedding-Modell-Wechsel ist ein kompletter Re-Index zwingend. Speichern Sie ein Versionsfeld im Payload, um Embedding-Drift frühzeitig erkennen zu können.
Ab wann ist Hybrid-Search gegenüber reinem Vektor-Search klar besser?
Sobald Nutzer nach Eigennamen, Produktnummern, Fehlercodes oder Akronymen suchen – BM25 findet Exact-Matches, die Vektor-Search regelmäßig verfehlt. Hybrid-Search ist fast immer gleich gut oder besser, der Mehraufwand gering.
Wie viele Beispiel-Queries brauche ich für ein aussagekräftiges Eval?
Mindestens 50 handgelabelte Paare; ergänzend erzeugt der RAGAS-Generator synthetische Paare aus Ihren Dokumenten (Schritt 6).
Fazit
Die drei tragenden Bausteine einer produktiven RAG-Pipeline sind: hierarchisches Chunking (kleine Chunks für Retrieval, große Parent-Chunks für das LLM), Hybrid-Search mit RRF statt naivem Score-Mixing, und Cross-Encoder-Reranking für die finalen Top-5. Ohne Evaluation mit Faithfulness und Contextual Recall wissen Sie nicht, ob Ihr System zuverlässig antwortet; RAGAS und DeepEval liefern die Zahlen für die Freigabe.
Weiterführende Anleitungen und Quellen
- Qdrant lokal einrichten: RAG-System mit Embeddings aufbauen
- Ollama und Open WebUI: Lokales LLM mit Docker betreiben
- Dify: KI-Apps und Agenten-Plattform mit Docker einrichten
- Grafana-Dashboards bauen und Datenquellen einbinden
Quellen: Qdrant – Hybrid Search with Reranking (offizielle Dokumentation), qdrant.tech; Confident AI / DeepEval – RAG Evaluation Metrics, confident-ai.com; RAGAS-Dokumentation – Testset Generation, docs.ragas.io; Cohere – Rerank API v2, docs.cohere.com; Weaviate – Hybrid Search, weaviate.io.


