Wer heute einen Chatbot baut, der wirklich firmeninterne Dokumente, Handbücher oder Support-Tickets kennt, kommt an einer Retrieval-Augmented-Generation-Pipeline kaum vorbei. Der Suchbegriff “Retrieval Augmented Generation” verzeichnet laut aktuellen Keyword-Daten rund 3.600 Suchanfragen pro Monat in Deutschland, Tendenz steigend. Das Prinzip klingt einfach: Statt ein Sprachmodell blind raten zu lassen, füttert man es mit exakt den Textstellen, die zur Frage passen. In der Praxis stecken darin aber jede Menge Stolperfallen, von falscher Chunk-Größe bis zu Vektor-Datenbanken, die beim ersten Praxistest einfach leer bleiben.

Dieses Tutorial zeigt Schritt für Schritt, wie Sie eine vollständige RAG-Pipeline in Python aufbauen: von der Dokumenten-Aufnahme über Chunking und Embeddings bis zur Anbindung an ein lokales Sprachmodell via Ollama. Am Ende steht ein lauffähiges Projekt, das Sie direkt auf eigene Daten anwenden können. Sie brauchen dafür keine Cloud-Infrastruktur, ein normaler Entwickler-Laptop reicht für den Einstieg völlig aus.

Der Aufbau gliedert sich in 14 konkrete Schritte, ergänzt um ein vollständiges, kopierbares Beispielprojekt, eine Liste der häufigsten Fehler und einen ausführlichen Troubleshooting-Teil für den Fall, dass etwas nicht auf Anhieb funktioniert. Rechnen Sie für den kompletten Durchlauf, inklusive Installation und erstem Testlauf, mit rund 90 Minuten.

Was ist Retrieval-Augmented Generation und warum jetzt?

Retrieval-Augmented Generation kombiniert zwei getrennte Systeme: eine Suchkomponente (Retrieval) und ein generatives Sprachmodell (Generation). Statt das gesamte Wissen im Modell selbst zu speichern, holt sich das System bei jeder Anfrage passende Textausschnitte aus einer externen Datenquelle und legt sie dem Modell als Kontext vor. Das Modell muss sich Fakten also nicht merken, es liest sie im Moment der Antwort.

Der Vorteil gegenüber reinem Prompting: Ein RAG-System bleibt aktuell, ohne dass man das Basismodell neu trainieren muss. Ändert sich ein Handbuch, tauscht man einfach den entsprechenden Eintrag im Index aus. Der Vorteil gegenüber Fine-Tuning: RAG ist günstiger, schneller einsatzbereit und die Quelle jeder Antwort lässt sich nachvollziehen, weil man die abgerufenen Textstellen mit ausgeben kann. Wer stattdessen das Modellverhalten selbst anpassen möchte, findet dazu Hintergründe im Tutorial zu LLM Fine-Tuning mit LoRA, das einen anderen, komplementären Ansatz beschreibt.

RAG ist mittlerweile die Standardarchitektur für Firmensuche, technischen Support, juristische Recherche und interne Wissensdatenbanken. Der Grund ist pragmatisch: Ein Modell wie GPT, Claude, Llama oder DeepSeek kennt Ihre internen PDFs, Confluence-Seiten oder Ticket-Historien schlicht nicht, wenn Sie sie ihm nicht zeigen. RAG ist der Mechanismus, der genau das übernimmt, zuverlässig und nachvollziehbar.

RAG oder einfach ein größeres Kontextfenster?

Eine berechtigte Frage vorweg: Moderne Sprachmodelle schlucken inzwischen selbst Hunderttausende Tokens auf einmal, warum also nicht einfach das komplette Handbuch in jeden Prompt packen? In der Praxis scheitert das an drei Punkten. Erstens an den Kosten: Jeder einzelne Request mit einem riesigen Kontext kostet, egal ob genutzt oder nicht, und bei hohem Anfragevolumen summiert sich das schnell. Zweitens an der Präzision: Modelle finden relevante Details in sehr langen Kontexten nachweislich unzuverlässiger als in kurzen, fokussierten Ausschnitten, ein Effekt, der in der Forschung häufig als Verwässerung der Aufmerksamkeit über lange Sequenzen beschrieben wird. Drittens an der Nachvollziehbarkeit: Ein RAG-System kann exakt benennen, welche drei Chunks zur Antwort geführt haben, ein Modell mit einem riesigen, undifferenzierten Kontext kann das nicht.

Das heißt nicht, dass große Kontextfenster nutzlos sind. Für einzelne, klar abgegrenzte Dokumente, etwa die Analyse eines einzelnen Vertrags, kann direktes Einfügen in den Prompt schneller zum Ziel führen als eine ganze RAG-Pipeline aufzusetzen. Sobald aber mehrere hundert oder tausend Dokumente im Spiel sind, die sich zudem laufend ändern, führt an Retrieval kein Weg vorbei. Die beiden Ansätze schließen sich außerdem nicht aus: Viele produktive Systeme nutzen RAG, um die relevantesten Dokumente vorzufiltern, und geben dem Modell dann bewusst einen größeren, aber gezielten Kontext aus diesen wenigen Treffern.

Voraussetzungen: Software, Versionen und Hardware

Bevor es losgeht, sollten folgende Werkzeuge installiert sein. Die Versionsangaben entsprechen dem Stand Ende August 2026 und sollten vor der Installation kurz gegengeprüft werden, da sich vor allem LangChain aktuell in schnellem Takt weiterentwickelt.

  • Python 3.11 oder neuer
  • langchain ab Version 1.3.18 und langchain-core ab 1.6.1
  • sentence-transformers für lokale Embeddings (aktuelle Version aus PyPI)
  • Eine Vektor-Datenbank: chromadb für lokale Tests oder ein laufender Qdrant-Server für produktivere Setups
  • Ollama (aktuelle Version) für den lokalen LLM-Betrieb, alternativ ein API-Zugang zu OpenAI, Claude oder Gemini
  • Mindestens 8 GB RAM für kleine Testkorpora, ab 16 GB für Korpora im dreistelligen MB-Bereich
  • Optional eine GPU mit mindestens 8 GB VRAM, falls Sie größere lokale Modelle über Ollama laufen lassen möchten

