Zurück zum Pillar-Leitfaden
Cluster RAG-Tutorial 13. April 2026 14 Min. Lesezeit

RAG mit Qdrant und Llama in einem Nachmittag

Retrieval-Augmented Generation gilt als Einstiegsarchitektur für unternehmensinterne KI. Dieses Tutorial zeigt den vollständigen Aufbau mit Qdrant, jina-embeddings-v3, einem Cross-Encoder-Reranker und Llama 3.3 auf vLLM — produktionsreif, lokal, reproduzierbar.

Teil des Pillar-Leitfadens On-Premise-KI für den Mittelstand.

RAG — Retrieval-Augmented Generation — verbindet ein Sprachmodell mit einer Wissensbasis, ohne das Modell selbst zu trainieren. Das Prinzip ist einfach: Eine Nutzerfrage wird eingebettet, gegen eine Vektordatenbank abgefragt, die passenden Passagen werden mit der Frage an ein LLM gereicht, das daraus eine Antwort generiert. Der Reiz: Wissensstand änderbar ohne Training, Quellen nachvollziehbar, Datenhoheit bleibt im Haus. Dieses Tutorial beschreibt eine produktionsreife Referenzarchitektur — keine Spielzeug-Demo.

Architektur: Chunker, Embedder, Qdrant, vLLM

Die Pipeline zerfällt in zwei Phasen. Die Ingest-Phase liest Dokumente, zerteilt sie in Chunks, erzeugt Embeddings und schreibt beides in Qdrant. Die Query-Phase bettet die Frage ein, zieht die Top-k Kandidaten aus Qdrant, rerankt sie mit einem Cross-Encoder und übergibt die finalen Passagen an vLLM, das die Antwort generiert.

Komponenten in dieser Referenz: Qdrant 1.12 als Vektor-Store, jina-embeddings-v3 (1024 Dimensionen, 8k Kontext) als Embedder, BAAI/bge-reranker-v2-m3 als Cross-Encoder, Llama 3.3 70B Instruct auf vLLM als Generator, OpenAI-kompatible API als Interface. Alles lokal, alles quelloffen.

Warum diese Auswahl?

Qdrant liefert HNSW-Indexing, Payload-Filter, Replikation und eine saubere REST/gRPC-API in einem einzigen Docker-Container. jina-embeddings-v3 ist auf MTEB-DE stabil unter den Top-3 und unterstützt Matryoshka-Truncation. Der Reranker bge-reranker-v2-m3 deckt 100+ Sprachen ab, auch Deutsch. vLLM bringt Paged-Attention und Continuous Batching — unverzichtbar, sobald mehrere Nutzer gleichzeitig Fragen stellen.

Chunking-Strategien

Die Qualität eines RAG-Systems entscheidet sich im Chunking. Ein schlechter Chunker zerreißt Sätze, mischt Kontexte oder erzeugt redundante Schnipsel. Drei Strategien sind praxisrelevant.

StrategiePrinzipStärkeEinsatz
FixedFeste Tokenzahl, z.B. 512 Tokens, 64 OverlapSchnell, deterministischHomogene Texte, Logs, Support-Tickets
RecursiveSpaltet entlang Hierarchie: Absatz → Satz → TokenRespektiert natürliche GrenzenTechnische Dokumentation, Handbücher
SemanticSatzgrenzen + Embedding-Ähnlichkeit als Split-PunktKohärente ThemenblöckeVerträge, Fachartikel, gemischte Inhalte

Für deutschsprachige Unternehmensdokumente empfehlen wir RecursiveCharacterTextSplitter aus LangChain mit Chunk-Größe 600 Tokens und Overlap 80 Tokens, angepasst auf deutsche Satzzeichen. Bei PDF-Quellen: vorher mit unstructured oder docling sauber in Markdown konvertieren, Tabellen extra behandeln.

Qdrant-Docker-Setup und Collection-Anlage

Qdrant startet als einzelner Container. Für eine produktive Installation mit persistentem Storage und Authentifizierung:

# docker-compose.yml
services:
  qdrant:
    image: qdrant/qdrant:v1.12.4
    restart: unless-stopped
    ports:
      - "6333:6333"
      - "6334:6334"
    volumes:
      - ./qdrant_storage:/qdrant/storage
    environment:
      QDRANT__SERVICE__API_KEY: ${QDRANT_API_KEY}
      QDRANT__SERVICE__ENABLE_TLS: "false"
      QDRANT__STORAGE__OPTIMIZERS__DEFAULT_SEGMENT_NUMBER: 4

