Mittelstufe 22 minRAG

Lokales RAG mit ChromaDB und Ollama: Tutorial Python

Lokales RAG mit ChromaDB, Ollama und Python besteht aus drei ineinandergreifenden Bausteinen: einem Vektorspeicher, der Daten dauerhaft auf dem Datenträger speichert (ChromaDB), einem Embedding-Modell, das Ihre Chunks in Vektoren umwandelt (nomic-embed-text über Ollama), und einem Chat-LLM, das seine Antworten auf die gefundenen Textpassagen stützt. Kein API-Schlüssel, keine Datenlecks. Dieser Leitfaden führt Sie in 22 Minuten von einer unbearbeiteten PDF-Datei zu einem Chatbot, der seine Quellen zitiert.

Von Mohamed Meguedmi·Aktualisierung 2026-08-27·Unter Windows, macOS und Linux getestet

#Warum dieser Stack für lokales RAG

Viele RAG-Tutorials beginnen mit LangChain oder LlamaIndex. Diese Frameworks sind leistungsfähig, verbergen aber, was unter der Haube passiert. Hier schreiben wir die Pipeline von Hand mit nur drei Abhängigkeiten. Sie werden jeden Schritt verstehen und wissen, was Sie später optimieren können.

ChromaDB
Open-Source-Vektorspeicher in reinem Python mit integriertem persistentem Modus (SQLite + HNSW-Index). Es muss kein Server gestartet werden.
Ollama
Stellt sowohl das Embedding-Modell (nomic-embed-text) als auch das Chat-LLM (Qwen 3.5, Granite 4.2, Gemma 4) bereit. Ein einziger HTTP-Endpunkt unter localhost:11434.
Natives Python
Ein paar Funktionen, kein Framework. Sie können LangChain später bei Bedarf anbinden, aber für den Einstieg ist das nicht nötig.
i
Was Sie erhalten
Ein Python-Skript mit etwa 150 Zeilen, das die PDFs aus einem Ordner einliest, sie in Chunks aufteilt, in ChromaDB indexiert und Fragen auf Französisch mit Zitaten beantwortet. Alles lokal, keine ausgehenden Anfragen.

#Voraussetzungen

Das Kit „Copilote Local“

Dieser Guide führt Sie zum Modell. Das Kit führt Sie zum Copiloten, der in Ihrem Editor Code schreibt.

  • Lebenslanger Online-Zugang
  • PDF + Dateien
  • Erstattung binnen 30 Tagen
Python 3.10+
ChromaDB erfordert mindestens 3.10. Prüfen Sie die Version mit python --version.
Ollama installiert und gestartet
Der Daemon lauscht standardmäßig auf http://localhost:11434. Wenn Sie bei null anfangen, folgen Sie zunächst dem Leitfaden zur Installation von Ollama.
8 GB RAM
Mit 16 GB läuft es komfortabel. Das 9B-Chat-Modell in Q4 benötigt etwa 6 GB, das Embedding-Modell etwa 300 MB.
Eine GPU ist nicht erforderlich
Inferenz auf der CPU funktioniert, nur langsamer. Beim Einlesen eines großen Korpus beschleunigt eine GPU mit mindestens 6 GB die Berechnung der Embeddings erheblich.

#1. ChromaDB installieren und Ollama vorbereiten

Wir erstellen eine saubere virtuelle Umgebung, installieren die drei benötigten Bibliotheken und laden die Modelle in Ollama herunter.

Python-Umgebung
python -m venv .venv
source .venv/bin/activate  # sous Windows : .venv\Scripts\activate
pip install chromadb ollama pypdf

Drei Pakete: chromadb für den Vector Store, ollama für den offiziellen Python-Client, pypdf zum Lesen von PDF-Dateien. Das ist alles.

Ollama-Modelle
ollama pull nomic-embed-text
ollama pull qwen3.5:9b

nomic-embed-text ist ein mehrsprachiges Embedding-Modell mit 137 Millionen Parametern, das Vektoren mit 768 Dimensionen erzeugt. Leichtgewichtig, schnell, gut auf Französisch. Qwen 3.5 9B (6,6 GB, 256k Kontext, mehrsprachig, Apache 2.0) übernimmt den abschließenden Chat: Es ist 2026 die Standardwahl für 8 GB. Sie können es durch granite4.2:8b (ressourcensparender) oder gemma4:12b ersetzen, ohne etwas am Code zu ändern.

