Zum Inhalt springen
sw
en

Tippe um zu suchen

KI entwickeln

RAG selbst bauen: Embeddings, Vektor-DB, Retrieval

Die komplette RAG-Pipeline von Chunking ueber Embeddings bis Prompt-Augmentierung - inkl. Praxisbeispiel, typischen Fallstricken und Qualitaetsmessung.

11 Min Lesezeit Expert Zuletzt aktualisiert:

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.

StrategieFunktionsweiseWann sinnvoll
Fixed-Size ChunkingFeste Anzahl Tokens/Zeichen pro Chunk, unabhaengig von SatzgrenzenGuter Startpunkt fuer die meisten Faelle, einfach zu implementieren
Recursive ChunkingVersucht zuerst an Absaetzen zu trennen, dann an Saetzen, dann an Woertern, bis Zielgroesse erreicht istStandardwahl in der Praxis - respektiert Struktur, bleibt aber vorhersehbar
Struktur-basiertes ChunkingNutzt native Formatierung (Markdown-Ueberschriften, HTML-Tags, PDF-Abschnitte) als TrennstellenGut strukturierte Quellen (Doku, Wiki-Seiten, Markdown)
Semantisches ChunkingTrennt dort, wo sich das Thema laut Embedding-Distanz merklich aendertSehr 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:

MethodeStaerkeSchwaeche
Dense Retrieval (Embeddings)Versteht Bedeutung, findet UmschreibungenSchwaeche bei exakten Codes/Begriffen
Sparse/Keyword-Suche (z.B. BM25)Exakte Begriffstreffer, bewaehrt, schnellVersteht keine Umschreibungen/Synonyme
Hybrid SearchKombiniert beide, meist per gewichteter Score-MischungEtwas 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:

MetrikMisstFehlerquelle bei schlechtem Wert
Context PrecisionWie relevant sind die gefundenen Chunks fuer die Frage?Retrieval holt zu viel Unpassendes
Context RecallWurden alle noetigen Informationen ueberhaupt gefunden?Retrieval uebersieht relevante Chunks
FaithfulnessStuetzt sich die Antwort tatsaechlich auf den gelieferten Kontext, oder halluziniert sie?Generierung ignoriert/verfaelscht den Kontext
Answer/Response RelevancyBeantwortet 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

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 …
Kommentar schreiben