Eltern-Kind-Chunking und hierarchische Indizierung für eine bessere RAG-Auswertung
Warum kleine Datenblöcke das Match gewinnen, aber die richtige Antwort verfehlen, und wie Eltern-Kind-Chunking, Deduplizierungsverfahren sowie hierarchische Indizierung den Kontext bei der RAG-Auswertung wiederherstellen.
Quellen: Parent–Child Chunking and Hierarchical Indexing in RAG: A Practical Guide to Better Retrieval
Wenn ein RAG-System eine Antwort liefert, die fast richtig ist, aber das entscheidende Detail fehlt, liegt das Problem in der Regel darin, wie der Text aufgeteilt wurde, und nicht bei den Embeddings. Zwei Techniken begegnen diesem Problem direkt: das Eltern-Kind-Chunking, bei dem kleine Teile gesucht, aber größere Teile gelesen werden, sowie die hierarchische Indizierung, die die Suche durch Verfolgung des eigenen Aufbaus eines Dokuments eingrenzt. Beide Methoden zielen darauf ab, einem Sprachmodell gleichzeitig Präzision und Kontext zu bieten.
Warum die Aufteilung der schwierige Schritt ist
Der grundlegende Ablauf ist einfach: Dokumente in Abschnitte aufteilen, diese einbetten, speichern, die top-k Ergebnisse für eine Abfrage abrufen und an ein Modell weiterleiten. Wenn etwas schiefgeht, liegt das Problem in der Regel bereits beim ersten Schritt. Zu große Abschnitte verringern die Relevanz und trüben die Einbettung, während zu kleine Abschnitte die umliegenden Sätze ausschließen, von denen eine Frage abhängt. Feste Größen, rekursive Verfahren, semantische Ansätze sowie die Aufteilung in Satzfenster sind alle Versuche, diesen Spannungen innerhalb einer Texteinheit entgegenzuwirken.
Eine andere Herangehensweise lohnt sich: Man sollte auf die Suche nach einer perfekten Größe verzichten und stattdessen zwei Größen zusammen verwenden, wobei jede für eine andere Aufgabe optimiert ist. Bevor man diese Lösung einführt, ist es hilfreich, genau zu erkennen, wo eine einzelne Größe versagt.
Ein kleines Beispiel für ein Versagen
Betrachten Sie einen zweisatzigen Abschnitt aus einem technischen Dokument. Der erste Satz besagt, dass Caching ein System erheblich schneller macht. Der zweite warnt davor, dass eine mangelhafte Cache-Invalidierung veraltete Daten hinterlassen kann.
Ein Teilungswerkzeug von geringer Größe könnte ihn in zwei Teile aufteilen. Teil A ist der Satz, der die Leistung lobt, und Teil B ist der Satz über veraltete Daten durch fehlerhafte Invalidierung. Nun die Frage: Welches ist das Risiko von Caching? Die untenstehende Skript verarbeitet die Frage sowie beide Teile und vergleicht sie anschließend mithilfe der Kosinusähnlichkeit.
from sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer("all-MiniLM-L6-v2")
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
query = "What is a risk of caching?"
chunk_a = "Caching improves system performance significantly."
chunk_b = "However, improper cache invalidation can lead to stale data issues."
query_embedding = model.encode(query)
score_a = cosine_similarity(query_embedding, model.encode(chunk_a))
score_b = cosine_similarity(query_embedding, model.encode(chunk_b))
print(f"Chunk A similarity: {score_a:.4f} - '{chunk_a}'")
print(f"Chunk B similarity: {score_b:.4f} - '{chunk_b}'")
Beim Ausführen wird die Ähnlichkeit jedes Abschnitts zur Frage angezeigt.
Chunk A similarity: 0.5891 — 'Caching improves system performance significantly.'
Chunk B similarity: 0.5103 — 'However, improper cache invalidation can lead to stale data issues.'
Chunk A erzielt eine höhere Punktzahl, etwa 0,59 gegenüber rund 0,51, obwohl es Chunk B ist, der die Frage beantwortet. Beide erwähnen Caching, weshalb sie als zusammenhängend erscheinen, und das Embedding-Modell hat keinen besonderen Grund, das Wort „Risiko“ mit einem Satz über veraltete Daten zu verknüpfen, in dem dieses Wort nie vorkommt. Ein Retriever, der nur die beste Übereinstimmung berücksichtigt, würde fälschlicherweise den falschen Teil des Textes an das Modell weiterleiten.
Die Lektion ist, dass semantische Ähnlichkeit keine Vollständigkeit garantiert. Ein Chunk kann zwar als thematisch passend eingestuft werden, dennoch die für die Frage benötigten Fakten enthalten, und je kleiner der Chunk ist, desto wahrscheinlicher ist es, dass sich diese Fakten in einem benachbarten Satz befinden.
Eltern-Kind-Chunking
Die Idee besteht darin, gleichzeitig zwei unterschiedliche Granularitäten desselben Textes zu speichern, wobei jede eine andere Aufgabe erfüllt.
Kinderblöcke sind klein und präzise. Sie umfassen in der Regel 100 bis 400 Token und dienen dazu, Suchvorgänge mit hoher Trefferquote zu verankern. Ihre geringe Größe erzeugt schärfere Vektoren, da weniger unverwandtes Material das ausmacht, was jeder Vektor darstellt, abmildert. Allerdings führt eine Reduzierung auf nur wenige Wörter zu negativen Auswirkungen, da ein Fragment möglicherweise die für eine zuverlässige Übereinstimmung notwendige eigenständige Bedeutung fehlt.
Eltern-Blöcke sind groß und kontextbezogen. Sie umfassen oft 800 bis 3000 Token oder mehr und decken einen ganzen Abschnitt oder eine logische Einheit ab; sie dienen dem Schlussfolgern und nicht der Abgleichung. Im Caching-Beispiel enthält ein Eltern-Block mit dem vollständigen Textbeispiel beide Teile der Idee, sodass das Modell unabhängig davon, welcher Kind-Block abgeglichen wird, dennoch das vollständige Bild erhält. Die Obergrenze ergibt sich aus dem Kontextfenster des Modells, da mehrere große Eltern-Blöcke, die in einen einzigen Prompt zusammengefasst werden, alles andere verdrängen können, was der Prompt noch enthalten muss.
Die Beziehung lässt sich in einem Satz zusammenfassen: Kinder-Blöcke sind es, die gesucht werden, und Eltern-Blöcke sind es, die gelesen werden. Ein Kind-Block muss präzise genug sein, um im Ähnlichkeitsvergleich zu gewinnen, und sein Eltern-Block sorgt dafür, dass das Modell den Kontext erhält, von dem die Antwort dieses Kind-Blocks abhängt.
Wie es in der Praxis aussieht
Stellen Sie sich einen Abschnitt mit dem Titel „Optimierung der Datenbankleistung“ vor. Ein Eltern-Kind-Splitter arbeitet in zwei Schritten. Zunächst wird die Grenze des Elternteils ermittelt – der gesamte Abschnitt behandelt Themen wie Indexierung, Abfragenoptimierung, Caching sowie die damit verbundenen Kompromisse. Anschließend wird derselbe Text in kleinere Kinderabschnitte aufgeteilt, zum Beispiel:
- einen Kinderabschnitt über Indexierung, die die Abfragengeschwindigkeit durch verkürzte Scannzeiten verbessert,
- einen Kinderabschnitt über Caching, das wiederholte Berechnungen verringert, jedoch das Risiko von Veralteten Daten mit sich bringt,
- einen Kinderabschnitt über das Umformulieren von SQL-Abfragen zur Optimierung.
Was dieses Konzept funktionieren lässt, ist, dass jedes Kind sich daran erinnert, woher es stammt. Die einfachste Methode besteht darin, den vollständigen Text des Elternteils in den Metadaten jedes Kindes zu speichern, wie in dieser Indexierungsfunktion der Fall ist.
def build_parent_child_index(document_section, parent_text, child_splitter, embedding_model):
child_chunks = child_splitter(parent_text)
records = []
for child_text in child_chunks:
records.append({
"text": child_text,
"embedding": embedding_model.encode(child_text),
"parent_text": parent_text # full parent stored directly on every child
})
return records
parent_text = """Indexing improves query speed by reducing scan time across large tables.
Caching reduces repeated computation but may introduce staleness if not invalidated properly.
Query optimization involves rewriting SQL queries to use more efficient execution plans."""
child_texts = [
"Indexing improves query speed by reducing scan time across large tables.",
"Caching reduces repeated computation but may introduce staleness if not invalidated properly.",
"Query optimization involves rewriting SQL queries to use more efficient execution plans.",
]
records = [
{"text": t, "parent_text": parent_text} for t in child_texts
]
for r in records:
print(f"Child: {r['text'][:50]}...")
print(f" → linked to parent ({len(r['parent_text'])} chars)\n")
Das Kopieren des Elterneintrags auf jeden Kindereintrag kostet zusätzlichen Speicher, erfordert aber zu Zeitpunkt der Abfrage keine separate Suche, da der Elterneintrag bereits im Kindereintrag vorhanden ist. Die Alternative besteht darin, nur einen parent_id zu speichern und den Elterneintrag bei Bedarf abzurufen, was im großen Maßstab Platz spart, aber einen zusätzlichen Suchschritt beinhaltet. Auf jeden Fall bleibt jedes Kind unabhängig suchbar, ähnlich wie jeder andere Datensatz, und verfügt zugleich über einen direkten Pfad zu seinem vollständigen Kontext.
Kleine Daten abrufen, große erweitern
Das umkehrt den üblichen Ablauf. Bei herkömmlichen RAG-Verfahren erfolgt zunächst die Abfrage, anschließend die Übermittlung der Daten und schließlich die Antwort. Bei der Elternteil-Kind-Version wird zunächst ein Kind abgerufen, es wird auf seinen Elterneintrag erweitert, der erweiterte Kontext übermittelt und anschließend die Antwort gegeben. Hier ist dieser Ablauf im Kontext des Caching-Beispiels umgesetzt.
def parent_child_retrieve(query, child_records, embedding_model, top_k=3):
query_embedding = embedding_model.encode(query)
scored = [
(np.dot(query_embedding, r["embedding"]) /
(np.linalg.norm(query_embedding) * np.linalg.norm(r["embedding"])), r)
for r in child_records
]
scored.sort(key=lambda x: x[0], reverse=True)
top_children = scored[:top_k]
# Expand each matched child to its parent - but deduplicate first
seen_parents = set()
expanded_context = []
for score, record in top_children:
parent = record["parent_text"]
if parent not in seen_parents:
expanded_context.append(parent)
seen_parents.add(parent)
return expanded_context
for r in records:
r["embedding"] = model.encode(r["text"])
context = parent_child_retrieve("What is a risk of caching?", records, model, top_k=2)
print("Context sent to the LLM:\n")
for c in context:
print(c)
Achten Sie auf die Überprüfung von seen_parents. Wenn die beiden obersten Kinder zum selben Elternteil gehören – was häufig der Fall ist, da benachbarte Abschnitte derselben Kategorie oft ähnliche Bewertungen erhalten – würde eine getrennte Verarbeitung jedes Kindes denselben Elternteext zweimal senden und Kontext verschwenden. Durch das Entfernen der Duplikate beim Elternteil erscheint jeder Kontextteil nur einmal, unabhängig davon, wie viele Kinder übereinstimmen.
Bei dem Caching-Verfahren erhält das Modell nun den Satz, der die Übereinstimmung erzielt hat, zusammen mit der dazugehörigen Erklärung, sodass beide Teile unabhängig davon, welches Kind an erster Stelle lag, gemeinsam ankommen.
Warum es funktioniert
- Präzision mit Kontext. Die Kinder finden den richtigen Platz, und die Elternteile liefern das, was ein einzelnes Kind nicht tragen kann.
- Besseres Schlussfolgern. Modelle haben Schwierigkeiten mit getrennten Fragmenten, und ein Elternteil stellt den narrativen Zusammenhang wieder her.
Hierarchische Indizierung
Wenn die Eltern-Kind-Beziehung zwei Ebenen umfasst, ist die hierarchische Indizierung vielfältiger. Anstelle eines Elternteils und seiner Kinder wird eine Struktur erstellt, die die Organisation des Dokuments widerspiegelt. Eine Beispielhierarchie könnte so aussehen.
Document: "Cloud Architecture Guide"
Section: Networking
Subsection: Load Balancing
Chunk: Round-robin method
Chunk: Least connections method
Subsection: CDN usage
Section: Security
Subsection: IAM policies
Subsection: Encryption
Beachten Sie, woher diese Hierarchie stammt. Einige Systeme leiten die Suche nach Kategorien zu, wie zum Beispiel Abteilung, Thema oder Metadatentags, die unabhängig von einem bestimmten Dokument zugewiesen werden. Hier stammt die Hierarchie aus den eigenen Überschriften und Abschnitten des Dokuments, also aus der Art und Weise, wie sein Autor es strukturiert hat. Beides verengt den Suchraum vor der eigentlichen Suche, wobei das eine eine gesamte Wissensbasis nach Themen organisiert und das andere ein einzelnes Dokument nach seinem Inhaltsschema.
Die meisten Dokumente sind keine einfachen Texte. Überschriften und Unterkapitel kodieren bereits die Beziehungen zwischen den Ideen. Die hierarchische Indizierung bewahrt diese Struktur und ermöglicht es dem System, zunächst den richtigen Abschnitt, anschließend den Unterkapitel und schließlich den genauen Textabschnitt zu finden – ähnlich wie eine Person zunächst einen Inhaltssatz durchblättert, bevor sie intensiv liest.
Die Suche in Aktion
Nehmen wir an, die Anfrage lautet „Wie funktioniert das Lastverteilungssystem mit der geringsten Anzahl an Verbindungen?“ Das System findet den Abschnitt „Netzwerk“, beschränkt sich auf die Unterseite „Lastverteilung“ und ruft anschließend den Abschnitt ab, der erklärt, dass bei dieser Methode der Server mit der geringsten Anzahl an aktiven Verbindungen ausgewählt wird.
Jeder dieser Einschränkungsschritte erfordert einen konkreten Mechanismus, da nichts davon von selbst stattfindet. Möglichkeiten sind ein trainierter Klassifikator, der vorhersagt, zu welchem Abschnitt eine Anfrage gehört, ein Aufruf eines LLMs, das die Anfrage liest und aus einer bekannten Liste den wahrscheinlichsten Abschnitt auswählt, oder eine regelbasierte Abgleichung von Abschnittsüberschriften und Schlüsselwörtern. Unabhängig von der gewählten Methode wird sie auf jeder Ebene angewandt – einmal für den Abschnitt und erneut für die Unterseite – bevor die Suche auf Abschnittsebene beginnt.
Ist die zusätzliche Tiefe lohnenswert? Zwei Ebenen reichen für die meisten Dokumente aus. Eine vollständige mehrstufige Hierarchie lohnt sich bei langen, tief strukturierten Materialien, in denen eine Abteilung so viele unterschiedliche Untertopikken enthält, dass eine Suche in allen gleichzeitig die Relevanz verringern würde. Eine kurze FAQ-Seite oder ein Artikel zu einem einzigen Thema benötigt selten mehr als zwei Ebenen, während ein hundertseitiger Architekturleitfaden mit Dutzenden von Unterkapiteln dies möglicherweise tut. Wie bei jeder Indexierungsentscheidung sollte man vor dem Hinzufügen von Komplexität abwägen.
Kurze Anpassungstipps
Beginnen Sie mit Kindereinstellungen in der Mitte des Bereichs, etwa 200 Tokens, und überprüfen Sie die Erinnerungsfähigkeit anhand einiger echter Fragen, bevor Sie etwas ändern. Protokollieren Sie, welche Elterneinträge für jede Abfrage erweitert werden, damit Sie Abschnitte erkennen können, die stets mitgeliefert werden und möglicherweise zu umfangreich sind. Begrenzen Sie die Anzahl der an das Modell gesendeten Elterneinträge, da bereits drei oder vier große Abschnitte ein eher begrenztes Kontextfenster füllen können, und halten Sie den ursprünglichen Kindertext für Zitate bereit, damit die Nutzer genau sehen können, welcher Satz übereinstimmt.
Vergleich der beiden
- Umfang: Elternteil-Kind-Modelle arbeiten mit zwei Granularitäten desselben Textes, während hierarchische Indexierungen das gesamte Gliederungsschema modellieren.
- Hauptvorteil: Elternteil-Kind-Modelle retten den Kontext um eine Übereinstimmung herum, während hierarchische Modelle den Suchbereich einengen.
- Kosten: Bei der Eltern-Kind-Struktur sind Verbindungen zwischen Kindern und Eltern erforderlich, während eine Hierarchie eine Strukturerkennung sowie einen Routing-Schritt auf jeder Ebene benötigt.
- Passgenauigkeit: Die Eltern-Kind-Struktur eignet sich für die meisten langen Dokumente, während eine Hierarchie große, tief strukturierte Quellen besser abbildet.
Sie ergänzen sich gut, da eine Hierarchie am Ende in Eltern-Kind-Blöcke enden kann.
Ein Pipeline-System, das Sie bereitstellen können
- Eingabeverarbeitung. Dokumente analysieren und Überschriften erkennen. Der Lader muss die Überschrifteninformationen speichern, also festhalten, welcher Text zu welcher Abschnitt gehört, anstatt alles in einen einzigen Datenstrom zu zusammenfassen – schließlich hängen die folgenden Schritte davon ab.
- Blöckenaufteilung. Aus vollständigen Abschnitten Eltern-Blöcke erstellen und diese anschließend in Kind-Blöcke aufteilen, wobei zunächst die Grenze zum Eltern-Block bestimmt wird.
- Indizierung. Embedden Sie die Kinderelemente und speichern Sie die Links zu den Elternelementen – entweder indem Sie den Elterntext in die Metadaten jedes Kindes kopieren oder indem Sie einen
parent_idzur Abfrage speichern. - Abfrage. Embedden Sie die Frage, durchsuchen Sie die Kinderelemente, wählen Sie die besten Ergebnisse aus, holen Sie deren Elternelemente unter Vermeidung von Doppelungen ab, erstellen Sie den erweiterten Kontext und senden Sie ihn an das Modell.
Fehler, die vermieden werden sollten
- Nur Kinderelemente. Dadurch erhält man Präzision, verliert aber den Kontext für das logische Schließen – wie im Caching-Beispiel, bei dem das Element B niemals den Satz erhält, der die Antwort vervollständigt.
- Nur Elternelemente. Dadurch bleibt der Kontext erhalten, doch die Genauigkeit der Abfrage leidet, denn das Embedden eines mehrthematischen Abschnitts als ein einziger Vektor verschwimmt die Informationen genauso wie ein zu großer Datenblock.
parent_text oder parent_id wird eine spätere Erweiterung unmöglich. Metadaten müssen beim Erstellen des Datensatzes erfasst werden, da sie nach dem Wegwerfen nicht wiederhergestellt werden können.Wo es am meisten bringt
Diese Ansätze sind besonders nützlich, wo Dokumente lang, strukturiert und miteinander verknüpft sind: Unternehmenswissensdatenbanken, interne Dokumentationen, Support-Bots, Rechts- und Compliance-Archive, Ingenieur-Wikis sowie Forschungshilfen. In solchen Materialien kann keine einzige Datensatzgröße sowohl Präzision als auch Kontext gewährleisten.
RAG entwickelt sich von einer einfachen Such-und-Generier-Methodik hin zu einem Modell, das zunächst durch strukturiertes Wissen navigiert und anschließend über das Findene reflektiert. Das Eltern-Kind-Chunking verbessert das lokale Verständnis, sodass die umliegenden Erklärungen immer zusammen mit dem passenden Ergebnis bereitstehen. Hierarchisches Indexieren verbessert die globale Navigation, wodurch die Suche direkt im richtigen Bereich beginnt und nicht in allen Daten. Flaches Chunking ähnelt dem Herausreißen von Seiten aus einem Buch und dem Vertrauen darauf, dass das Modell den Inhalt erraten kann. Beide Techniken lassen das „Buch“ weiterhin gebunden, ermöglichen aber dennoch das Auffinden einer genauen Zeile. In der Praxis bedeutet das weniger Antworten, die nur annähernd richtig sind – ein Problem, das Nutzer am meisten frustriert – sowie eine Suchschicht, deren Verhalten leichter erklärt werden kann, wenn jemand fragt, warum ein bestimmter Abschnitt dem Modell vorgelegt wurde.