11. April 2026

Das Problem mit reinem Vector-RAG

Vektor-Datenbanken sind stark, wenn es um semantische Ähnlichkeit und langen Fließtext geht. Sie sind aber notorisch unzuverlässig bei atomaren Fakten, Zahlen und strikten Entity-Beziehungen. Ein klassisches Beispiel aus dem Machine-Learning-Mastery-Tutorial von Matthew Mayo: Fragt man ein Vector-RAG, für welches Team ein bestimmter Basketballspieler aktuell spielt, landen im Retrieval-Top-k leicht mehrere Teams, weil alle im latenten Raum nahe am Spielernamen liegen. Das Modell hat dann die Wahl – und halluziniert gelegentlich die falsche Beziehung.

Die Ursache ist strukturell: Embeddings sind verlustbehaftet, Nähe ist kein Wahrheitsbegriff. Wer harte Fakten retrieven will, braucht Strukturen, die Fakten als Fakten behandeln – also einen Graphen.

Die drei Tier im Überblick

Das Tutorial baut eine föderierte Architektur mit drei Retrieval-Quellen, die alle gleichzeitig angefragt werden. Die Ergebnisse wandern parallel in den Kontext, und ein promptbasiertes Regelwerk zwingt das Modell, Konflikte deterministisch aufzulösen.

Das Raffinierte: Es gibt keinen algorithmischen Router, der entscheidet, welche Quelle gefragt wird. Alle drei Quellen werden immer abgefragt, die Ergebnisse als [PRIORITY 1], [PRIORITY 2], [PRIORITY 3] in den System-Prompt gestanzt, und das LM bekommt eine strikte Hierarchie vorgeschrieben.

Tier 1 – Der Fakten-Graph

Statt Neo4j oder ArangoDB nutzt das Tutorial einen selbstgebauten, in-memory QuadStore (etwa 200 Zeilen Python, verfügbar im zugehörigen GitHub-Repo). Intern mappt er Strings auf Integer-IDs und hält vier Indizes (spoc, pocs, ocsp, cspo) für Konstantzeit-Lookups über beliebige Dimensionen. Die API ist bewusst minimal:

from quadstore import QuadStore

facts_qs = QuadStore()
facts_qs.add("LeBron James", "played_for", "Ottawa Beavers", "NBA_2023_regular_season")
facts_qs.add("Ottawa Beavers", "based_in", "downtown Ottawa", "NBA_trivia")
facts_qs.add("LeBron James", "average_mpg", "12.0", "NBA_2023_regular_season")

Die Fakten werden manuell gepflegt – das ist der Preis für Determinismus. Der context-Slot im Quad erlaubt es, denselben Sachverhalt in verschiedenen Zeiträumen zu tracken, ohne dass sich die Tripel gegenseitig überschreiben.

Tier 2 – Der Statistik-Graph

Strukturell identisch zu Tier 1, aber befüllt aus einer JSONL-Datei, die zuvor aus einem CSV-Datensatz (im Beispiel: NBA-Saison-Statistiken) per Skript in Quads konvertiert wurde. Hier leben aggregierte Kennzahlen und breitere historische Daten. Die Trennung zu Tier 1 ist wichtig, weil Tier 2 oft mit Abkürzungen, veralteten Zuordnungen und Mehrdeutigkeiten behaftet ist. Der Prompt deklariert Tier 2 explizit als „supplementary background only".

Tier 3 – Vektor-Dokumente

ChromaDB als persistente Collection, befüllt mit unstrukturierten Textchunks. Nichts Besonderes – Standard-Retrieval per String-Ähnlichkeit zur Query:

chroma_client = chromadb.PersistentClient(path="./chroma_db")
collection = chroma_client.get_or_create_collection(name="basketball")
collection.upsert(documents=[doc1, doc2], ids=["doc1", "doc2"])

Dieser Layer liefert den Fließtext-Kontext, der in den Graphen naturgemäß fehlt: Hintergrundgeschichten, Verletzungsverläufe, Spielberichte.

Routing per NER statt per Router

Der Schritt, der Graph und Vektor verklammert, ist Named Entity Recognition mit spaCy. Aus dem User-Prompt werden Entities extrahiert und anschließend als strikte Lookups gegen beide Quad-Stores gefeuert, parallel dazu läuft die Vektor-Suche über den Original-Prompt:

nlp = spacy.load("en_core_web_sm")

def extract_entities(text):
    doc = nlp(text)
    return list({ent.text for ent in doc.ents})

def get_facts(qs, entities):
    facts = []
    for entity in entities:
        facts.extend(qs.query(subject=entity))
        facts.extend(qs.query(object=entity))
    return list({tuple(f) for f in facts})

Kein komplexes Query-Rewriting, kein Klassifikator – Entity-Extraktion reicht, weil die Graphen eh auf Entities indiziert sind.

Prompt-enforced Conflict Resolution

Der Clou ist der System-Prompt. Statt Reciprocal Rank Fusion oder gelernte Re-Ranker bekommt das Modell alle drei Streams gelabelt in den Kontext und eine harte Regel dazu:

  1. Wenn Priority 1 eine direkte Antwort enthält, benutze ausschließlich diese.
  2. Priority 2 kann wie ein Widerspruch aussehen – es ist es nicht. Ignoriere Team-Abkürzungen aus P2, wenn P1 ein Team nennt.
  3. P2 nur, wenn P1 auf das gefragte Attribut keine Antwort hat.
  4. P3 als ergänzender Fließtext-Kontext.
  5. Wenn nichts passt: „I do not have enough information." Kein Raten.

Das ist im Kern ein Adjudicator-Pattern, das im Prompt selbst lebt. Es funktioniert, weil die Hierarchie explizit und lokal ist – das Modell muss sie nicht erlernen, sondern nur befolgen. Im Tutorial reicht dafür bereits llama3.2:3b via Ollama.

Wann Graph-RAG die richtige Wahl ist

Für reines Q&A über lange, explorative Dokumente bleibt klassisches Vector-RAG die pragmatischere Wahl.

Pitfalls und Trade-offs

Tooling

Das Kern-Pattern – mehrere gelabelte Retrieval-Streams plus harte Prompt-Hierarchie – lässt sich aus dem Basketball-Spielzeug des Tutorials gut in ernsthafte Domänen übertragen. Der Aufwand rechnet sich dort, wo ein einziger falscher Entity-Link mehr kostet als die Graph-Pflege.

Quellen

Nach oben