Wer bereits ein lokales Modell über Ollama betreibt, kann direkt bei Schritt 9 einsteigen. Eine Einführung in die Grundinstallation liefert das Tutorial Ollama Setup: Lokale LLMs in 12 Schritten, die offizielle Installationsanleitung findet sich zusätzlich direkt bei Ollama. Wer stattdessen ein größeres Modell über vLLM betreiben will, findet die passende Anleitung im Llama 4 vLLM Setup.

Bei der Hardware-Planung lohnt sich eine grobe Faustregel: Die Chunking- und Embedding-Phase ist CPU- beziehungsweise GPU-lastig, aber nur zeitlich begrenzt, sie läuft einmalig oder bei Datenänderungen. Die Generation-Phase mit dem lokalen LLM läuft dagegen bei jeder Nutzeranfrage und bestimmt damit maßgeblich, wie viel RAM oder VRAM Sie dauerhaft vorhalten müssen. Ein 7- bis 8-Milliarden-Parameter-Modell läuft in quantisierter Form noch auf vielen Laptops ohne dedizierte GPU, größere Modelle profitieren spürbar von mindestens 16 GB VRAM.

Architektur-Überblick: So funktioniert eine RAG-Pipeline

Eine RAG-Pipeline lässt sich in sechs Phasen gliedern: Datenaufnahme, Chunking, Embedding, Indexierung, Retrieval und Generation. Die ersten vier Phasen laufen einmalig oder bei jeder Datenänderung (Offline-Pfad), die letzten beiden bei jeder einzelnen Nutzeranfrage (Online-Pfad). Diese Trennung ist wichtig, weil sie erklärt, wo Kosten und Latenz entstehen: Das Einbetten eines kompletten Wissensbestands passiert selten, die Anfrage eines Nutzers dagegen ständig.

Im Online-Pfad wird die Nutzerfrage zunächst selbst in einen Vektor umgewandelt, mit demselben Embedding-Modell, das auch für den Index verwendet wurde. Dieser Vektor wird gegen die Datenbank verglichen, die ähnlichsten Textstellen (Top-k) werden zurückgegeben. Diese Textstellen landen zusammen mit der ursprünglichen Frage in einem Prompt, der an das Sprachmodell geht. Das Modell generiert daraus die finale Antwort, im Idealfall mit Quellenangabe.

In den folgenden 14 Schritten bauen wir genau diese Architektur nach, mit Python, Chroma beziehungsweise Qdrant als Vektor-Datenbank und Ollama als lokalem Generator.

Schritt 1 und 2: Projekt aufsetzen und Abhängigkeiten installieren

Legen Sie zunächst ein sauberes Projektverzeichnis mit virtueller Umgebung an. Das verhindert, dass sich Paketversionen mit anderen Projekten auf demselben Rechner in die Quere kommen.

mkdir rag-pipeline && cd rag-pipeline
python3 -m venv venv
source venv/bin/activate

pip install "langchain>=1.3.18" langchain-core sentence-transformers \
    chromadb qdrant-client requests tiktoken pypdf

Prüfen Sie danach, ob Ollama lokal erreichbar ist. Falls Sie Ollama noch nicht eingerichtet haben, folgen Sie zunächst der Basisinstallation und laden Sie ein Modell wie llama3 oder ein Modell aus der DeepSeek-Familie herunter, wie im Tutorial DeepSeek V4 Flash mit Ollama beschrieben.

curl http://localhost:11434/api/tags

Kommt eine JSON-Liste mit installierten Modellen zurück, ist Ollama einsatzbereit. Kommt stattdessen ein Verbindungsfehler, starten Sie den Ollama-Dienst mit ollama serve in einem separaten Terminal.

Schritt 3 und 4: Dokumente einlesen und normalisieren

Der erste inhaltliche Schritt besteht darin, Rohdaten in ein einheitliches Format zu bringen. Egal ob PDF, Markdown oder HTML-Export, am Ende braucht jede Pipeline reinen Text plus Metadaten wie Quelle, Erstellungsdatum und Dokument-ID. Diese Metadaten werden später für Filterung und Quellenangabe gebraucht, sie im Nachhinein zu ergänzen ist deutlich aufwendiger als sie von Anfang an mitzuführen.

from pathlib import Path
from pypdf import PdfReader

def load_documents(folder: str):
    docs = []
    for path in Path(folder).glob("**/*"):
        if path.suffix.lower() == ".pdf":
            reader = PdfReader(str(path))
            text = "\n".join(page.extract_text() or "" for page in reader.pages)
        elif path.suffix.lower() in (".md", ".txt"):
            text = path.read_text(encoding="utf-8", errors="ignore")
        else:
            continue

        docs.append({
            "id": str(path),
            "text": text,
            "metadata": {
                "source": path.name,
                "path": str(path),
            },
        })
    return docs

documents = load_documents("data/")
print(f"{len(documents)} Dokumente geladen")

Ein häufiger Fehler an dieser Stelle: PDF-Extraktion liefert bei gescannten Dokumenten leeren oder kaputten Text zurück, weil kein durchsuchbarer Text im PDF steckt, sondern nur ein Bild. In diesem Fall braucht es vorab eine OCR-Stufe, sonst landet nichts als Leerstring im Index, und die spätere Suche liefert für dieses Dokument nie Treffer.

Schritt 5 und 6: Chunking-Strategie implementieren

