Letzten Monat haben Sie einen Chatbot entwickelt. Er teilte Ihrem Kunden zuversichtlich mit, dass Sie nach Mongolei versenden. Das tun Sie nicht. Das LLM wusste nicht, was es nicht wusste, also hat es geraten.
Das ist das RAG-Problem – und die Lösung ist kein besseres Prompting.
Retrieval-Augmented Generation (RAG) löst einen spezifischen Fehlerfall: Wenn ein LLM nicht über den nötigen Kontext verfügt, um korrekt zu antworten, halluziniert es. RAG fügt Ihre tatsächlichen Daten in das Gespräch ein, bevor das LLM eine Antwort generiert. Keine Daten, keine Halluzinationen. Theoretisch einfach genug. Die Ausführung ist es, wo die meisten Implementierungen scheitern.
Diese Anleitung führt Sie durch die Produktion von RAG – warum es funktioniert, wo Teams stecken bleiben, wie die Architektur aufgebaut wird und welche spezifischen Setup-Entscheidungen die Leistung ausmachen oder brechen.
Warum RAG Fine-Tuning schlägt (und wann nicht)
Bevor Sie sich für RAG entscheiden, müssen Sie wissen, was Sie eigentlich lösen. Es gibt einen grundlegenden Unterschied zwischen dem Lehren eines LLM und dem Geben von Daten zum Nachschlagen.
Fine-Tuning aktualisiert Modellgewichte. Sie füttern das Modell mit Tausenden von Beispielen Ihrer Daten und erwarteten Ausgaben, und es lernt Muster. Das Wissen wird Teil des Modells. Das kostet Geld (Hunderte bis Tausende pro Trainingslauf), braucht Zeit (Stunden bis Tage) und verändert das Verhalten des Modells dauerhaft. Sie können es nicht einfach mit neuen Informationen aktualisieren, ohne neu zu trainieren.
RAG ruft zur Inferenzzeit relevanten Kontext ab. Sie speichern Ihre Daten in einer separaten Datenbank, und wenn ein Benutzer eine Frage stellt, rufen Sie die relevanten Teile ab und fügen sie in den Prompt ein. Das Modell sieht Ihre aktuellen Daten bei jeder Anfrage. Sie können die Daten aktualisieren, ohne etwas neu zu trainieren.
Hier ist die praktische Wahl:
- Verwenden Sie RAG, wenn: Ihre Daten sich regelmäßig ändern (Produktdokumentation, Preise, Lagerbestand, Kundenprofile), Sie aktuelle Informationen benötigen, Sie ohne Neudefinition aktualisieren möchten oder Sie noch nicht sicher sind, was das LLM lernen muss.
- Verwenden Sie Fine-Tuning, wenn: Ihre Daten stabil und umfangreich sind, das Muster subtil ist (Schreibstil, Tonfall, spezialisierte Argumentation) und die Inferenzgeschwindigkeit wichtiger ist als die Aktualität der Daten.
- Verwenden Sie beides, wenn: Sie über stabiles Domänenwissen (Fine-Tuning) und dynamische Daten (RAG) verfügen. Dies ist in spezialisierten Bereichen üblich – Fine-Tuning auf medizinische Terminologie, RAG für Patientengeschichten.
Die meisten Teams sollten mit RAG beginnen. Es ist billiger zu bauen, einfacher zu iterieren und erfordert nicht die Datentiefen, die Fine-Tuning erfordert. Die Leistungslücke zwischen gut abgestimmtem RAG und Fine-Tuning ist schmaler als vor zwei Jahren – insbesondere mit Claude Sonnet 4 und GPT-4o, die größere Kontextfenster haben.
Die RAG-Pipeline: Vier nicht verhandelbare Stufen
Ein produktives RAG-System hat vier Stufen. Überspringen Sie eine und das Ganze bricht zusammen.
1. Ingestion. Sie laden Ihre Daten (PDFs, Webseiten, Datenbanken, strukturierte Dokumente) und konvertieren sie in Chunks, die klein genug für die Einbettung sind. Ein Chunk ist typischerweise 300–800 Tokens – ein Absatz, kein Buchkapitel. Das Ziel ist Granularität: Jeder Chunk sollte eine einzelne Frage beantworten, ohne Kontext aus fünf anderen Chunks zu benötigen.
2. Embedding. Jeder Chunk wird in einen Vektor konvertiert – eine Liste von Zahlen, die seine semantische Bedeutung darstellt. Sie verwenden ein Embedding-Modell (wie OpenAIs text-embedding-3-small oder Cohere’s embed-english-v3.0), um diese Vektoren zu erstellen. Die Vektoren leben in einer Vektordatenbank (Pinecone, Weaviate, Milvus oder sogar PostgreSQL mit pgvector-Erweiterung). Das Embedding-Modell ist von Ihrem LLM getrennt.
3. Retrieval. Wenn ein Benutzer eine Frage stellt, betten Sie die Frage mit demselben Embedding-Modell ein und durchsuchen dann die Vektordatenbank nach den ähnlichsten Chunks. Ähnlichkeit wird durch Kosinus-Distanz gemessen – wie „nah“ die Vektoren zueinander sind. Sie rufen typischerweise die Top 3–10 Chunks ab, je nachdem, wie viel Kontext Ihr LLM verarbeiten kann.
4. Generation. Sie erstellen einen Prompt, der die abgerufenen Chunks enthält, und senden ihn an Ihr LLM. Das LLM generiert eine Antwort basierend auf sowohl der Frage als auch dem Kontext. Wenn der Kontext gut ist, ist die Antwort korrekt. Wenn der Kontext falsch ist, wird das LLM trotzdem halluzinieren – nur mit höherer Zuversicht, da es etwas hat, womit es arbeiten kann.
Hier ist ein konkreter Ablauf:
# Schritt 1: Ingestion (einmalig, offline)
raw_documents = load_pdfs("./docs/")
chunks = split_into_chunks(raw_documents, chunk_size=500)
embeddings = [embedding_model.embed(chunk) for chunk in chunks]
vector_db.insert(chunks, embeddings) # Speichern in Pinecone/Weaviate
# Schritt 2: Retrieval (pro Benutzerabfrage, Echtzeit)
user_question = "Wie ist Ihre Versandrichtlinie für Kanada?"
query_embedding = embedding_model.embed(user_question)
relevant_chunks = vector_db.search(query_embedding, top_k=5)
# Schritt 3: Generation
context = "\
\
".join([chunk.text for chunk in relevant_chunks])
prompt = f"""
Sie sind ein hilfreicher Assistent. Beantworten Sie die Frage des Benutzers basierend auf
dem unten stehenden Kontext. Wenn der Kontext die Antwort nicht enthält,
sagen Sie dies ausdrücklich.
Kontext:
{context}
Frage: {user_question}
"""
response = llm.generate(prompt) # Claude oder GPT-4o
print(response)
Das ist das Skelett. Jedes Produktionssystem fügt Komplexität hinzu: Behandlung mehrdeutiger Abfragen, Neubewertung von Ergebnissen, mehrstufige Abrufe, Fallback-Strategien und Überwachung auf Drift. Aber wenn Sie diese Vier-Stufen-Pipeline verstehen, verstehen Sie RAG.
Embedding-Modelle: Warum Ihre Wahl zählt
Die meisten Teams wählen ein Embedding-Modell einmal aus und vergessen es. Das ist ein Fehler. Ihr Embedding-Modell bestimmt, was „relevant“ bedeutet.
Es gibt drei Hauptkategorien:
| Embedding-Modell | Stärken | Schwächen | Kosten (pro 1 Mio. Tokens) |
|---|---|---|---|
| OpenAI text-embedding-3-small | Schnell, gute Allzweckleistung, winzig (1536 Dims). Erreicht 62,3 auf MTEB | Proprietär, erfordert API-Schlüssel, langsam für die Skalierung von Re-Embeddings | $0,02 |
| Cohere embed-english-v3.0 | Unterstützt Suchaufgaben nativ (kann für Retrieval vs. Semantik optimiert werden), MTEB-Score 55,6, flexibel | Höhere Latenz als OpenAI small, Kosten summieren sich bei Skalierung | $0,10 |
| Sentence-transformers (Open-Source, lokal) | Kostenlos, läuft auf Ihrer Hardware, keine API-Aufrufe, all-MiniLM-L6-v2 ist winzig (384 Dims) |
Schwächere semantische Verständigung als kommerzielle Modelle, MTEB ~32, erfordert GPU für Geschwindigkeit | $0 (Infrastrukturkosten) |
Im Jahr 2024–2025 kommt die Wahl normalerweise auf Folgendes heraus:
- Public SaaS (schnelle Iteration, Inhaltsvielfalt): OpenAI’s text-embedding-3-small. Es ist günstig, schnell und praxiserprobt. Verwenden Sie dies, es sei denn, Sie haben einen bestimmten Grund, dies nicht zu tun.
- Private Daten (Sicherheit, Compliance, Offline): Sentence-transformers lokal ausführen oder ein selbst gehostetes Modell wie llama2-embeddings. Sie tauschen etwas Qualität gegen Kontrolle.
- Domänenspezifisches Retrieval (Medizin, Recht, Finanzen): Versuchen Sie zuerst ein domänenspezifisches Modell (z. B. biosensetransformers für Medizin), greifen Sie dann auf text-embedding-3-small zurück, wenn es unterperformt.
Hier ist ein Entscheidungsbaum, den ich in der Praxis verwende: Beginnen Sie mit dem kleinen Modell von OpenAI. Wenn das Top-k-Retrieval offensichtliche Antworten verfehlt (die relevante Chunk existiert, rangiert aber nicht in den Top 5), wechseln Sie zu text-embedding-3-large. Wenn die Latenz zum Problem wird und die Genauigkeit immer noch akzeptabel ist, wechseln Sie zu sentence-transformers und führen Sie es lokal aus. Optimieren Sie nicht zuerst die Kosten – optimieren Sie zuerst die Korrektheit, dann die Kosten.
Wenn grundlegendes Retrieval fehlschlägt: Fortgeschrittene Ranking-Techniken
Sie haben die Pipeline gebaut. Sie rufen die Top-5-Chunks ab. Und manchmal sind sie nutzlos.
Das passiert, weil die Vektorähnlichkeit nicht immer mit der semantischen Relevanz übereinstimmt. Ihre Abfrage könnte „Wie kann ich stornieren?“ lauten, und der Retriever ruft fünf Chunks über Rückerstattungen ab, während der Benutzer eigentlich die Stornierungsrichtlinie benötigt. Ähnliche Vektoren, unterschiedliche Absicht.
Zwei Techniken beheben dies:
Re-Ranking. Sie rufen mehr Chunks ab, als Sie benötigen (sagen wir, Top 20), und verwenden dann ein dediziertes Re-Ranking-Modell, um sie neu zu bewerten. Der Re-Ranker ist leichter und schneller als das Haupt-LLM, daher ist er günstig im Betrieb. Modelle wie Cohere’s reranker-v3-turbo oder Jina’s reader-lm bewerten die Chunks anhand der Relevanz für die ursprüngliche Abfrage, nicht nur anhand der Vektorähnlichkeit.
Beispielcode:
# Mehr Kandidaten abrufen
retrieved_chunks = vector_db.search(query_embedding, top_k=20)
# Sie neu bewerten
from cohere import Client
co = Client(api_key="your-key")
reranked = co.rerank(
query=user_question,
documents=[chunk.text for chunk in retrieved_chunks],
top_n=5,
model="reranker-v3-turbo"
)
# Die Top-5 neu bewerteten Ergebnisse verwenden
final_chunks = [retrieved_chunks[result.index] for result in reranked.results]
Hybrid-Suche. Verlassen Sie sich nicht nur auf semantische Suche. Kombinieren Sie sie mit Stichwortsuche (BM25). Semantische Suche findet „ähnliche Ideen“, Stichwortsuche findet exakte Begriffe. Für eine Abfrage wie „Versand nach Kanada“ findet die Stichwortsuche sofort Chunks, die „Kanada“ und „Versand“ enthalten. Semantische Suche könnte einen längeren Weg nehmen.
Implementieren Sie Hybrid-Suche so:
# Semantische Suche (Vektorähnlichkeit)
semantic_results = vector_db.search(query_embedding, top_k=10)
# Stichwortsuche (BM25)
keyword_results = vector_db.bm25_search(user_question, top_k=10)
# Zusammenführen und Duplikate entfernen
merged = list(dict.fromkeys(semantic_results + keyword_results))
# Die zusammengeführte Liste neu bewerten
final_chunks = rerank(merged, user_question, top_n=5)
In Produktionssystemen, die ich gebaut habe (bei AlgoVesta, für Finanzdaten), fängt die Hybrid-Suche ~15% mehr relevante Ergebnisse ab als die semantische Suche allein. Die zusätzlichen Abfragekosten sind minimal – BM25 ist schnell.
Chunking-Strategien: Größe, Überlappung und warum es wichtig ist
Ihr Embedding-Modell sieht jeweils nur einen Chunk. Ein zu kleiner Chunk verpasst Kontext. Ein zu großer Chunk begräbt die Antwort in Rauschen.
Es gibt keine universelle Chunk-Größe. Es hängt von Ihren Daten und Ihrem Anwendungsfall ab:
- Chunk-Größe 256–512 Tokens: Gut für Q&A-Datensätze, FAQs, Kundensupport-Dokumente. Die Antwort passt in einen Chunk. Claude zählt ~4 Tokens pro Wort, also 256 Tokens = ~60 Wörter = zwei Absätze.
- Chunk-Größe 512–1024 Tokens: Gut für technische Dokumentation, Produktleitfäden, Richtliniendokumente. Der vollständige Kontext ist manchmal erforderlich, um eine Frage zu beantworten – Sie benötigen die Einleitung plus den spezifischen Abschnitt.
- Chunk-Größe 1024+ Tokens: Gut für dichte, vernetzte Materialien (Forschungsarbeiten, Rechtsverträge). Kontext ist über Abschnitte hinweg wichtig, und Sie möchten Teilungen minimieren, die zusammenhängende Ideen trennen.
Testen Sie dies lokal vor der Bereitstellung. Hier ist ein schneller Test:
# Ein Beispieldokument laden
doc = load_pdf("sample.pdf")
# Verschiedene Chunk-Größen testen
for chunk_size in [256, 512, 1024]:
chunks = split_into_chunks(doc, size=chunk_size)
print(f"Größe {chunk_size}: {len(chunks)} Chunks")
# Prüfen Sie für jede Testabfrage, ob die Antwort vollständig enthalten ist
# im abgerufenen Chunk (nicht in zwei Chunks aufgeteilt)
for query in test_queries:
embedding = embedding_model.embed(query)
top_chunk = vector_db.search(embedding, top_k=1)[0]
answer_in_chunk = check_answer_coverage(query, top_chunk)
print(f" {query}: {'✓' if answer_in_chunk else '✗'}")
Überlappung ist eine weitere Variable. Wenn Chunk A die Tokens 0–512 ist und Chunk B die Tokens 513–1024, sind sie ohne Überlappung nebeneinander. Die Grenze dazwischen könnte einen Satz aufteilen. Verwenden Sie stattdessen Überlappung: Chunk A ist 0–512, Chunk B ist 256–768. Jetzt wird der überlappende Bereich 256–512 in beiden Chunks dargestellt, und Satzgrenzen sind weniger wahrscheinlich, Abruf-Fehlgriffe zu verursachen.
Guter Standard: chunk_size=800, overlap=200. Das bedeutet 80% einzigartiger Inhalt, 20% Wiederholung. Für technische Dokumente erhöhen Sie die Überlappung auf 300 oder 400. Für FAQ-Daten reduzieren Sie auf 100.
Aufbau der Feedback-Schleife: Warum Überwachung Intuition schlägt
Sie stellen RAG bereit. Benutzer stellen Fragen. Woher wissen Sie, ob es funktioniert?
Intuition skaliert nicht. Sie brauchen Metriken.
Retrieval-Metriken (rufen Sie die richtigen Chunks ab?):
- Mean Reciprocal Rank (MRR): An welcher Position befindet sich die richtige Antwort in Ihren abgerufenen Ergebnissen für eine gegebene Abfrage? Wenn die Antwort im Top-Ergebnis steht, ist MRR = 1. Top 5, MRR = 0,2. Durchschnitt über alle Abfragen. Ziel ist >0,7 in der Produktion.
- Normalized Discounted Cumulative Gain (NDCG): Ähnlich wie MRR, berücksichtigt aber die Ranking-Qualität über mehrere Ergebnisse hinweg. Anspruchsvoller, schwieriger zu berechnen, aber besser für große Ergebnislisten.
- Hitrate (Top-k): Prozentsatz der Abfragen, bei denen die richtige Antwort in Ihren Top-k-Ergebnissen erscheint. Einfach und nützlich. Ziel ist >85% bei k=5.
Generierungsmetriken (generiert das LLM gute Antworten bei gutem Kontext?):
- BLEU / ROUGE: Messen Sie die Überlappung zwischen generierter Antwort und erwarteter Antwort. Imperfekt – zwei verschiedene richtige Antworten werden unterschiedlich bewertet – aber schnell in großem Maßstab zu berechnen.
- Semantische Ähnlichkeit: Betten Sie sowohl die generierte als auch die erwartete Antwort ein, messen Sie die Kosinus-Distanz. Fehlerverzeihender als BLEU, aber langsamer.
- Menschliche Bewertung (Stichproben): Überprüfen Sie jede Woche manuell 10–20 Abfragen auf Genauigkeit der LLM-Antwort. Langsam, aber Ground Truth.
Hier ist ein minimaler Monitoring-Stack:
import json
from datetime import datetime
class RAGMonitor:
def __init__(self):
self.metrics = []
def log_retrieval(self, query, retrieved_chunks, correct_chunk_id):
"""Protokolliert Retrieval-Ergebnisse für die MRR-Berechnung."""
rank = next(
(i for i, chunk in enumerate(retrieved_chunks)
if chunk.id == correct_chunk_id),
None
)
mrr = 1 / (rank + 1) if rank is not None else 0
self.metrics.append({