Startseite / Artikel

Query-Time RAG bei zehn Millionen Dokumenten: Kanäle, Router, Agenten und Bewertungen

RAG bei zehn Millionen Dokumenten: ein Abrufprozess unter 200 ms mit SQL-Filtern, hybrider Suche und Neubewertung der Ergebnisse, sowie Abfragerouting, Bewertungen und einem Qdrant-Skript.

Verlag: REACTAPP.TOP8 Min. Lesezeit1776 Wörter

Quellen: Scaling RAG to 10 Million Documents Part 2: Optimizing retrieval and generation

Die Einpflege von zehn Millionen Dokumenten in eine Vektordatenbank ist erst die halbe Miete. Der erste Teil dieses Themas befasste sich mit der Einpflege: Dokumentenroutern wie Docling, Unstructured und Apache Tika, layoutorientiertes Aufteilen in Blöcke, Delta-Indexierung mit MinHash sowie einem Dual-Speicher-Design, das reale Dateien indexiert, ohne Daten zu beschädigen oder die Kosten in die Höhe zu treiben.

Der zweite Teil betrifft die Abfristelle. Millionen von eingebetteten Blöcken nützen nichts, wenn die Abfrage acht Sekunden dauert, an bestimmten Fehlercodes scheitert, vertrauliche Lohndaten preisgibt oder eine in einer alten Datei versteckte Prompt-Injection zulässt. Im Folgenden wird ein Abfrafenster vorgestellt, das innerhalb von weniger als 200 ms antwortet, sowie intelligente Abfratriage, agierende Orchestrierung, kontinuierliche Bewertung und ein vollständiges Skript.

1. Ein mehrstufiges Abfrafenster

In typischen Tutorials wird empfohlen, die Benutzeranfrage einzubetten und eine top_k=5-Ähnlichkeitssuche in der gesamten Datenbank durchzuführen. Bei zehn Millionen Dokumenten führt dieser Ansatz zu zwei großen Problemen.

  1. Dichte Embeddings erfassen abstrakte Bedeutungen gut, versagen jedoch bei exakten Schlüsselwörtern, alphanumerischen Teilenummern und Fehlercodes. Eine Suche nach Error 504 Gateway Timeout kann allgemeine Diskussionen zu Zeitüberschreitungen liefern, anstatt die genaue Fehlerbehebungseite anzuzeigen.
  2. Die Bewertung jedes Kandidaten mit einem aufwändigen Relevanzmodell ist für interaktiven Gebrauch viel zu langsam.

Deshalb behandeln Produktionsysteme die Dokumentensuche als Trichter, der die Kandidaten schrittweise einengt:

[ 10,000,000 Total Document Chunks ]
                 │
                 ▼
     [ Step 1: SQL Pre-Filter ] ────────► Filter by Tenant / Dept / Role / Region
                 │
                 ▼
       [ ~50,000 Candidates ]
                 │
                 ▼
     [ Step 2: Hybrid Search ] ─────────► Dense Vectors (Qdrant) + Sparse BM25
                 │
                 ▼
        [ Top 100 Candidates ]
                 │
                 ▼
       [ Step 3: Cross-Encoder ] ───────► Cohere Rerank / BGE-Reranker
                 │
                 ▼
       [ Final Top 5 Chunks ] ──────────► Passed to LLM Context Window

Phase 1: relationelles Vorfiltern

Bevor überhaupt hochdimensionale mathematische Verfahren angewandt werden, reduzieren Metadatenfilter basierend auf Benutzerrolle, Abteilung oder Dokumentstatus den Suchraum. Durch die Reduzierung von zehn Millionen Dateneinheiten auf etwa 50.000 wird die Latenzzeit verringert und gleichzeitig die Zugriffskontrolle gewährleistet:

from qdrant_client.models import Filter, FieldCondition, MatchValue

# Restrict search space by user session permissions before distance scoring
user_access_filter = Filter(
    must=[
        FieldCondition(key="department", match=MatchValue(value="Engineering")),
        FieldCondition(key="is_active", match=MatchValue(value=True))
    ]
)

Phase 2: hybride Suche

