Startseite / Artikel

Warum RAG in der Produktion versagt: Chunking, Embeddings, Datenabruf und Bewertung.

Warum RAG in der Produktion versagt, sobald die Demo vorbei ist: Grenzen der Datenblöcke, Blindstellen bei der Embedding-Verarbeitung, Limits von Top-K, veraltete Daten sowie der von den Teams übersprungene Bewertungsschritt.

Verlag: REACTAPP.TOP10 Min. Lesezeit2199 Wörter

Quellen: RAG Without the Hype: What Retrieval-Augmented Generation Actually Takes to Work in Production

Das Standard-RAG-Tutorial verläuft reibungslos. Ein PDF wird geladen und in Abschnitte unterteilt, diese Abschnitte werden eingebettet und in einer Vektordatenbank gespeichert. Anschließend wird eine Benutzerfrage eingebettet, mit den nächstgelegenen Abschnitten abgeglichen und in einen Prompt eingefügt. Die Antwort kommt präzise, und es scheint, als wäre der schwierige Teil vorbei.

In der Produktion sieht das anders aus. Fragen beziehen sich auf mehrere Dokumente. Der relevante Kontext wird an den Rändern der Abschnitte abgeschnitten. Die Suche liefert Abschnitte, die im semantischen Raum der Anfrage nahe liegen, aber im Kontext falsch sind – mit den richtigen Worten, aber der falschen Bedeutung. Das Modell antwortet anschließend selbstsicher auf Basis von veralteten, widersprüchlichen Informationen oder ohne die notwendigen Einschränkungen, die drei Sätze weiter außerhalb der Abschnittsgrenze standen. Am schlimmsten ist, dass die Ausgabe korrekt klingt und nichts darauf hindeutet, dass die Suche fehlerhaft verlaufen ist.

Eine RAG-Demo ist leicht zu erstellen, doch eine Version, die mit einem echten Korpus und echten Nutzern zuverlässig funktioniert, ist es nicht. Der Unterschied entsteht durch drei Faktoren: wie Dokumente in Blöcke aufgeteilt werden, wie Embeddings Nuancen erfassen und wie die Suche entscheidet, was als relevant gilt. Jeder dieser Aspekte weist Fehlermöglichkeiten auf, die leicht übersehen werden können und nach dem Start teuer zu beheben sind.

Blöckenaufteilung – wo Informationen verloren gehen

Standardmäßig verwendet das Tutorial eine rekursive Zeichensegmentierung mit fester Größe und etwas Überlappung.

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200
)
chunks = splitter.split_documents(documents)

Das reicht für eine Demo aus. Es versagt jedoch, wenn die Dokumentstruktur eine Bedeutung trägt, die die Zeichenzählung ignoriert – und zwar auf drei gängige Weisen.

  • Geteilter Kontext. Eine Spezifikation könnte vorsehen, dass der maximale unterstützte Wert 500 beträgt und dass diese Grenze nur im Hochverfügbarkeitsmodus mit aktivierter primärer Failover-Konfiguration gilt. Wird bei 1.000 Zeichen abgeschnitten, kann die Zahl in einem Teilabschnitt und die entsprechende Bedingung im nächsten liegen. Jemand, der nach der Hochverfügbarkeitsgrenze fragt, erhält den Teilabschnitt mit dem Wert 500 ohne weitere Angaben. Die Antwort existiert zwar im Korpus, doch durch die Aufteilung in Teilabschnitte wird der Zusammenhang unterbrochen.
  • Verwaiste Überschriften. Eine Überschrift wie „Sicherheitskonfiguration“ sowie der erste Absatz darunter, der besagt, dass für jede Verbindung TLS erforderlich ist, können in unterschiedlichen Abschnitten landen. Ein Suchalgorithmus, der den Absatz findet, verfügt über keinen Kontext bezüglich der Überschrift, und die Einbettung des Satzes gibt nicht an, dass er zur Sicherheitseinstellung gehört.
  • Störungen in Tabellen und Listen. Splitter mit fester Größe schneiden oft eine Tabelle mitten in einer Zeile oder eine Liste mitten in einem Eintrag ab. Ein Spaltenkopf in einem Abschnitt und die Daten in einem anderen machen beides für sich genommen unlesbar.

Produktionsreife Aufteilungsmethoden stützen sich in der Regel auf drei Techniken.

strukturorientiertes Aufteilen erfolgt an den eigenen Grenzen des Dokuments, also an Überschriften, Absätzen und Abschnitten, anstelle an Zeichenzählungen. Markdown kann an Überschriften zweiter oder dritter Ebene geteilt werden, und HTML an seinen Überschriften-, Absatz- oder Abschnitts-Tags.

from langchain.text_splitter import MarkdownHeaderTextSplitter

