Чанкинг документов: как размер куска ломает или спасает RAG
Если RAG находит вроде бы правильный документ, но отвечает неточно или обрывочно — в девяти случаях из десяти дело не в модели и не в эмбеддере, а в том, каким по размеру получился кусок текста, который система отдала модели как контекст. Слишком маленький чанк обрубает мысль на середине, слишком большой — топит нужную строчку в потоке постороннего текста. Разберём этот компромисс подробно: что именно ломается в каждом из двух случаев, какие приёмы реально помогают и как подобрать размер под свои документы, а не под чужой пример из туториала.
Содержание
- Почему размер чанка вообще имеет значение
- Слишком маленький чанк: информация есть, а понять её нельзя
- Слишком большой чанк: сигнал тонет в шуме
- Таблица: как проявляется каждая крайность
- Приём первый: overlap — перекрытие между соседними чанками
- Приём второй: разбивка по смысловым границам, а не по символам
- Приём третий: метаданные — сохранить более широкий контекст даже для короткого чанка
- Как подобрать размер чанка под свои документы
Почему размер чанка вообще имеет значение
RAG не отдаёт модели весь корпус документов целиком — у языковой модели есть контекстное окно (лимит токенов на вход), и подсовывать туда сотни страниц физически невозможно. Поэтому документы заранее режут на куски — чанки, каждый чанк превращают в эмбеддинг (вектор), и на этапе запроса поиск находит несколько ближайших по смыслу чанков, которые и уходят в промпт вместе с вопросом. Общая механика этого пайплайна — сбор документов, чанкинг, эмбеддинги, векторная база, поиск, промпт — подробно разобрана в статье про RAG-пайплайн с нуля, здесь же фокус ровно на одном параметре этого пайплайна — размере чанка.
Размер чанка кажется технической мелочью — число символов в конфиге сплиттера. На практике это параметр, который определяет сразу две вещи: что именно эмбеддинг-модель закодирует в вектор (и что найдёт поиск по похожему вопросу) и что именно окажется в контексте у LLM в момент генерации ответа. Ошибиться можно в обе стороны, и обе ошибки выглядят одинаково — «RAG отвечает не то» — но чинятся по-разному, поэтому важно разобрать оба сценария отдельно, прежде чем менять цифры в конфиге.
Слишком маленький чанк: информация есть, а понять её нельзя
Когда чанк слишком короткий — условно, одно-два предложения — возникает проблема, которую проще всего показать на примере. Представьте абзац документа:
Компания предоставляет отсрочку платежа для клиентов категории B.
Стандартный срок отсрочки составляет 30 календарных дней с даты
поставки. Для клиентов, работающих с компанией более трёх лет,
срок увеличивается до 45 дней по отдельному соглашению.
Если резать по фиксированному короткому лимиту без учёта смысла, получатся примерно такие два чанка:
- Чанк А: «Стандартный срок отсрочки составляет 30 календарных дней с даты поставки.»
- Чанк Б: «Для клиентов, работающих с компанией более трёх лет, срок увеличивается до 45 дней по отдельному соглашению.»
Оба чанка по отдельности звучат как законченные утверждения. Но ни один из них не говорит явно, о какой категории клиентов идёт речь — это слово осталось в предыдущем предложении, которое не попало ни в один из двух кусков. Пользователь спрашивает «какая отсрочка у клиентов категории B» — поиск находит чанк А (он про отсрочку и про 30 дней), эмбеддинг которого действительно похож на вопрос по формальным признакам. Модель получает этот чанк как контекст и честно отвечает «30 дней» — хотя формально нужная информация в базе была, просто не в том куске, который дошёл до модели, и без слова-якоря «категория B», которое объяснило бы, к чему относится это число.
Это ключевой момент, который часто упускают: RAG в такой ситуации не «не нашёл» информацию — по метрикам поиска (retrieval) всё выглядит хорошо, релевантный по теме чанк найден. Проблема на уровне не поиска, а смысловой целостности самого чанка — из него выдернули утверждение, но оставили за бортом то, к чему это утверждение относится. Модель, получившая обрубленный фрагмент, вынуждена либо отвечать неполно, либо — что хуже — молча домысливать недостающий контекст, выдавая уверенный, но частично или полностью неверный ответ. Та же проблема проявляется и на уровне местоимений («он», «эта система», «данный пункт») и ссылок «выше по тексту» — они превращаются в указатели в никуда, если то, на что они ссылаются, осталось в соседнем куске.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть ИИ на сервереСлишком большой чанк: сигнал тонет в шуме
Обратная крайность — резать документ на крупные блоки «с запасом», чтобы уж точно не потерять ни одной мысли. Здесь проблема не в целостности отдельного утверждения, а в том, как устроен сам механизм векторного поиска.
Эмбеддинг-модель превращает весь текст чанка в один вектор фиксированной размерности — вне зависимости от того, сколько разных тем в этот чанк попало. Если чанк большой и содержит, например, три разных пункта регламента подряд (про отсрочку платежа, про порядок возврата и про условия расторжения договора), вектор такого чанка получается «усреднённым» — он в какой-то мере похож сразу на все три темы, но ни на одну из них не похож остро и однозначно.
На этапе поиска это выглядит так: пользователь спрашивает узко — «как оформить возврат товара». Косинусное сходство между вопросом и большим «размазанным» чанком оказывается ниже, чем могло бы быть с отдельным компактным чанком ровно про возврат. Хуже того — менее релевантный по факту, но более узкий чанк из другого документа иногда обходит в ранжировании нужный, но «разбавленный» посторонними темами кусок, просто потому что его вектор острее совпадает с вопросом.
Есть и вторая, более прямая цена больших чанков — они впустую тратят контекстное окно модели. Если top-k поиска возвращает 4 чанка по 2000 символов каждый, а реально релевантны в каждом из них 2-3 предложения, модель получает в промпте 8000 символов, из которых добрая половина — посторонний текст соседних тем. Это снижает шанс, что модель сфокусируется на нужном фрагменте среди шума, и напрямую съедает токены, за которые вы платите при работе через платный API, не принося пользы ответу.
Таблица: как проявляется каждая крайность
| Симптом | Слишком маленький чанк | Слишком большой чанк |
|---|---|---|
| Что происходит с поиском (retrieval) | Находит формально похожий, но обрубленный кусок | Релевантный чанк проигрывает в ранжировании из-за «размытого» вектора |
| Что происходит с ответом модели | Модель домысливает недостающий контекст — уверенно и часто неверно | Модель тонет в постороннем тексте, отвечает расплывчато или упускает деталь |
| Что с контекстным окном | Используется экономно, но информации может не хватить даже при большом top-k | Тратится на нерелевантный текст, реального контекста меньше, чем кажется |
| Типичный признак в логах ретривера | Найденный чанк — законченное на вид, но «повисшее в воздухе» предложение | Найденный чанк — длинный кусок из нескольких явно разных тем |
Приём первый: overlap — перекрытие между соседними чанками
Первый и самый дешёвый в реализации приём — не резать чанки строго встык, а делать соседние куски частично перекрывающимися: конец одного чанка повторяется в начале следующего. Смысл в том, что если важная мысль оказалась ровно на границе между двумя кусками (как в примере с отсрочкой платежа выше), overlap с высокой вероятностью оставляет её целиком хотя бы в одном из двух соседних чанков, а не разрывает пополам в обоих.
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=150,
separators=["\n\n", "\n", ". ", " "],
)
chunks = splitter.split_text(document_text)
Overlap обычно задают в процентах от размера чанка — распространённый ориентир на старте эксперимента 10-20% (например, 800 символов чанка и 100-150 символов перекрытия), но это именно ориентир, а не универсальная константа: для одних документов и запросов эффективнее будет больше, для других — почти не нужен. У overlap есть цена — он увеличивает общее количество чанков в базе (часть текста хранится и индексируется дважды), а значит растёт и размер векторной базы, и время индексации, и потенциально время поиска на больших объёмах. Для небольшого корпуса это несущественно, для десятков тысяч документов — стоит закладывать в ресурсы сервера заранее.
Важно понимать границы этого приёма: overlap снижает вероятность разрыва мысли ровно на границе двух чанков, но не решает проблему больших «размытых» чанков и не гарантирует, что длинная многоабзацная мысль уместится целиком хотя бы в одном куске — для этого нужен уже следующий приём.
Приём второй: разбивка по смысловым границам, а не по символам
Более фундаментальное решение — резать документ не по формальному лимиту символов, а по естественной структуре текста: заголовкам разделов, абзацам, границам предложений, элементам списков и таблиц. Идея простая — чанк должен заканчиваться там, где заканчивается мысль (или логический блок документа), а не там, где случайно оборвался счётчик символов.
Для документов в Markdown или с явной иерархией заголовков это делается разбивкой по заголовкам с последующей досборкой слишком длинных секций:
from langchain.text_splitter import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
headers_to_split_on = [("#", "h1"), ("##", "h2"), ("###", "h3")]
md_splitter = MarkdownHeaderTextSplitter(headers_to_split_on)
sections = md_splitter.split_text(markdown_text)
# каждую секцию, которая всё ещё слишком длинная для одного эмбеддинга,
# доразбиваем по абзацам/предложениям, а не по сырым символам
sub_splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=100,
separators=["\n\n", "\n", ". ", " "],
)
final_chunks = []
for sec in sections:
final_chunks.extend(sub_splitter.split_text(sec.page_content))
Для документов со сложной структурой — таблицами, вложенными списками, PDF с многоколоночной вёрсткой — механический сплиттер по символам почти неизбежно режет прямо посреди таблицы: заголовок колонки попадает в один чанк, значения строки — в другой. Для таких документов разумнее не писать разбор структуры с нуля, а взять готовый инструмент документ-парсинга — например, в статье про RAGFlow с разбором сложных документов разобрано, как система распознаёт таблицы и разделы до этапа чанкинга и не режет их пополам.
Внутренне более связный чанк даёт двойной выигрыш: эмбеддинг такого куска точнее описывает одну конкретную мысль (а не обрывок или смесь тем), и сама модель при генерации ответа получает законченный, а не покалеченный фрагмент.
Приём третий: метаданные — сохранить более широкий контекст даже для короткого чанка
Третий приём не меняет сам текст чанка, а добавляет к нему контекст снаружи — метаданные о происхождении: из какого документа взят чанк, из какого раздела или подраздела, какой у документа заголовок, дата версии, иногда — краткое саммари всего документа или секции.
chunk_record = {
"chunk_id": "reglament_v3_sec4_002",
"text": "Стандартный срок отсрочки составляет 30 календарных дней с даты поставки.",
"metadata": {
"source": "reglament_klientov_v3.pdf",
"section": "4. Условия оплаты — категория B",
"section_path": "Раздел 4 > Категория B > Отсрочка платежа",
},
}
Практическая польза в двух местах. Во-первых, при сборке промпта метаданные подставляются вместе с текстом чанка — модель видит не голый обрывок, а обрывок с подписью «откуда он», что подсказывает недостающий контекст без раздувания самого чанка:
context = "\n\n".join(
f'[Источник: {c["metadata"]["source"]}, {c["metadata"]["section_path"]}]\n{c["text"]}'
for c in found_chunks
)
Во-вторых, метаданные о разделе позволяют при необходимости подтянуть в контекст соседние чанки того же section_path, даже если поиск по эмбеддингу выбрал их не первыми — это не заменяет overlap и разбивку по смыслу, а работает поверх них как дополнительная страховка. Отдельный бонус — метаданные выводятся в интерфейсе «источники» у пользователя, повышая доверие к ответу, и это же поле первым делом смотрят при диагностике, если система начала путать факты — подробнее об этом в статье RAG выдавал чушь: нашли проблему в чанкинге.
Как подобрать размер чанка под свои документы
Универсального «правильного» числа не существует, и любая конкретная цифра из статьи или туториала (включая цифры выше) — это стартовая точка для эксперимента, а не рецепт для копирования. Оптимальный размер чанка зависит одновременно от нескольких факторов:
- характер документов — короткие независимые пункты FAQ режутся иначе, чем длинный регламент со сложной вложенностью, а тот — иначе, чем техническая документация с блоками кода, которые нельзя разрывать посередине;
- типичные вопросы пользователей — если вопросы узкие и точечные («какой лимит для категории B»), выгоднее компактные, сфокусированные чанки; если вопросы шире и требуют синтеза нескольких соседних фактов («опишите процедуру целиком»), может понадобиться либо более крупный чанк, либо больший top-k при поиске;
- модель эмбеддингов — у разных моделей разный практический предел, после которого качество кодирования смысла в один вектор начинает деградировать; выбор конкретной модели разобран отдельно в статье про embeddings-модели для RAG;
- контекстное окно и бюджет промпта — сколько чанков реально помещается в контекст модели вместе с системным промптом и вопросом, не вытесняя друг друга.
Практический план эксперимента на своих данных, а не по чужим цифрам:
- Соберите тестовый набор реальных вопросов (20-50 штук для старта) с указанием, в каком документе и разделе лежит правильный ответ.
- Проиндексируйте документы с несколькими конфигурациями — например, чанк 300/500/800/1200 символов с overlap около 15% от размера, плюс отдельно вариант с разбивкой по смысловым границам.
- Прогоните вопросы на каждой конфигурации и фиксируйте не только «правильно/неправильно», но и отдельно — является ли ошибка следствием разрыва на границе чанков. Смотрите не только на ответ модели, а на сырые чанки, которые вернул поиск — тот самый лог, который выводит простой
print()вызова ретривера до генерации ответа. - Считайте долю вопросов с ошибкой именно из-за границ фрагментов отдельно от общей доли ошибок — так видно, чинит ли изменение размера чанка проблему, или она перетекает в другую категорию (эмбеддер, ранжирование).
- Выбирайте конфигурацию по минимуму именно «граничных» ошибок при приемлемом росте объёма базы — рост от overlap и мелкой нарезки может быть заметным на больших корпусах.
Эксперимент занимает часы, а не дни, если тестовый набор вопросов уже собран, и его стоит повторять при заметном изменении характера документов — конфигурация под один корпус не переносится автоматически на другой.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть ИИ на сервереНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
С какого размера чанка начинать, если данных ещё нет?
Разумная стартовая точка для текста на естественном языке — около 500-800 символов с overlap 10-20% от этого размера. Это точка отсчёта для эксперимента, а не готовый оптимум.
Overlap и разбивка по смыслу — взаимозаменяемые приёмы?
Нет, их стоит комбинировать. Разбивка по смыслу снижает число разрывов мысли внутри чанка, а overlap подстраховывает случаи, когда даже смысловая граница разделяет связанные предложения.
Может ли больший top-k заменить подбор размера чанка?
Частично. Он помогает, если информация размазана по нескольким мелким чанкам. Но если конкретный чанк обрублен так, что смысл утрачен внутри него, больший k просто добавит в контекст больше таких же обрывков.
Нужен ли одинаковый размер чанка для всех разделов документа?
Нет. Разумнее резать по смысловым границам — короткий раздел FAQ может стать одним чанком целиком, а длинная процедура — несколькими с overlap. Единое число символов — упрощение для механической нарезки, а не требование архитектуры RAG.
Может ли reranker компенсировать неудачный размер чанка?
Он точнее пересортирует уже найденных кандидатов, но не может «дособрать» информацию, которой физически нет ни в одном чанке-кандидате из-за неудачной нарезки — подробнее в статье про reranker-модели в RAG.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →