RAG im Eigenbetrieb: DSGVO-konform suchen

Sprachmodelle sind beeindruckend – kennen aber Ihre internen Dokumente nicht. RAG (Retrieval-Augmented Generation) schließt diese Lücke: Es durchsucht Ihre Wissensbasis und gibt dem Modell die passenden Stellen als Kontext mit. Diese Anleitung baut ein vollständig selbst betriebenes RAG-System – mit Qdrant, BGE-M3, Reranking und einem lokalen LLM. So bleibt jedes Dokument im Haus.

Das Wichtigste vorab

  • Prinzip: Frage → passende Abschnitte suchen → als Kontext ins Modell → belegte Antwort.
  • Bausteine: Vektordatenbank (Qdrant), Embedding-Modell (BGE-M3), Reranker, lokales LLM (vLLM).
  • Datenhoheit: Alle Schritte laufen lokal – weder Dokumente noch Fragen verlassen Ihr Netz.
  • Aktualität ohne Training: Neue Dokumente werden indexiert, nicht antrainiert – sofort verfügbar, jederzeit löschbar.

Was ist RAG?

Ein Sprachmodell antwortet aus seinem Trainingswissen – Ihre Verträge, Handbücher und Tickets kennt es nicht. Man könnte es nachtrainieren, doch das ist teuer, träge und bei sich ändernden Daten unpraktikabel. RAG dreht den Spieß um: Zu jeder Frage werden zunächst die thematisch passenden Abschnitte aus Ihren Dokumenten gesucht und dem Modell als Kontext mitgegeben. Das Modell formuliert die Antwort dann ausschließlich aus diesem belegten Material.

Der Kern ist die semantische Suche: Texte werden in Vektoren (Zahlenfolgen) übersetzt, die ihre Bedeutung abbilden. Ähnliche Bedeutung heißt geringer Abstand im Vektorraum – so findet das System auch dann die richtige Stelle, wenn die Frage andere Wörter nutzt als das Dokument.

Ingestion-Pipeline: Dokumente werden in Abschnitte zerlegt, vom Embedding-Modell BGE-M3 in Vektoren umgewandelt und in der Vektordatenbank Qdrant gespeichert.
Abbildung 1: Aufbau der Wissensbasis – Dokumente werden indexiert, nicht antrainiert.

Warum Eigenbetrieb & DSGVO

Bei einem RAG-System fließen genau die sensibelsten Daten zusammen: Ihre internen Dokumente und die konkreten Fragen Ihrer Mitarbeitenden. Gehen diese an eine externe Cloud-API, geben Sie Geschäftsgeheimnisse und personenbezogene Daten aus der Hand. Im Eigenbetrieb dagegen bleibt alles lokal – das ist die einfachste und sauberste Grundlage für DSGVO-Konformität, Geheimhaltung und Auditierbarkeit. Als Sprachmodell dient ein lokaler vLLM-Server, wie wir ihn im Beitrag Proxmox 9 mit GPU für eine vLLM-VM aufgesetzt haben.

Die Bausteine

Ein RAG-Stack besteht aus wenigen, klar abgegrenzten Komponenten – alle quelloffen und selbst betreibbar:

BausteinAufgabeEmpfehlung
Embedding-ModellText in Bedeutungs-Vektoren übersetzenBAAI/bge-m3 (mehrsprachig); Alternativen: multilingual-e5-large, jina-embeddings-v3
VektordatenbankVektoren speichern & blitzschnell durchsuchenQdrant; Alternativen: pgvector, Weaviate, Milvus
RerankerTrefferliste präzise neu sortierenBAAI/bge-reranker-v2-m3
LLMAntwort aus dem Kontext formulierenvLLM mit lokalem Modell (z. B. Llama 3.1)
OrchestrierungBausteine verbindenschlankes Python (hier gezeigt); Alternativen: LlamaIndex, Haystack

1.Vektordatenbank starten

Qdrant läuft als schlanker Container. Wichtig: Qdrant ist standardmäßig ohne Authentifizierung – setzen Sie unbedingt einen API-Key, sonst kann jeder im Netz Ihren Index lesen und löschen.

bashQdrant per Docker starten
# Secure Qdrant with an API key (port 6333 = REST/dashboard, 6334 = gRPC)
docker run -d --name qdrant \
  -p 6333:6333 -p 6334:6334 \
  -e "QDRANT__SERVICE__API_KEY=dein-qdrant-schluessel" \
  -v "$(pwd)/qdrant_storage:/qdrant/storage:z" \
  qdrant/qdrant

