Dokumente in Chunks zerlegen
Drei Zerlegungsmethoden an der httpx-Dokumentation praktisch vergleichen – feste Länge, nach Überschriften, nach Überschriften mit Längenbegrenzung. Unterwegs tappen wir in eine echte Falle: Kommentare in Codeblöcken werden für Überschriften gehalten.
- Etwa 35 Minuten
- Niveau: Fortgeschritten
- Getestet: 2026-09-14, reines Python, keine API-Aufrufe
Code und Programmausgaben stehen genau so da, wie sie gelaufen sind – Kommentare und Ausgaben sind daher auf Chinesisch.
RAG sucht nicht nach ganzen Dokumenten, sondern nach den kleinen Stücken (Chunks), in die ein Dokument zerlegt wird. Eine Seite über Timeouts hat vielleicht mehrere Abschnitte; fragt ein Nutzer „wie schalte ich Timeouts ab“, willst du nur den kurzen Abschnitt zum Abschalten zurückholen, nicht die ganze Seite.
Wie man zerlegt, wirkt wie ein kleines technisches Detail, beeinflusst die Wirkung von RAG aber stark. Sind die Chunks zu groß, wird der relevante Inhalt bei der Suche von viel Irrelevantem verwässert; sind sie zu klein, wird ein zusammenhängender Gedanke zerrissen, und man bekommt nur einen halben Satz zurück. Diese Lektion vergleicht einige Zerlegungsarten an der echten httpx-Dokumentation.
Methode eins: feste Länge
Die einfachste Art: ohne Rücksicht auf den Inhalt alle 800 Zeichen einen Schnitt. Damit nicht genau ein Satz halbiert wird, überlappen benachbarte Chunks um 100 Zeichen.
def split_fixed(text, size=800, overlap=100):
"""办法一:每 size 个字符切一刀,相邻两块重叠 overlap 个字符。"""
chunks, start = [], 0
while start < len(text):
chunks.append(text[start:start + size])
start += size - overlap
return chunks
Schauen wir, was aus timeouts.md, der Seite über Timeouts, wird. Das ist Chunk 2:
le.com/api/v1/example", timeout=None)
```
## Setting a default timeout on a client
You can set a timeout on a client instance, which results in the given
`timeout` being used as the default for requests made with this client:
```python
client = httpx.Client() # Use a default 5s timeout everywhere.
client = httpx.Client(timeout=10.0) # Use a default 10s timeout everywhere.
client = httpx.Client(timeout=None) # Disable all timeouts by default.
```
## Fine tuning the configuration
HTTPX also allows you to specify the timeout behavior in more fine grained detail.
There are four different types of timeouts that may occur. These are **connect**,
**read**, **write**, and **pool** timeouts.
* The **connect** timeout specifies the maximum amount of time to wait until
a socket
Er beginnt mit einer abgeschnittenen URL le.com/api/v1/example" und endet mitten im Satz bei „until a socket“. Dazwischen spannt er über zwei Unterabschnitte, halb über das Standard-Timeout eines Clients, halb über die vier Timeout-Arten. Die Bedeutung dieses Chunks ist gemischt: Fragt ein Nutzer „welche vier Timeouts gibt es“, enthält er nur die halbe Antwort; fragt er „wie setze ich ein Standard-Timeout für einen Client“, bringt er einen Haufen Irrelevantes mit.
Feste Länge ist einfach, funktioniert mit jedem Text und liefert gleich lange Chunks. Die Struktur des Dokuments ignoriert sie aber völlig.
Methode zwei: nach Überschriften
Die httpx-Dokumentation ist Markdown und ohnehin durch Überschriften ## und ### in Abschnitte gegliedert. Jeder Abschnitt behandelt eine Sache, eine natürliche Zerlegungseinheit. Also zerlegen wir nach Überschriften: Bei jeder Überschrift beginnt ein neuer Chunk.
Am direktesten geht es mit einem regulären Ausdruck, der vor jeder Zeile schneidet, die mit # beginnt:
def split_by_heading_naive(text):
"""办法二(有问题的版本):凡是以 # 开头的行都当成标题切开。"""
parts = re.split(r"(?m)^(?=#{1,3} )", text)
return [p.strip() for p in parts if p.strip()]
Schauen wir, in welche Chunks timeouts.md zerfällt, jeweils nur die erste Zeile:
有问题的按标题切法,timeouts.md 的各块开头:
[ 153 字符] HTTPX is careful to enforce timeouts everywhere by default.
[ 93 字符] ## Setting and disabling timeouts
[ 87 字符] # Using the top-level API:
[ 186 字符] # Using a client instance:
[ 87 字符] # Using the top-level API:
[ 127 字符] # Using a client instance:
[ 424 字符] ## Setting a default timeout on a client
[1386 字符] ## Fine tuning the configuration
[ 207 字符] # A client with a 60s timeout for connecting, and a 10s timeout elsewhere.
# Using the top-level API: ist keine Überschrift, sondern ein Python-Kommentar in einem Codeblock. Python-Kommentare sehen genauso aus wie eine Markdown-Überschrift erster Ordnung: # plus Leerzeichen. Also wird der Codeblock mitten durchgeschnitten, vom Abschnitt „Setting and disabling timeouts“ bleiben nur 93 Zeichen Erklärtext, und alle Codebeispiele landen in anderen Chunks.
Solche Fehler sind gut versteckt. Das Programm meldet nichts, auch die Zahl der Chunks wirkt vernünftig; man bemerkt es nur, wenn man Chunk für Chunk anschaut. Über die ganze Dokumentation erzeugte die fehlerhafte Version 236 Chunks, 60 mehr als die korrekte, und alle zusätzlichen sind solche zerstückelten Codeteile.
Die Korrektur: sich merken, ob man gerade in einem Codeblock ist (bei jedem ``` umschalten), und ein # im Codeblock grundsätzlich nicht als Überschrift zählen:
def split_by_heading(text):
"""办法二(修正版):同样按标题切,但跳过代码块里以 # 开头的注释行。"""
chunks, current, in_code = [], [], False
for line in text.splitlines():
if line.startswith("```"):
in_code = not in_code
if not in_code and re.match(r"#{1,3} ", line) and current:
chunks.append("\n".join(current).strip())
current = []
current.append(line)
if current:
chunks.append("\n".join(current).strip())
return [c for c in chunks if c]
Nach der Korrektur:
修正后的按标题切法,timeouts.md 的各块开头:
[ 153 字符] HTTPX is careful to enforce timeouts everywhere by default.
[ 586 字符] ## Setting and disabling timeouts
[ 424 字符] ## Setting a default timeout on a client
[1594 字符] ## Fine tuning the configuration
Vier Chunks, jeder ein vollständiger Abschnitt, Erklärtext und Codebeispiele beisammen.
Die Lehre aus dieser Falle: Nach dem Zerlegen immer ein paar Chunks ausgeben und anschauen. Jedes Dokumentformat hat seine eigenen Fallen: HTML hat Navigationsleisten und Fußzeilen, PDF hat Kopfzeilen, Seitenzahlen und zerschnittene Tabellen, Code hat Funktionsgrenzen.
Methode drei: nach Überschriften, mit Längenbegrenzung
Auch das Zerlegen nach Überschriften hat ein Problem: Manche Abschnitte sind sehr lang. Über die ganze httpx-Dokumentation hat der längste Chunk nach Überschriften 5530 Zeichen. Ist ein Chunk zu lang, wird sein Inhalt uneinheitlich und die Suche schlechter; und wie Modul 01, Lektion 5 gezeigt hat, lesen Embedding-Modelle wie bge-small-zh-v1.5 höchstens 512 Tokens und verwerfen den Rest.
Also noch ein Schritt: Chunks über der Grenze werden an Leerzeilen (also Absätzen) weiter zerlegt, und jedem Teilstück wird die Überschrift vorangestellt, zu der es gehört; so weiß jedes Stück, worum es geht.
def split_by_heading_capped(text, max_size=1500):
"""办法三:先按标题切;太长的块再按空行(段落)切开,并在每一小块前面补上所属的标题。"""
chunks = []
for section in split_by_heading(text):
if len(section) <= max_size:
chunks.append(section)
continue
title = section.splitlines()[0] if section.startswith("#") else ""
current = ""
for para in section.split("\n\n"):
if current and len(current) + len(para) > max_size:
chunks.append(current.strip())
current = title + "\n\n" if title else ""
current += para + "\n\n"
if current.strip():
chunks.append(current.strip())
return chunks
Vergleich
Statistik der vier Zerlegungsarten über die gesamte httpx-Dokumentation (vollständiger Code in code/04-rag/chunking.py; ohne API-Aufrufe, du solltest exakt dieselben Zahlen bekommen):
固定长度:179 块,平均 739 字符,最短 12,最长 800
按标题(有问题):236 块,平均 493 字符,最短 8,最长 5530
按标题(修正):176 块,平均 662 字符,最短 8,最长 5530
按标题+限长:196 块,平均 596 字符,最短 8,最长 2193
Mit Längenbegrenzung sinkt der längste Chunk von 5530 auf 2193. Das liegt noch über der Grenze von 1500, weil dieser Chunk einen langen Codeblock ohne Leerzeilen enthält; mein Code schneidet nur an Leerzeilen und zerschneidet keine Codeblöcke. Das ist eine bewusste Abwägung: lieber ein etwas längerer Chunk als halbierter Code.
Der kürzeste Chunk hat nur 8 Zeichen: Abschnitte, die nur aus einer Überschrift ohne Inhalt bestehen. Für die Suche sind sie fast wertlos; in echten Projekten kann man sie mit dem nächsten Chunk zusammenlegen oder einfach verwerfen.
Die folgenden Lektionen verwenden alle die Art „nach Überschriften + Längenbegrenzung“.
Wie groß ein Chunk sein sollte
Eine Standardantwort gibt es nicht, aber ein paar Anhaltspunkte:
- Nicht über der Grenze des Embedding-Modells. bge-small-zh und multilingual-e5-small haben beide 512 Tokens. Im Englischen entspricht ein Token etwa 4 Zeichen; 1500 Zeichen sind rund 400 Tokens, innerhalb der Grenze.
- Ein Chunk behandelt am besten eine Sache. Nach der Struktur des Dokuments zu zerlegen, erreicht das leichter als nach Länge.
- Bedenken, wie das Gefundene genutzt wird. Legt man jeweils 5 Chunks à 600 Zeichen in den Prompt, sind das 3000 Zeichen, etwa 750 Tokens, nicht viel. Bei 5000 Zeichen pro Chunk wären es über zehntausend Tokens.
Die endgültige Chunkgröße entscheidet die Evaluation aus Lektion 6: mehrere Größen durch das Evaluationsset laufen lassen und schauen, welche am besten sucht.
Braucht man Überlappung?
Bei fester Länge verhindert Überlappung, dass ein halbierter Satz auf beiden Seiten unvollständig bleibt. Beim Zerlegen nach Struktur fallen die Grenzen ohnehin auf Absätze oder Überschriften, meist braucht es keine Überlappung.
Überlappung hat auch einen Preis: Derselbe Inhalt steht in zwei Chunks, beide werden bei der Suche vielleicht gemeinsam gefunden und belegen wertvolle Plätze.
Übungen
- Setz
sizevonsplit_fixedauf 300 und 2000 und schau, wietimeouts.mdzerlegt wird. Was hältst du für passender? - Ändere
split_by_heading_cappedso, dass Chunks, die nur eine Überschrift ohne Inhalt haben (etwa kürzer als 50 Zeichen), mit dem folgenden Chunk zusammengelegt werden. - Nimm ein eigenes Dokument (die README eines Projekts, einen Wiki-Export deiner Firma, den Text eines PDFs), zerlege es mit allen drei Methoden und schau Chunk für Chunk, ob etwas kaputt geschnitten wurde.
Selbsttest
1. Was ist das größte Problem beim Zerlegen nach fester Länge?
Es ignoriert die Struktur des Dokuments, schneidet oft mitten in Sätzen, Code oder URLs, und ein Chunk kann zwei unzusammenhängende Unterabschnitte mischen. Solche Chunks sind inhaltlich unvollständig und uneinheitlich, was Suche und Antworten verschlechtert.
2. Warum muss man beim Zerlegen nach Markdown-Überschriften Codeblöcke besonders behandeln?
Python-Kommentare in Codeblöcken beginnen mit # plus Leerzeichen, genau wie eine Markdown-Überschrift. Ohne besondere Behandlung werden Kommentare für Überschriften gehalten, Codeblöcke mitten durchgeschnitten und Erklärtext und Codebeispiele in verschiedene Chunks verteilt.
3. Warum stellt man beim Begrenzen der Chunklänge jedem Teilstück die zugehörige Überschrift voran?
Wird ein langer Abschnitt in mehrere Stücke zerlegt, enthalten die hinteren vielleicht nur Fließtext, ohne dass erkennbar ist, worum es geht. Mit der Überschrift trägt jedes Stück sein Thema, wird bei der Suche leichter richtig zugeordnet, und im Prompt kennt das Modell den Kontext.
Fragen und Diskussion
Hängst du in dieser Lektion fest? Frag hier. Und wenn du die Frage von jemandem beantworten kannst, tu es gern.
Eine Frage bringt 3 Punkte, eine Antwort 6. Beiträge erscheinen nach der Prüfung.
Diskussion wird geladen…