→
Prüfen, ob Ollama antwortet
Ein einfaches curl http://localhost:11434/api/tags sollte Ihre Modelle auflisten. Wenn nichts ausgegeben wird, läuft der Daemon nicht: ollama serve in einem anderen Terminal starten.

#2. Das Embedding-Modell konfigurieren

Ein Embedding ist ein Vektor, der den Sinn eines Textes darstellt. Zwei textliche Inhalte, die semantisch nahe beieinander liegen, haben ähnliche Vektoren. Dies ist der Motor des RAG: Man sucht die Chunks, deren Embedding dem der Frage am ähnlichsten ist.

embed.py — schneller Test
import ollama

resp = ollama.embeddings(
    model="nomic-embed-text",
    prompt="Le contrat est résilié de plein droit en cas de manquement grave."
)

vec = resp["embedding"]
print(f"Dimension du vecteur : {len(vec)}")
print(f"5 premières valeurs : {vec[:5]}")

Sie sollten die Ausgabe „Dimension du vecteur : 768“ sehen. Wenn es mit model not found fehlschlägt, wurde ollama pull nomic-embed-text nicht ausgeführt.

i
Warum nomic-embed-text
Auf FR-Benchmarks (MTEB-fr) belegt nomic-embed-text bei Modellen unter 200 M Parametern den Top 5. Für rein französisch sprachige Texte macht mxbai-embed-large oft besser, kostet jedoch 670 M. nomic ist ein hervorragender Kompromiss zwischen Qualität und Geschwindigkeit zum Start.

#3. Einlesen von PDF-Dateien auf Französisch

Die Ingestion erledigt drei Aufgaben: Sie liest die Seiten eines PDF-Dokuments, teilt den Text in Chunks angemessener Größe auf und speichert jeden Chunk mit seinem Embedding in ChromaDB im persistenten Modus.

ingest.py
import os
import chromadb
import ollama
from pypdf import PdfReader

client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_or_create_collection(name="docs")

def chunk_text(text, size=800, overlap=100):
    chunks = []
    start = 0
    while start < len(text):
        end = min(start + size, len(text))
        chunks.append(text[start:end])
        start += size - overlap
    return chunks

def ingest_pdf(path):
    reader = PdfReader(path)
    name = os.path.basename(path)
    for page_num, page in enumerate(reader.pages):
        text = page.extract_text() or ""
        for i, chunk in enumerate(chunk_text(text)):
            emb = ollama.embeddings(
                model="nomic-embed-text",
                prompt=chunk
            )["embedding"]
            collection.add(
                ids=[f"{name}-p{page_num}-c{i}"],
                embeddings=[emb],
                documents=[chunk],
                metadatas=[{"source": name, "page": page_num + 1}],
            )
    print(f"OK : {name} ingéré ({len(reader.pages)} pages)")

if __name__ == "__main__":
    for f in os.listdir("./pdfs"):
        if f.endswith(".pdf"):
            ingest_pdf(f"./pdfs/{f}")

Der Chunker teilt den Text in Blöcke von 800 Zeichen mit einer Überlappung von 100 Zeichen auf. Das ist ein Ausgangspunkt: weder zu klein (zu wenig Kontext) noch zu groß (das Signal wird verwässert). Bei sehr dichten juristischen Inhalten reduzieren Sie die Blockgröße auf 500 Zeichen. Bei technischen Handbüchern mit großzügigem Layout erhöhen Sie sie auf 1200 Zeichen.

→
Der persistente Modus von ChromaDB
PersistentClient(path="./chroma_db") erstellt einen Ordner, der auch nach Neustarts erhalten bleibt. SQLite speichert die Metadaten, ein HNSW-Index die Vektoren. Es muss kein Server gestartet werden, und Docker ist nicht nötig. Um später in den Client/Server-Modus zu wechseln, genügt es, PersistentClient durch HttpClient zu ersetzen.