Nicht ins Internet stellen: Qdrant gehört in ein internes Netzsegment. Der API-Key schützt vor unbefugtem Zugriff, ersetzt aber keine Netzwerk-Isolation und kein TLS über einen Reverse-Proxy.

2.Umgebung einrichten

Eine frische Python-Umgebung und die nötigen Bibliotheken – wieder mit dem schnellen Paketmanager uv:

bashPython-Umgebung & Bibliotheken
uv venv --python 3.12 --seed
source .venv/bin/activate

# Vector-DB client, embedding/reranker models, OpenAI client, PDF parser
uv pip install qdrant-client sentence-transformers openai pypdf

3.Collection & Modell

Wir verbinden uns mit Qdrant und legen eine Collection an – den Container für unsere Vektoren. BGE-M3 liefert 1024-dimensionale Vektoren; als Abstandsmaß nutzen wir Kosinus-Ähnlichkeit.

pythonrag.py — Verbindung & Collection
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams
from sentence_transformers import SentenceTransformer

qdrant = QdrantClient(url="http://localhost:6333", api_key="dein-qdrant-schluessel")
modell = SentenceTransformer("BAAI/bge-m3")   # local, multilingual

# Create the collection: 1024 dimensions, cosine distance
if not qdrant.collection_exists("wissensbasis"):
    qdrant.create_collection(
        collection_name="wissensbasis",
        vectors_config=VectorParams(size=1024, distance=Distance.COSINE),
    )

4.Dokumente aufbereiten

Dokumente werden in überlappende Abschnitte (Chunks) zerlegt. Zu große Chunks verwässern die Bedeutung, zu kleine zerreißen den Zusammenhang. Ein Überlapp verhindert, dass Sätze an der Grenze verloren gehen.

pythonrag.py — Laden & Chunking
from pathlib import Path
from pypdf import PdfReader

def lade_text(pfad: Path) -> str:
    if pfad.suffix.lower() == ".pdf":
        return "\n".join(p.extract_text() or "" for p in PdfReader(str(pfad)).pages)
    return pfad.read_text(encoding="utf-8")

# Split into ~800-word chunks with 120 words of overlap
def chunke(text: str, groesse: int = 800, overlap: int = 120):
    woerter = text.split()
    schritt = groesse - overlap
    for i in range(0, len(woerter), schritt):
        yield " ".join(woerter[i:i + groesse])

5.Indexieren

Jetzt erzeugen wir für jeden Chunk einen Vektor und speichern ihn samt Metadaten (Text und Quelle) in Qdrant. Die Quelle ist später Gold wert: für Zitate, für Filter und für das Recht auf Löschung.

pythonrag.py — Indexierungs-Funktion
import uuid
from qdrant_client.models import PointStruct

# Load, chunk and index every file in a folder
def indexiere(ordner: str = "dokumente"):
    punkte = []
    for datei in Path(ordner).glob("**/*"):
        if datei.is_dir():
            continue
        for stueck in chunke(lade_text(datei)):
            vektor = modell.encode(stueck, normalize_embeddings=True)
            punkte.append(PointStruct(
                id=str(uuid.uuid4()),
                vector=vektor.tolist(),
                payload={"text": stueck, "quelle": datei.name},
            ))
    qdrant.upsert(collection_name="wissensbasis", points=punkte)
    print(f"{len(punkte)} Chunks indexiert.")

6.Vektorsuche

Die Frage wird mit demselben Modell eingebettet und gegen den Index abgeglichen. Qdrant liefert die k ähnlichsten Abschnitte – bewusst etwas mehr, als wir am Ende brauchen, denn der Reranker im nächsten Schritt siebt die wirklich besten heraus.

pythonrag.py — Retrieval
def suche(frage: str, k: int = 8):
    qv = modell.encode(frage, normalize_embeddings=True).tolist()
    treffer = qdrant.query_points(
        collection_name="wissensbasis", query=qv, limit=k, with_payload=True,
    ).points
    return [(t.payload["text"], t.payload["quelle"], t.score) for t in treffer]
Abfrage-Fluss: Frage wird eingebettet, per Vektorsuche in Qdrant abgeglichen, neu sortiert (Reranking), als Kontext in einen Prompt gegeben und vom lokalen LLM zu einer Antwort mit Quellen verarbeitet.
Abbildung 2: Der Weg von der Frage zur belegten Antwort – vollständig lokal.

