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.
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:
| Baustein | Aufgabe | Empfehlung |
|---|---|---|
| Embedding-Modell | Text in Bedeutungs-Vektoren übersetzen | BAAI/bge-m3 (mehrsprachig); Alternativen: multilingual-e5-large, jina-embeddings-v3 |
| Vektordatenbank | Vektoren speichern & blitzschnell durchsuchen | Qdrant; Alternativen: pgvector, Weaviate, Milvus |
| Reranker | Trefferliste präzise neu sortieren | BAAI/bge-reranker-v2-m3 |
| LLM | Antwort aus dem Kontext formulieren | vLLM mit lokalem Modell (z. B. Llama 3.1) |
| Orchestrierung | Bausteine verbinden | schlankes 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.
# 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:
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.
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.
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.
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.
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]
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.
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.
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:
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:
qdrant-client
sentence-transformers
openai
pypdf
fastapi
uvicorn[standard]
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:
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
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:
# One-off script: reads the "dokumente" folder and indexes everything
from rag import indexiere
if __name__ == "__main__":
indexiere("dokumente")
# Drop your files into ./dokumente, then start a one-off container
docker compose run --rm rag-api python ingest.py
Qualität verbessern
- Chunking justieren: Größe und Überlapp sind die wirkungsvollsten Stellschrauben. Faustregel: 500–1000 Wörter, 10–20 % Überlapp – am besten an Absätzen statt mitten im Satz schneiden.
- Hybrid-Suche: BGE-M3 liefert auch spärliche (stichwortartige) Vektoren. Die Kombination aus dichter und spärlicher Suche findet sowohl Bedeutung als auch exakte Begriffe (Artikelnummern, Eigennamen).
- Reranking nicht weglassen: Es ist der größte Qualitätssprung pro Zeile Code.
- Metadaten-Filter: Nach Abteilung, Mandant oder Datum filtern – das schärft die Treffer und setzt Zugriffsrechte durch.
- Quellen anzeigen: Belege machen Antworten überprüfbar und schaffen Vertrauen.
- Evaluieren: Eine kleine Sammlung echter Fragen mit erwarteten Quellen zeigt schnell, ob Änderungen wirklich besser sind.
DSGVO & Sicherheit
Der Eigenbetrieb liefert die Grundlage – die Umsetzung macht den Unterschied:
- Datenhoheit: Embeddings, Vektor-DB und LLM laufen im eigenen Netz. Keine Frage, kein Dokument verlässt das Haus.
- Zugriffskontrolle: Rechte aus den Quellsystemen in den Metadaten abbilden und bei der Suche als Filter erzwingen – Nutzer sehen nur, was sie dürfen.
- Recht auf Löschung: Weil jeder Chunk seine Quelle kennt, lässt sich ein Dokument gezielt und vollständig aus dem Index entfernen.
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
| Symptom | Ursache & Lösung |
|---|---|
| Antworten gehen am Thema vorbei | Chunks zu groß/klein oder Reranking fehlt. Chunking justieren, Reranker einsetzen, k erhöhen. |
| Modell erfindet Fakten | Prompt nicht streng genug. Klar auf „nur aus dem Kontext“ einschränken, temperature senken. |
| Dimensions-Fehler beim Upsert | Vektorgröße der Collection passt nicht zum Modell. Collection mit korrekter size neu anlegen. |
401/403 von Qdrant | API-Key fehlt oder falsch. Schlüssel im Client wie im Container abgleichen. |
| Indexieren ist langsam | Embeddings einzeln statt im Batch. Texte als Liste an encode() geben und die GPU nutzen. |
| Exakte Begriffe werden nicht gefunden | Reine 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):
- Qdrant – Local Quickstart (Docker, Collections, API-Key)
- BAAI – BGE-M3 (mehrsprachiges Embedding-Modell)
- BAAI – bge-reranker-v2-m3 (Cross-Encoder)
- vLLM – Pooling-Modelle (Embeddings & Reranking)
- Qdrant – offizielles Docker-Image
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.
Dieser Beitrag dient der allgemeinen Information. Code, Modell- und Versionsangaben sind Beispiele und an Ihre Umgebung anzupassen. Stand: August 2026.