headers_to_split_on = [
    ("#", "document_title"),
    ("##", "section"),
    ("###", "subsection"),
]

splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
chunks = splitter.split_text(markdown_document)
# Each chunk now includes header metadata: {"section": "Security Configuration"}

Elternkontext-Anreicherung speichert zu jedem Chunk den Überschriftspfad, der dorthin führt. Wenn der Chunk abgerufen wird, erhält die Anfrage die Titel der Abschnitte und Unterabschnitte neben dem Text, wodurch dem Modell eine Struktur vermittelt wird, die im Embedding-Raum nicht enthalten ist.

Eltern-Kind-Chunking verwendet kleine Chunks für eine präzisere Abfrage sowie größere Eltern-Chunks, die diese enthalten. Die Abfrage passt auf den kleinen Teil, und die Anfrage erhält den Eltern-Chunk. Kleine Chunks fassen spezifische Ideen genauer ein, während die Eltern-Chunks die umliegenden Informationen liefern.

# Small child chunk for retrieval
child_chunk = "The maximum supported value is 500."

# Parent chunk injected into prompt when child is retrieved
parent_chunk = """
## High Availability Configuration
When operating in high-availability mode with primary failover enabled,
the following limits apply:
- Maximum supported value: 500
- Minimum heartbeat interval: 100ms
- Failover timeout: 30 seconds
"""

Der Ansatz ist teurer in der Implementierung und Speicherung, liefert aber deutlich bessere Antworten auf Fragen, die einen umliegenden Kontext erfordern – was den Großteil der tatsächlichen Fragen umfasst.

Embeddings und die Grenzen der Ähnlichkeit

Embeddings platzieren Text in einem Vektorraum, in dem ähnliche Bedeutungen nah beieinander liegen, und die Abfrage liefert die dem Query am nächsten liegenden Abschnitte. Das funktioniert, solange semantische Nähe eine angemessene Entsprechung für Relevanz darstellt. Es versagt jedoch, wenn diese auseinanderdriften.

  • Verneinung. Sätze, die besagen, ein System unterstütze weder IPv6 noch unterstütze es es, erhalten nahezu identische Vektoren, da beide das IPv6-Support thematisieren. Die Verneinung verändert den Vektor kaum. Eine Frage zu IPv6 kann sowohl beide Abschnitte als auch den falschen zurückgeben, und das Modell muss ohne es zu bemerken einen Widerspruch auflösen oder aus dem falschen Abschnitt antworten.
  • Wortschatzunterschied. Ein Benutzer schreibt „Login“, während in den Dokumentationen von „Authentifizierung“ und „Anmelden“ die Rede ist. In der Regel liegen diese Begriffe nahe genug beieinander, sodass eine Suche funktioniert, doch es treten Randfälle auf, wenn der Jargon des Benutzers von der Verwendung in der Dokumentation abweicht.
  • Spezifität. Eine Frage zur Konfiguration des Connection Pools und eine zur Standard-Timeout-Einstellung des Pools befinden sich sehr nah beieinander, wodurch beide dieselben Informationen liefern. Wenn der Timeout-Wert in einem Absatz unter mehreren Abschnitten über Pools steht, wird nicht immer der beste Teil an erster Stelle angezeigt.

Drei Lösungen beheben diese Einschränkungen.

Hybride Suche kombiniert dichte Suchverfahren, die die Bedeutung erfassen, mit spärlichen Suchverfahren wie BM25 oder TF-IDF, die genaue Begriffe erfassen. Bei der Suche nach einer Versionnummer, einer Konfigurationsdatei oder einem Fehlercode ist die Schlüsselwortabgleichsmethode effektiver als die semantische Ähnlichkeitsprüfung. Viele Vektordatenbanken bieten heute hybride Suche standardmäßig an, und das untenstehende Beispiel verwendet in Qdrant eine Kombination aus spärlichen und dichten Suchverfahren.

from qdrant_client import QdrantClient
from qdrant_client.models import SparseVector, NamedSparseVector

# Many vector databases now support hybrid search natively
# This example uses Qdrant's sparse + dense hybrid search
results = client.query_points(
    collection_name="docs",
    prefetch=[
        models.Prefetch(
            query=sparse_query_vector,  # BM25 sparse vector
            using="sparse",
            limit=20,
        ),
        models.Prefetch(
            query=dense_query_vector,   # Embedding dense vector
            using="dense",
            limit=20,
        ),
    ],
    query=models.FusionQuery(fusion=models.Fusion.RRF),  # Reciprocal rank fusion
    limit=5,
)