Starten Sie die Ingestion für einen Ordner ./pdfs/, der Ihre Dokumente enthält:

Ingestion starten
mkdir -p pdfs
# placez vos PDF dans ./pdfs/
python ingest.py
!
Gescannte PDF-Dateien = kein Text
pypdf extrahiert nur den nativen Text. Wenn Ihre PDF-Dateien Scans von Bildern sind, gibt extract_text() einen leeren Wert zurück. In diesem Fall muss man zunächst ein OCR-Verfahren (z. B. Tesseract oder ein Vision-Modell wie Qwen 3.5 9B, multimodal, über Ollama) verwenden, bevor die Inhalte eingefügt werden.

#4. Top-k-Suche in ChromaDB

Sobald die Chunks indexiert sind, besteht die Suche darin, ein Embedding der Frage zu erzeugen und anschließend bei Chroma die k nächstgelegenen Vektoren anhand der Kosinusdistanz abzufragen. Das geschieht sofort, selbst bei 100.000 Chunks.

search.py
import chromadb
import ollama

client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_collection(name="docs")

def search(question, k=4):
    q_emb = ollama.embeddings(
        model="nomic-embed-text",
        prompt=question
    )["embedding"]
    results = collection.query(
        query_embeddings=[q_emb],
        n_results=k,
    )
    chunks = results["documents"][0]
    metas = results["metadatas"][0]
    return list(zip(chunks, metas))

if __name__ == "__main__":
    hits = search("Quelles sont les conditions de résiliation ?")
    for chunk, meta in hits:
        print(f"[{meta['source']} p.{meta['page']}]")
        print(chunk[:200], "...\n")

k=4 ist ein guter Standardwert. Ist der Wert zu klein, entgeht Ihnen relevanter Kontext; ist er zu groß, überfrachten Sie das LLM mit irrelevanten Informationen und überschreiten das Kontextfenster. Für sehr präzise Fragen reicht k=2 aus. Für übergreifende Fragen erhöhen Sie den Wert auf 6.

#5. Chat-Schleife mit Quellenangaben

Jetzt setzen wir alles zusammen: Wir suchen die relevanten Chunks, erstellen einen Prompt mit dem Kontext, senden ihn über Ollama an Qwen 3.5 und bitten das Modell, seine Quellen zu zitieren.

chat.py
import ollama
from search import search

SYSTEM = """Tu es un assistant qui répond uniquement à partir du CONTEXTE fourni.
Si la réponse n'est pas dans le contexte, dis-le clairement.
Cite tes sources entre crochets sous la forme [source.pdf p.X]."""

def ask(question):
    hits = search(question, k=4)
    context = "\n\n".join(
        f"[{m['source']} p.{m['page']}]\n{c}" for c, m in hits
    )
    prompt = f"CONTEXTE :\n{context}\n\nQUESTION : {question}"
    resp = ollama.chat(
        model="qwen3.5:9b",
        messages=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": prompt},
        ],
        options={"temperature": 0.2, "num_ctx": 8192},
    )
    return resp["message"]["content"]

if __name__ == "__main__":
    while True:
        q = input("\nQuestion (vide pour quitter) > ").strip()
        if not q:
            break
        print("\n" + ask(q))

Drei Details sind entscheidend. Erstens, temperature=0.2: Gewünscht ist eine faktentreue Antwort, keine kreative. Zweitens, num_ctx=8192: Das standardmäßige Kontextfenster von Ollama (2048) ist zu kurz, sobald man 4 Chunks mit jeweils 800 Zeichen einfügt. Drittens, der Systemprompt zwingt das Modell, „ich weiß es nicht“ zu sagen, statt zu halluzinieren – das ist der wichtigste Schutz gegen Halluzinationen beim RAG.

→
Streaming für eine bessere Benutzererfahrung
Ersetzen Sie ollama.chat durch ollama.chat(..., stream=True) und iterieren Sie über die Antwort, um die Tokens schrittweise anzuzeigen. Dies ist entscheidend, sobald dieser Code in eine echte Schnittstelle integriert wird (FastAPI + WebSocket, oder Streamlit).