Die Collection legen wir mit 1024 Dimensionen, Cosine-Distance und HNSW-Parametern an, die bei bis zu zehn Millionen Vektoren gute Recall-Werte liefern:

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

client = QdrantClient(url="http://localhost:6333", api_key=API_KEY)

client.create_collection(
    collection_name="wissensbasis",
    vectors_config=VectorParams(
        size=1024,
        distance=Distance.COSINE,
    ),
    hnsw_config=HnswConfigDiff(m=16, ef_construct=200),
    on_disk_payload=True,
)

Die Payload-Struktur hält Dokument-ID, Quelle, Seitenzahl, Chunk-Index und den Rohtext des Chunks — essenziell, um dem Nutzer später eine belastbare Quelle zurückzugeben.

Ingest-Script: Embedding und Upsert

Das Ingest-Script lädt Dokumente, chunkt sie, ruft das Embedding-Modell auf und schreibt Vektor plus Payload in Qdrant. Wir nutzen jina-embeddings-v3 über die sentence-transformers-Bibliothek lokal auf GPU.

from sentence_transformers import SentenceTransformer
from qdrant_client.models import PointStruct
from langchain_text_splitters import RecursiveCharacterTextSplitter
import uuid, glob, pathlib

model = SentenceTransformer("jinaai/jina-embeddings-v3", trust_remote_code=True)
splitter = RecursiveCharacterTextSplitter(
    chunk_size=600, chunk_overlap=80,
    separators=["\n\n", "\n", ". ", "? ", "! ", " "],
)

def ingest(path: str):
    text = pathlib.Path(path).read_text(encoding="utf-8")
    chunks = splitter.split_text(text)
    vectors = model.encode(chunks, task="retrieval.passage", batch_size=32)
    points = [
        PointStruct(
            id=str(uuid.uuid4()),
            vector=v.tolist(),
            payload={"source": path, "chunk_idx": i, "text": c},
        )
        for i, (c, v) in enumerate(zip(chunks, vectors))
    ]
    client.upsert(collection_name="wissensbasis", points=points)

for f in glob.glob("./corpus/**/*.md", recursive=True):
    ingest(f)

Task-Prefix beachten: jina-embeddings-v3 unterscheidet zwischen retrieval.passage (Dokument-Seite) und retrieval.query (Anfrage-Seite). Wer das verwechselt, verliert fünf bis zehn Prozentpunkte Recall.

Retrieval mit Reranking

Bei der Anfrage bettet derselbe Encoder die Frage ein — diesmal mit retrieval.query. Wir ziehen zunächst 20 Kandidaten aus Qdrant und filtern anschließend mit dem Cross-Encoder auf die Top-5 herunter.

from sentence_transformers import CrossEncoder

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

def retrieve(query: str, top_k: int = 5):
    q_vec = model.encode([query], task="retrieval.query")[0].tolist()
    candidates = client.search(
        collection_name="wissensbasis",
        query_vector=q_vec,
        limit=20,
        with_payload=True,
    )
    pairs = [(query, c.payload["text"]) for c in candidates]
    scores = reranker.predict(pairs)
    ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
    return [c.payload for c, _ in ranked[:top_k]]

Der Reranker verdoppelt die Relevanz der Top-Treffer — gemessen an manueller Annotation auf einem internen Test-Set mit 200 Fragen stieg MRR@5 von 0,61 auf 0,83.

Generation gegen vLLM via OpenAI-API

vLLM liefert einen OpenAI-kompatiblen Endpoint. Das ist strategisch wichtig: Die Generation-Logik bleibt portabel — von OpenAI zu lokalem Llama, oder umgekehrt, ohne Anwendungscode zu ändern.

# vLLM starten
python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.3-70B-Instruct \
  --tensor-parallel-size 2 \
  --max-model-len 16384 \
  --gpu-memory-utilization 0.92 \
  --api-key $VLLM_API_KEY
from openai import OpenAI

llm = OpenAI(base_url="http://vllm:8000/v1", api_key=VLLM_API_KEY)

SYSTEM = """Du bist ein interner Assistent. Beantworte die Frage
ausschließlich anhand der bereitgestellten Kontextpassagen. Zitiere
die Quelle in eckigen Klammern. Wenn der Kontext keine Antwort erlaubt,
sage das explizit."""