Abfragedehnung bereichert die Frage, bevor sie eingebettet wird. Eine Abfrage zur Authentifizierung kann durch Begriffe wie Anmelden, Sign-in, Authentifizierung und Identitätsüberprüfung erweitert werden, wodurch sie in einen umfangreicheren Bereich des Raums gelangt. Ein kurzer Aufruf eines LLM kann die Varianten erzeugen, oder eine statische Synonymkartei kann Fachbegriffe abdecken.

Wahl des Embedding-Modells verdient ebenfalls Aufmerksamkeit. Viele Teams verwenden standardmäßig allgemeine OpenAI-Modelle wie text-embedding-ada-002 oder text-embedding-3-small. Für medizinische, rechtliche oder technische Korpora leisten oft domain-spezifische oder feinabgestimmte Modelle deutlich bessere Ergebnisse. Wenn die Informationsauswertung trotz guter Aufteilung der Daten schlecht bleibt, lohnt es sich, Alternativen anhand einer repräsentativen Stichprobe von Abfragen zu vergleichen.

Probleme bei der Informationsauswertung in großem Maßstab

Chunking und Embeddings bestimmen, ob Inhalte gefunden werden können, während die Abfrageverarbeitung entscheidet, ob sie für eine gegebene Anfrage tatsächlich gefunden werden. Einige Fehler treten nur bei einem großen Korpus oder vielfältigen Anfragen auf.

Wahl von K. Die meisten Systeme holen die K stärksten Chunks nach Ähnlichkeit heran und fügen sie alle dem Prompt hinzu. Ein kleines K übersieht Fakten, wenn eine Antwort die Unterstützung mehrerer Chunks benötigt. Ein großes K füllt das Kontextfenster mit unwichtigen Chunks, die das Signal schwächen, und kann bei langen Prompts den wirklich relevanten Chunk außerhalb der effektiven Aufmerksamkeitsreichweite des Modells bringen.

Das praktische Vorgehen besteht darin, mehr Ergebnisse zu holen – beispielsweise die 20 oder 30 besten Kandidaten – und diese anschließend neu zu ranken. Ein Cross-Encoder verarbeitet Abfrage und Datenblöcke gemeinsam und bewertet die Relevanz direkt, anstatt zwei unabhängig berechnete Embeddings zu vergleichen. Dadurch ist er langsamer, da die Embeddings der Datenblöcke nicht im Voraus berechnet werden können, doch er unterscheidet zwischen relevanten und nahezu relevanten Ergebnissen deutlich besser. Schnelle, annähernde Suche reduziert den Umfang der Ergebnisse, und der Cross-Encoder wählt schließlich die endgültige Liste aus.

from sentence_transformers import CrossEncoder

reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
# Retrieve broad candidate set
candidates = vector_store.similarity_search(query, k=20)
# Re-rank with cross-encoder
pairs = [(query, chunk.page_content) for chunk in candidates]
scores = reranker.predict(pairs)
# Sort by re-ranking score, take top 5
reranked = sorted(zip(scores, candidates), reverse=True)[:5]
final_chunks = [chunk for _, chunk in reranked]

Veraltetheit. Ein vor sechs Monaten eingebetteter Vektor-Speicher enthält Dokumente, die seitdem geändert wurden, und das Embedding hat davon keine Kenntnis. Die Abfrage liefert mit hoher Zuverlässigkeit den veralteten Abschnitt, und das Modell antwortet darauf. Die Lösung besteht im Filtern von Metadaten: Man speichert das letzte Änderungsdatum jedes Teils und filtert nach dem Zeitraum vor oder nach der Ähnlichkeitssuche. Bei aktiv gepflegter Dokumentation werden Teile, die älter als ein festgelegter Schwellenwert sind, entfernt oder niedriger bewertet.

from datetime import datetime, timedelta

# Filter to chunks updated in the last 90 days
cutoff = datetime.now() - timedelta(days=90)

results = vector_store.similarity_search_with_filter(
    query=query,
    filter={"last_modified": {"$gte": cutoff.isoformat()}},
    k=10
)

Falsches Selbstvertrauen. Ein Modell antwortet auf Basis dessen, was es erhält, selbst wenn die abgerufenen Teile nichts mit der Frage zu tun haben. Es weiß nicht, dass die Informationsbeschaffung mangelhaft war. Dies schadet am meisten dem Vertrauen der Nutzer, denn die Antwort wirkt autoritativ, obwohl sie falsch ist. Die Lösung besteht darin, vor der Generierung eine Relevanzprüfung einzufügen. Entweder werden die abgerufenen Teile bewertet und die Generierung unterdrückt, wenn die beste Bewertung unter einem bestimmten Schwellenwert liegt, oder dem Modell wird im Systemprompt mitgeteilt, dass es nicht über ausreichende Informationen verfügt, wobei anschließend überprüft werden muss, ob es diesen Ausweg ordnungsgemäß nutzt.