7.Reranking

Die Vektorsuche ist schnell, aber grob. Ein Cross-Encoder liest Frage und Abschnitt gemeinsam und bewertet die Passung deutlich genauer. So landen die wirklich relevanten Stellen oben – die halbe Miete für gute Antworten.

pythonrag.py — Reranking
from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")

def rerank(frage: str, kandidaten, top_n: int = 4):
    paare = [(frage, text) for text, _, _ in kandidaten]
    scores = reranker.predict(paare)
    rang = sorted(zip(scores, kandidaten), key=lambda x: x[0], reverse=True)
    return [k for _, k in rang[:top_n]]

8.Antwort erzeugen

Die besten Abschnitte wandern als Kontext in den Prompt. Eine klare Anweisung sorgt dafür, dass das Modell nur aus dem belegten Material antwortet und die Quelle nennt – das beugt Halluzinationen vor. Als Modell dient der lokale vLLM-Server über seine OpenAI-kompatible API.

pythonrag.py — Antwort mit Quellen
from openai import OpenAI

llm = OpenAI(base_url="http://localhost:8000/v1", api_key="dein-geheimer-schluessel")

def beantworte(frage: str) -> str:
    kontext = rerank(frage, suche(frage))
    quellen = "\n\n".join(f"[{q}] {t}" for t, q, _ in kontext)
    prompt = (
        "Beantworte die Frage ausschliesslich anhand des Kontexts. "
        "Nenne die Quelle in eckigen Klammern. Reicht der Kontext nicht, sage das.\n\n"
        f"Kontext:\n{quellen}\n\nFrage: {frage}"
    )
    antwort = llm.chat.completions.create(
        model="llama3.1-8b",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.1,
    )
    return antwort.choices[0].message.content

print(beantworte("Wie lange bewahren wir Eingangsrechnungen auf?"))

9.Als API bereitstellen

Eine schlanke FastAPI-Hülle macht das System für Anwendungen, Chat-Oberflächen oder das Intranet nutzbar – und läuft am besten selbst als Container:

pythonapp.py — RAG-Endpunkt
from fastapi import FastAPI
from pydantic import BaseModel
from rag import beantworte

app = FastAPI()

class Anfrage(BaseModel):
    frage: str

@app.post("/fragen")
def fragen(a: Anfrage):
    return {"antwort": beantworte(a.frage)}

Die Abhängigkeiten landen in einer requirements.txt, der Dienst in einem schlanken Image:

textrequirements.txt
qdrant-client
sentence-transformers
openai
pypdf
fastapi
uvicorn[standard]
dockerfileDockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY rag.py ingest.py app.py .
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8800"]

Eine docker-compose.yml startet Qdrant und die RAG-API gemeinsam als Stack – mit automatischem Neustart und persistentem Speicher:

yamldocker-compose.yml
services:
  qdrant:
    image: qdrant/qdrant:latest
    restart: unless-stopped
    environment:
      - QDRANT__SERVICE__API_KEY=dein-qdrant-schluessel
    volumes:
      - ./qdrant_storage:/qdrant/storage
  rag-api:
    build: .
    restart: unless-stopped
    depends_on:
      - qdrant
    ports:
      - "8800:8800"
    volumes:
      - ./dokumente:/app/dokumente
      - ./models:/models
bashStack bauen & starten
docker compose up -d --build

Netzwerk im Compose-Stack: Die Dienste erreichen sich über ihren Service-Namen. Setzen Sie in rag.py die Qdrant-URL daher auf http://qdrant:6333 und die vLLM-URL auf die Adresse Ihres LLM-Servers – am besten beides über Umgebungsvariablen.

Die Wissensbasis befüllen Sie einmalig als Container-Lauf mit demselben Image – eine winzige ingest.py ruft dafür die Indexierungs-Funktion aus rag.py auf:

pythoningest.py — Wissensbasis befüllen
# One-off script: reads the "dokumente" folder and indexes everything
from rag import indexiere

if __name__ == "__main__":
    indexiere("dokumente")
bashDokumente einmalig indexieren
# Drop your files into ./dokumente, then start a one-off container
docker compose run --rm rag-api python ingest.py

Qualität verbessern

DSGVO & Sicherheit

