Chunking strategies for RAG: how to split documents into chunks for embedding and retrieval. Demonstrates three strategies side-by-side — fixed-size with overlap (cheap but loses coreferences at chunk boundary), recursive markdown-aware (respects document structure, keeps headers in metadata), and late chunking (Jina 2024 — embed whole doc with long-context model first, then split the token embeddings, preserving global context for pronouns and cross-references). ADR — Choose chunker by content type: - Markdown / HTML / docs: recursive structure-aware splitter (RecursiveCharacterTextSplitter / MarkdownHeaderTextSplitter). Header path -> metadata for filtering and citations. - Code: AST-based (tree-sitter) — split on functions/classes, never mid-symbol; embed signature + docstring + body together. - PDF / DOCX / slides: layout-aware (Unstructured.io, Marker) — preserve tables and section breaks. - Long narrative with many coreferences: late chunking — only viable if you have a long-context embedding model (Jina, Voyage); +20% recall on anaphora. - Default for unknown / mixed text: recursive 500/50 with sentence-aware fallback. - Avoid: one-size-fits-all 500-token splitter on heterogeneous corpus, naive split with zero overlap, chunk_size > embedding model max (silent truncation). - Always: enrich every chunk with metadata payload (source_url, section_path, doc_type, date, permissions) for pre-ANN filtering and citations. Trade-off summary: too small loses surrounding context (LLM can't answer); too big dilutes the embedding signal (ANN recall drops). Sweet spot for general RAG is 300-500 tokens with 10-20% overlap, plus parent-child for technical docs that need both precise retrieval and wide context.
Документ длиннее, чем влезает в один embedding-вызов (типичный лимит — 8K токенов, у длинно-контекстных моделей 32K). RAG не может «съесть» книгу целиком: её надо порезать на куски, посчитать вектор для каждого, сложить в vector DB и доставать top-K на запрос. Звучит как мелочь — на деле chunker определяет потолок качества всей системы. Если порезал плохо, никакой ребранёр и никакая GPT-7 не вытащат: ответ просто не лежит ни в одном из top-K.
Эмпирика: при переходе от naive 500-токенного сплиттера к структурно-осознанному chunker'у recall@5 поднимается на 10-25%, а с late chunking — ещё на 15-20% сверху для текстов с местоимениями и cross-references. Это бесплатный апгрейд качества RAG без смены модели.
Chunking — это «унижение» хорошего текста: ты режешь связное на куски, чтобы embedding-модель смогла их съесть. Цель — порезать так, чтобы каждый кусок отвечал на один атомарный вопрос и помнил, откуда он.
Два направления оптимизации: семантическая связность куска (не резать посреди мысли) и сохранение глобального контекста (чтобы «он сказал...» не потеряло «Джон»).
Три параллельных chunker'а делят один и тот же поток предложений, а потом сходятся к общей embedding/storage/query-инфраструктуре:
docs -> classifier -> sentence splitter. Классификатор определяет формат (md/code/pdf) и направляет в подходящий chunker.fixed (наивный 500/50), recursive (markdown-aware с separator-fallback), late (Jina-style: сначала эмбеддинг, потом разрез вектора).embedder -> vector DB (Qdrant/pgvector). Late chunking подключается к embedder'у напрямую (он эмбеддит документ целиком), остальные идут через metadata-обогащение.user -> query embedder -> ANN -> LLM -> answer. Эта часть одинакова для всех стратегий — отличается только что лежит в индексе.Самый базовый сплиттер: каждые 500 токенов, с overlap 50. CPU only, индексируется за минуты на любом железе.
На диаграмме видно поломку: документ «...John was angry. He said the API failed...» разрезан так, что Chunk-1 кончается на «John was angry.», а Chunk-2 начинается с «He said...». На запрос «Who said the API failed?» ANN находит Chunk-2 — но в нём нет «John», только «He». LLM честно отвечает «I do not have enough context». Это не баг модели — это баг chunker'а.
Когда подходит: FAQ, snippet-search, прототипы, тексты без местоимений и без жёсткой структуры.
Пробует separators по порядку: \n\n → \n → ". " → " ". Никогда не режет посреди слова, старается не резать абзацы. Главное — сохраняет заголовки в metadata (section_path: ["API Auth", "OAuth"]).
В сценарии видно, как чанк помечается полным путём по иерархии разделов. На запросе «How does OAuth PKCE work?» ANN возвращает чанк с целым кодовым блоком внутри + LLM может процитировать раздел документа. Filter в Qdrant: WHERE section_path[0] = 'API Auth' — отсекает половину индекса до ANN.
Когда подходит: документация, base case для большинства general RAG, любые тексты с заголовочной структурой (Markdown, HTML, Wiki).
Идея Jina (2024): обычно мы сначала режем, потом эмбеддим — каждый чанк видит только себя. Late chunking делает наоборот: эмбеддит весь документ через long-context модель, получает token-level embeddings, и только потом пулит их по предложениям. Каждый итоговый chunk-вектор «знает» весь документ.
На диаграмме это видно по стрелке late -> embedder (целый документ) и обратной embedder -> late (token embeddings приходят назад для пулинга). На том же «Who said the API failed?» — Chunk-2 теперь содержит вектор, в котором «He» уже разрешён в «John» на стадии attention, и ANN находит его правильно.
Когда подходит: тексты с тяжёлыми coreferences (научные статьи, нарратив, юридические документы), бюджет на long-context embedding модель есть.
| Стратегия | Качество retrieval | Стоимость индексации | Сложность | Когда |
|---|---|---|---|---|
| Fixed-size 500/50 | baseline | бесплатно (CPU) | trivial | прототип, FAQ |
| Recursive separator | +10-15% recall@5 | бесплатно (CPU) | low | дефолт для docs |
| Markdown/HTML header | +15-20% | бесплатно | low | структурированные docs |
| Semantic (Kamradt) | +5-15% над recursive | $0.40 / 1M tok | medium | повествовательные тексты |
| Parent-child / hierarchical | +20% precision | бесплатно | medium | длинные сложные docs |
| Sentence-window | хорош для коротких ответов | бесплатно | low | FAQ, citations |
| Propositional (Chen 2024) | топ качества | $20-100 / 1M tok | high | high-value docs, мало |
| Contextual Retrieval (Anthropic) | -49% retrieval failures | $10-30 / 1M tok | medium | production RAG с бюджетом |
| Late chunking (Jina) | +20% recall на coref-heavy | стоимость long-ctx embed | medium | нарратив, статьи |
Размер чанка:
| Размер | Плюс | Минус | Когда |
|---|---|---|---|
| 100-200 tok | Точный retrieval | Мало контекста для LLM | FAQ, snippets |
| 300-500 tok | Баланс | Дефолт | Большинство RAG |
| 800-1500 tok | Контекст | Размытый embedding | Сложные tech docs |
| 2000+ tok | Целые секции | Слабый embedding | Только с parent-child |
Эмпирическое правило: одна идея на чанк. Если в чанке два разных вопроса — режь. Если ни на один не отвечает — увеличь.
Metadata payload — не игнорировать. Каждый чанк должен нести source_url, doc_id, section_path, page, date, author, tags, permissions, language, doc_type. Это даёт pre-filter в Qdrant (WHERE date > '2026-01-01' AND 'role:engineer' IN permissions) до ANN-поиска — экономит compute и обеспечивает citations + RBAC.
<h2> секциям.RecursiveCharacterTextSplitter, MarkdownHeaderTextSplitter, SemanticSplitterNodeParser, HierarchicalNodeParser, SentenceWindowNodeParser из коробки.<h1> не сохраняется как контекст, retrieval падает.::concept{slug="rag-architecture"} — общая картина RAG-системы, в которой chunker — первый из критичных компонентов.