Reale Suchvorgänge gibt es in zwei komplementären Formen, und es ist hilfreich, beide zu verstehen, bevor man sich den Fusionsalgorithmus ansieht.

Die dichte oder semantische Suche wandelt Text in Embeddings in der Vektordatenbank um und platziert Wörter mit ähnlichen Bedeutungen in der Nähe voneinander. Sie kommt gut mit Paraphrasen, Synonymen und konversationalen Ausdrucksweisen zurecht, kann jedoch ein exaktes Schlüsselwort übersehen und Dokumente zurückgeben, die lediglich ein gemeinsames Konzept teilen.

Die spärliche oder schlagwortbasierte Suche, mit Algorithmen wie BM25 oder dem älteren TF-IDF, bewertet einen Begriff danach, wie oft er in einem Dokument vorkommt im Verhältnis dazu, wie selten er im gesamten Korpus ist. Sie erfasst exakte Wörter zuverlässig, kennt aber nichts über ihre Bedeutung.

Die verlockende Antwort wäre, beide Methoden auszuführen und ihre Ergebnisse zusammenzuzählen – genau da kommt die Reciprocal Rank Fusion ins Spiel. RRF fügt mehrere gerankte Listen zu einer einzigen, vereinigten Liste zusammen, ohne dass es notwendig ist, die Rohwerte zu normalisieren. Ein Dokument, das in beiden Rankings weit oben liegt, rückt auch in der kombinierten Liste an die Spitze.

Phase 3: Neuranking mit Cross-Encoder

Hybride Suchverfahren liefern schnell etwa hundert Kandidaten, doch eine reine Sortierung nach Vektorentfernung kann dennoch irrelevante Ergebnisse hervorbringen. Zwei verschiedene Kodierungsverfahren erklären die Lösung dafür.

Ein Bi-Encoder ist die Grundlage herkömmlicher Vektor-Suchverfahren. Dokumente werden bei der Eingabe in Vektoren umgewandelt, die Anfrage des Benutzers wird zum Zeitpunkt der Abfrage zu einem weiteren Vektor, und einfache geometrische Methoden wie die Kosinusähnlichkeit oder das Skalarprodukt werden zur Vergleichung verwendet. Dies ist zwar schnell, aber nur mäßig präzise.

Ein Cross-Encoder liest die Abfrage zusammen mit einem Dokument und gibt eine Bewertung dafür aus, wie relevant dieses Dokument für die Abfrage ist. Er ist gründlich, daher langsam – schließlich würde das Bewerten einer Million Dokumente für eine Suche Stunden in Anspruch nehmen. Die Lösung besteht darin, beide Methoden zu kombinieren. Der Bi-Encoder findet die 100 besten Kandidaten, und der Cross-Encoder ordnet anschließend nur diese um und wählt die zehn besten aus, was weniger als 100 ms dauert.

2. Ein bedingter Abfragerouter

Die meisten Chat-Interfaces sind einfache Textfelder, sodass Nutzer alles eingeben können. Nachrichten wie „Zusammenfassung unseres bisherigen Gesprächs“ oder „Übersetzen der Ergebnisse ins Deutsche“ erfordern überhaupt keine Dokumentensuche, und ihr Versand durch den gesamten Prozess verschwendet Zeit und Geld.

Ein bedingter Router steht am Eingang als schneller Klassifikator, der entscheidet, wohin jede Nachricht gelangt:

User Query
    │
    ▼
[ Intent Classifier / Router ]
    │
    ├── Simple Math / Logic ──────────► Direct Calculator / Python REPL
    ├── Conversational / Follow-up ───► Direct LLM Memory Context
    └── Domain Knowledge Request ─────► Full Hybrid RAG Pipeline

Konzeptionell bittet ein kurzer Prompt das Modell, die Anfrage in einen der wenigen möglichen Verarbeitungswegen einzuordnen – beispielsweise die Abfrage in interner Dokumentation oder eine direkte Antwort:

# Conceptual Router Pattern
def route_query(user_query: str) -> str:
    prompt = f"""Classify the user query into one of these routes:
    - RETRIEVE: Needs internal company documentation/database lookup.
    - COMPUTE: Pure math, calculation, or logic.
    - DIRECT: Conversational, greetings, or basic language rewrites.

    Query: {user_query}
    Classification:"""

    # Run a fast, lightweight classifier (or small local SLM)
    decision = fast_classifier(prompt).strip()
    return decision