Chunking ist der Schritt, der in der Praxis am meisten über die spätere Antwortqualität entscheidet, und gleichzeitig der am meisten unterschätzte. Zu große Chunks bringen zu viel irrelevanten Kontext mit, das Modell vermischt Themen. Zu kleine Chunks zerreißen zusammengehörige Sätze, wichtige Bezüge gehen verloren. In der Praxis haben sich Chunk-Größen zwischen 512 und 1024 Tokens mit einer Überlappung von 10 bis 30 Prozent als solider Ausgangspunkt etabliert, die genaue Einstellung hängt aber stark vom Dokumententyp ab: Fließtext verträgt größere Chunks, Tabellen und Codeblöcke brauchen eher kleinere, klar abgegrenzte Einheiten.

def chunk_text(text: str, max_words: int = 300, overlap_words: int = 60):
    words = text.split()
    chunks = []
    start = 0
    while start < len(words):
        end = min(start + max_words, len(words))
        chunk = " ".join(words[start:end])
        chunks.append(chunk)
        if end == len(words):
            break
        start = end - overlap_words
    return chunks

all_chunks = []
for doc in documents:
    for i, chunk in enumerate(chunk_text(doc["text"])):
        all_chunks.append({
            "id": f"{doc['id']}-{i}",
            "text": chunk,
            "metadata": {**doc["metadata"], "chunk_index": i},
        })

print(f"{len(all_chunks)} Chunks erzeugt")

Für Markdown- oder Code-Dokumente lohnt es sich, statt einer reinen Wortzählung entlang von Überschriften oder Funktionsgrenzen zu splitten. LangChain bringt dafür fertige Splitter mit (etwa MarkdownHeaderTextSplitter, dokumentiert in der LangChain-Dokumentation), die semantische Grenzen respektieren, statt stur nach Wortanzahl zu schneiden.

Feste Chunk-Größe versus semantisches Chunking

Die im Beispiel gezeigte Wortzählung ist die einfachste Variante und für den Einstieg völlig ausreichend. Sie hat aber einen Nachteil: Sie schneidet mitten im Satz oder mitten in einer Aufzählung, ohne Rücksicht auf Inhalt zu nehmen. Semantisches Chunking geht einen Schritt weiter und schneidet dort, wo sich das Thema tatsächlich ändert, etwa erkannt an Absatzgrenzen, Themenwechseln oder eingebetteten Ähnlichkeitswerten zwischen aufeinanderfolgenden Sätzen. Der Aufwand dafür ist höher, weil zusätzliche Berechnungen pro Dokument nötig sind, der Effekt auf die Antwortqualität kann sich bei heterogenen Korpora aber lohnen.

Für die meisten Projekte ist ein pragmatischer Mittelweg sinnvoll: feste Chunk-Größe als Grundlage, kombiniert mit einem Split entlang offensichtlicher Struktur-Marker wie Überschriften, Aufzählungszeichen oder Leerzeilen. Das lässt sich mit wenigen Zeilen zusätzlicher Logik in die bestehende chunk_text-Funktion einbauen, ohne gleich ein komplettes semantisches Chunking-Modell zu betreiben.

Sonderfall: Tabellen, Code und strukturierte Daten

Tabellen sind der klassische Fall, bei dem naives Chunking komplett danebenliegt. Wird eine Tabelle mitten in der Wortzählung durchtrennt, verliert die zweite Hälfte ihre Spaltenüberschriften und wird für das Modell praktisch unlesbar. Bewährt hat sich, Tabellen als eigene, in sich geschlossene Chunks zu behandeln und bei Bedarf die Spaltenüberschriften in jeden Teil-Chunk zu duplizieren, falls eine Tabelle die maximale Chunk-Größe übersteigt. Für Quellcode gilt Ähnliches: Schneiden Sie möglichst an Funktions- oder Klassengrenzen, nicht mitten in einer Codezeile, sonst wird der Chunk für das Modell bedeutungslos.

Schritt 7 und 8: Embeddings erzeugen und Vektor-Datenbank befüllen

Jetzt wird aus Text Mathematik. Ein Embedding-Modell wandelt jeden Chunk in einen Vektor um, typischerweise mit einigen hundert bis wenigen tausend Dimensionen. Ähnliche Inhalte landen dabei nahe beieinander im Vektorraum. Für den lokalen Einstieg eignet sich sentence-transformers mit einem aktuellen Modell aus den Top-Rängen des MTEB-Leaderboards, etwa aus der BGE- oder GTE-Modellfamilie. Wer stattdessen eine Cloud-API nutzen will, greift häufig zu OpenAIs text-embedding-3-small, laut offizieller OpenAI-Dokumentation mit einem MTEB-Score von rund 62,3 und einem Preis von 0,02 US-Dollar pro Million Tokens ein gutes Kosten-Qualitäts-Verhältnis.

Eine wichtige Randnotiz für internationale und speziell deutschsprachige Projekte: Nicht jedes Embedding-Modell ist gleich gut für Deutsch trainiert. Viele englischsprachig optimierte Modelle liefern für deutsche Fachbegriffe, zusammengesetzte Wörter oder Umlaute spürbar schlechtere Ähnlichkeitswerte als für englischen Text. Bevor Sie sich auf ein Modell festlegen, lohnt sich ein kurzer Test mit eigenen, typischen deutschen Fragen und ein Blick auf die mehrsprachigen Einträge im MTEB-Leaderboard, etwa Modelle aus der BGE-M3-Familie, die explizit für Mehrsprachigkeit trainiert wurden.

from sentence_transformers import SentenceTransformer
import chromadb

model = SentenceTransformer("BAAI/bge-large-en-v1.5")
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_or_create_collection("wissensbasis")

texts = [c["text"] for c in all_chunks]
embeddings = model.encode(texts, batch_size=64, show_progress_bar=True)

collection.add(
    ids=[c["id"] for c in all_chunks],
    embeddings=embeddings.tolist(),
    metadatas=[c["metadata"] for c in all_chunks],
    documents=texts,
)

