Отметим: когда precision retrieval падает ниже 0.7, а latency p99 растёт, первым делом проверяют чанкинг. Fixed-size splitting рубит предложения посередине, и модель начинает галлюцинировать. Правильная разбивка документов — основа точного поиска. Наша команда AI-инженеров реализовала 15+ RAG-проектов для FinTech и HealthTech, средний прирост recall — 20%. Мы гарантируем, что после оптимизации чанкинга релевантность ответов вырастет как минимум на 15%. При этом окупаемость достигается за счёт сокращения затрат на GPU: в одном из проектов экономия на аренде составила существенную сумму в месяц.
Сравнение: Recursive splitter повышает recall на 20–30% по сравнению с fixed-size — это подтверждено нашими A/B-тестами в 10 проектах. Fixed-size уступает Recursive в точности в 1.3–1.5 раза при равном размере чанка.
Почему важен правильный чанкинг?
Размер и границы чанков критически влияют на качество RAG: слишком маленькие фрагменты теряют контекст, слишком большие — снижают точность поиска и превышают context window модели. Semantic chunking группирует семантически близкие предложения, повышая точность на 15–30%. Использование RAG без правильного чанкинга — как поиск иголки в стоге сена с закрытыми глазами. Retrieval accuracy напрямую зависит от того, как разбит документ.
Как выбрать стратегию чанкинга под ваши данные?
Fixed-size chunking
Самый простой, но наименее эффективный:
def fixed_size_chunk(text: str, chunk_size: int = 500,
overlap: int = 50) -> list[str]:
tokens = text.split() # Упрощённо
chunks = []
for i in range(0, len(tokens), chunk_size - overlap):
chunk = ' '.join(tokens[i:i + chunk_size])
chunks.append(chunk)
return chunks
Проблема: разрезает предложения и абзацы посередине. Мы не рекомендуем этот метод для продакшна.
Recursive character text splitter (LangChain)
Разбивает по иерархии разделителей:
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # ~250 слов
chunk_overlap=200, # 50-слово перекрытие
separators=[
"\n\n", # Параграфы (приоритет)
"\n", # Строки
". ", # Предложения
", ", # Части предложений
" ", # Слова (последний resort)
"" # Символы
]
)
chunks = splitter.create_documents(
texts=[document_text],
metadatas={"source": "document.pdf", "page": 1}
)
Мы используем этот сплиттер в 70% проектов — он даёт отличный баланс между качеством и скоростью.
Semantic chunking
Разбивка по смысловым границам:
from sentence_transformers import SentenceTransformer
import numpy as np
class SemanticChunker:
def __init__(self, model_name: str = 'all-MiniLM-L6-v2',
threshold: float = 0.7):
self.model = SentenceTransformer(model_name)
self.threshold = threshold
def chunk(self, text: str) -> list[str]:
sentences = self._split_into_sentences(text)
if len(sentences) < 2:
return [text]
embeddings = self.model.encode(sentences)
chunks = []
current_chunk = [sentences[0]]
for i in range(1, len(sentences)):
sim = np.dot(embeddings[i], embeddings[i-1]) / (
np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[i-1])
)
if sim < self.threshold:
chunks.append(' '.join(current_chunk))
current_chunk = []
current_chunk.append(sentences[i])
if current_chunk:
chunks.append(' '.join(current_chunk))
return self._merge_small_chunks(chunks, min_words=50)
Этот метод требует больше вычислительных ресурсов, но оправдывает себя на научных статьях и сложной документации.
Document structure-aware chunking
Сохранение иерархии документа:
class StructureAwareChunker:
def chunk_markdown(self, text: str, max_chunk_tokens: int = 300) -> list[dict]:
sections = re.split(r'\n(#{1,3}\s+.+)', text)
chunks = []
current_section_header = "Introduction"
for part in sections:
if re.match(r'#{1,3}\s+', part):
current_section_header = part.strip()
else:
sub_chunks = self._split_section(part, max_chunk_tokens)
for sub_chunk in sub_chunks:
if sub_chunk.strip():
chunks.append({
'text': sub_chunk,
'section': current_section_header,
'breadcrumb': current_section_header
})
return chunks
Мы часто комбинируем его с Recursive splitter для достижения максимальной точности.
Sentence-level chunking
Разбивка по границам предложений — простой и быстрый метод для коротких текстов, например новостей. Используется, когда семантическая целостность предложения критична.
Рекомендуемые параметры чанков
| Тип документа | Размер чанка (токенов) | Перекрытие | Рекомендуемая стратегия |
|---|---|---|---|
| Код | 200–400 | 50 | Recursive |
| Техническая документация | 800–1200 | 200 | Structure-aware |
| Новости | 400–600 | 100 | Recursive или Sentence-level |
| Научные статьи | 1000–1500 | 300 | Semantic |
Сравнение стратегий чанкинга
| Критерий | Fixed-size | Recursive | Semantic | Structure-aware |
|---|---|---|---|---|
| Точность поиска | Низкая | Средняя | Высокая | Высокая |
| Сложность реализации | Очень низкая | Низкая | Средняя | Средняя |
| Скорость обработки | Высокая | Высокая | Средняя | Высокая |
| Подходит для | Код, сырые данные | Большинство текстов | Научные статьи | Техдоки, PDF |
| Сохранение контекста | Нет | Да | Частично | Да |
На практике Recursive splitter — самая универсальная стратегия. Semantic и Structure-aware применяем для документов с высокой ценностью контекста. Semantic chunking может дать прирост точности до 10–15% по сравнению с Recursive на научных статьях.
Как parent-child индексация улучшает retrieval?
Small-to-big retrieval — индексируем маленькие чанки для точного поиска, но в контекст передаём большие родительские. Это даёт прирост точности до 25% без потери контекста.
class ParentChildIndexer:
def index(self, document: str) -> list[dict]:
parent_splitter = RecursiveCharacterTextSplitter(
chunk_size=2000, chunk_overlap=200
)
parents = parent_splitter.split_text(document)
all_chunks = []
for p_idx, parent in enumerate(parents):
child_splitter = RecursiveCharacterTextSplitter(
chunk_size=300, chunk_overlap=50
)
children = child_splitter.split_text(parent)
for child in children:
all_chunks.append({
'child_text': child,
'parent_text': parent,
'parent_idx': p_idx
})
return all_chunks
Недавно в проекте для финтех-компании мы заменили стандартный фиксированный чанкинг на комбинацию Structure-aware и Recursive. Recall вырос с 58% до 84%, а latency p99 снизилась на 30%. Инженеры отмечают: «Правильный чанкинг — это 80% успеха RAG».
Детальная настройка гиперпараметров
- chunk_size: от 200 до 2000 токенов в зависимости от типа документа.
- overlap: 10–20% от размера чанка.
- similarity threshold для semantic: 0.65–0.75.
Подбираются экспериментально на выборке из 1000+ запросов.
Что входит в нашу работу
- Анализ корпуса документов и бизнес-требований
- Прототипирование 2–3 стратегий чанкинга
- A/B-тестирование на репрезентативной выборке
- Оптимизация гиперпараметров (chunk size, overlap, similarity threshold)
- Интеграция с векторной БД (ChromaDB, pgvector, Qdrant)
- Мониторинг и итеративное улучшение
Ориентировочные сроки
В зависимости от объёма и сложности, полная настройка занимает от 1 до 3 недель. Пилотный запуск — 3–5 дней. Мы предоставляем гарантию на повышение recall не менее 15%.
Свяжитесь с нами, чтобы провести аудит вашего RAG-пайплайна. Оценим стратегию чанкинга и предложим оптимальное решение. Закажите пилотный запуск — мы настроим чанкинг на ваших данных за 3 дня.