3. Mehr als nur eine Suche: Orchestrierung und Rückmeldung

Viele reale Anfragen erfordern mehrere Überlegungsschritte. Betrachten wir beispielsweise eine Anfrage, bei der die Verkaufszahlen für APAC im Jahr 2025 mit den Compliance-Richtlinien von 2024 verglichen und Abweichungen erkannt werden sollen. Dabei handelt es sich um drei Unteraufgaben: Die Verkaufszahlen für APAC aus dem Jahr 2025 finden und extrahieren, die Compliance-Richtlinien von 2024 abrufen sowie nach Konflikten zwischen ihnen suchen.

Ein Einmal-System versagt hier, da es versucht, alles in einer einzigen Anfrage zu beantworten. Agentic RAG löst dieses Problem mithilfe von Orchestrierungsframeworks wie LangGraph oder CrewAI, die das Problem in koordinierte Spezialaufgaben aufteilen. Ein Planer kann mehrere Agenten erstellen, wobei jeder Agent denselben Abrufprozess verwendet, um seine eigenen Beweismittel zu sammeln:

                  [ Orchestrator / Planner ]
                              │
            ┌─────────────────┴─────────────────┐
            ▼                                   ▼
    [ Researcher Agent ]                [ Compliance Agent ]
    • Retrieves 2025 Sales Data         • Retrieves 2024 Regulations
    • Extracts regional tables          • Parses policy constraints
            │                                   │
            └─────────────────┬─────────────────┘
                              ▼
                      [ Synthesis Agent ]
                      • Reconciles numbers
                      • Validates output consistency
                              │
                    Confidence Score Check
                       │             │
              [ Low Confidence ]     [ High Confidence ]
                       │                     │
                       ▼                     ▼
              Loop back & refine     Final Guardrail Validation

Selbstkorrektur

Wenn die Abfrage Ergebnisse mit niedrigen Ähnlichkeitswerten liefert oder wenn das Synthesieagent eine Widersprüchlichkeit feststellt, sollte das Agent reagieren. Es lehnt den schwachen Text ab, schreibt die Anfrage um, indem es Abkürzungen erweitert oder Synonyme verwendet, und fragt anschließend erneut den Abfrageserver ab, anstatt eine erfundene, allgemeine Antwort zu liefern. Agenten koordinieren und entscheiden, doch ohne eine solide RAG-Pipeline haben sie nichts, womit sie arbeiten können.

4. Kontinuierliche Bewertung

Ein Produktionsystem benötigt eine kontinuierliche Bewertung durch automatisierte Frameworks wie Ragas, TruLens oder DeepEval. Diese prüfen mehrere Dimensionen:

┌──► Faithfulness (Is the answer grounded in the retrieved chunks?)
RAG Evaluation ─┼──► Answer Relevance (Did it actually answer the user's prompt?)
                ├──► Context Recall (Did retrieval find all necessary reference chunks?)
                └──► System Latency & Token Cost (Is it cost-effective at scale?)

Neben den Betriebsmetriken gibt es mehrere Service-Level-Indikatoren, die zeigen, wie gut die Informationsabrufung funktioniert:

  • Die Treue gibt den Anteil der Aussagen in der Antwort an, der durch den abgerufenen Kontext gestützt wird – dies dient als Überprüfung auf Halluzinationen.
  • Die Kontextgenauigkeit ist der Anteil der abgerufenen Teile unter allen in der Anfrage enthaltenen Teilen, die tatsächlich relevant sind.
  • Die Kontextrückholung prüft, ob alle für die Frage notwendigen Fakten abgerufen wurden.
  • Die Latenz, sowohl von Anfang bis Ende als auch pro Schritt, umfasst Router, Filter, hybride Suche, Neubewertung und Generierung.

Jeder Indikator benötigt einen Zielwert, damit Qualitäts- und Leistungsvereinbarungen überprüft werden können.

5. Ein vollständiges Skript für Informationsabrufung und Generierung

Das untenstehende Python-Skript kümmert sich um die Ausführungsebene. Es führt eine Vorausfilterung von Metadaten durch, holt Vektoren aus Qdrant ab, erstellt Kontext und ruft einen lokalen LM Studio-Server zur Inferenz auf:

from openai import OpenAI
from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue

# 1. Initialize Clients
# Pointing to local LM Studio running on port 8080
ai_client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="lm-studio")
qdrant_client = QdrantClient(url="http://localhost:6333")
COLLECTION_NAME = "enterprise_knowledge_base"
EMBEDDING_MODEL = "nomic-ai/nomic-embed-text-v1.5"
def retrieve_and_generate(user_query: str, user_department: str) -> str:
    print(f"\n🔍 Processing query: \"{user_query}\" for department: [{user_department}]")

    # 2. Vectorize the User Query
    query_resp = ai_client.embeddings.create(
        input=[user_query],
        model=EMBEDDING_MODEL
    )
    query_vector = query_resp.data[0].embedding
    # 3. Stage 1 & 2: SQL Pre-Filter + Vector Search
    # Filter by user department and active document status
    access_filter = Filter(
        must=[
            FieldCondition(key="department", match=MatchValue(value=user_department))
        ]
    )
    search_results = qdrant_client.search(
        collection_name=COLLECTION_NAME,
        query_vector=query_vector,
        query_filter=access_filter,
        limit=3
    )
    if not search_results:
        return "No relevant or authorized documents found."
    # 4. Context Assembly with Breadcrumbs
    context_blocks = []
    for hit in search_results:
        breadcrumb = hit.payload.get("breadcrumb", "General")
        text = hit.payload.get("text", "")
        context_blocks.append(f"[{breadcrumb}]\n{text}")
    full_context = "\n\n---\n\n".join(context_blocks)
    # 5. Generation via Local LLM
    system_prompt = (
        "You are an enterprise technical assistant. "
        "Answer the user query strictly using the provided context. "
        "If the context does not contain the answer, explicitly state that you do not know.\n\n"
        f"Context:\n{full_context}"
    )
    completion = ai_client.chat.completions.create(
        model="local-model",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_query}
        ],
        temperature=0.1
    )
    return completion.choices[0].message.content
# Example Execution
if __name__ == "__main__":
    response = retrieve_and_generate(
        user_query="How do I enable TLS 1.3 in config.yaml?",
        user_department="Engineering"
    )
    print("\n🤖 Final Answer:\n", response)

Die Form einer Produktionsarchitektur

Der Übergang vom Prototyp zu einem System, das Millionen von Unternehmensdokumenten verarbeitet, erfordert, RAG als vollständige Ingenieurdisziplin zu betrachten und nicht nur als einzelnen Bibliotheksaufruf. Vier Gewohnheiten fassen dies zusammen:

  • Reinigend einlesen. Struktur ist wichtiger als Cleverness – daher sollten spezialisierte Parser verwendet werden, Layout-Hinweise beibehalten und unveränderte Dateien durch Delta-Hashing vom Neindekatisieren ausgenommen werden.
  • Früh filtern. Sicherheitsbereiche und relationale Metadaten gehören in einen SQL-Schritt, der vor jeder aufwändigen Vektorberechnung ausgeführt wird.
  • Suchstrategien mischen. Kombinieren Sie dichte Vektoren mit BM25-Schlüsselwortabgleich sowie einem Cross-Encoder zur Neubewertung der Ergebnisse.
  • Die Grenzen schützen. Leiten Sie bedingungsweise weiter, um unnötige Abfragen zu vermeiden, wenden Sie Eingangs- und Ausgangsbeschränkungen an und beteiligen Sie Menschen an hochriskanten Aktionen.

Verwandte Literatur

  • Warum RAG immer noch falsche Antworten liefert: Hybrid-Suche, Neubewertung und Recall@K — Saubere Textabschnitte garantieren keine guten Antworten. Erfahren Sie, wo bei der Vektorabrufung Fehler auftreten und wie Fusion, ein cross-encoder-basierter Neubewertungsalgorithmus, HyDE sowie die Recall@K-Messmethode dies beheben.