print(f"{collection.count()} Vektoren in der Datenbank")

Für produktivere Setups mit mehr als ein paar Hunderttausend Chunks empfiehlt sich statt Chroma ein dedizierter Qdrant-Server, der auch verteilt betrieben werden kann. Der Aufbau unterscheidet sich nur im Client:

from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct

qc = QdrantClient(url="http://localhost:6333")
qc.recreate_collection(
    collection_name="wissensbasis",
    vectors_config=VectorParams(size=embeddings.shape[1], distance=Distance.COSINE),
)

points = [
    PointStruct(id=i, vector=emb.tolist(), payload=c["metadata"] | {"text": c["text"]})
    for i, (emb, c) in enumerate(zip(embeddings, all_chunks))
]
qc.upsert(collection_name="wissensbasis", points=points)

Beim Retrieval wird die Nutzerfrage mit demselben Modell eingebettet wie zuvor die Chunks, das ist keine Option, sondern Pflicht: Mischt man zwei verschiedene Embedding-Modelle, liefert die Ähnlichkeitssuche im schlimmsten Fall komplett zufällige Ergebnisse. Anschließend werden die k ähnlichsten Chunks zurückgegeben, üblich sind Werte zwischen 5 und 10.

def retrieve(query: str, k: int = 6, where: dict | None = None):
    query_embedding = model.encode([query])[0].tolist()
    results = collection.query(
        query_embeddings=[query_embedding],
        n_results=k,
        where=where,
    )
    return list(zip(results["documents"][0], results["metadatas"][0]))

kontext = retrieve("Wie setze ich das Passwort zurück?", k=6)
for text, meta in kontext:
    print(meta["source"], "->", text[:80], "...")

Reine Vektorsuche hat eine Schwäche: Sie erkennt semantische Ähnlichkeit, tut sich aber schwer mit exakten Begriffen wie Fehlercodes, Produktnamen oder IDs, die selten genau in der Trainingsverteilung des Embedding-Modells vorkamen. Hybrid Search kombiniert deshalb die Vektorsuche mit klassischer Stichwortsuche (etwa BM25) und führt beide Ergebnislisten zusammen. Für gemischte Korpora aus Fließtext und strukturierten Daten ist das fast immer die robustere Wahl gegenüber reiner Vektorsuche.

Schritt 11 und 12: Generation mit lokalem LLM verbinden

Die abgerufenen Chunks werden jetzt in einen Prompt eingebettet, der das Modell explizit anweist, sich an den gelieferten Kontext zu halten. Diese Anweisung ist kein Detail, sondern der Kern der ganzen Übung: Ohne klare Instruktion greift ein Sprachmodell bei Wissenslücken gerne auf sein Vortrainingswissen zurück, selbst wenn der richtige Kontext direkt daneben im Prompt steht.

import requests

def build_prompt(query: str, kontext: list) -> str:
    context_block = "\n\n".join(
        f"[Quelle: {meta['source']}]\n{text}" for text, meta in kontext
    )
    return f"""Du bist ein Assistent, der ausschließlich auf Basis des folgenden Kontexts antwortet.
Wenn die Antwort nicht eindeutig im Kontext steht, sage das ehrlich.

Kontext:
{context_block}

Frage: {query}
Antwort:"""

def generate(query: str, model_name: str = "llama3"):
    kontext = retrieve(query)
    prompt = build_prompt(query, kontext)

    response = requests.post(
        "http://localhost:11434/api/generate",
        json={"model": model_name, "prompt": prompt, "stream": False},
        timeout=120,
    )
    return response.json()["response"], kontext

antwort, quellen = generate("Wie setze ich das Passwort zurück?")
print(antwort)

Ein typischer Ausschnitt der Antwort sieht dann so aus:

Um das Passwort zurückzusetzen, öffnen Sie die Login-Seite und klicken Sie auf
"Passwort vergessen". Sie erhalten daraufhin einen Link per E-Mail, der 30
Minuten gültig ist. [Quelle: handbuch-login.md]

Wer statt eines lokalen Modells lieber die Claude- oder OpenAI-API anbinden möchte, findet den passenden Einstieg in LLM-Benchmark-Setup, wo verschiedene Modell-Endpunkte im direkten Vergleich eingerichtet werden.

Schritt 13 und 14: Evaluierung, Logging und Monitoring

Ohne Testfragen wissen Sie nicht, ob Ihre Pipeline tatsächlich funktioniert oder nur zufällig plausibel klingt. Legen Sie deshalb ein kleines Set aus 15 bis 30 realistischen Fragen mit bekannten, korrekten Antworten an und prüfen Sie regelmäßig zwei Dinge: Landet das richtige Dokument überhaupt unter den Top-k-Treffern (Retrieval-Recall), und stimmt die generierte Antwort mit dem abgerufenen Kontext überein (Groundedness).

testfragen = [
    {"frage": "Wie setze ich das Passwort zurück?", "erwartete_quelle": "handbuch-login.md"},
    {"frage": "Welche Zahlungsmethoden werden unterstützt?", "erwartete_quelle": "handbuch-billing.md"},
]

treffer = 0
for t in testfragen:
    _, kontext = generate(t["frage"])
    quellen = [meta["source"] for _, meta in kontext]
    if t["erwartete_quelle"] in quellen:
        treffer += 1
    else:
        print(f"MISS: '{t['frage']}' -> erwartet {t['erwartete_quelle']}, erhalten {quellen}")

print(f"Retrieval-Recall: {treffer}/{len(testfragen)}")

Loggen Sie im laufenden Betrieb mindestens die gestellte Frage, die abgerufenen Quellen und die generierte Antwort. Das ist die Grundlage, um später zu erkennen, ob bestimmte Fragetypen systematisch schlecht beantwortet werden, etwa weil das zugehörige Dokument fehlt oder falsch gechunkt wurde.

Vollständiges Beispielprojekt im Überblick