Der Eigenbetrieb liefert die Grundlage – die Umsetzung macht den Unterschied:

pythonloeschen.py — Recht auf Löschung (Art. 17 DSGVO)
from qdrant_client.models import Filter, FieldCondition, MatchValue

# Remove all vectors of a document by its source
qdrant.delete(
    collection_name="wissensbasis",
    points_selector=Filter(must=[
        FieldCondition(key="quelle", match=MatchValue(value="personalakte_mueller.pdf")),
    ]),
)

Tipp: Behandeln Sie die Vektordatenbank wie jede andere Datenquelle mit personenbezogenen Daten – mit Verzeichnis von Verarbeitungstätigkeiten, Löschkonzept und dokumentierten Zugriffsrechten.

Troubleshooting

SymptomUrsache & Lösung
Antworten gehen am Thema vorbeiChunks zu groß/klein oder Reranking fehlt. Chunking justieren, Reranker einsetzen, k erhöhen.
Modell erfindet FaktenPrompt nicht streng genug. Klar auf „nur aus dem Kontext“ einschränken, temperature senken.
Dimensions-Fehler beim UpsertVektorgröße der Collection passt nicht zum Modell. Collection mit korrekter size neu anlegen.
401/403 von QdrantAPI-Key fehlt oder falsch. Schlüssel im Client wie im Container abgleichen.
Indexieren ist langsamEmbeddings einzeln statt im Batch. Texte als Liste an encode() geben und die GPU nutzen.
Exakte Begriffe werden nicht gefundenReine Bedeutungssuche. Hybrid-Suche (dicht + spärlich) ergänzen.

Häufige Fragen

Was ist RAG eigentlich genau?

RAG steht für Retrieval-Augmented Generation. Statt sich nur auf das Trainingswissen des Sprachmodells zu verlassen, werden zur Frage passende Abschnitte aus eigenen Dokumenten gesucht und dem Modell als Kontext mitgegeben. So entstehen aktuelle, belegbare Antworten – ganz ohne erneutes Training.

Warum RAG im Eigenbetrieb statt einer Cloud-API?

Weil bei RAG die sensibelsten Daten zusammenfließen: interne Dokumente und konkrete Fragen. Im Eigenbetrieb verlassen sie das eigene Netz nicht – die einfachste Grundlage für DSGVO-Konformität, Geheimhaltung und Auditierbarkeit, ohne Anbieterabhängigkeit.

Welches Embedding-Modell eignet sich für deutschsprachige Inhalte?

BAAI/bge-m3 ist eine sehr gute, mehrsprachige Wahl: über 100 Sprachen, bis zu 8192 Token Kontext und dichte wie spärliche Retrieval-Verfahren. Gute Alternativen sind multilingual-e5-large und jina-embeddings-v3.

Brauche ich ein Framework wie LlamaIndex oder Haystack?

Nein. Wie gezeigt genügen wenige Zeilen Python. Frameworks helfen bei komplexeren Pipelines (viele Dateiformate, Agenten, Auswertung), bringen aber Abhängigkeiten mit. Für den Einstieg ist der schlanke Weg oft der bessere.

Wie setze ich das Recht auf Löschung um?

Da jeder Abschnitt mit seiner Quelle gespeichert wird, lassen sich alle Vektoren eines Dokuments gezielt über einen Metadaten-Filter aus Qdrant löschen (siehe Code oben). Danach taucht der Inhalt in keiner Antwort mehr auf.

Läuft das auch ohne GPU?

Embeddings und Reranking laufen auf der CPU, nur langsamer. Das eigentliche Sprachmodell profitiert dagegen stark von einer GPU – siehe unseren Beitrag zum Aufbau eines lokalen vLLM-Servers.

Quellen

Externe Quellen, Stand August 2026 (öffnen in einem neuen Tab):

On-Premise KI mit Nokkela

Ihre Dokumente, Ihr Wissen – im eigenen Haus

Sie möchten internes Wissen sicher durchsuchbar machen, ohne Daten aus der Hand zu geben? Wir planen, bauen und betreiben Ihre RAG-Plattform – von der Vektordatenbank über die Modelle bis zur Integration in Ihre Systeme. Herstellerunabhängig und DSGVO-konform.

Anfrage Mehr zu nokkela.ai

Dieser Beitrag dient der allgemeinen Information. Code, Modell- und Versionsangaben sind Beispiele und an Ihre Umgebung anzupassen. Stand: August 2026.