#6. Konkreter Fall: juristischer Chatbot für Verträge

Stellen wir uns ein Büro vor, das 200 Dienstleistungsverträge im PDF-Format abfragen möchte. Mit dem oben genannten Stack steht in weniger als einer Stunde ein Assistent bereit, der Fragen wie diese beantwortet:

Typische Frage
„Welche Verträge enthalten eine Klausel für ein nachvertragliches Wettbewerbsverbot von mehr als 12 Monaten?“
Was passiert
Das Embedding der Frage findet die Chunks, die semantisch ähnliche Schlüsselwörter enthalten (Wettbewerbsverbot, nach Beendigung, Dauer). Qwen 3.5 liest diese 4 Passagen und antwortet mit den Namen der betreffenden Dateien.
Datenschutzgarantie
Keine Daten verlassen den Rechner. Keine API-Schlüssel. Keine Telemetrie. Das unterscheidet einen lokalen RAG von einem OpenAI-Wrapper.
!
Einschränkungen, die Sie kennen sollten
Ein einfaches RAG beantwortet gezielte Fragen („Was ist Klausel X?“) gut, aggregierende Fragen („Wie viele Verträge enthalten X?“) dagegen schlecht. Für Letztere braucht man entweder einen Agenten, der die Datenbank in mehreren Schritten abfragt, oder ein GraphRAG. Das ist ein anderes Thema.

#Fehlerbehebung

ChromaDB langsam bei der Ingestion
Der Engpass ist fast immer die Embedding-Anfrage an Ollama. Prüfen Sie mit ollama ps, ob nomic-embed-text auf der GPU läuft. Auf der CPU können Sie mit ~50 Chunks pro Sekunde rechnen, auf der GPU mit ~500.
« model not found »
Ollama findet nomic-embed-text nicht. Führen Sie ollama pull nomic-embed-text erneut aus und prüfen Sie das Ergebnis mit ollama list.
Antworten, die Quellen erfinden
Ein 9B-Modell halluziniert noch gelegentlich. Wechseln Sie zu mistral-small (24B, ~14 GB, sehr gut auf Französisch) oder qwen3.8:27b, wenn Sie genügend VRAM haben. Oder schalten Sie nach ChromaDB einen Reranker (Cross-Encoder), um falsch positive Treffer herauszufiltern.
Schlechte Embeddings für Französisch
nomic-embed-text ist mehrsprachig, aber für rein französische Inhalte nicht optimal. Testen Sie für juristische oder medizinische Inhalte Solon-embeddings-large-0.1 oder bge-m3 (über sentence-transformers außerhalb von Ollama laden).
ChromaDB wächst ohne Grenzen
Jede erneute Indexierung fügt Duplikate hinzu. Bevor Sie eine PDF-Datei erneut einlesen, führen Sie collection.delete(where={"source": name}) aus, um die alten Chunks zu löschen.

#Weiterführende Informationen

Sie verfügen über ein funktionierendes RAG-System. Hier sind die naheliegenden nächsten Aufgaben, um es weiterzuentwickeln:

Embedding-Modelle für Französisch vergleichen
Unser Leitfaden „Die besten Embedding-Modelle FR“ vergleicht BGE, E5, Solon und nomic anhand französischsprachiger Inhalte.
Chunking verbessern
„Chunking-Strategien“ erläutert semantisches Chunking sowie die Aufteilung nach Markdown-Überschriften oder Absätzen – damit lässt sich oft der größte Zugewinn an Genauigkeit erzielen.
Einen Reranker hinzufügen
„Einen Reranker zur eigenen Pipeline hinzufügen“: +15 % Relevanz, indem ein Cross-Encoder nach Chroma eingesetzt wird. Der nächste logische Schritt.
Hybride Suche
„Hybride Suche mit BM25 und Vektoren“ kombiniert lexikalische und semantische Suche und ist unverzichtbar, sobald viel Fachjargon oder viele Eigennamen vorkommen.
Hat Ihnen dieser Guide geholfen?

Haben Sie Feedback, einen Fehler entdeckt oder möchten Sie etwas präzisieren? Geben Sie uns Bescheid – so wird der Guide für alle besser.