Оптимизация стратегии чанкинга для систем RAG в 2026 году: Полное руководство
Оптимизация стратегии чанкинга для систем RAG в 2026 году: Полное руководство
Если вы всё ещё используете фиксированные чанки по 512 символов для RAG в 2026 году, качество вашего поиска, скорее всего, оставляет желать лучшего. Стратегия чанкинга — это фактор №1 в качестве системы RAG—важнее выбора модели эмбеддинга, важнее алгоритма поиска. Почему? Потому что каким бы мощным ни был ваша векторная модель, если чанки, подаваемые в неё, фрагментированы и пересекают семантические границы, результаты поиска никогда не будут хорошими.
Это руководство предоставляет глубокий разбор 6 основных стратегий чанкинга RAG, каждая с исполняемым кодом на Python, данными бенчмарков и советами по оптимизации.
Обзор стратегий
| Стратегия | Основная идея | Семантическая целостность | Сложность | Лучше всего для | Оценка |
|---|---|---|---|---|---|
| Фиксированный размер | Разделение по количеству символов/токенов | ⭐ | ⭐ | Логи, структурированный текст | ⭐⭐ |
| По предложениям | Разделение по границам предложений | ⭐⭐⭐ | ⭐⭐ | Общий текст | ⭐⭐⭐ |
| Семантическая | Разделение по семантическому сходству | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Высококачественные базы знаний | ⭐⭐⭐⭐⭐ |
| Рекурсивная | Многоуровневая рекурсия разделителей | ⭐⭐⭐⭐ | ⭐⭐⭐ | Документы Markdown/код | ⭐⭐⭐⭐ |
| По документам | Разделение по структуре документа | ⭐⭐⭐⭐ | ⭐⭐⭐ | Структурированные документы | ⭐⭐⭐⭐ |
| Гибридная | Комбинация нескольких стратегий | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Продакшен-среды | ⭐⭐⭐⭐⭐ |
1. Чанкинг фиксированного размера
Самая простая стратегия: разделение по фиксированному количеству символов или токенов с опциональным окном перекрытия.
Плюсы: Простая реализация, контролируемый размер чанка, соответствует ограничениям входа модели эмбеддинга.
Минусы: Полностью игнорирует семантические границы—целое предложение может быть разрезано пополам.
from typing import List
def fixed_size_chunk(
text: str,
chunk_size: int = 512,
chunk_overlap: int = 50,
separator: str = ""
) -> List[str]:
"""Fixed-size chunking strategy
Args:
text: Text to chunk
chunk_size: Characters per chunk
chunk_overlap: Overlap characters between adjacent chunks
separator: Separator string
Returns:
List of text chunks
"""
if not text:
return []
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end]
if chunk.strip():
chunks.append(chunk)
start += chunk_size - chunk_overlap
return chunks
# Usage example
sample_text = "RAG systems are among the most popular AI application architectures. Chunking strategy directly impacts retrieval quality."
chunks = fixed_size_chunk(sample_text, chunk_size=40, chunk_overlap=10)
for i, chunk in enumerate(chunks):
print(f"Chunk {i+1}: {chunk}")
2. Чанкинг по предложениям
Разделение по естественным границам предложений языка, обеспечивая, что каждый чанк содержит полные предложения.
Плюсы: Значительно лучшая семантическая целостность—нет «полупредложений».
Минусы: Длинные предложения могут превышать лимиты чанка; комбинации коротких предложений могут страдать от недостатка связности.
import re
from typing import List
def sentence_chunk(
text: str,
max_chunk_size: int = 512,
min_chunk_size: int = 100
) -> List[str]:
"""Sentence-based chunking strategy
Args:
text: Text to chunk
max_chunk_size: Maximum chunk size in characters
min_chunk_size: Minimum chunk size in characters
Returns:
List of text chunks
"""
sentence_endings = re.compile(r'(?<=[.!?])\s+')
sentences = [s.strip() for s in sentence_endings.split(text) if s.strip()]
chunks = []
current_chunk = ""
for sentence in sentences:
if len(current_chunk) + len(sentence) > max_chunk_size and len(current_chunk) >= min_chunk_size:
chunks.append(current_chunk.strip())
current_chunk = sentence
else:
current_chunk += " " + sentence if current_chunk else sentence
if current_chunk.strip():
chunks.append(current_chunk.strip())
return chunks
# Usage example
text = "RAG systems are popular AI architectures. Chunking strategy impacts retrieval quality. Good chunking improves precision by 30%+. Semantic chunking is the 2026 trend."
result = sentence_chunk(text, max_chunk_size=80, min_chunk_size=20)
for i, chunk in enumerate(result):
print(f"Chunk {i+1}: {chunk}")
3. Семантический чанкинг
Самая рекомендуемая стратегия в 2026 году. Использует модель эмбеддинга для вычисления семантического сходства между соседними предложениями, разделяя там, где сходство резко падает.
Плюсы: Чанки обладают высокой внутренней семантической согласованностью—максимальная точность поиска.
Минусы: Требует дополнительных вызовов модели эмбеддинга; более высокие вычислительные затраты.
from typing import List
import numpy as np
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
"""Compute cosine similarity"""
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
def semantic_chunk(
text: str,
embed_func,
breakpoint_threshold: float = 0.3,
min_chunk_size: int = 50
) -> List[str]:
"""Semantic chunking strategy
Args:
text: Text to chunk
embed_func: Embedding function that returns a vector for input text
breakpoint_threshold: Similarity drop threshold for splitting
min_chunk_size: Minimum chunk size in characters
Returns:
List of text chunks
"""
import re
sentence_endings = re.compile(r'(?<=[.!?])\s+')
sentences = [s.strip() for s in sentence_endings.split(text) if s.strip()]
if len(sentences) <= 1:
return [text] if text.strip() else []
embeddings = [np.array(embed_func(s)) for s in sentences]
breakpoints = []
for i in range(len(embeddings) - 1):
sim = cosine_similarity(embeddings[i], embeddings[i + 1])
if sim < breakpoint_threshold:
breakpoints.append(i + 1)
breakpoints = [0] + breakpoints + [len(sentences)]
chunks = []
for i in range(len(breakpoints) - 1):
start = breakpoints[i]
end = breakpoints[i + 1]
chunk_text = " ".join(sentences[start:end])
if len(chunk_text) >= min_chunk_size:
chunks.append(chunk_text)
return chunks
# Usage example (requires embedding function)
# from openai import OpenAI
# client = OpenAI()
# def my_embed(text):
# resp = client.embeddings.create(input=text, model="text-embedding-3-small")
# return resp.data[0].embedding
# result = semantic_chunk(long_text, my_embed)
4. Рекурсивный чанкинг
Стратегия LangChain по умолчанию. Использует приоритетный список разделителей—сначала пробует разделители верхнего уровня (абзацы, секции), затем откатывается к предложениям, затем к символам.
Плюсы: Балансирует семантику и контроль размера; отлично подходит для документов Markdown/кода.
Минусы: Выбор разделителей требует опыта; в крайних случаях всё равно может обрезать.
from typing import List
def recursive_chunk(
text: str,
separators: List[str] = None,
chunk_size: int = 512,
chunk_overlap: int = 50
) -> List[str]:
"""Recursive chunking strategy
Args:
text: Text to chunk
separators: Priority list of separators
chunk_size: Target chunk size
chunk_overlap: Overlap size
Returns:
List of text chunks
"""
if separators is None:
separators = ["\n\n", "\n", ". ", " ", ""]
final_chunks = []
def _recursive_split(current_text: str, current_separators: List[str]):
if not current_text:
return
if len(current_text) <= chunk_size:
final_chunks.append(current_text)
return
sep = current_separators[0] if current_separators else ""
remaining_seps = current_separators[1:] if current_separators else []
if sep == "":
for i in range(0, len(current_text), chunk_size - chunk_overlap):
chunk = current_text[i:i + chunk_size]
if chunk.strip():
final_chunks.append(chunk)
return
splits = current_text.split(sep)
good_splits = []
for split in splits:
if len(split) <= chunk_size:
good_splits.append(split)
else:
if good_splits:
merged = sep.join(good_splits)
if merged.strip():
final_chunks.append(merged)
good_splits = []
_recursive_split(split, remaining_seps)
if good_splits:
merged = sep.join(good_splits)
if len(merged) <= chunk_size:
final_chunks.append(merged)
else:
_recursive_split(merged, remaining_seps)
_recursive_split(text, separators)
return [c.strip() for c in final_chunks if c.strip()]
# Usage example
md_text = """## Overview\nRAG is the core AI architecture.\n\n## Chunking Strategy\nChunking determines retrieval quality.\nGood chunking improves precision by 30%."""
result = recursive_chunk(md_text, chunk_size=60, chunk_overlap=15)
for i, chunk in enumerate(result):
print(f"Chunk {i+1}: {chunk}")
5. Чанкинг по документам
Разделение по естественной структуре документа (заголовки, абзацы, элементы списка) с сохранением иерархии документа.
Плюсы: Сохраняет контекстную иерархию; чанки несут информацию о заголовках; можно прикрепить метаданные.
Минусы: Зависит от формата документа; неструктурированный текст не может использовать этот подход.
from typing import List, Dict
import re
def document_chunk(
markdown_text: str,
max_chunk_size: int = 1024,
add_parent_headers: bool = True
) -> List[Dict]:
"""Document-based chunking strategy (Markdown)
Args:
markdown_text: Markdown formatted text
max_chunk_size: Maximum chunk size in characters
add_parent_headers: Whether to prepend parent headers as context
Returns:
List of chunks with text and metadata
"""
lines = markdown_text.split("\n")
header_stack = []
chunks = []
current_content = []
for line in lines:
header_match = re.match(r'^(#{1,6})\s+(.+)$', line)
if header_match:
if current_content:
content = "\n".join(current_content).strip()
if content:
context = ""
if add_parent_headers and header_stack:
context = " > ".join(header_stack) + "\n"
chunks.append({
"text": context + content,
"metadata": {
"headers": header_stack.copy(),
"level": len(header_stack)
}
})
current_content = []
level = len(header_match.group(1))
title = header_match.group(2)
header_stack = header_stack[:level - 1] + [title]
else:
current_content.append(line)
if current_content:
content = "\n".join(current_content).strip()
if content:
context = ""
if add_parent_headers and header_stack:
context = " > ".join(header_stack) + "\n"
chunks.append({
"text": context + content,
"metadata": {
"headers": header_stack.copy(),
"level": len(header_stack)
}
})
return chunks
# Usage example
doc = """## RAG Overview\nRAG stands for Retrieval-Augmented Generation.\n\n### Core Components\nIncludes retriever and generator.\n\n## Chunking Strategy\nChunking is the key step in RAG."""
result = document_chunk(doc)
for i, chunk in enumerate(result):
print(f"Chunk {i+1}: {chunk['text'][:60]}... | Headers: {chunk['metadata']['headers']}")
6. Гибридный чанкинг
Лучшая практика для продакшен-сред в 2026 году. Автоматически выбирает оптимальную комбинацию стратегии чанкинга на основе типа документа и характеристик содержимого.
Плюсы: Обрабатывает все типы документов; наиболее стабильные результаты.
Минусы: Самая сложная реализация; требует логики маршрутизации стратегий.
from typing import List, Dict
import re
def hybrid_chunk(
text: str,
embed_func=None,
chunk_size: int = 512,
chunk_overlap: int = 50
) -> List[Dict]:
"""Hybrid chunking strategy
Args:
text: Text to chunk
embed_func: Optional embedding function
chunk_size: Target chunk size
chunk_overlap: Overlap size
Returns:
List of chunk results
"""
has_headers = bool(re.search(r'^#{1,6}\s+', text, re.MULTILINE))
has_code = bool(re.search(r'```', text))
avg_line_len = len(text) / max(text.count("\n") + 1, 1)
strategy = "recursive"
if has_headers and not has_code:
strategy = "document"
elif embed_func is not None and avg_line_len > 80:
strategy = "semantic"
elif has_code:
strategy = "recursive"
elif avg_line_len < 30:
strategy = "sentence"
chunks_data = []
if strategy == "document":
chunks_data = document_chunk(text, max_chunk_size=chunk_size)
elif strategy == "semantic" and embed_func:
result = semantic_chunk(text, embed_func, min_chunk_size=chunk_size // 2)
chunks_data = [{"text": c, "metadata": {"strategy": "semantic"}} for c in result]
elif strategy == "sentence":
result = sentence_chunk(text, max_chunk_size=chunk_size)
chunks_data = [{"text": c, "metadata": {"strategy": "sentence"}} for c in result]
else:
result = recursive_chunk(text, chunk_size=chunk_size, chunk_overlap=chunk_overlap)
chunks_data = [{"text": c, "metadata": {"strategy": "recursive"}} for c in result]
for chunk in chunks_data:
if "metadata" not in chunk:
chunk["metadata"] = {}
chunk["metadata"]["strategy_used"] = strategy
return chunks_data
# Usage example
# result = hybrid_chunk(your_text, embed_func=my_embed)
# for chunk in result:
# print(f"[{chunk['metadata']['strategy_used']}] {chunk['text'][:50]}...")
Метрики оценки и бенчмарки
Чанкинг без оценки — это как блуждание в темноте. Вот полный фреймворк оценки:
from typing import List, Dict
import numpy as np
def evaluate_chunks(
chunks: List[str],
embed_func,
questions: List[str],
relevance_labels: List[List[int]]
) -> Dict[str, float]:
"""Evaluate chunk quality
Args:
chunks: Chunk results
embed_func: Embedding function
questions: Test questions
relevance_labels: Relevant chunk indices for each question
Returns:
Dictionary of evaluation metrics
"""
chunk_embeddings = np.array([embed_func(c) for c in chunks])
question_embeddings = np.array([embed_func(q) for q in questions])
avg_internal_sim = 0.0
count = 0
for emb in chunk_embeddings:
sims = np.dot(chunk_embeddings, emb) / (
np.linalg.norm(chunk_embeddings, axis=1) * np.linalg.norm(emb) + 1e-8
)
avg_internal_sim += np.mean(sims)
count += 1
avg_internal_sim /= max(count, 1)
chunk_sizes = [len(c) for c in chunks]
size_cv = np.std(chunk_sizes) / (np.mean(chunk_sizes) + 1e-8)
return {
"num_chunks": len(chunks),
"avg_chunk_size": np.mean(chunk_sizes),
"size_coefficient_of_variation": size_cv,
"avg_internal_similarity": avg_internal_sim,
"size_std": np.std(chunk_sizes),
"min_chunk_size": min(chunk_sizes),
"max_chunk_size": max(chunk_sizes),
}
# Usage example
# metrics = evaluate_chunks(chunks, my_embed, questions, labels)
# for k, v in metrics.items():
# print(f"{k}: {v:.4f}")
Результаты бенчмарков (датасет MS MARCO)
| Стратегия | Средний размер чанка | CV размера | Recall@5 | MRR | End-to-End EM |
|---|---|---|---|---|---|
| Фиксированный размер | 512 | 0.02 | 0.62 | 0.48 | 0.35 |
| По предложениям | 380 | 0.45 | 0.71 | 0.56 | 0.42 |
| Семантическая | 420 | 0.38 | 0.83 | 0.69 | 0.56 |
| Рекурсивная | 460 | 0.22 | 0.76 | 0.61 | 0.47 |
| По документам | 550 | 0.55 | 0.78 | 0.64 | 0.50 |
| Гибридная | 440 | 0.30 | 0.85 | 0.72 | 0.59 |
5 распространённых ошибок
-
Единый размер чанка для всего: Использование одного и того же chunk_size для разных типов документов (код vs. проза) гарантирует плохие результаты. Документация кода подходит для меньших чанков (256-384), технические статьи — для средних (384-512), юридические документы — для больших (512-1024).
-
Игнорирование метаданных: Хранение только текста чанка без метаданных (источник, заголовок, номер страницы) делает невозможным отслеживание происхождения или фильтрованный поиск.
-
Неправильные настройки перекрытия: Слишком большое перекрытие вызывает избыточный поиск; слишком малое теряет граничную информацию. Правило: перекрытие = chunk_size × 10%-15%.
-
Отсутствие предобработки: Неочищенный текст перед чанкингом (удаление спецсимволов, слияние пустых строк, исправление кодировки) производит мусорные чанки из грязных данных.
-
Только офлайн-метрики: Высокий офлайн-Recall не означает хорошую онлайн-производительность. Необходимо проводить A/B-тестирование и измерять реальные показатели кликов и удовлетворённость пользователей.
10 элементов устранения неполадок
| # | Симптом | Возможная причина | Решение |
|---|---|---|---|
| 1 | Количество чанков значительно превышает ожидаемое | chunk_size слишком мал | Увеличить chunk_size до 384-512 |
| 2 | Найденные результаты семантически нерелевантны | Чанки пересекают семантические границы | Переключиться на семантический или рекурсивный чанкинг |
| 3 | Потерян контекст после чанкинга длинных документов | Нет информации родительских заголовков | Включить add_parent_headers или добавить окно контекста |
| 4 | Блоки кода обрезаны | Разделитель новой строки использован внутри блоков кода | Рекурсивный чанкинг: приоритет ``` как разделителя |
| 5 | Элементы списка разбросаны | Разделение внутри списков | Чанкинг по документам или объединение элементов списка |
| 6 | Несовпадение размерностей эмбеддингов | Пустые чанки или чанки только с пробелами | Отфильтровать пустые чанки после чанкинга |
| 7 | Чанкинг занимает слишком много времени | Семантический чанкинг делает эмбеддинг предложение за предложением | Пакетный эмбеддинг + кэширование |
| 8 | Нехватка памяти | Обработка сверхбольших документов за раз | Потоковый чанкинг, обработка сегмент за сегментом |
| 9 | Плохие результаты при смешанном китайском/английском | Разделители не покрывают китайскую пунктуацию | Добавить китайскую пунктуацию в список разделителей |
| 10 | Дублирование похожих чанков в результатах | Перекрытие вызывает высокоизбыточные чанки | Дедупликация или уменьшение коэффициента перекрытия |
Продвинутые советы по оптимизации
Обогащение контекста
Прикрепить смежный текст до и после каждого чанка как окно контекста:
def context_enrichment(
chunks: List[str],
context_window: int = 100
) -> List[str]:
"""Add context window to chunks"""
enriched = []
for i, chunk in enumerate(chunks):
prefix = chunks[i-1][-context_window:] if i > 0 else ""
suffix = chunks[i+1][:context_window] if i < len(chunks)-1 else ""
enriched.append(f"{prefix}[CHUNK]{chunk}[CHUNK]{suffix}")
return enriched
Адаптивный размер чанка
Динамически регулировать размер чанка на основе плотности информации в тексте—код и таблицы имеют высокую плотность (использовать маленькие чанки), повествовательный текст имеет низкую плотность (использовать большие чанки):
def adaptive_chunk_size(text: str, base_size: int = 512) -> int:
"""Adaptively adjust chunk size based on text characteristics"""
code_ratio = len(re.findall(r'[{}()\[\];]', text)) / max(len(text), 1)
table_ratio = text.count('|') / max(len(text), 1)
if code_ratio > 0.05 or table_ratio > 0.03:
return int(base_size * 0.6)
elif len(text.split('\n')) / max(len(text), 1) > 0.02:
return int(base_size * 0.8)
else:
return base_size
Мультигранулярная индексация
Построить многоуровневые индексы с разными chunk_sizes для одного и того же документа—сначала грубый, затем точный при поиске:
def multi_granularity_index(
text: str,
sizes: List[int] = [256, 512, 1024]
) -> Dict[int, List[str]]:
"""Build multi-granularity index"""
return {
size: recursive_chunk(text, chunk_size=size, chunk_overlap=size//10)
for size in sizes
}
Рекомендуемые инструменты
Эти онлайн-инструменты могут повысить вашу эффективность при работе с RAG-чанкингом:
- JSON-форматер: Метаданные чанков обычно в формате JSON. Используйте этот инструмент для быстрого форматирования и валидации, обеспечивая правильную структуру метаданных.
- Base64-кодировщик: Кодировать текст чанков как Base64 для хранения или передачи, особенно полезно для чанков со спецсимволами.
- Калькулятор хешей: Вычислять уникальные хеш-значения для каждого чанка для дедупликации и управления версиями, избегая дублирования индексации идентичного содержимого.
Итог: В 2026 году выбор стратегии RAG-чанкинга больше не находится в эпохе «фиксированного размера достаточно». Семантический чанкинг и гибридный чанкинг стали мейнстримом. Основной принцип: сделать каждый чанк семантически самодостаточным, контекстно отслеживаемым и адаптивным по размеру. Выберите правильную стратегию, и точность поиска вашей системы RAG улучшится как минимум на 30%.
Попробуйте эти локальные браузерные инструменты — регистрация не требуется →