Alle bisherigen Schritte lassen sich zu einer durchgängigen Pipeline zusammensetzen. Die folgende Datei rag_pipeline.py deckt Ingestion, Chunking, Indexierung, Retrieval und Generation in einem lauffähigen Skript ab und dient als Ausgangspunkt für eigene Projekte.

# rag_pipeline.py
import chromadb
import requests
from pathlib import Path
from sentence_transformers import SentenceTransformer

MODEL_NAME = "BAAI/bge-large-en-v1.5"
LLM_MODEL = "llama3"

model = SentenceTransformer(MODEL_NAME)
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_or_create_collection("wissensbasis")

def chunk_text(text, max_words=300, overlap_words=60):
    words = text.split()
    chunks, start = [], 0
    while start < len(words):
        end = min(start + max_words, len(words))
        chunks.append(" ".join(words[start:end]))
        if end == len(words):
            break
        start = end - overlap_words
    return chunks

def index_folder(folder: str):
    for path in Path(folder).glob("**/*.md"):
        text = path.read_text(encoding="utf-8", errors="ignore")
        for i, chunk in enumerate(chunk_text(text)):
            emb = model.encode([chunk])[0].tolist()
            collection.add(
                ids=[f"{path.name}-{i}"],
                embeddings=[emb],
                metadatas=[{"source": path.name}],
                documents=[chunk],
            )

def ask(query: str, k: int = 6):
    q_emb = model.encode([query])[0].tolist()
    result = collection.query(query_embeddings=[q_emb], n_results=k)
    context = "\n\n".join(result["documents"][0])
    prompt = f"Beantworte NUR auf Basis dieses Kontexts:\n{context}\n\nFrage: {query}"
    r = requests.post(
        "http://localhost:11434/api/generate",
        json={"model": LLM_MODEL, "prompt": prompt, "stream": False},
        timeout=120,
    )
    return r.json()["response"]

if __name__ == "__main__":
    index_folder("data/")
    print(ask("Wie setze ich das Passwort zurück?"))

Führen Sie das Skript mit python rag_pipeline.py aus, nachdem Sie einen Ordner data/ mit Markdown-Dateien angelegt haben. Beim ersten Lauf entsteht die lokale Chroma-Datenbank im Unterordner chroma_db/, bei folgenden Läufen wird sie wiederverwendet.

Praxisbeispiel: Ein deutschsprachiger Support-Assistent

Um die einzelnen Schritte greifbarer zu machen, ein konkretes Beispiel: Ein mittelständisches Software-Unternehmen möchte seinen Support entlasten, indem ein Assistent Fragen zu rund 400 Handbuchseiten und 1.200 gelösten Support-Tickets beantwortet, beides auf Deutsch. Die Dokumente liegen als Markdown und PDF vor, die Tickets als CSV-Export aus dem Ticketsystem.

Im ersten Durchlauf werden alle Dokumente eingelesen und mit den Metadaten Quelle, Produktbereich und Erstellungsdatum versehen. Beim Chunking zeigt sich schnell: Handbuchseiten mit langen Fließtextabschnitten vertragen die vollen 300 Wörter pro Chunk, die kurzen Frage-Antwort-Paare aus den Tickets werden dagegen besser als jeweils ein eigener, kompakter Chunk behandelt, ohne künstliche Aufteilung. Nach dem Einbetten mit einem mehrsprachigen Modell und der Indexierung in Chroma zeigt ein erster Test mit 20 typischen Kundenfragen einen Retrieval-Recall von zunächst nur 14 von 20, weil Ticket-Chunks ohne Kontext zur ursprünglichen Frage kaum wiedererkennbar waren.

Die Lösung: Für jeden Ticket-Chunk wird die ursprüngliche Kundenfrage explizit vorangestellt, statt nur die Lösung zu indexieren. Nach dieser Anpassung und einer Erhöhung von Top-k von 4 auf 6 steigt der Recall im selben Testset auf 19 von 20. Dieses Muster, kleine, gezielte Anpassungen an Chunking und Top-k basierend auf einem festen Testset, ist typisch für die Praxis: Die größten Qualitätssprünge entstehen selten durch ein besseres Sprachmodell, sondern durch bessere Vorbereitung der Daten.

Tooling im Überblick: Versionen und Einsatzzweck

Die folgende Tabelle fasst die in diesem Tutorial verwendeten Werkzeuge mit Stand Ende August 2026 zusammen.

WerkzeugVersion / StandEinsatzzweck
langchain1.3.18 (27.08.2026)Orchestrierung, Splitter, Chains
langchain-core1.6.1 (27.08.2026)Kernabstraktionen für LangChain
sentence-transformersaktuelle PyPI-VersionLokale Embedding-Modelle
ChromaDBaktuelle PyPI-VersionLokale/eingebettete Vektor-Datenbank
Qdrantaktuelle ServerversionProduktiv-Vektordatenbank, auch verteilt
Ollamaaktuelle VersionLokaler Betrieb von Llama-, DeepSeek- u.a. Modellen

Bei Embedding-Modellen und Vektor-Datenbanken lohnt sich vor dem Produktivbetrieb ein Blick auf die exakte, gerade installierte Version, da sich Formate und Konfigurationsparameter zwischen Minor-Releases durchaus ändern können.

Kosten und Skalierung: Embedding-Preise im Vergleich

Wer statt eines lokalen Modells eine Cloud-Embedding-API nutzt, zahlt pro Million Tokens. Die Preise unterscheiden sich deutlich zwischen Anbietern und Modellgrößen, wie die folgende Übersicht zeigt.

ModellPreis Standard (pro 1 Mio. Tokens)Preis BatchDimensionen
OpenAI text-embedding-3-small0,02 $0,01 $1536
OpenAI text-embedding-3-large0,13 $0,065 $3072
OpenAI text-embedding-ada-002 (Legacy)0,10 $1536
Google Gemini Embedding 20,20 $0,10 $variabel, multimodal

