RAG in der Praxis: Eigene Wissensbasis für KI-Anwendungen
Baue eine KI, die auf deinen eigenen Dokumenten antwortet – Schritt für Schritt erklärt.
Wie du mit Retrieval Augmented Generation (RAG) eine KI baust, die auf deinen eigenen Dokumenten antwortet – von der Theorie bis zur ersten funktionierenden Pipeline.
Warum RAG?
Stell dir vor, du hast 500 interne Dokumente – Handbücher, Policies, Meeting-Protokolle – und willst, dass eine KI Fragen dazu beantwortet. Du könntest alles in den Prompt packen, aber selbst große Kontextfenster (100.000+ Token) reichen für viele Dokumentensammlungen nicht aus. 500 Dokumente à 10 Seiten? Das sind locker 2 Millionen Token. Zu teuer, zu langsam, zu unzuverlässig.
Die Alternative: RAG – Retrieval Augmented Generation. Statt alles in den Prompt zu laden, suchst du erst die relevanten Passagen heraus und gibst dem LLM nur diese. Das Ergebnis: präzisere Antworten, niedrigere Kosten, und das System bleibt aktuell ohne Retraining.
RAG ist heute die Standardarchitektur für dokumentenbasierte KI-Anwendungen – von internen Wissensdatenbanken über Kundenservice-Bots bis zu Code-Assistenten.
Die 5 Komponenten einer RAG-Pipeline
Dokumente → [1. Chunking] → [2. Embedding] → [3. Vektordatenbank]
↓
Nutzerfrage → [2. Embedding] → [4. Retrieval] → relevante Chunks
↓
[5. Generation] → Antwort
1. Chunking – Dokumente aufteilen
Bevor Dokumente in eine Vektordatenbank kommen, müssen sie in kleinere Einheiten aufgeteilt werden. Die Chunk-Größe ist einer der wichtigsten Parameter:
| Chunk-Größe | Vorteil | Nachteil | Geeignet für |
|---|---|---|---|
| Klein (128–256 Token) | Präzises Retrieval | Verliert Kontext | FAQs, kurze Fakten |
| Mittel (512 Token) | Gute Balance | – | Technische Docs, Artikel |
| Groß (1024+ Token) | Viel Kontext | Rauschen im Retrieval | Narrative Texte, Bücher |
Overlap ist entscheidend: Ein Overlap von 10–20% stellt sicher, dass Informationen an Chunk-Grenzen nicht verloren gehen.
Token vs. Zeichen: Viele Splitter messen standardmäßig in Zeichen, nicht in Token.
chunk_size=512bedeutet dann 512 Zeichen ≈ 100–150 Token – viel kleiner als erwartet. Für token-genaues Chunkingtiktokenalslength_functionübergeben.
from langchain.text_splitter import RecursiveCharacterTextSplitter
import tiktoken
enc = tiktoken.encoding_for_model("gpt-5")
splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=50,
length_function=lambda t: len(enc.encode(t)), # Token, nicht Zeichen
separators=["\n\n", "\n", ". ", " "]
)
chunks = splitter.split_documents(documents)
PDF-Ingestion: Clean-up zuerst
PDFs sind der häufigste Schmerzpunkt. Vor dem Chunking bereinigen:
- Zeilenumbrüche normalisieren (PDFs trennen Wörter oft mitten im Satz)
- Silbentrennung entfernen (
re.sub(r'(\w+)-\n(\w+)', r'\1\2', text)) - Headers, Footers, Seitenzahlen herausfiltern
- Tabellen und Abbildungen separat behandeln (oft als Bild extrahieren)
Fortgeschrittene Strategien:
- Semantic Chunking: Chunks werden an semantischen Grenzen getrennt, nicht nach Token-Anzahl
- Parent-Child Chunking: Kleine Chunks für Retrieval, große Parent-Chunks für Kontext
- Hierarchisches Chunking: Dokument → Abschnitt → Absatz – Retrieval auf mehreren Ebenen
2. Embedding – Text in Vektoren
Jeder Chunk wird durch ein Embedding-Modell in einen Vektor umgewandelt – eine Liste von Zahlen, die die semantische Bedeutung repräsentiert.
from openai import OpenAI
client = OpenAI()
def embed(text: str) -> list[float]:
response = client.embeddings.create(
model="text-embedding-3-small", # 1536 Dimensionen, günstig
input=text
)
return response.data[0].embedding
Modell-Vergleich:
| Modell | Dimensionen | Kosten | Qualität |
|---|---|---|---|
text-embedding-3-small | 1536 | $0.02/1M Token | ★★★★☆ |
text-embedding-3-large | 3072 | $0.13/1M Token | ★★★★★ |
nomic-embed-text (lokal) | 768 | kostenlos | ★★★★☆ |
bge-m3 (lokal, multilingual) | 1024 | kostenlos | ★★★★★ |
Für deutsche Dokumente: bge-m3 oder multilingual-e5-large sind oft besser als OpenAI-Modelle.
3. Vektordatenbank – Vektoren speichern und suchen
Die Vektordatenbank speichert Chunks + ihre Embeddings und ermöglicht schnelle Ähnlichkeitssuche.
import chromadb
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_or_create_collection("meine_docs")
# Chunks einpflegen
collection.add(
documents=[chunk.page_content for chunk in chunks],
embeddings=[embed(chunk.page_content) for chunk in chunks],
metadatas=[chunk.metadata for chunk in chunks],
ids=[f"chunk_{i}" for i in range(len(chunks))]
)
4. Retrieval – Relevante Chunks finden
Bei einer Nutzerfrage wird die Frage ebenfalls eingebettet und die ähnlichsten Chunks gesucht.
Reine Vektorsuche findet semantisch ähnliche Chunks, versagt aber bei exakten Begriffen (Produktnummern, Namen).
Hybrid Search kombiniert Vektorsuche mit BM25 (klassische Keyword-Suche). In den meisten Produktionsszenarien schlägt sie reine Vektorsuche – besonders bei exakten Begriffen wie Produktnummern, Gesetzesparagraphen oder Namen. Bei sehr homogener, sauber strukturierter Dokumentation (z.B. internes Wiki mit einheitlicher Terminologie) kann pure Dense Search ausreichen.
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
# Vektorsuche
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
# BM25 Keyword-Suche
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5
# Hybrid: 60% Vektor, 40% BM25
ensemble = EnsembleRetriever(
retrievers=[vector_retriever, bm25_retriever],
weights=[0.6, 0.4]
)
Reranking als zweiter Schritt ist in der Praxis oft der größte Hebel nach solidem Chunking – in vielen Setups bringt es spürbar bessere Antworten bei wenig Mehrkosten:
from langchain.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank
compressor = CohereRerank(model="rerank-multilingual-v3.0", top_n=3)
reranking_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=ensemble
)
5. Generation – Antwort erzeugen
Die gefundenen Chunks werden in den Prompt eingebettet und das LLM generiert die Antwort. Wichtig: Kontext und Regeln sauber trennen.
from openai import OpenAI
def rag_answer(question: str, chunks: list[dict]) -> str:
# Chunks mit IDs für Quellen-Zitation
context_blocks = []
for i, chunk in enumerate(chunks):
context_blocks.append(f"[Quelle {i+1}: {chunk['source']}]\n{chunk['text']}")
context = "\n\n---\n\n".join(context_blocks)
# Context Budget: max. ~2000 Token Kontext, nicht einfach k erhöhen
# Lieber weniger, dafür hochwertige Chunks (nach Reranking)
response = client.chat.completions.create(
model="gpt-5-mini",
messages=[
{
"role": "system",
"content": "Du bist ein hilfreicher Assistent. Antworte präzise und sachlich."
" Wenn der Kontext widersprüchlich ist, weise darauf hin."
},
{
"role": "user", # Kontext als eigene Message, nicht im System-Prompt
"content": f"""KONTEXT:
{context}
FRAGE: {question}
Regeln:
- Antworte ausschließlich basierend auf dem Kontext oben.
- Gib pro Aussage die Quellen-ID an (z.B. [Quelle 1]).
- Wenn die Antwort nicht im Kontext steht, sage: "Diese Information liegt mir nicht vor."""""
}
]
)
return response.choices[0].message.content
Context Budget statt k erhöhen: Erhöhe nicht einfach
kbis das Modell verwirrt ist. Budgetiere stattdessen den Kontext (z.B. max. 1500–3000 Token) und nimm die bestbewerteten Passagen nach dem Reranking. Qualität schlägt Quantität.
Query Transformations – oft unterschätzt
Nach solidem Chunking und Retrieval ist Query Transformation der nächste große Hebel:
- Query Rewriting: Die Nutzerfrage wird vor dem Retrieval umformuliert – präziser, mit mehr Kontext. Besonders hilfreich bei kurzen oder mehrdeutigen Fragen.
- Multi-Query Retrieval: Aus einer Frage werden 3–5 Varianten generiert, jede wird separat abgerufen, Ergebnisse werden dedupliziert. Erhöht Recall deutlich.
- HyDE (Hypothetical Document Embeddings): Das LLM generiert eine hypothetische Antwort, deren Embedding für die Suche genutzt wird – oft besser als das Frage-Embedding direkt.
- Self-Query + Metadaten-Filter: Das LLM extrahiert Filter aus der Frage (z.B. “Dokumente aus 2024”) und übergibt sie als Metadaten-Filter an die Vektordatenbank.
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-5-mini", temperature=0)
multi_retriever = MultiQueryRetriever.from_llm(
retriever=vectorstore.as_retriever(),
llm=llm
)
# Generiert automatisch mehrere Frage-Varianten und merged die Ergebnisse
Häufige Probleme und Lösungen
Problem: Das RAG findet die richtigen Chunks nicht
Diagnose: Teste Retrieval isoliert – stelle Fragen und prüfe manuell ob die zurückgegebenen Chunks relevant sind.
Ursachen und Lösungen:
- Chunks zu groß → Chunk-Größe reduzieren
- Embedding-Modell passt nicht zur Sprache → multilinguales Modell verwenden
- Frage und Dokument nutzen unterschiedliche Begriffe → Hybrid Search aktivieren
- Zu wenige Chunks abgerufen →
kerhöhen (z.B. von 3 auf 8)
Problem: Das LLM halluziniert trotz richtigem Kontext
Diagnose: Faithfulness-Score mit RAGAS messen.
Lösungen:
- System-Prompt verschärfen: „Antworte nur mit Informationen aus dem Kontext”
- Temperatur auf 0 setzen für faktische Antworten
- Stärkeres Modell verwenden (GPT-5 statt GPT-5-mini)
- Kontext kürzen: Zu viele irrelevante Chunks verwirren das LLM
Problem: Antworten sind zu allgemein
Ursache: Chunks enthalten zu wenig spezifische Information.
Lösung: Metadaten in Chunks einbetten (Dokumentname, Datum, Abschnitt) und im Prompt referenzieren lassen.
Evaluation mit RAGAS
Ohne Evaluation weißt du nicht ob dein RAG gut ist. RAGAS ist der Standard:
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision
# Testdatensatz: Fragen + erwartete Antworten + abgerufene Chunks
dataset = {
"question": ["Was ist die Rückgabefrist?", ...],
"answer": [rag_answer(q) for q in questions],
"contexts": [retrieve_chunks(q) for q in questions],
"ground_truth": ["30 Tage ab Kaufdatum", ...]
}
results = evaluate(
dataset,
metrics=[faithfulness, answer_relevancy, context_precision]
)
print(results)
# faithfulness: 0.87 ← Antwortet das LLM nur mit Kontext-Infos?
# answer_relevancy: 0.79 ← Beantwortet die Antwort die Frage?
# context_precision: 0.82 ← Sind die abgerufenen Chunks relevant?
Zielwerte als Orientierung (team- und datensatzabhängig):
- Faithfulness > 0.85
- Answer Relevancy > 0.75
- Context Precision > 0.80
Wichtig: Absolute Zielwerte sind weniger entscheidend als ein stabiler Eval-Workflow. Baue ein “Golden Set” von 20–50 repräsentativen Fragen mit erwarteten Antworten und führe Regression-Tests bei jeder Änderung durch. Shadow Evaluation im Monitoring (Produktionsfragen automatisch bewerten) zeigt Qualitätsprobleme früh.
Von Prototyp zu Produktion
Ein funktionierender Prototyp ist ein guter Anfang – aber Produktion stellt andere Anforderungen:
- Caching: Gleiche Fragen nicht zweimal embedden und suchen. Redis oder ein einfaches Dictionary reichen für den Start.
- Streaming: Antworten Wort für Wort streamen statt warten bis alles fertig ist – deutlich bessere UX.
- Metadaten-Filter: Nutzer sollen nur Dokumente sehen dürfen, für die sie berechtigt sind. Vektordatenbanken unterstützen Metadaten-Filter beim Retrieval.
- Monitoring: Jede Anfrage loggen – Frage, abgerufene Chunks, Antwort, Latenz. Nur so erkennst du Qualitätsprobleme früh.
- Kosten: Bei 1000 Anfragen/Tag mit je 2000 Token Kontext und GPT-5-mini: ~$3/Tag. Mit Caching und Prompt-Optimierung oft 50–70% günstiger.
Baue einen FAQ-Bot für eine PDF-Dokumentation
Nimm eine beliebige PDF-Dokumentation (z.B. ein Produkthandbuch oder eine Firmen-Policy) und baue damit eine RAG-Pipeline, die Fragen dazu beantworten kann.
- PDF laden und in Text umwandeln (PyMuPDF oder pdfplumber)
- Text in Chunks aufteilen: 512 Token, 50 Token Overlap
- Embeddings generieren: OpenAI text-embedding-3-small oder lokales Modell
- Chunks in Chroma oder FAISS speichern
- Retrieval testen: Stelle 5 Fragen, prüfe ob die richtigen Chunks gefunden werden
- Prompt bauen: System-Prompt mit Kontext + User-Frage
- Antwortqualität mit RAGAS evaluieren (Faithfulness + Answer Relevancy)
RAG oder Fine-Tuning – was ist besser?
Für dokumentenbasierte Fragen ist RAG fast immer besser: günstiger, aktualisierbar, nachvollziehbar. Fine-Tuning lohnt sich wenn du den Stil, die Tonalität oder domänenspezifisches Verhalten ändern willst – nicht für Faktenwissen.
Wie groß sollen meine Chunks sein?
Faustregel: 256–512 Token mit 10–20% Overlap. Für technische Dokumentation eher kleiner (256), für narrative Texte eher größer (512–1024). Wichtiger als die Größe: semantische Kohärenz – ein Chunk sollte eine abgeschlossene Idee enthalten.
Welche Vektordatenbank soll ich nehmen?
Für Prototypen: Chroma (lokal, einfach). Für Produktion mit <1M Vektoren: Qdrant oder Weaviate. Für Enterprise mit Millionen Vektoren: Pinecone oder pgvector in PostgreSQL. Wenn du schon PostgreSQL nutzt: pgvector ist oft die einfachste Wahl.
Wie messe ich ob mein RAG gut ist?
Mit RAGAS: Faithfulness (Antwortet das LLM nur mit dem was im Kontext steht?), Answer Relevancy (Beantwortet die Antwort die Frage?), Context Precision (Sind die abgerufenen Chunks relevant?). Ziel: Faithfulness > 0.8, Answer Relevancy > 0.7.
Was tun wenn das RAG halluziniert?
Erst prüfen: Retrieval-Problem oder Generation-Problem? Wenn die richtigen Chunks gefunden werden aber die Antwort falsch ist → System-Prompt verschärfen ('Antworte nur basierend auf dem Kontext'). Wenn die falschen Chunks gefunden werden → Chunking und Embedding verbessern.
- RAG = Retrieval + Augmentation + Generation: Erst suchen, dann anreichern, dann antworten
- Chunking-Qualität entscheidet über RAG-Qualität – zu groß verliert Fokus, zu klein verliert Kontext
- Hybrid Search (Vektor + BM25) schlägt reine Vektorsuche in den meisten Produktionsszenarien
- Evaluation ist nicht optional: Ohne RAGAS-Metriken weißt du nicht ob dein RAG gut ist
- Reranking nach dem Retrieval verdoppelt oft die Antwortqualität ohne Mehrkosten