def answer(query: str):
    passages = retrieve(query, top_k=5)
    context = "\n\n".join(
        f"[{i+1}] Quelle: {p['source']}\n{p['text']}"
        for i, p in enumerate(passages)
    )
    resp = llm.chat.completions.create(
        model="meta-llama/Llama-3.3-70B-Instruct",
        messages=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": f"Kontext:\n{context}\n\nFrage: {query}"},
        ],
        temperature=0.1,
        max_tokens=800,
    )
    return resp.choices[0].message.content, passages

Typischer Fehler: Temperatur 0,7 oder höher bei RAG. Für Fakt-Antworten gegen geschlossene Wissensbasen liegt der richtige Wert zwischen 0,0 und 0,2. Höhere Werte erzeugen Halluzinationen auch bei korrektem Kontext.

Betrieb, Monitoring und häufige Fehler

Eine RAG-Pipeline scheitert im Betrieb selten an einzelnen Komponenten — sondern an drei wiederkehrenden Mustern:

  • Stale Data: Neue Dokumente werden nicht re-indiziert. Lösung: Scheduled Reingest, Change-Detection per Content-Hash.
  • Query-Drift: Nutzer stellen Fragen, für die keine Quellen existieren. Lösung: Confidence-Threshold auf Reranker-Score, bei Unterschreiten explizite Nicht-Antwort.
  • Context-Overflow: Fünf Chunks à 600 Tokens plus Systemprompt sprengen das Kontextfenster bei langen Historien. Lösung: Kontextlänge überwachen, ggf. Top-k reduzieren.

Für das Monitoring empfehlen wir Langfuse oder Phoenix — beide protokollieren Embeddings, Retrieval-Resultate und Generation-Outputs und lassen sich selbst hosten. Wer das auslässt, betreibt RAG im Blindflug.

Selbsttest

Wo steht Ihr Unternehmen auf dem Weg zur internen KI?

Sieben Minuten, zwölf Fragen, belastbare Standortbestimmung: Der KI-Reifegrad-Selbsttest liefert eine ehrliche Einschätzung zu Daten, Infrastruktur, Organisation und Governance — mit konkreten nächsten Schritten.

Selbsttest starten

Häufige Fragen

Welche Chunk-Größe ist für deutschsprachige Fachdokumente sinnvoll?

Für technische Dokumentation, Handbücher und Verträge haben sich Chunks von 400 bis 800 Tokens mit 60 bis 100 Tokens Overlap bewährt. Kleinere Chunks verbessern die Präzision bei Faktfragen, verlieren aber Kontext. Größere Chunks erhalten den Kontext besser, erhöhen aber das Risiko irrelevanter Passagen. Semantic Chunking mit Satzgrenzen schlägt in den meisten Tests fixes Chunking.

Warum Reranking, wenn der Vektor-Retriever bereits Scores liefert?

Bi-Encoder berechnen Query und Dokument unabhängig und vergleichen sie per Cosine. Cross-Encoder verarbeiten beides gemeinsam und liefern präzisere Relevanz-Scores. In der Praxis hebt ein Reranker die Trefferquote der Top-3-Ergebnisse um 15 bis 25 Prozentpunkte. Der zusätzliche Latenzaufwand liegt bei 80 bis 150 ms für 20 Kandidaten.

Wie viele Vektoren verkraftet ein einzelner Qdrant-Knoten?

Ein Knoten mit 64 GB RAM und NVMe-SSD verarbeitet bis etwa 20 Millionen Vektoren der Dimension 1024 im HNSW-Index produktiv. Mit Scalar Quantisierung (int8) und On-Disk-Payload lassen sich 60 bis 80 Millionen Vektoren betreiben, bei moderatem Qualitätsverlust von unter einem Prozent Recall. Für den Mittelstand reicht ein einzelner Knoten fast immer aus.

Brauche ich zwingend vLLM oder reicht llama.cpp?

Für produktive RAG-Pipelines mit parallelen Anfragen ist vLLM die robustere Wahl — Paged-Attention und Continuous Batching steigern den Durchsatz um den Faktor 5 bis 10. llama.cpp ist für Einzelnutzer-Szenarien, CPU-Betrieb oder kleine Edge-Deployments attraktiv. Ab zehn parallelen Nutzern sollte vLLM oder TGI zum Einsatz kommen.

RAG sauber aufsetzen — von der Architektur bis zum Betrieb

Wir planen, implementieren und auditieren RAG-Pipelines für mittelständische Unternehmen. On-Premise, DSGVO-konform, mit belastbaren Evaluations-Metriken.