Für eine Wissensbasis mit rund 1.000 Seiten bei etwa 512 Tokens pro Seite bewegen sich die Embedding-Kosten mit text-embedding-3-small im Batch-Modus im Cent-Bereich, nicht im zweistelligen Euro-Bereich. Wiederkehrend teuer wird nicht das Einbetten des Wissensbestands, sondern die laufende Chat-Nutzung durch viele gleichzeitige Nutzer. Wer komplett lokal bleiben will, umgeht diese Kosten ganz, zahlt dafür mit höherem Hardware- und Wartungsaufwand.

Bei der Modellwahl lohnt sich außerdem ein Blick auf die Batch-API der jeweiligen Anbieter, sofern die Einbettung nicht in Echtzeit erfolgen muss. Batch-Verarbeitung halbiert bei OpenAI die Kosten gegenüber dem Standardtarif, verlängert dafür aber die Verarbeitungszeit von Sekunden auf teils mehrere Stunden. Für die einmalige oder tägliche Indexierung eines Wissensbestands ist dieser Kompromiss in aller Regel unproblematisch, für die Embeddings einer Live-Nutzeranfrage dagegen nicht geeignet, dort braucht es die synchrone Standard-API.

Häufige Fehler und Stolperfallen

Die folgenden Fehler tauchen in fast jedem RAG-Projekt in der ein oder anderen Form auf, unabhängig davon, ob mit LangChain, LlamaIndex oder einer selbst geschriebenen Pipeline gearbeitet wird. Wer sie kennt, spart sich Stunden der Fehlersuche, denn die Symptome (schlechte oder fehlende Antworten) sind fast immer identisch, die Ursachen aber sehr unterschiedlich.

  • Zu große oder zu kleine Chunks: Große Chunks vermischen Themen und verwässern die Suche, winzige Chunks zerreißen den Zusammenhang. Beides führt zu schlechteren Antworten.
  • Fehlende Überlappung zwischen Chunks: Ohne Overlap gehen Informationen an Chunk-Grenzen verloren, etwa Überschriften oder Bedingungssätze, die sich über zwei Absätze erstrecken.
  • Unterschiedliche Embedding-Modelle für Index und Anfrage: Wird für Chunks ein anderes Modell verwendet als für die Suchanfrage, liefert die Ähnlichkeitssuche unbrauchbare Ergebnisse.
  • Zu niedriges Top-k: Wird nur ein einziger Chunk zurückgegeben, fehlt dem Modell oft der nötige Kontext für eine vollständige Antwort.
  • Fehlende Anweisung, sich an den Kontext zu halten: Ohne explizite Systemanweisung greift das Modell bei Unsicherheit auf sein Vortrainingswissen zurück und halluziniert munter weiter, obwohl der richtige Kontext im Prompt steht.
  • Kein Zugriffsschutz bei Mandantendaten: Wird nicht nach Nutzer- oder Mandanten-ID gefiltert, können Nutzer über die Suche versehentlich fremde Dokumente sehen.
  • Veraltete Indizes: Wird ein geändertes Dokument nicht neu eingebettet, liefert das System weiterhin die alte, überholte Antwort.

Fortgeschrittene Tipps für produktive RAG-Systeme

Sobald die Basis-Pipeline läuft, lohnen sich einige Erweiterungen, die in produktiven Systemen den größten Qualitätsunterschied ausmachen. An erster Stelle steht Reranking: Nach der ersten Vektorsuche mit einem breiten Top-k (etwa 20 Treffer) sortiert ein zweites, meist kleineres Modell (ein Cross-Encoder) die Treffer nach tatsächlicher Relevanz zur Frage neu, bevor nur die besten drei bis fünf an das Sprachmodell gehen. Das kostet etwas zusätzliche Latenz, verbessert die Trefferqualität aber spürbar, weil die reine Vektor-Ähnlichkeit allein oft nicht fein genug unterscheidet.

Zweitens: Metadaten-Filterung. Statt die gesamte Datenbank zu durchsuchen, lässt sich der Suchraum vorab über Filter wie Produktversion, Sprache oder Abteilung eingrenzen. Das reduziert nicht nur Rauschen, sondern ist bei Mandantentrennung praktisch Pflicht. Drittens: LLM-gestützte Indexierung. Statt nur den Rohtext einzubetten, lässt man ein Sprachmodell vorab kurze Zusammenfassungen oder synthetische Beispielfragen pro Dokument generieren und bettet diese zusätzlich ein. Das verbessert den Treffer, wenn Nutzerfragen anders formuliert sind als der Originaltext.

Viertens: Caching. Häufig wiederkehrende Anfragen oder bereits berechnete Embeddings sollten zwischengespeichert werden, das spart bei hohem Anfragevolumen spürbar Latenz und API-Kosten. Wer die Antwortqualität verschiedener Basismodelle in der Generation-Phase systematisch vergleichen will, findet dafür einen strukturierten Ansatz im LLM-Benchmark-Setup-Tutorial.

Vektor-Datenbanken im Vergleich

Die Wahl der Vektor-Datenbank hängt stark vom Einsatzzweck ab. Für den lokalen Einstieg und kleine Projekte reicht eine eingebettete Lösung, für produktive Systeme mit hohem Anfragevolumen empfiehlt sich ein dedizierter Server.

DatenbankBetriebsartTypischer Einsatz
ChromaEingebettet, lokalPrototypen, kleine Projekte, Entwicklung
QdrantServer, self-hosted oder CloudProduktivbetrieb mit Filterung und hohem Durchsatz
MilvusServer, verteiltSehr große Korpora, Enterprise-Skalierung
WeaviateServer, self-hosted oder CloudHybrid Search mit eingebauten Modulen
PineconeVollständig verwaltet (SaaS)Produktivbetrieb ohne eigene Infrastruktur

