Chunking: Intelligente Aufteilung Ihrer Dokumente
Auf dieser Seite
Lernziele
- Verstehen Sie, warum Chunking für die Qualität von RAG entscheidend ist
- Beherrschen Sie verschiedene Aufteilungsstrategien
- Wählen Sie die richtige Chunk-Größe und den richtigen Overlap
Warum Dokumente aufteilen?
Embedding-Modelle verfügen über ein Token-Limit: 8.191 für OpenAI. Dieses Limit ist der erste Grund, Dokumente aufzuteilen, aber es ist nicht der wichtigste. Selbst ein Dokument, das weit unter diesem Limit liegt, liefert schlechte Ergebnisse, wenn es als einzelner Block kodiert wird, da der resultierende Vektor alle Inhalte durchmittelt und Details verliert. Nehmen Sie beispielsweise eine 40-seitige Personalhandlung, die Urlaubsregelungen, Remote-Arbeit, Aufwendungsberichte und Krankenversicherung abdeckt: Ihre einzelne Embedding-Repräsentation ähnelt keiner dieser vier Themen wirklich, und eine Suche nach „wie viele Urlaubstage” wird sie unter einem deutlich kürzeren, aber viel gezielteren Dokument rangieren.
Chunking löst dieses Problem, indem es das Dokument in kohärente Teile zerlegt, wobei jeder Teil eine spezifische Idee oder einen Absatz erfasst. Dies ist der Schritt, an dem eine RAG-Pipeline die meisten Qualitätsgewinne oder -verluste erleidet – noch bevor Sie ein Embedding-Modell auswählen.
Strategie 1: Chunking mit fester Größe
Die einfachste Methode zählt Wörter und schneidet in regelmäßigen Abständen. Sie ignoriert die Textstruktur vollständig, was sie brutal macht, benötigt aber nur drei Zeilen Code und dient als wesentliche Basislinie: Jede ausgefeiltere Strategie muss beweisen, dass sie eine bessere Leistung erbringt als diese.
def chunker_fixed_size(
text: str,
size: int = 500,
overlap: int = 100
) -> list[str]:
"""Teilt Text in Chunks fester Größe mit Overlap auf."""
words = text.split()
chunks = []
for i in range(0, len(words), size - overlap):
chunk = " ".join(words[i:i + size])
if chunk.strip():
chunks.append(chunk)
return chunks
long_text = "A very long document... " * 500
chunks = chunker_fixed_size(long_text, size=300, overlap=50)
print(f"{len(chunks)} chunks generated")
Der Parameter overlap verdient besondere Aufmerksamkeit. Ohne Overlap landet ein Schnitt irgendwann mitten in einer Erklärung: Der Satz „die Kündigungsfrist beträgt drei Monate” landet in einem Chunk, und „außer während der Probezeit” im nächsten. Weder beantwortet die Frage korrekt, noch ist das zweite sogar irreführend. Durch einen Overlap von 10–20 % der Chunk-Größe garantieren Sie, dass jede Idee mindestens einmal intakt im Index vorhanden ist. Dies ist ein guter Ausgangspunkt; passen Sie ihn später basierend auf der Dichte Ihrer Dokumente an.
Strategie 2: Chunking basierend auf Trennzeichen
Statt Wort für Wort zu schneiden, können Sie die natürlichen Grenzen des Textes respektieren. Die Idee besteht darin, zuerst den stärksten Trennzeichen zu versuchen – den doppelten Zeilenumbruch, der zwei Absätze trennt – und dann zu immer schwächeren Trennzeichen überzugehen, solange das resultierende Stück die maximale Größe überschreitet. Ein Blogbeitrag wird somit Absatz für Absatz aufgeteilt, und nur ein monströser Absatz wird in Sätze zerlegt.
import re
def chunker_separators(
text: str,
separators: list[str] = ["\n\n", "\n", ". ", " "],
max_size: int = 1000
) -> list[str]:
"""Teilt Text unter Beachtung natürlicher Trennzeichen auf."""
chunks = []
current_chunk = ""
# Split by the strongest separator first
segments = [text]
for sep in separators:
new_segments = []
for segment in segments:
parts = segment.split(sep)
for part in parts:
if len(part.split()) > max_size:
new_segments.append(part)
else:
if (current_chunk and
len((current_chunk + sep + part).split()) > max_size):
chunks.append(current_chunk.strip())
current_chunk = part
else:
current_chunk = (
current_chunk + sep + part
if current_chunk else part
)
segments = new_segments
if current_chunk.strip():
chunks.append(current_chunk.strip())
return chunks
Strategie 3: Chunking nach Markdown-Sektionen
Wenn Ihr Korpus aus strukturierten Dokumenten besteht, übernehmen Überschriften bereits die Arbeit für Sie: Der Autor hat bereits die Bedeutungseinheiten abgegrenzt. Das Aufteilen auf den Überschriftenebenen # und ## erzeugt Chunks, die genau den Sektionen entsprechen, und die folgende Funktion behält die Überschrift zusammen mit dem Text bei, den wir unmittelbar danach zur Anreicherung verwenden werden.
def chunker_markdown(text: str, max_level: int = 2) -> list[dict]:
"""Teilt ein Markdown-Dokument nach Sektionen auf."""
chunks = []
current_section = {"title": "", "content": "", "level": 0}
for line in text.split("\n"):
# Detect headings
match = re.match(r"^(#{1,6})\s+(.+)$", line)
if match:
level = len(match.group(1))
title = match.group(2)
if level <= max_level and current_section["content"].strip():
chunks.append({
"title": current_section["title"],
"text": current_section["content"].strip(),
"level": current_section["level"]
})
if level <= max_level:
current_section = {
"title": title,
"content": "",
"level": level
}
else:
current_section["content"] += line + "\n"
else:
current_section["content"] += line + "\n"
if current_section["content"].strip():
chunks.append({
"title": current_section["title"],
"text": current_section["content"].strip(),
"level": current_section["level"]
})
return chunks
Semantisches Chunking
Bei Text ohne explizite Struktur – wie eine Besprechungsübersicht oder ein Telefonat-Transkript – wissen weder die Größe noch die Trennzeichen, wo sich das Thema ändert. Embeddings tun dies. Sie kodieren jeden Satz, vergleichen jeden Satz mit dem vorherigen und schneiden dort, wo die Ähnlichkeit zusammenbricht: Genau dort hat der Sprecher vom Budget auf die Personalbeschaffung gewechselt. Beachten Sie, dass text-embedding-3-small hier ausreicht, da Sie keine absolute Präzision, sondern einen relativen Bruch suchen.
from openai import OpenAI
import numpy as np
client = OpenAI()
def chunker_semantic(
text: str,
similarity_threshold: float = 0.75,
min_sentence_length: int = 20
) -> list[str]:
"""Gruppiert Sätze nach semantischer Ähnlichkeit."""
# Split into sentences
sentences = re.split(r"(?<=[.!?])\s+", text)
sentences = [s for s in sentences if len(s) > min_sentence_length]
if len(sentences) <= 1:
return [text]
# Embeddings for each sentence
resp = client.embeddings.create(
input=sentences,
model="text-embedding-3-small" # small is sufficient here
)
embs = np.array([d.embedding for d in sorted(resp.data, key=lambda x: x.index)])
# Find break points
chunks = []
chunk_sentences = [sentences[0]]
for i in range(1, len(sentences)):
sim = float(np.dot(embs[i], embs[i - 1]))
if sim < similarity_threshold:
# Semantic break detected
chunks.append(" ".join(chunk_sentences))
chunk_sentences = [sentences[i]]
else:
chunk_sentences.append(sentences[i])
if chunk_sentences:
chunks.append(" ".join(chunk_sentences))
return chunks
Anreicherung von Chunks mit Kontext
Ein isolierter Chunk verliert oft die Information, die ihn interpretierbar macht. „Die Laufzeit beträgt drei Jahre” bedeutet nichts, wenn der Leser oder das Embedding-Modell nicht weiß, dass es sich um den Abschnitt zur Herstellergarantie handelt. Das Präfixieren jedes Chunks mit dem Dokumenttitel und dem Sektionstitel stellt diesen Kontext direkt in den Vektor selbst wieder her, und eine Suche nach „Garantie-Laufzeit” findet dann den entsprechenden Abschnitt.
def enrich_chunk(chunk: str, doc_title: str, section: str = "") -> str:
"""Fügt Kontext zum Chunk hinzu für eine bessere Einbettung."""
prefix = f"Dokument: {doc_title}"
if section:
prefix += f" | Sektion: {section}"
return f"{prefix}\n\n{chunk}"
Welche Chunk-Größe sollten Sie wählen?
Die Wahl beinhaltet einen Kompromiss zwischen Präzision und Kontext: Ein kurzer Chunk zielt fein, wird aber amputiert, während ein langer Chunk alles versteht, aber das Signal verwässert.
| Größe (Tokens) | Vorteile | Nachteile |
|---|---|---|
| 100–200 | Sehr präzise, gute Granularität | Verliert Kontext |
| 300–500 | Guter Kompromiss | Standard |
| 500–1000 | Reichhaltiger Kontext | Weniger präzise, teurer |
| 1000+ | Maximaler Kontext | Verwässert Informationen |
Es gibt keinen universellen Wert: Die ideale Größe hängt von Ihren Dokumenten und Abfragen ab. Die einzige zuverlässige Methode besteht darin, dasselbe Korpus mit zwei oder drei Größen zu indizieren und dann die Metriken aus Lektion 10 an einer Reihe von echten Fragen zu messen. Eine Wissensdatenbank für Support, die aus kurzen Antworten besteht, toleriert 200 Tokens; ein juristisches Korpus wird 800 erfordern.
Zusammenfassung
- Chunking ist der einflussreichste Schritt in der RAG-Pipeline
- Verwenden Sie feste Größe als Basislinie und Trennzeichen, um die Struktur zu respektieren
- Semantisches Chunking erkennt Themenwechsel automatisch
- Overlap und kontextuelle Anreicherung verbessern die Qualität
- Testen Sie verschiedene Größen und messen Sie mit Suchmetriken