Token-Budgets gegen semantische Grenzen: Richtiges Aufteilen von RAG-Blöcken
Erfahren Sie, wie sich Tokenisierung und Chunking unterscheiden, wo jede Methode in einem RAG-Pipeline-Workflow eine Rolle spielt, und wie Dokumente nach Bedeutung aufgeteilt werden können, während die Größe mit dem echten Tokenisierer gemessen wird.
Quellen: Chunking vs Tokenization: The Two Splits Every AI Engineer Keeps Confusing
Ein RAG-Pipeline kann auf jeder sichtbaren Ebene gesund erscheinen – mit vernünftigen Embeddings, plausiblen Ähnlichkeitswerten und einem gut gefüllten Vektorlager – und dennoch Fragmente liefern, die mitten im Satz enden. In vielen solchen Fällen liegt das Problem im Einlesen-Code, wo eine Aufteilungsfunktion auf einer falschen Annahme darüber beruht, wie Text in Tokens umgewandelt wird. Diese Anleitung trennt diese beiden Konzepte klar voneinander, zeigt an, wo jedes in der Pipeline vorkommt, und erläutert einen Aufteilungsansatz, der zunächst die Bedeutung berücksichtigt und gleichzeitig eine genaue Kontrolle über die Token-Budgets gewährleistet.
Ingenieure sagen regelmäßig Ausdrücke wie „Teile es in 512 Token auf“ oder „tokenbasiertes Aufteilen“, wobei diese Formulierungen zwei unterschiedliche Operationen zu einer zusammenfassen. Sie lösen verschiedene Probleme, laufen in verschiedenen Phasen ab und brechen auf unterschiedliche Weise auf. Genau zu wissen, wo die eine aufhört und die andere beginnt, ermöglicht es einem, über ein Abrufsystem nachzudenken, anstatt Parameter ständig anzupassen, bis eine Bewertungsscore verbessert wird.
Zwei Operationen, zwei Verantwortliche
Die kürzesten nützlichen Definitionen lauten wie folgt:
- Tokenisierung wandelt Text in die diskreten ganzzahligen Einheiten um, die ein Modell tatsächlich verarbeitet. Sie gehört zum Modell.
- Aufteilen in Blöcke teilt Dokumente in selbstständige Teile auf, die eingebettet, indiziert und abgerufen werden können. Sie gehört zu Ihrem Pipeline sowie Ihren Daten.
Die Tokenisierung findet jedes Mal statt, wenn ein String ein Modell erreicht – egal ob man es beabsichtigt oder nicht. Das Chunking hingegen ist eine Entscheidung, die man im Voraus trifft, wie ein Korpus strukturiert werden soll, bevor das Modell es überhaupt sieht. Die erste Methode ist fest vorgegeben und meist unsichtbar; die zweite muss man über Wochen hinweg entwerfen, messen und verbessern.
Beide Prozesse wirken ständig miteinander zusammen. Ein Trenner, der das Verhalten des Tokenisierers ignoriert, erzeugt stillschweigend schlechte Chunks. Dennoch handelt es sich um getrennte Schritte, und ihr Als-Eins-und-das-Selbe-Betrachten ist die Ursache für eine überraschend große Anzahl feiner Fehler bei der Informationsabrufung.
Wie ein Modell Text liest
Kein Transformier-LLM, egal ob GPT-4o, Claude, Llama oder etwas anderes in der Produktion, liest Wörter. Es liest eine Sequenz von Ganzzahlen, wobei jede Zahl auf ein festes Wörterbuch verweist, das während des Trainings erlernt wurde. Der Tokenisierer ist der deterministische Algorithmus, der den Rohtext in diese Sequenz umwandelt. Er wird einmal ausgewählt, beim Trainieren des Modells, und als Teil der Modelldateien mitgeliefert. Man kann ihn nicht nach jedem Antrag austauschen.
Subwort-Wörterbücher: BPE, WordPiece, SentencePiece
Die meisten aktuellen Modelle verlassen sich auf die Subwort-Tokenisierung, typischerweise aus den Familien BPE, WordPiece oder SentencePiece. Das gemeinsame Prinzip besteht darin, mit Zeichen oder Rohbytes zu beginnen und die häufigsten benachbarten Paare wiederholt in neue Wörterbucheinträge zusammenzufügen, bis eine gewünschte Größe des Wörterbuchs erreicht ist – in der Regel zwischen 32k und 128k Einträgen oder mehr.
Was das im täglichen Einsatz bedeutet:
- Häufig vorkommende englische Wörter wie
the,isodermodelwerden in der Regel jeweils mit einem Token dargestellt. - Seltener vorkommende Wörter zerfallen in Teilwörter, sodass
TokenisierungzuTokenplusIsierungwerden kann. - Ungewöhnliche Zeichenfolgen, einschließlich Code-Identifikatoren, nicht-lateinischer Schriften, Emojis und erfundener Produktnamen, können zu vielen Tokens pro Wort führen und gelegentlich fast einem Token pro Zeichen.
Deshalb kann ein Satz in Hindi oder Tamil zwei bis drei Mal so viele Token kosten wie ein englischer Satz mit derselben Bedeutung, und deshalb kann ein Identifikator wie xgboost_lstm_ensemble_v2 etwa acht Token verbrauchen, obwohl ursprünglich nur zwei erwartet wurden. Die genauen Verhältnisse hängen vom Tokenisierer ab, und neuere Wörterbücher haben bei einigen Sprachen die Lücke verringert – messen Sie daher mit dem Modell, das Sie verwenden. Für jedes Produkt, das nicht-englischsprachige Märkte bedient, beispielsweise eine LLM-Schicht, die Unterlagen von indischen Börsen oder Handelsauftragsprotokolle zusammenfasst, handelt es sich dabei um eine echte Kosten- und Kontextbudgetierungssache und nicht um etwas Neues oder Kurioses.
Warum die Tokenanzahl über die Rechnung hinaus wichtig ist
Die Tokenanzahl bestimmt nicht nur den Preis einer Anfrage. Sie beeinflusst drei Aspekte:
- Wie viel vom Kontextfenster Sie nutzen. Ein 128k-Fenster wird in Tokens gemessen, nicht in Wörtern oder Zeichen. Ein Abschnitt mit 1.000 Wörtern hat keinen festen Kostenwert; die Anzahl der Tokens ändert sich je nach Vokabular, Sprache und Inhaltstyp, da Code, Prosa und Zahlen sehr unterschiedlich tokenisiert werden.
- Worauf sich die Aufmerksamkeit richtet. Transformer richten ihre Aufmerksamkeit auf verschiedene Tokenpositionen, wodurch die Art und Weise, wie ein Text in Tokens unterteilt wird, beeinflusst, wie kohärent das Modell die einzelnen Teile miteinander verknüpfen kann. Pathologische Aufteilungen, die das Verständnis beeinträchtigen, kommen selten vor, können aber durch bestimmte Kodierungen entstehen.
- Wo die Trunkierung stattfindet. Wenn der Eingabeteil das Fenster überschreitet, wird er auf Tokenebene abgeschnitten – nicht an der Grenze zwischen Sätzen oder Abschnitten – es sei denn, Sie regeln das selbst. Unaufmerksame Trunkierung kann dazu führen, dass ein Wort unvollständig bleibt.
Betrachten Sie Ihre eigenen Zeichenketten einmal
Falls Sie Ihre echten Produkttexte noch nie mit OpenAI’s tiktoken oder Anthropics Tokenisierer verarbeitet haben, lohnt es sich, dies einmal zu tun. Wenn man sein eigenes Prompt-Muster in unerwartete Fragmente zerlegt sieht, wird eine Abstraktion zu etwas Konkretem. Markdown-Syntax, JSON-Klammern und Code-Frames sind besonders problematisch: Sie werden weniger effizient tokenisiert als gewöhnlicher Prosa-Text.
Der untenstehende Auszug lädt die cl100k_base-Kodierung herunter und zählt die Tokens in einem typischen Shell-Befehl. Beachten Sie, wie Flags und Dateierweiterungen jeweils zwei bis drei Tokens beanspruchen – genau diese Art von Inhalt führt unbemerkt zu einer Erhöhung des Budgets.
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
tokens = enc.encode("pip-compile requirements.in --output-file requirements.txt")
print(len(tokens), tokens)
# CLI flags and file extensions often split into 2-3 tokens each —
# this is exactly the kind of string that silently inflates your token budget
Wie Sie entscheiden, was das Modell sehen darf
Das Chunking findet auf einer höheren Ebene statt, im Pipeline-System und nicht im Modell selbst. Es nimmt ein großes Dokument wie einen PDF, eine Confluence-Seite, ein Code-Repository oder einen 40-seitigen regulatorischen Bescheid und teilt es in kleinere Einheiten auf, die unabhängig eingebettet, indiziert und abgerufen werden können. Das Chunk ist die Grundlage der Abrufschicht.
Zwei Einschränkungen machen das Chunking notwendig – wobei keines davon wirklich mit der Tokenisierung zu tun hat:
- Lange Embeddings werden unklar. Wenn zu viele unzusammenhängende Ideen in einen einzigen Vektor gepackt werden, entsteht ein Durchschnitt aller davon, der nichts genau abbildet. Ein gutes Chunk enthält eine kohärente Idee, nicht ein ganzes Dokument.
Strategien zur Aufteilung in Blöcke und warum jede von ihnen versagt
Festgrößige Fenster
Der einfachste Ansatz besteht darin, den Text alle N Token oder Zeichen zu teilen, wobei in der Regel ein gewisser Überschneidungsbereich zwischen den benachbarten Blöcken besteht. Die meisten Teams beginnen hier, weil dafür nur wenige Zeilen Code benötigt werden. Die untenstehende Funktion kodiert den gesamten Text, teilt die Tokenliste in Fenster von chunk_size auf, decodiert jedes Fenster wieder in Text um und bewegt sich dabei um chunk_size - overlap vor, sodass aufeinanderfolgende Blöcke einen Überschneidungsbereich haben.
def fixed_chunk(text, chunk_size=512, overlap=50):
tokens = enc.encode(text)
chunks = []
start = 0
while start < len(tokens):
end = start + chunk_size
chunk_tokens = tokens[start:end]
chunks.append(enc.decode(chunk_tokens))
start += chunk_size - overlap
return chunks
Die Schwäche besteht darin, dass diese Funktion kein Konzept von Sätzen hat. Sie setzt eine Trennlinie mitten in einem Satzteil, wodurch eine Aussage von der sie stützenden Zahl oder ein Codebeispiel vom erklärenden Satz getrennt wird. Diese halben Satzteile werden in Vektoren eingebettet, die zwischen Themen liegen und nur unzureichend passen – genau das ist das zu Beginn beschriebene Symptom. Zwei weitere Einschränkungen: overlap muss unter chunk_size bleiben, sonst macht die Schleife keinen Fortschritt, und das Dekodieren eines beliebigen Token-Abschnitts kann einen Mehrbyte-Charakter in zwei Teile teilen, wodurch an der Kante eines Chunk ein Ersatzcharakter entsteht.
Rekursive Aufteilung an natürlichen Grenzen
Ein rekursiver Splitter bevorzugt Absatzzeilen, fällt bei Bedarf auf Satzzeilen zurück, danach auf Worttrennungen, und führt erst eine harte Trennung durch, wenn nichts anderes passt. LangChain’s RecursiveCharacterTextSplitter sowie die Knotenparser in LlamaIndex folgen diesem Muster. Für textreiche Korpora ist das eine weitaus bessere Standardlösung, da die eigene Struktur des Dokuments vor dem Einhaltungslimit berücksichtigt wird.
Semantisches Chunking
Anstatt eine feste Größenbeschränkung vorzugeben, embeddet das semantische Chunking einzelne Sätze oder kleine Gruppen davon und setzt eine Grenze dort, wo die Kosinusähnlichkeit zwischen benachbarten Embeddings abnimmt – also dort, wo das Thema wechselt. Die entstehenden Blöcke sind durch ihre Bedeutung miteinander verbunden und nicht durch ihre Länge. Der Nachteil ist eine zusätzliche Embedding-Berechnung für jedes Dokument während der Verarbeitung.
strukturbezogenes Chunking
Inhalt mit eingebauter Struktur, wie Markdown mit Überschriften, Quellcode-Dateien, API-Spezifikationen oder Schema-Dokumentation, sollte entsprechend dieser Struktur aufgeteilt werden. Im Code stellt die Definition einer Funktion eine offensichtliche Trennlinie dar. In einer Markdown-Wissensdatenbank ist eine ##-Seite eine solche Trennlinie. Bei Dokumenten wie dem README eines Mehr-Agentensystems führt eine Aufteilung nach Abschnittsüberschriften zu viel verständlicheren Ergebnissen als jede Berechnung auf der Grundlage der Anzahl der Tokens. Eine ausführlichere Behandlung zur Zusammenhaltung von Unterstützungsbelegen findet sich unter Chunking that preserves evidence.
LLM-gestütztes Chunking
Am teuren Ende kann man eine LLM bitten, das Dokument zu lesen und selbst Grenzen vorzuschlagen, wobei das Ziel darin besteht, Abschnitte zu erstellen, die jeweils für sich stehen und eine plausible Frage beantworten. Dies ist langsam und kostspielig, lohnt sich aber bei hochwertigen Korpora wie regulatorischen Unterlagen oder Verträgen, wo ein Fehlversuch bei der Suche ernsthafte Konsequenzen hat.
Wo sich die beiden Ansätze treffen
Der interessante Punkt ist der Ort, an dem diese Konzepte aufeinandertreffen – und er lässt sich in einem Satz ausdrücken: Die Größe der Blöcke wird normalerweise in Tokens angegeben, doch die Grenzen der Blöcke sollten der Bedeutung folgen, wobei diese beiden Ziele in entgegengesetzte Richtungen drängen.
Das strenge Abschneiden genau bei der Marke von 512 Token mit chunk_size=512 erfüllt das Budget, ignoriert aber die Bedeutung. Das Aufteilen nur an Absatzzeichen ohne Obergrenze respektiert die Bedeutung, vernachlässigt jedoch, dass ein Absatz 40 Token und der nächste 4.000 haben kann. Der größere Abschnitt kann die maximale Sequenzlänge des Embedding-Modells überschreiten, wodurch der abgerufte Kontext in Bezug auf die Menge an Informationen pro Teil extrem ungleichmäßig ist.
Ein solider Splitter berücksichtigt daher beide Aspekte in einer festen Reihenfolge:
- Zuerst die semantischen Grenzen ermitteln: Abschnitte, Absätze, Sätze, Codeblöcke.
- Kleine benachbarte Einheiten zusammenfassen, solange die Hinzufügung einer weiteren nicht das Token-Budget überschreitet.
Die untenstehende Funktion implementiert diese Vorgehensweise. Sie erwartet sections, die bereits nach Absätzen oder Überschriften aufgeteilt wurden. Für jede dieser Abschnitte wird die tatsächliche Länge des Tokens gemessen; falls deren Hinzufügen das aktuelle Blöckchen überlaufen würde, werden die angesammelten Abschnitte als ein Blöckchen ausgeliefert. Ein Abschnitt, der allein bereits max_tokens überschreitet, wird an den Tokengrenzen geteilt; alles Weitere wird dem laufenden Blöckchen hinzugefügt. Was am Ende übrig bleibt, bildet den endgültigen Blöckchen.
def chunk_by_tokens_with_semantic_bias(sections, max_tokens=512):
chunks, current, current_len = [], [], 0
for section in sections: # pre-split by paragraph/header
section_len = len(enc.encode(section))
if current_len + section_len > max_tokens and current:
chunks.append("\n".join(current))
current, current_len = [], 0
if section_len > max_tokens:
# single section too big — hard split on token boundaries, not chars
section_tokens = enc.encode(section)
for i in range(0, len(section_tokens), max_tokens):
chunks.append(enc.decode(section_tokens[i:i+max_tokens]))
else:
current.append(section)
current_len += section_len
if current:
chunks.append("\n".join(current))
return chunks
Die entscheidende Gestaltungsentscheidung besteht darin, dass die Größe mit dem Tokenizer des Ziel-Embedding-Modells gemessen wird, anstatt aus Zeichen geschätzt zu werden, und dass eine erzwungene Aufteilung nur an einer von dem Code kontrollierten Token-Grenze erfolgen kann. Eine Verbesserung, die in Betracht gezogen werden sollte: Die Zeilenumbrüche, die bei "\n".join verwendet werden, fügen pro Abschnitt etwa ein Token hinzu, das in der laufenden Zählung nicht berücksichtigt wird; daher sollte unterhalb der festgelegten Obergrenze des Modells etwas Puffer gelassen werden.
Welche Rolle nimmt jeder Schritt im Pipeline ein?
Räumlich gesehen ist das Chunking eine Vorverarbeitungsproblematik. Es findet während der Eingabe statt, noch bevor ein Modell involviert wird, und das Ergebnis wird gespeichert. Die Tokenisierung hingegen ist eine Nachverarbeitungsproblematik. Sie erfolgt in der Eingabeschicht des Modells bei jeder einzelnen Anfrage, einschließlich der Embedding-Aufrufe während der Eingabe und der Generierungsaufrufe zum Zeitpunkt der Abfrage. Wenn man sich dieses Bild vor Augen hält, weiß man, welchen Code man öffnen muss, wenn etwas schiefgeht.
Die beiden Fehlerarten voneinander unterscheiden
Wenn man ein schlechtes Suchergebnis prüft, ähneln sich zwei Symptome, deuten aber auf unterschiedliche Schichten hin:
- Der abgerufte Chunk ist unvollständig oder endet mitten im Satz. Dies ist ein Chunking-Fehler. Die Grenzlogik hat eine semantische Einheit geteilt, und die Lösung liegt im Trennmechanismus, nicht bei der Wahl des Embedding-Modells.
len(text) / 4 geschätzt wurden, während die tatsächliche Anzahl höher ausfällt.Sowohl das eine als auch das andere wird häufig dem „schlechten Embedding-Modell“ zugeschrieben. Das ist selten die eigentliche Ursache; in den meisten Fällen liegt das Problem bei der Grenzlogik oder der Tokenzählung.
Produktions-Checkliste
Beim Aufbau oder Überprüfen eines RAG- oder Agent-Pipelines sollten Sie folgende Punkte berücksichtigen:
- Zählen Sie die Tokens mit dem tatsächlichen Tokenisierer des von Ihnen genutzten Modells. Verwenden Sie
tiktokenfür OpenAI-Modelle sowie den modellspezifischen Tokenisierer für open-weight-Modelle. Verwenden Sie niemalslen(text) // 4als Zähler in der Produktion. - Messen Sie die Größe der Blöcke in den Tokens des Embedding-Modells. Embedding-Modelle verwenden oft einen anderen Tokenisierer als das Generierungsmodell und setzen eine strengere maximale Sequenzlänge fest.
- Berechnen Sie die Überlappung explizit. Die Überlappung verbraucht einen Teil des effektiven Fensters, weshalb sie in die Berechnung gegenüber der Eingabegrenze des Embedding-Modells einbezogen werden sollte, anstatt erst durch eine Trunkierungswarnung darauf aufmerksam zu werden.
- Setzen Sie die Struktur der Größe vor. Wählen Sie standardmäßig eine strukturbezogene oder rekursive Aufteilung und reservieren Sie Fenster mit fester Größe für wirklich unstrukturierten Text.
- Protokollieren Sie die Tokenzahlen sowie Entscheidungen zu Grenzen während der Eingabe. Wenn die Qualität nachlässt, möchten Sie sofort erkennen können, ob ein Block aufgrund der Größe oder der Struktur gekürzt wurde.
- Erneut tokenisieren nach dem Wechsel der Embedding-Modelle. Eine Strategie, die für ein bestimmtes Vokabular optimiert wurde, überträgt sich nicht problemlos; 512 Token umfassen unter einem anderen Tokenisierer eine andere Textmenge.
Zusammenfassung
Die Tokenisierung beantwortet die Frage des Modells, wie ein String überhaupt gelesen werden soll. Das Chunking beantwortet Ihre Frage danach, welche kleinste, selbstständige Einheit es wert ist, abgerufen zu werden. Die erste wird bereits im Moment des Modellauswahls festgelegt; die zweite ist eine Gestaltungsgröße, die vollständig in Ihrem Kontrolle liegt, und dort liegt in der Regel der eigentliche ingenieurtechnische Hebelwert in einem RAG-System.
Die beiden Arten von Fehlern führen auch zu unterschiedlichen Problemen. Fehler bei der Token-Zählung zeigen sich als ungewöhnliche Abbrüche und überschrittene Budgets, die zumindest sichtbar sind. Fehler beim Aufteilen in Blöcke führen dazu, dass das System ohne einzigen Fehler läuft, aber dennoch die falsche Hälfte des richtigen Satzes ausgibt – was weitaus schwieriger zu erkennen ist. Messen Sie mit dem echten Tokenisator, teilen Sie zunächst nach Bedeutung auf und dokumentieren Sie, warum jede Trennlinie genau dort platziert wurde.