Der Wechsel von Chroma (Details in der Chroma-Dokumentation) zu Qdrant im späteren Projektverlauf ist überschaubar, da beide über einen ähnlichen Client arbeiten. Der größte Umstellungsaufwand liegt meist nicht im Code, sondern im Betrieb: Ein dedizierter Server braucht Monitoring, Backups und eine Strategie für Reindexierung bei Schema-Änderungen.

Ein Punkt, der beim Wechsel gerne übersehen wird: Die Distanzmetrik muss zum Embedding-Modell passen. Die meisten modernen Modelle sind für Kosinus-Ähnlichkeit optimiert, einige ältere für euklidischen Abstand. Wird die falsche Metrik konfiguriert, liefert die Suche zwar Ergebnisse, aber in einer für das Modell untypischen und damit schlechteren Reihenfolge. Ein Blick in die Modell-Dokumentation vor der Produktivsetzung erspart hier stundenlanges Debugging scheinbar zufälliger Suchtreffer.

Checkliste für den Produktivbetrieb

Bevor eine RAG-Pipeline produktiv geht, lohnt sich ein kurzer Realitätscheck anhand weniger Punkte. Erstens: Existiert ein festes Testset mit mindestens 20 bis 30 realistischen Fragen, gegen das jede Änderung an Chunking, Modell oder Top-k automatisch geprüft wird? Ohne dieses Testset wird jede Anpassung zum Blindflug. Zweitens: Gibt es eine Strategie für den Umgang mit Anfragen, zu denen der Index keine passenden Chunks liefert? Das System sollte in diesem Fall ehrlich mitteilen, dass keine Antwort gefunden wurde, statt zu raten.

Drittens: Ist die Zugriffskontrolle auf Dokumentenebene tatsächlich in der Retrieval-Logik verankert, nicht nur in der Oberfläche der Anwendung? Viertens: Existiert ein Prozess, der neue oder geänderte Dokumente automatisch nachindiziert, statt dass jemand manuell an das Update denken muss? Und fünftens: Wird geloggt, wie oft das System keine Antwort findet oder Nutzer die Antwort als falsch markieren? Diese Rückmeldungen sind die wertvollste Quelle, um die Pipeline über die Zeit gezielt zu verbessern, statt auf Verdacht an Parametern zu drehen.

Fehlerbehebung: Die häufigsten Probleme und Lösungen

Die folgende Liste deckt die Probleme ab, die in der Praxis am häufigsten für Frust sorgen, zusammen mit der jeweiligen Ursache und Lösung. Gehen Sie die Punkte im Zweifel der Reihe nach durch, bevor Sie an Modell oder Prompt herumschrauben, denn die meisten RAG-Probleme liegen nicht in der Generation, sondern schon im Retrieval.

  • Leere Suchergebnisse trotz vorhandener Dokumente: Meist liegt es an einer leeren oder falsch benannten Collection. Prüfen Sie mit collection.count(), ob überhaupt Vektoren gespeichert wurden.
  • Connection refused bei Ollama: Der Ollama-Dienst läuft nicht. Starten Sie ihn mit ollama serve und prüfen Sie den Port 11434 mit curl http://localhost:11434.
  • Dimension mismatch bei Qdrant: Die Collection wurde mit einer anderen Vektorgröße angelegt als das aktuell verwendete Embedding-Modell erzeugt. Legen Sie die Collection mit recreate_collection neu an, sobald Sie das Embedding-Modell wechseln.
  • Embedding-Berechnung dauert extrem lange: Ohne GPU kann das Einbetten großer Korpora Stunden dauern. Reduzieren Sie die Batch-Größe bei Speicherproblemen oder wechseln Sie auf eine kleinere Modellvariante.
  • Modell ignoriert den Kontext und halluziniert: Prüfen Sie zuerst, ob der Kontext überhaupt im Prompt ankommt (Debug-Ausgabe), und verschärfen Sie die Systemanweisung, ausschließlich auf Basis des Kontexts zu antworten.
  • Falsche oder abgeschnittene Umlaute im Text: Meist ein Encoding-Problem beim Einlesen. Öffnen Sie Dateien explizit mit encoding="utf-8" und prüfen Sie die Quelle bei PDF-Exporten.
  • Rate-Limit-Fehler bei Cloud-Embedding-APIs: Reduzieren Sie die Batch-Größe pro Anfrage und bauen Sie eine Wiederholungslogik mit exponentiellem Backoff ein.
  • Doppelte Chunks im Index nach mehrfachem Lauf: Ohne stabile, deterministische IDs pro Chunk entstehen bei jedem erneuten Indexierungslauf neue Einträge. Verwenden Sie IDs aus Dateiname plus Chunk-Index, nicht aus Zufallswerten.
  • Hohe Antwortlatenz bei jeder Anfrage: Prüfen Sie, ob Top-k unnötig hoch gewählt ist oder ob ein zu großes lokales Modell auf zu schwacher Hardware läuft.

Mehrsprachigkeit und gemischte Dokumenttypen

Viele deutsche Unternehmen arbeiten mit gemischtsprachigen Beständen: deutsche Handbücher, englische API-Dokumentation, vielleicht französische oder polnische Vertriebsunterlagen für andere Märkte. Hier lohnt sich die Wahl eines explizit mehrsprachigen Embedding-Modells, statt für jede Sprache separat zu indexieren. Mehrsprachige Modelle bilden semantisch ähnliche Inhalte über Sprachgrenzen hinweg nahe beieinander im Vektorraum ab, sodass eine deutsche Frage durchaus einen relevanten englischen Chunk finden kann, sofern dessen Inhalt tatsächlich passt.

