Warum Ihre RAG-Pipeline 2026 mehr braucht als einfache Vektorsuche
Retrieval-Augmented Generation — kurz RAG — ist längst kein Experiment mehr. Es ist die Standardarchitektur für KI-Anwendungen, die auf aktuelle, unternehmensspezifische Daten zugreifen müssen. Aber hier kommt der Haken: Die meisten RAG-Pipelines, die heute in Produktion laufen, liefern bestenfalls mittelmäßige Ergebnisse.
Und das liegt fast nie am LLM selbst. Es liegt am Retrieval.
In der Praxis sieht die Realität ernüchternd aus: Reine Vektorsuche erreicht typischerweise nur etwa 62 % Genauigkeit. BM25 allein kommt auf rund 58 %. Kombiniert man beide Ansätze über Hybrid Search, steigt die Genauigkeit auf 79 %. Fügt man dann noch ein Reranking hinzu, landen wir bei 91 %. Das sind Zahlen aus echten Produktionssystemen — keine Laborwerte, die unter Idealbedingungen zustande kamen.
In diesem Artikel zeige ich Ihnen, wie Sie Ihre RAG-Pipeline systematisch von einer naiven Implementierung zu einem produktionsreifen System hochziehen. Wir arbeiten uns durch vier zentrale Hebel: intelligentes Chunking, Hybrid Search mit BM25 und Vektoren, Reranking mit Cross-Encodern und automatisierte Evaluierung mit dem RAGAS-Framework. Alles mit funktionierendem Python-Code, den Sie direkt einsetzen können.
RAG-Architektur im Überblick: Wo die Optimierungshebel liegen
Bevor wir in die einzelnen Techniken einsteigen, lohnt sich ein Blick auf die Gesamtarchitektur. Jede RAG-Anwendung besteht im Grunde aus zwei Hauptphasen:
- Indexierungsphase: Dokumente werden geladen, in Chunks aufgeteilt, in Embeddings umgewandelt und in einem Vektorspeicher abgelegt.
- Abfragephase: Eine Benutzeranfrage wird ebenfalls in ein Embedding umgewandelt, relevante Chunks werden aus dem Vektorspeicher geholt, und ein LLM generiert auf Basis dieser Chunks eine Antwort.
Die Optimierungshebel verteilen sich auf beide Phasen:
- Chunking-Strategie — bestimmt die Qualität der Indexierung
- Retrieval-Methode — entscheidet, welche Chunks überhaupt gefunden werden
- Reranking — filtert die wirklich relevantesten Chunks heraus
- Evaluierung — misst die tatsächliche Qualität der gesamten Pipeline
Der entscheidende Punkt: Optimieren Sie immer von innen nach außen. Zuerst das Chunking stabilisieren, dann das Retrieval verbessern, anschließend Reranking hinzufügen und schließlich kontinuierlich evaluieren. Wer mit Reranking anfängt, ohne ordentliches Chunking zu haben, poliert an den falschen Schrauben. Klingt offensichtlich, passiert aber erstaunlich oft.
Intelligente Chunking-Strategien: Der größte Hebel für RAG-Qualität
Ich sage das aus Erfahrung: Chunking ist der einzelne größte Hebel für die Qualität einer RAG-Pipeline. Sind die Chunks zu groß, verwässert der Kontext und das LLM wird abgelenkt. Sind sie zu klein, fehlt der Zusammenhang und die Antworten werden fragmentiert.
Die richtige Balance zu finden ist eine Kunst — aber eine, die auf klaren Prinzipien basiert.
Fixed-Size Chunking: Der Ausgangspunkt
Die einfachste Strategie teilt Dokumente in Blöcke fester Größe mit Überlappung auf. Klingt simpel, funktioniert aber überraschend gut als Baseline:
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=64,
separators=["\n\n", "\n", ". ", " ", ""],
length_function=len,
)
chunks = text_splitter.split_documents(documents)
print(f"Dokument in {len(chunks)} Chunks aufgeteilt")
Der RecursiveCharacterTextSplitter ist hier die richtige Wahl, weil er intelligent an natürlichen Textgrenzen trennt — zuerst an Absätzen, dann an Zeilen, dann an Sätzen. Die optimale Chunk-Größe liegt typischerweise zwischen 256 und 512 Token. Neuere Untersuchungen zeigen, dass kleinere Chunks zu deutlich geringerer Retrieval-Latenz führen, allerdings auf Kosten des Kontexts.
Semantisches Chunking: Zusammengehöriges zusammenhalten
Semantisches Chunking geht einen Schritt weiter. Statt an festen Zeichenzahlen zu trennen, gruppiert es Sätze anhand ihrer semantischen Ähnlichkeit. Ändert sich das Thema, wird ein neuer Chunk begonnen:
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
semantic_splitter = SemanticChunker(
embeddings=embeddings,
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=85,
)
semantic_chunks = semantic_splitter.split_documents(documents)
print(f"Semantisches Chunking: {len(semantic_chunks)} Chunks")
Der Vorteil: Zusammengehörige Informationen bleiben in einem Chunk, egal ob sie 200 oder 800 Zeichen umfassen. Der Nachteil (und der ist nicht zu unterschätzen): Sie brauchen ein Embedding-Modell bereits beim Chunking, was zusätzliche Rechenleistung und Kosten verursacht.
Dokumentenstruktur-basiertes Chunking
Für strukturierte Dokumente — also Markdown, HTML oder PDFs mit klarer Überschriftenstruktur — bietet sich ein strukturbasierter Ansatz an:
from langchain_text_splitters import MarkdownHeaderTextSplitter
headers_to_split_on = [
("#", "hauptueberschrift"),
("##", "abschnitt"),
("###", "unterabschnitt"),
]
markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on,
strip_headers=False,
)
md_chunks = markdown_splitter.split_text(markdown_dokument)
# Jeder Chunk behält seine Metadaten
for chunk in md_chunks[:3]:
print(f"Abschnitt: {chunk.metadata.get('abschnitt', 'N/A')}")
print(f"Inhalt: {chunk.page_content[:100]}...")
print("---")
Mein ehrlicher Rat: Starten Sie mit Fixed-Size Chunking als Baseline und messen Sie die Qualität. Wechseln Sie erst dann zu semantischem Chunking, wenn die Messwerte wirklich unbefriedigend sind. In der Praxis liefert der RecursiveCharacterTextSplitter mit sorgfältig gewählter Chunk-Größe (400–512 Token) und 10–15 % Überlappung oft erstaunlich gute Ergebnisse — gut genug, dass sich der Mehraufwand für semantisches Chunking nicht lohnt.
Hybrid Search: BM25 und Vektorsuche kombinieren
So, jetzt wird's richtig spannend. Hier liegt der nächste große Qualitätssprung.
Reine Vektorsuche hat ein fundamentales Problem: Sie findet semantisch ähnliche Texte, versagt aber bei exakten Begriffen, Fachterminologie, Produktbezeichnungen oder regulatorischen Codes. BM25-Keyword-Suche hat das umgekehrte Problem — exakte Wortübereinstimmungen werden gefunden, konzeptuell verwandte Inhalte mit anderer Formulierung aber nicht.
Hybrid Search kombiniert schlicht beide Welten: die semantische Tiefe der Vektorsuche mit der Präzision der Schlüsselwortsuche.
Reciprocal Rank Fusion (RRF): Ergebnisse intelligent verschmelzen
Das Standardverfahren zur Kombination beider Suchergebnisse ist Reciprocal Rank Fusion (RRF). Die Idee dahinter ist eigentlich simpel: Jedes Dokument bekommt einen Score basierend auf seinem Rang in jeder Ergebnisliste, und diese Scores werden addiert. Die Formel:
RRF_Score(d) = Σ 1 / (k + rank_i(d))
Dabei ist k eine Konstante (typischerweise 60), die verhindert, dass hoch gerankte Dokumente zu dominant werden.
Implementierung mit LangChain und bm25s
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain.retrievers import EnsembleRetriever
# Dokumente vorbereiten (bereits gechunkte Dokumente)
# ... chunks aus dem vorherigen Schritt
# 1. Vektorspeicher aufbauen
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = FAISS.from_documents(chunks, embeddings)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 20})
# 2. BM25-Retriever aufbauen
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 20
# 3. Hybrid Search via EnsembleRetriever (RRF)
hybrid_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6], # Vektorsuche leicht gewichten
)
# Abfrage ausfuehren
ergebnisse = hybrid_retriever.invoke(
"Welche Datenschutzrichtlinien gelten fuer Cloud-Speicher?"
)
for i, doc in enumerate(ergebnisse[:5]):
print(f"\n--- Ergebnis {i+1} ---")
print(doc.page_content[:200])
Die Gewichtung (weights) ist ein wichtiger Stellhebel. Für technische Dokumentation mit vielen Fachbegriffen empfehle ich eine stärkere BM25-Gewichtung (0,5/0,5 oder sogar 0,6/0,4). Für allgemeine Wissensbasen funktioniert eine leichte Vektordominanz (0,4/0,6) besser. Aber ganz ehrlich: Testen Sie verschiedene Gewichtungen mit Ihren eigenen Daten. Eine universelle Lösung gibt es hier schlicht nicht.
Hybrid Search mit Weaviate (Datenbankebene)
Alternativ können Sie Hybrid Search direkt auf der Datenbankebene implementieren. Vektordatenbanken wie Weaviate, Qdrant oder Milvus bieten native Hybrid-Search-Unterstützung — das ist deutlich performanter als die clientseitige Fusion:
import weaviate
from weaviate.classes.query import HybridFusion
client = weaviate.connect_to_local()
collection = client.collections.get("Dokumente")
# Hybrid Search mit nativer RRF-Fusion
response = collection.query.hybrid(
query="Datenschutzrichtlinien Cloud-Speicher",
alpha=0.6, # 0 = reines BM25, 1 = reine Vektorsuche
fusion_type=HybridFusion.RELATIVE_SCORE,
limit=10,
return_metadata=["score", "explain_score"],
)
for obj in response.objects:
print(f"Score: {obj.metadata.score:.4f}")
print(f"Inhalt: {obj.properties['content'][:150]}")
print("---")
Die native Hybrid-Search-Implementierung auf Datenbankebene bringt Relevanzsteigerungen von 35–50 % gegenüber reiner Vektorsuche — und das bei nur minimal erhöhter Latenz. Wenn Sie ohnehin eine Vektordatenbank einsetzen, ist das der einfachste Weg zur Verbesserung.
Reranking: Präzision auf das nächste Level heben
Hybrid Search verbessert den Recall — aber Recall allein reicht nicht. Sie wollen nicht einfach alle irgendwie relevanten Dokumente finden, sondern die relevantesten ganz oben haben.
Genau das leistet Reranking.
Das Prinzip ist denkbar simpel: Zuerst holen Sie mit Hybrid Search eine größere Menge an Kandidaten (typischerweise 20–50 Chunks). Dann bewertet ein spezialisierteres Modell — der Reranker — diese Kandidaten neu und sortiert die besten an die Spitze. Nur die Top-Ergebnisse (3–5 Chunks) gehen letztlich an das LLM.
Warum Reranking so effektiv ist
Der Unterschied liegt in der Architektur. Bei der initialen Retrieval-Phase verwenden wir Bi-Encoder, die Query und Dokument getrennt in Embeddings umwandeln und dann per Kosinus-Ähnlichkeit vergleichen. Das ist schnell, aber — sagen wir mal — etwas oberflächlich.
Reranker verwenden dagegen Cross-Encoder, die Query und Dokument gemeinsam durch ein Transformer-Modell schicken. Dadurch kann das Modell feinkörnige Aufmerksamkeits-Scores über alle Token beider Texte berechnen. Das Ergebnis ist ein viel tieferes Verständnis der Relevanz als ein simpler Kosinus-Vergleich es je liefern könnte.
Cross-Encoder-Reranking mit LangChain
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
# Basis-Retriever (unser Hybrid-Retriever von oben)
basis_retriever = hybrid_retriever # Gibt 20 Ergebnisse zurueck
# Cross-Encoder-Modell laden
cross_encoder = HuggingFaceCrossEncoder(
model_name="BAAI/bge-reranker-v2-m3"
)
# Reranker konfigurieren: nur Top-5 Ergebnisse durchlassen
reranker = CrossEncoderReranker(
model=cross_encoder,
top_n=5,
)
# Compression Retriever: kombiniert Retrieval + Reranking
optimierter_retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=basis_retriever,
)
# Abfrage — jetzt mit Reranking
ergebnisse = optimierter_retriever.invoke(
"Welche Verschluesselungsstandards muessen fuer DSGVO-Konformitaet eingesetzt werden?"
)
for i, doc in enumerate(ergebnisse):
print(f"\nRang {i+1}: {doc.page_content[:200]}...")
Das Modell BAAI/bge-reranker-v2-m3 ist aktuell eine der besten Open-Source-Optionen für mehrsprachiges Reranking. Es unterstützt über 100 Sprachen und liefert exzellente Ergebnisse auch für deutschsprachige Dokumente — was bei vielen Reranking-Modellen leider nicht selbstverständlich ist.
API-basiertes Reranking mit Cohere
Falls Sie kein eigenes GPU-Hosting betreiben möchten (und mal ehrlich, das will nicht jeder), ist der Cohere Rerank API eine hervorragende Alternative für Produktionsumgebungen:
from langchain_cohere import CohereRerank
from langchain.retrievers import ContextualCompressionRetriever
# Cohere Reranker (API-Key als Umgebungsvariable COHERE_API_KEY)
cohere_reranker = CohereRerank(
model="rerank-multilingual-v3.0",
top_n=5,
)
# Mit Hybrid-Retriever kombinieren
produktion_retriever = ContextualCompressionRetriever(
base_compressor=cohere_reranker,
base_retriever=hybrid_retriever,
)
ergebnisse = produktion_retriever.invoke(
"Wie implementiere ich ein Audit-Logging fuer KI-Entscheidungen?"
)
Cohere Rerank 3 unterstützt über 100 Sprachen und bietet mit der Nimble-Variante eine latenzoptimierte Version für zeitkritische Anwendungen. Die 100–500 ms zusätzliche Latenz durch Reranking klingen erstmal nach viel, werden aber durch bessere Antwortqualität und niedrigere LLM-Kosten (weniger irrelevante Token im Kontext) mehr als wettgemacht.
Die vollständige RAG-Pipeline: Alles zusammenführen
Jetzt setzen wir alle Bausteine zu einer produktionsreifen RAG-Pipeline zusammen. Hier der vollständige Code — kopieren, anpassen, einsetzen:
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever, ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import DirectoryLoader, TextLoader
# === PHASE 1: Indexierung ===
# Dokumente laden
loader = DirectoryLoader(
"./wissensbasis/",
glob="**/*.txt",
loader_cls=TextLoader,
loader_kwargs={"encoding": "utf-8"},
)
dokumente = loader.load()
# Chunking
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=64,
separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = text_splitter.split_documents(dokumente)
print(f"{len(dokumente)} Dokumente in {len(chunks)} Chunks aufgeteilt")
# Embeddings und Vektorspeicher
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = FAISS.from_documents(chunks, embeddings)
# === PHASE 2: Optimiertes Retrieval ===
# Hybrid Search
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 20})
bm25_retriever = BM25Retriever.from_documents(chunks, k=20)
hybrid_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6],
)
# Reranking
cross_encoder = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-v2-m3")
reranker = CrossEncoderReranker(model=cross_encoder, top_n=5)
optimierter_retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=hybrid_retriever,
)
# === PHASE 3: Generierung ===
llm = ChatOpenAI(model="gpt-4o", temperature=0)
prompt = ChatPromptTemplate.from_template("""
Beantworte die folgende Frage ausschliesslich auf Basis des bereitgestellten Kontexts.
Wenn der Kontext die Frage nicht beantworten kann, sage das ehrlich.
Kontext:
{context}
Frage: {question}
Antwort:""")
def format_docs(docs):
return "\n\n---\n\n".join(
f"[Quelle: {doc.metadata.get('source', 'Unbekannt')}]\n{doc.page_content}"
for doc in docs
)
# RAG-Kette zusammenbauen
rag_chain = (
{
"context": optimierter_retriever | format_docs,
"question": RunnablePassthrough(),
}
| prompt
| llm
| StrOutputParser()
)
# Ausfuehren
antwort = rag_chain.invoke(
"Welche technischen Massnahmen sind fuer DSGVO-konforme Datenverarbeitung erforderlich?"
)
print(antwort)
Diese Pipeline kombiniert alle besprochenen Optimierungen: intelligentes Chunking mit dem RecursiveCharacterTextSplitter, Hybrid Search über den EnsembleRetriever mit RRF-Fusion, Cross-Encoder-Reranking für maximale Präzision und einen sorgfältig formulierten Prompt, der das LLM an den bereitgestellten Kontext bindet. Kein Hexenwerk, aber solides Handwerk.
RAG-Pipeline evaluieren: Qualität messen mit RAGAS
Eine optimierte Pipeline bringt wenig, wenn Sie nicht messen können, ob sie tatsächlich besser ist. Hier kommt RAGAS ins Spiel.
Das RAGAS-Framework (Retrieval Augmented Generation Assessment) bietet einen systematischen Ansatz zur Evaluierung — und das Beste daran: Es funktioniert ohne manuell annotierte Ground-Truth-Daten. Das spart Wochen an Annotationsarbeit.
Die vier Kernmetriken von RAGAS
- Faithfulness: Ist die generierte Antwort faktisch konsistent mit dem abgerufenen Kontext? Ein Score von 1,0 bedeutet: Jede Behauptung in der Antwort lässt sich aus dem Kontext ableiten. Diese Metrik ist Ihr wichtigstes Werkzeug gegen Halluzinationen.
- Answer Relevancy: Wie relevant ist die generierte Antwort für die ursprüngliche Frage? Misst, ob die Antwort tatsächlich die gestellte Frage beantwortet — und nicht am Thema vorbeigeht.
- Context Precision: Sind die abgerufenen Chunks tatsächlich relevant für die Beantwortung der Frage? Ein hoher Wert bedeutet, dass der Retriever punktgenau die richtigen Informationen liefert.
- Context Recall: Wurden alle notwendigen Informationen abgerufen? Kleiner Haken: Dies ist die einzige RAGAS-Metrik, die tatsächlich Ground-Truth-Daten benötigt.
RAGAS in der Praxis
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall,
)
from ragas.llms import LangchainLLMWrapper
from ragas.embeddings import LangchainEmbeddingsWrapper
from datasets import Dataset
# Testdatensatz vorbereiten
testfragen = [
"Welche Verschluesselungsstandards sind DSGVO-konform?",
"Wie lange muessen Audit-Logs aufbewahrt werden?",
"Welche Rechte haben Betroffene nach Artikel 15 DSGVO?",
]
# Antworten und Kontexte sammeln
ergebnisse_liste = []
for frage in testfragen:
# Kontext abrufen
docs = optimierter_retriever.invoke(frage)
kontexte = [doc.page_content for doc in docs]
# Antwort generieren
antwort = rag_chain.invoke(frage)
ergebnisse_liste.append({
"question": frage,
"answer": antwort,
"contexts": kontexte,
"ground_truth": "", # Optional fuer Context Recall
})
# Als HuggingFace Dataset formatieren
eval_dataset = Dataset.from_list(ergebnisse_liste)
# RAGAS-Evaluierung ausfuehren
eval_llm = LangchainLLMWrapper(ChatOpenAI(model="gpt-4o-mini"))
eval_embeddings = LangchainEmbeddingsWrapper(
OpenAIEmbeddings(model="text-embedding-3-small")
)
ergebnis = evaluate(
dataset=eval_dataset,
metrics=[faithfulness, answer_relevancy, context_precision],
llm=eval_llm,
embeddings=eval_embeddings,
)
print("=== RAGAS Evaluierungsergebnisse ===")
print(f"Faithfulness: {ergebnis['faithfulness']:.3f}")
print(f"Answer Relevancy: {ergebnis['answer_relevancy']:.3f}")
print(f"Context Precision: {ergebnis['context_precision']:.3f}")
Als grobe Orientierung: Scores über 0,8 gelten als starke Performance. Liegt Faithfulness unter 0,7, hat Ihre Pipeline ein Halluzinationsproblem — meistens, weil irrelevanter Kontext ans LLM weitergereicht wird. Liegt Context Precision unter 0,7, liegt das Problem beim Retrieval selbst: Der richtige Kontext wird nicht gefunden oder zu weit unten gerankt.
Iterativ optimieren: Der RAGAS-Feedback-Loop
Die eigentliche Stärke von RAGAS liegt nicht in einer einzelnen Messung, sondern im iterativen Einsatz. Der Ablauf: Definieren Sie einen festen Testdatensatz mit 20–50 repräsentativen Fragen. Messen Sie die Baseline. Ändern Sie genau einen Parameter — Chunk-Größe, Retriever-Gewichtung, Reranker-Modell. Messen Sie erneut. Vergleichen Sie.
Das klingt banal, ist es aber nicht. Dieser systematische Ansatz ist der Unterschied zwischen Raten und tatsächlichem Optimieren.
konfigurationen = [
{"chunk_size": 256, "overlap": 32, "reranker": "bge-reranker-v2-m3"},
{"chunk_size": 512, "overlap": 64, "reranker": "bge-reranker-v2-m3"},
{"chunk_size": 512, "overlap": 64, "reranker": "cohere-rerank-v3"},
{"chunk_size": 768, "overlap": 96, "reranker": "bge-reranker-v2-m3"},
]
# Fuer jede Konfiguration Pipeline aufbauen, evaluieren, Ergebnisse vergleichen
for config in konfigurationen:
pipeline = build_pipeline(**config) # Eigene Hilfsfunktion
score = evaluate_pipeline(pipeline, testfragen)
print(f"Config: {config} => Faithfulness: {score['faithfulness']:.3f}")
Produktionsoptimierungen: Caching, Parallelisierung und Monitoring
Neben den vier Haupthebeln gibt es weitere Optimierungen, die in Produktionsumgebungen einen echten Unterschied machen. Hier die wichtigsten.
Query-Caching für wiederkehrende Anfragen
In vielen Anwendungen stellen Nutzer immer wieder ähnliche Fragen. Caching kann hier die Latenz um bis zu 70 % reduzieren:
from langchain_community.cache import InMemoryCache
from langchain.globals import set_llm_cache
# Einfaches In-Memory-Caching fuer Entwicklung
set_llm_cache(InMemoryCache())
# Fuer Produktion: Redis-basiertes Caching
from langchain_community.cache import RedisCache
import redis
redis_client = redis.Redis(host="localhost", port=6379)
set_llm_cache(RedisCache(redis_client))
Embedding-Caching
Neben dem LLM-Caching lohnt es sich, auch Embeddings zu cachen. Wenn Ihre Wissensbasis nicht ständig aktualisiert wird, müssen Dokumente nicht bei jedem Neustart neu eingebettet werden:
from langchain.storage import LocalFileStore
from langchain_openai import OpenAIEmbeddings
from langchain.embeddings import CacheBackedEmbeddings
# Embeddings auf Festplatte cachen
file_store = LocalFileStore("./embedding_cache/")
cached_embeddings = CacheBackedEmbeddings.from_bytes_store(
OpenAIEmbeddings(model="text-embedding-3-small"),
file_store,
namespace="text-embedding-3-small",
)
# Beim ersten Aufruf werden Embeddings berechnet und gespeichert
# Alle weiteren Aufrufe laden sie aus dem Cache
vectorstore = FAISS.from_documents(chunks, cached_embeddings)
Query-Transformation für besseres Retrieval
Eine oft unterschätzte Technik: Query-Transformation. Hier formuliert das LLM die Benutzeranfrage um, bevor sie an den Retriever geht — in Fachsprache, mit Synonymen oder als sogenannter HyDE-Ansatz (Hypothetical Document Embeddings):
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
# HyDE: Hypothetische Antwort generieren, dann diese embedden
hyde_prompt = ChatPromptTemplate.from_template("""
Schreibe einen kurzen, sachlichen Absatz, der die folgende Frage direkt beantwortet.
Verwende Fachbegriffe und sei spezifisch.
Frage: {question}
Antwort:""")
hyde_chain = hyde_prompt | ChatOpenAI(model="gpt-4o-mini") | StrOutputParser()
# Die hypothetische Antwort wird als Suchquery verwendet
hypothetische_antwort = hyde_chain.invoke(
{"question": "Wie funktioniert AES-256-Verschluesselung bei der Datenuebertragung?"}
)
# Diese Antwort enthält die Fachbegriffe, die im Vektorspeicher
# mit hoher Wahrscheinlichkeit zu relevanten Treffern fuehren
ergebnisse = vectorstore.similarity_search(hypothetische_antwort, k=10)
HyDE ist besonders wirkungsvoll, wenn Benutzer umgangssprachliche Fragen stellen, die Wissensbasis aber in Fachsprache geschrieben ist. Die hypothetische Antwort überbrückt diese lexikalische Lücke elegant.
Häufige Fehler und wie Sie sie vermeiden
Nach der Arbeit mit zahlreichen RAG-Pipelines in Produktionsumgebungen fallen bestimmte Fehler immer wieder auf. Hier die größten Stolperfallen:
- Zu große Chunks: Alles über 1.000 Token führt fast immer zu verwässerten Ergebnissen. Das LLM bekommt zu viel irrelevanten Kontext und verliert den Fokus.
- Keine Überlappung: Chunks ohne Überlappung können relevante Informationen an der Trennstelle verlieren. 10–15 % Überlappung kosten wenig, bringen aber viel.
- Nur Vektorsuche: Wie gezeigt, lässt reine Vektorsuche bis zu 30 % relevanter Ergebnisse auf der Strecke. Hybrid Search ist der Mindeststandard.
- Zu viel Kontext ans LLM: Mehr ist hier definitiv nicht besser. 3–5 hoch relevante Chunks übertreffen 20 mittelmäßig relevante Chunks bei Weitem. Genau hier zahlt sich Reranking aus.
- Keine Evaluierung: Ohne RAGAS oder ein vergleichbares Framework optimieren Sie im Blindflug. Jede Änderung sollte messbar sein — sonst wissen Sie nie, ob Sie gerade verbessern oder verschlimmbessern.
Häufig gestellte Fragen
Wie groß sollten Chunks in einer RAG-Pipeline sein?
Die optimale Chunk-Größe liegt typischerweise zwischen 256 und 512 Token. Kleinere Chunks (um 256 Token) ermöglichen präziseres Retrieval, verlieren aber Kontext. Größere Chunks (um 512 Token) behalten mehr Zusammenhang, können aber irrelevante Informationen enthalten. Starten Sie mit 512 Token und 64 Token Überlappung als Baseline, und testen Sie dann systematisch mit RAGAS, ob Anpassungen für Ihren spezifischen Anwendungsfall Verbesserungen bringen.
Was bringt Hybrid Search im Vergleich zu reiner Vektorsuche?
In Produktionssystemen zeigt sich ein konsistenter Qualitätssprung: Reine Vektorsuche erreicht typischerweise 62 % Genauigkeit, Hybrid Search (BM25 + Vektor mit RRF-Fusion) liegt bei 79 %. Das entspricht einer Verbesserung von etwa 27 %. Besonders groß ist der Gewinn bei Fachterminologie, Produktbezeichnungen und regulatorischen Codes — also exakten Begriffen, die bei reiner semantischer Suche gerne mal untergehen.
Welchen Reranker sollte ich für deutschsprachige Dokumente verwenden?
Für mehrsprachige Anwendungen einschließlich Deutsch empfehle ich den BAAI/bge-reranker-v2-m3 als Open-Source-Option oder Cohere Rerank 3 Multilingual als API-Lösung. Beide unterstützen über 100 Sprachen und liefern exzellente Ergebnisse für deutschsprachige Texte. Der BGE-Reranker ist kostenlos und läuft lokal; Cohere bietet geringere Latenz und erfordert kein GPU-Hosting. Welche Variante besser passt, hängt letztlich von Ihrer Infrastruktur ab.
Wie messe ich die Qualität meiner RAG-Pipeline ohne Ground-Truth-Daten?
Das RAGAS-Framework ermöglicht eine referenzfreie Evaluierung. Die Metriken Faithfulness, Answer Relevancy und Context Precision benötigen keine manuell annotierten Referenzantworten — nur Context Recall braucht Ground-Truth-Daten. Scores über 0,8 gelten als starke Performance in den meisten Anwendungsfällen. Für den Einstieg reichen die drei referenzfreien Metriken völlig aus.
Lohnt sich Reranking trotz der zusätzlichen Latenz?
Kurze Antwort: Ja. Die typische Reranking-Latenz von 100–500 ms wird in den allermeisten Fällen durch deutlich bessere Antwortqualität und niedrigere LLM-Kosten mehr als ausgeglichen. Durch Reranking reduzieren Sie die Anzahl der ans LLM übergebenen Chunks von 20 auf 3–5, was Token-Kosten senkt und Halluzinationen reduziert. In Produktionssystemen steigert Reranking die Genauigkeit von 79 % auf 91 % — das ist ein Qualitätssprung, den Ihre Nutzer spüren werden.