SYSTEM_PROMPT = """
You are a documentation assistant. Answer questions using only the provided context.

If the context does not contain enough information to answer the question confidently,
respond with: "I don't have sufficient information in the available documentation to
answer this question reliably."

Do not speculate or answer from general knowledge when documentation context is insufficient.
"""

Bewertung – der Schritt, den Teams überspringen

Ein RAG-System ohne Bewertung weist eine unbekannte Qualität auf. Es kann bei einigen Abfragen gut und bei anderen schlecht abschneiden, und die einzige Möglichkeit, dies herauszufinden, sind Nutzerbeschwerden. Die Bewertung besteht aus drei Teilen.

Qualität der Informationsbeschaffung. Bei Testabfragen mit bekannten, korrekten Queldokumenten wird gemessen, wie oft das richtige Dokument unter den obersten K Ergebnissen erscheint. Die Recall-Rate bei fünf Abfragen zeigt an, ob der richtige Textausschnitt in den ersten fünf Ergebnissen enthalten ist, während die durchschnittliche rekursive Rangwertung angibt, wie hoch seine Position ist. Vor dem Einsatz sollte ein Testset mit 100 bis 200 repräsentativen Abfragen durchgeführt werden.

Genauigkeit der Antwort. Spiegelt die erzeugte Antwort den beschafften Kontext wider, ohne Behauptungen aufzustellen, die in den Textausschnitten nicht enthalten sind? Eine Konfiguration mit einem LLM als Bewertungsinstanz kann dies bewerten, indem sie die Abfrage, die beschafften Textausschnitte sowie die Antwort erhält und bewertet, inwieweit der Kontext die Antwort stützt.

FAITHFULNESS_PROMPT = """
Given the following context and answer, rate whether the answer is fully
supported by the context on a scale of 1-5.

Context: {context}
Answer: {answer}

Score (1=not supported, 5=fully supported):
"""

# Automate across your evaluation set
scores = []
for query, context, answer in eval_set:
    score = judge_model.complete(
        FAITHFULNESS_PROMPT.format(context=context, answer=answer)
    )
    scores.append(score)
print(f"Average faithfulness: {sum(scores)/len(scores):.2f}")

End-to-End-Korrektheit. Liefern Systeme bei Abfragen mit bekannten Antworten tatsächlich die richtige Antwort? Dies ist der schwierigste Teil zur Automatisierung – zugleich aber auch der bedeutendste. Selbst eine manuell erstellte Sammlung von 50 Fragen mit überprüften Antworten, die vor und nach jeder Änderung bei den Chunking-Methoden, Embedding-Modellen oder Sucheinstellungen ausgeführt wird, kann Regressionen aufdecken.

Das Bewertungsset fungiert als Regressionstestsuite. Ein neues Embedding-Modell, eine andere Chunk-Größe oder eine angepasste Systemanweisung sollten jeweils eine messbare Veränderung der Ergebniswerte bewirken. Wenn eine Änderung die durchschnittliche Genauigkeit erhöht, aber die Rückrufrate verringert – oder umgekehrt – muss dieser Kompromiss eine bewusste Entscheidung sein und nicht eine stillschweigende Implementierung.

Der Pipeline-Prozess ist das Produkt

Der Tutorial-Pipeline umfasst vier Schritte und verfügt über keine Fehlerbehandlung. Die Produktions-Pipeline hat sechs Schritte, bei jedem Schritt eine Fehlererkennung, Metadaten, die durchgehend hinzugefügt werden, sowie eine Bewertung vor jeder Veröffentlichung.

Die Fehler sind vorhersehbar. Ein Chunking, das die Struktur ignoriert, verliert am Rand den Kontext. Eine reine Embedding-basierte Suche übersieht exakte Übereinstimmungen und handhabt Negationen schlecht. Eine Suche ohne Neubewertung liefert ein verwatertes Top-K, das die besten Ergebnisse verdeckt. Eine Generierung ohne Relevanzprüfung erzeugt auf der Grundlage schwachen Kontexts zuversichtlich falsche Antworten.

Nichts davon ist unlösbar, und jede Lösung hat einen bekannten Preis. Ein strukturbewusstes Chunking erfordert das Verständnis des Dokumentformats. Eine hybride Suche bedeutet, sowohl dichte als auch spärliche Indizes zu nutzen. Die Neubewertung verursacht Verzögerungen bei der Suche. Die Bewertung erfordert den Aufbau und die Pflege eines Testsets.

Teams, deren RAG-Systeme zuverlässig funktionieren, treffen diese Entscheidungen absichtlich vor dem Start, und nicht als Reaktion auf falsche Antworten. Tutorials überspringen die schwierigen Teile, weil eine Demo sie nicht benötigt, aber ein zuverlässiges System schon.