RAG selbst bauen: Embeddings, Vektor-DB, Retrieval
Die komplette RAG-Pipeline von Chunking ueber Embeddings bis Prompt-Augmentierung - inkl. Praxisbeispiel, typischen Fallstricken und Qualitaetsmessung.
Warum ueberhaupt selbst bauen
RAG fuers Firmenwissen erklaert das Konzept aus Anwendersicht: Ein Sprachmodell bekommt zur Laufzeit passende Ausschnitte aus deiner eigenen Wissensbasis in den Prompt gemischt, statt nur aus seinem Trainingswissen zu antworten. Fertige Produkte (Copilot mit SharePoint-Anbindung, Cloudflare AutoRAG, diverse SaaS-Tools) erledigen das inzwischen weitgehend automatisch.
Dieser Artikel richtet sich an alle, die die Pipeline selbst zusammenbauen wollen - weil ein fertiges Produkt nicht passt, weil du volle Kontrolle ueber Chunking und Retrieval brauchst, oder schlicht, weil du verstehen willst, was unter der Haube eines RAG-Systems passiert, bevor du es produktiv einsetzt oder debuggst. Du solltest Embeddings und Vektoren, Tokens und Kontextfenster und den Umgang mit einer LLM-API bereits kennen - hier geht es direkt in die Praxis.
Die Pipeline im Ueberblick
Eine selbstgebaute RAG-Pipeline besteht aus zwei getrennten Phasen: einer Indexierungsphase (einmalig bzw. bei Aenderungen der Wissensbasis) und einer Anfragephase (bei jeder Nutzerfrage).
INDEXIERUNG (offline, bei jeder Aenderung der Quelldokumente)
Rohdokumente (PDF, Markdown, Confluence, ...)
→ 1. Chunking (in kleine Abschnitte zerlegen)
→ 2. Embeddings (jeden Chunk in einen Vektor umwandeln)
→ 3. Vektor-DB (Vektor + Chunk-Text + Metadaten speichern)
ANFRAGE (online, bei jeder Nutzerfrage)
Nutzerfrage
→ 4. Frage einbetten (gleiches Embedding-Modell wie oben!)
→ 5. Retrieval (aehnlichste Chunks aus der Vektor-DB holen)
→ 6. Prompt-Augment. (Chunks + Frage in den Prompt einbauen)
→ 7. LLM generiert Antwort auf Basis der gefundenen Chunks
Jeder dieser Schritte hat eigene Stellschrauben, und ein schwacher Schritt macht die ganze Kette schlecht - eine perfekte Vektor-DB nuetzt nichts, wenn das Chunking die Antwort mitten im Satz zerschnitten hat.
Schritt 1: Chunking - Dokumente sinnvoll zerlegen
Ein ganzes 50-Seiten-Handbuch als einen einzigen Embedding-Vektor zu speichern, funktioniert nicht - die Bedeutung verwischt, und im Prompt landen ohnehin nur wenige Tausend Tokens Platz. Deshalb wird jedes Dokument vorher in Chunks (Abschnitte) zerlegt, jeder Chunk bekommt sein eigenes Embedding.
| Strategie | Funktionsweise | Wann sinnvoll |
|---|---|---|
| Fixed-Size Chunking | Feste Anzahl Tokens/Zeichen pro Chunk, unabhaengig von Satzgrenzen | Guter Startpunkt fuer die meisten Faelle, einfach zu implementieren |
| Recursive Chunking | Versucht zuerst an Absaetzen zu trennen, dann an Saetzen, dann an Woertern, bis Zielgroesse erreicht ist | Standardwahl in der Praxis - respektiert Struktur, bleibt aber vorhersehbar |
| Struktur-basiertes Chunking | Nutzt native Formatierung (Markdown-Ueberschriften, HTML-Tags, PDF-Abschnitte) als Trennstellen | Gut strukturierte Quellen (Doku, Wiki-Seiten, Markdown) |
| Semantisches Chunking | Trennt dort, wo sich das Thema laut Embedding-Distanz merklich aendert | Sehr heterogene Dokumente ohne klare Struktur, hoeherer Rechenaufwand |
Als Faustregel gilt eine Chunk-Groesse von etwa 200 bis 500 Tokens fuer die meisten Wissensbasis-Anwendungen, mit einem Overlap (Ueberlappung) von 10-20% zwischen aufeinanderfolgenden Chunks. Der Overlap verhindert, dass ein wichtiger Satz genau auf der Chunk-Grenze zerschnitten wird und dadurch in keinem der beiden Chunks vollstaendig auftaucht.
# Recursive Chunking mit LangChain (konzeptionell, Bibliotheks-API kann variieren)
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=400, # Ziel: ca. 400 Tokens/Zeichen pro Chunk
chunk_overlap=60, # ~15% Ueberlappung zum Vorgaenger-Chunk
separators=["\n\n", "\n", ". ", " "], # erst Absaetze, dann Saetze, dann Woerter
)
chunks = splitter.split_text(dokument_text)
print(f"{len(chunks)} Chunks erzeugt")
Bei stark strukturierten Quellen (Markdown-Wiki, technische Doku mit Ueberschriften) lohnt sich strukturbasiertes Chunking: pro Ueberschriftsabschnitt ein Chunk, statt stur nach Zeichenzahl zu schneiden. Das erhaelt inhaltlich zusammengehoerige Bloecke und macht spaeter debugbar, welcher Chunk aus welchem Abschnitt stammt.
Schritt 2: Embeddings erzeugen
Jeder Chunk wird durch ein Embedding-Modell geschickt und als Vektor gespeichert - das genaue Prinzip steht in Embeddings und Vektoren anschaulich erklaert. Fuer den Selbstbau wichtig: Das Embedding-Modell fuer die Indexierung und fuer jede spaetere Suchanfrage muss exakt dasselbe sein, sonst landen Frage und Dokumente in inkompatiblen Vektorraeumen.
import openai
client = openai.OpenAI()
def embed(text: str) -> list[float]:
response = client.embeddings.create(
model="text-embedding-3-small", # Stand: 2026-07, Name/Preis pruefen
input=text,
)
return response.data[0].embedding
chunk_vektoren = [(chunk, embed(chunk)) for chunk in chunks]
Schritt 3: Vektor-Datenbank - Speichern und Indexieren
Die Vektor-Datenbank speichert jeden Vektor zusammen mit dem Original-Chunk-Text und Metadaten (Quelldokument, Abschnitt, Zugriffsrechte, Datum) und findet bei einer Anfrage in Millisekunden die naechsten Nachbarn - auch bei Millionen Eintraegen. Details zur Auswahl und zum Betrieb stehen in Vektordatenbanken; hier der Blick auf die Rolle im Selbstbau:
import chromadb
db = chromadb.PersistentClient(path="./meine_wissensbasis")
collection = db.get_or_create_collection("firmenwiki")
for i, (chunk_text, vektor) in enumerate(chunk_vektoren):
collection.add(
ids=[f"chunk-{i}"],
embeddings=[vektor],
documents=[chunk_text],
metadatas=[{"quelle": "handbuch.pdf", "abschnitt": i}],
)
Metadaten sind kein Beiwerk, sondern oft entscheidend fuer korrekte Ergebnisse: Ohne ein Feld wie abteilung oder zugriffslevel kann eine Suchanfrage vertrauliche Chunks liefern, auf die die anfragende Person eigentlich keinen Zugriff haben sollte. Die Vektor-Suche selbst kennt keine Berechtigungen - die muss die Anwendung zusaetzlich pruefen, entweder als Filter in der DB-Abfrage selbst oder als Nachbearbeitung der Treffer.
Schritt 4: Retrieval - die richtigen Chunks finden
Beim Retrieval wird die Nutzerfrage mit demselben Embedding-Modell in einen Vektor umgewandelt, dann sucht die Datenbank die aehnlichsten gespeicherten Chunks (meist per Kosinus-Aehnlichkeit, top_k z.B. 3-5 Treffer).
frage = "Wie setze ich mein VPN-Passwort zurueck?"
frage_vektor = embed(frage)
treffer = collection.query(query_embeddings=[frage_vektor], n_results=5)
Reine Vektor-Suche (auch “Dense Retrieval” genannt) hat einen blinden Fleck: Sie versteht Bedeutung gut, versagt aber manchmal bei exakten Begriffen - Fehlercodes, Produktnamen, Artikelnummern, die sich semantisch kaum von Nachbar-Begriffen unterscheiden. Deshalb kombinieren viele produktive Systeme zwei Ansaetze:
| Methode | Staerke | Schwaeche |
|---|---|---|
| Dense Retrieval (Embeddings) | Versteht Bedeutung, findet Umschreibungen | Schwaeche bei exakten Codes/Begriffen |
| Sparse/Keyword-Suche (z.B. BM25) | Exakte Begriffstreffer, bewaehrt, schnell | Versteht keine Umschreibungen/Synonyme |
| Hybrid Search | Kombiniert beide, meist per gewichteter Score-Mischung | Etwas mehr Implementierungsaufwand |
Ein weiterer Baustein fuer bessere Treffergenauigkeit ist Reranking: Statt die Top-5-Treffer der Vektor-Suche direkt zu verwenden, holt man zunaechst mehr Kandidaten (z.B. Top-20) per schneller Vektor-Suche und laesst dann ein spezialisiertes Cross-Encoder-Modell (der Reranker) jedes Kandidaten-Paar aus Frage und Chunk genauer bewerten. Ein Cross-Encoder liest Frage und Chunk gemeinsam und kann dadurch praeziser urteilen als ein vorab berechnetes Embedding - ist dafuer aber zu langsam, um direkt auf die ganze Datenbank angewendet zu werden. Deshalb der zweistufige Ablauf: schnelle Vektor-Suche zur Vorauswahl, langsamerer Reranker zur Feinsortierung der wenigen Kandidaten.
Anfrage kommt rein
→ Vektor-Suche liefert Top-20 Kandidaten (schnell, aber ungenau)
→ Reranker bewertet alle 20 Paare (Frage, Chunk) neu (langsam, aber praezise)
→ Die besten 3-5 nach Reranking gehen in den Prompt
Schritt 5: Prompt-Augmentierung
Die gefundenen Chunks werden jetzt in den Prompt eingebaut - meist mit klarer Struktur, damit das Modell Kontext und eigentliche Frage sauber unterscheidet, und mit einer expliziten Anweisung, sich nur auf den gelieferten Kontext zu stuetzen.
kontext = "\n\n---\n\n".join(chunk for chunk, _ in top_treffer)
system_prompt = """Du bist ein IT-Support-Assistent. Beantworte die Frage
NUR auf Basis des folgenden Kontexts. Wenn die Antwort nicht im Kontext
steht, sag das explizit - erfinde keine Information."""
user_prompt = f"""Kontext:
{kontext}
Frage: {frage}"""
antwort = client.messages.create(
model="claude-sonnet-5", # Stand: 2026-07, aktuelles Modell pruefen
max_tokens=500,
system=system_prompt,
messages=[{"role": "user", "content": user_prompt}],
)
Praxisbeispiel: die komplette Pipeline in einem Skript
import chromadb
import openai
client = openai.OpenAI()
db = chromadb.PersistentClient(path="./wissensbasis")
collection = db.get_or_create_collection("support_docs")
def embed(text: str) -> list[float]:
r = client.embeddings.create(model="text-embedding-3-small", input=text)
return r.data[0].embedding
# --- Indexierung (einmalig) ---
def indexiere(dokumente: list[str]):
for i, doc in enumerate(dokumente):
chunks = einfaches_chunking(doc, groesse=400, overlap=60)
for j, chunk in enumerate(chunks):
collection.add(
ids=[f"doc{i}-chunk{j}"],
embeddings=[embed(chunk)],
documents=[chunk],
)
# --- Anfrage (bei jeder Nutzerfrage) ---
def beantworte(frage: str) -> str:
treffer = collection.query(query_embeddings=[embed(frage)], n_results=5)
kontext = "\n\n---\n\n".join(treffer["documents"][0])
response = client.chat.completions.create(
model="gpt-4.1", # Stand: 2026-07, Modellname pruefen
messages=[
{"role": "system", "content": "Antworte nur auf Basis des Kontexts."},
{"role": "user", "content": f"Kontext:\n{kontext}\n\nFrage: {frage}"},
],
)
return response.choices[0].message.content
print(beantworte("Wie setze ich mein VPN-Passwort zurueck?"))
Dieses Skelett laesst sich in jede Richtung erweitern: Hybrid Search statt reiner Vektor-Suche, Reranking-Schritt vor der Prompt-Augmentierung, Metadaten-Filter fuer Zugriffsrechte, oder ein Caching-Layer, der wiederkehrende Fragen nicht jedes Mal neu einbetten und suchen muss.
Fallstricke in der Praxis
- Chunk-Groesse falsch gewaehlt. Zu klein: Kontext geht verloren, das Modell bekommt Bruchstuecke ohne Zusammenhang. Zu gross: Relevantes wird von irrelevantem Text im selben Chunk verwaessert, und weniger Chunks passen ins Kontextfenster.
- Fehlender Overlap. Ein wichtiger Satz genau auf der Chunk-Grenze kann in keinem der beiden Nachbar-Chunks vollstaendig auftauchen und wird dadurch nie gefunden.
- Falsches
top_k. Zu wenige Treffer (top_k=1-2) uebersehen relevante Zusatzinfos, die in einem anderen Chunk stehen; zu viele fluten den Prompt mit thematisch halb-passendem Fuellmaterial und lenken das Modell ab. - Veraltete Chunks. Ein RAG-System ist nur so aktuell wie sein Index. Aendert sich das Quelldokument, aber der Index wird nicht neu aufgebaut, liefert die Suche selbstbewusst falsche, veraltete Antworten.
- Kein Zugriffsrechte-Check. Die Vektor-Suche selbst kennt keine Berechtigungen - das muss die Anwendung explizit umsetzen (siehe Schritt 3).
- Relevanz mit Korrektheit verwechselt. Ein per Embedding gefundener Chunk ist thematisch passend, aber das garantiert nicht, dass die enthaltene Information richtig oder aktuell ist - Embeddings messen semantische Naehe, keine Faktentreue.
- Ein Embedding-Modell fuer alles. Ein generisches Embedding-Modell tut sich mit stark fachspezifischem Vokabular (interne Produktnamen, Fachjargon) manchmal schwerer als mit Alltagssprache - bei sehr spezialisierten Wissensbasen lohnt sich der Test eines domainspezifischen oder feinabgestimmten Modells.
Qualitaet messen statt nur “fuehlt sich gut an”
Ein RAG-System “wirkt” beim manuellen Ausprobieren schnell gut - Probleme zeigen sich oft erst bei systematischer Pruefung mit einem echten Testset aus Frage-Antwort-Paaren. Ein etabliertes Vier-Metriken-Schema (z.B. im Open-Source-Framework RAGAS) trennt Retrieval- von Generierungs-Qualitaet:
| Metrik | Misst | Fehlerquelle bei schlechtem Wert |
|---|---|---|
| Context Precision | Wie relevant sind die gefundenen Chunks fuer die Frage? | Retrieval holt zu viel Unpassendes |
| Context Recall | Wurden alle noetigen Informationen ueberhaupt gefunden? | Retrieval uebersieht relevante Chunks |
| Faithfulness | Stuetzt sich die Antwort tatsaechlich auf den gelieferten Kontext, oder halluziniert sie? | Generierung ignoriert/verfaelscht den Kontext |
| Answer/Response Relevancy | Beantwortet die generierte Antwort ueberhaupt die gestellte Frage? | Generierung driftet vom eigentlichen Thema ab |
Der praktische Ablauf: Ein Testset aus 20-50 realistischen Fragen mit bekannter “richtiger” Antwort (bzw. bekannten relevanten Quell-Chunks) durch die Pipeline schicken, alle vier Metriken berechnen (oft per LLM-as-Judge, siehe KI-Evaluationen und Testen), und bei Aenderungen an Chunking, Embedding-Modell oder top_k erneut messen. So wird aus “gefuehlt besser” ein nachvollziehbarer Vergleichswert.
Kurz zusammengefasst
- Die Pipeline hat zwei Phasen: Indexierung (Chunking → Embeddings → Vektor-DB, offline) und Anfrage (Frage einbetten → Retrieval → Prompt-Augmentierung → Generierung, online).
- Chunk-Groesse und Overlap sind die wichtigsten und am haeufigsten falsch gewaehlten Stellschrauben - recursive Chunking mit 200-500 Tokens und 10-20% Overlap ist ein solider Startpunkt.
- Embedding-Modell konsequent gleich halten, sowohl beim Indexieren als auch bei jeder Suchanfrage - ein Modellwechsel erfordert einen kompletten Re-Index.
- Hybrid Search (Vektor + Keyword) und Reranking (Cross-Encoder auf einer Vorauswahl) heben die Trefferqualitaet oft mehr als eine “bessere” Vektor-Datenbank allein.
- Zugriffsrechte muss die Anwendung selbst pruefen - die Vektor-Suche kennt keine Berechtigungen.
- Qualitaet systematisch messen statt nach Gefuehl: Context Precision/Recall fuer das Retrieval, Faithfulness/Relevancy fuer die Generierung, auf einem festen Testset.
Weiterlernen
- Pinecone: Chunking Strategies for LLM Applications - ausfuehrlicher Vergleich der Chunking-Ansaetze
- Pinecone: Rerankers and Two-Stage Retrieval - warum und wie Reranking funktioniert
- Ragas Documentation: Available Metrics - offizielle Doku zu Context Precision/Recall, Faithfulness, Relevancy
- OpenAI: Vector embeddings - offizielle Doku zu Embedding-Modellen
- Cloudflare: Vectorize Documentation - Doku zur Cloudflare-eigenen Vektordatenbank inkl. Workers-AI-Integration
- LangChain: Text Splitters - praktische Chunking-Implementierungen
Verwandte Themen: Embeddings und Vektoren anschaulich erklaert · Vektordatenbanken · RAG fuers Firmenwissen · Fine-Tuning vs. RAG vs. Prompting · KI-Evaluationen und Testen
Kommentare
Frage, Verbesserungsvorschlag oder eigene Erfahrung zu diesem Artikel? Schreib einen Kommentar. Neue Beiträge erscheinen nach kurzer Moderation.
- Lade Kommentare …