Bei stark gemischten Korpora, etwa wenn ein Teil der Dokumente aus sauberem Fließtext und ein anderer Teil aus Tabellenkalkulationen oder Chat-Protokollen besteht, hilft es, pro Dokumenttyp eine eigene Chunking-Strategie zu definieren, statt eine einzige Funktion für alles zu erzwingen. Der Mehraufwand in der Ingestion-Phase zahlt sich in der Retrieval-Qualität fast immer aus, weil jeder Dokumenttyp seine eigenen strukturellen Eigenheiten hat, die eine generische Wortzählung ignoriert.

Ein praktischer Tipp für gemischte Sprachbestände: Speichern Sie die erkannte Sprache jedes Dokuments als eigenes Metadatenfeld. Damit lässt sich die Suche bei Bedarf gezielt auf eine Sprache eingrenzen, etwa wenn ein Nutzer explizit nach deutschsprachigen Quellen fragt, ohne dass Sie dafür getrennte Indizes pflegen müssen.

Sicherheit und Datenschutz bei firmeninternen RAG-Systemen

Wer personenbezogene oder vertrauliche Daten in eine RAG-Pipeline einspeist, sollte von Anfang an zwei Dinge einplanen: Zugriffskontrolle und Datenminimierung. Zugriffskontrolle bedeutet, dass die Suche nach Nutzer- oder Mandanten-ID gefiltert wird, nicht erst die generierte Antwort nachträglich zensiert wird. Datenminimierung bedeutet, vor dem Indexieren zu prüfen, ob wirklich alle Inhalte eines Dokuments in den Index gehören, etwa wenn ein Handbuch versehentlich Kundendaten oder interne Zugangsdaten enthält.

Wird eine externe Cloud-API für Embeddings oder Generation genutzt, verlässt der Text die eigene Infrastruktur, das ist bei sensiblen Daten ein relevanter Faktor für die Auftragsverarbeitung nach DSGVO. Ein vollständig lokaler Aufbau mit Ollama und einer selbst gehosteten Vektor-Datenbank umgeht dieses Problem, da keine Inhalte das eigene Netzwerk verlassen. Wer sich generell mit Absicherung von KI-Diensten gegen manipulierte Eingaben beschäftigen will, findet ergänzend Hintergründe im Bereich KI & Machine Learning von shattered.io.

Häufig gestellte Fragen

Was kostet der Aufbau einer eigenen RAG-Pipeline?

Bei vollständig lokalem Betrieb mit Ollama und Chroma entstehen keine laufenden API-Kosten, nur der einmalige Hardware-Aufwand. Bei Nutzung von Cloud-Embeddings wie text-embedding-3-small liegen die Kosten für das Einbetten eines mittelgroßen Wissensbestands im Cent- bis niedrigen Euro-Bereich, laufende Kosten entstehen vor allem durch die Chat-Nutzung des Generierungsmodells, nicht durch die Indexierung selbst. Für ein kleines internes Projekt lässt sich die gesamte Pipeline daher oft mit einem Budget im niedrigen zweistelligen Euro-Bereich pro Monat betreiben.

Brauche ich zwingend eine GPU?

Nein, für kleine bis mittlere Korpora reicht eine CPU aus, das Einbetten dauert dann nur länger. Für große Korpora oder größere lokale Sprachmodelle über Ollama beschleunigt eine GPU mit mindestens 8 GB VRAM den Prozess deutlich.

Welche Vektor-Datenbank ist für Einsteiger am besten?

Für den Einstieg eignet sich Chroma, weil es ohne separaten Server auskommt und sich direkt in Python einbetten lässt. Für produktive Systeme mit vielen gleichzeitigen Anfragen empfiehlt sich der Umstieg auf einen dedizierten Server wie Qdrant, der zusätzlich Metadaten-Filterung und horizontale Skalierung mitbringt, ohne dass sich die grundlegende Retrieval-Logik im Code ändert.

Wie groß sollten Chunks sein?

Ein guter Ausgangspunkt sind 512 bis 1024 Tokens pro Chunk mit 10 bis 30 Prozent Überlappung. Die optimale Größe hängt vom Dokumententyp ab und sollte anhand eigener Testfragen überprüft werden.

Kann ich RAG komplett lokal ohne Cloud-APIs betreiben?

Ja. Mit sentence-transformers für Embeddings, Chroma oder Qdrant als lokaler Datenbank und Ollama für die Generierung läuft die gesamte Pipeline ohne jede externe API-Anbindung.

Wie oft muss ich den Index aktualisieren?

Das hängt von der Änderungsfrequenz der Quelldokumente ab. Bei sich häufig ändernden Inhalten wie Support-Tickets bietet sich eine inkrementelle, ereignisgesteuerte Aktualisierung an, die neue oder geänderte Dokumente direkt beim Speichern neu einbettet. Bei stabilen Handbüchern reicht ein regelmäßiger Batch-Lauf, etwa täglich oder wöchentlich, verbunden mit einem einfachen Zeitstempel-Vergleich, damit unveränderte Dokumente nicht unnötig erneut eingebettet werden.

Ist RAG DSGVO-konform für Unternehmensdaten nutzbar?

RAG an sich ist nur eine Architektur, die DSGVO-Konformität hängt von der konkreten Umsetzung ab. Entscheidend sind Zugriffskontrolle nach Mandant, Datenminimierung beim Indexieren und die Frage, ob Daten eine eigene Infrastruktur verlassen. Ein vollständig lokaler Aufbau reduziert die Zahl der Auftragsverarbeiter, die betrachtet werden müssen.

Was ist der Unterschied zwischen RAG und Fine-Tuning?

Fine-Tuning verändert die Gewichte eines Modells dauerhaft anhand von Trainingsdaten, RAG lässt das Modell unverändert und liefert stattdessen bei jeder Anfrage passenden externen Kontext. RAG ist günstiger und schneller aktualisierbar, Fine-Tuning eignet sich eher, wenn sich Stil oder Verhalten des Modells grundlegend ändern soll. Details zum Fine-Tuning-Ansatz liefert das Tutorial LLM Fine-Tuning mit LoRA.