LangChain text splitters: как резать текст на чанки правильно
Как подобрать размер чанка и перекрытие в RecursiveCharacterTextSplitter, почему считать нужно в токенах и как сверить подсчёт с точным количеством от шлюза.
5 мин · · разработка · экономия
LangChain text splitters разбивают длинный документ на фрагменты (чанки), которые помещаются в контекст модели и хорошо ищутся. Для RAG и суммаризации от этого шага зависит и качество, и цена: слишком крупные чанки тратят токены, слишком мелкие теряют смысл. Главная ловушка в том, что RecursiveCharacterTextSplitter по умолчанию считает размер в символах, а модель и счёт идут в токенах. Ниже разберём параметры, покажем подсчёт в токенах и способ проверить его точным числом от шлюза. Код следует документации LangChain (раздел Text splitters); сигнатуры сверяйте с ней.
LangChain text splitters: как работает RecursiveCharacterTextSplitter
По документации LangChain, это рекомендуемый сплиттер для обычного текста. Он принимает список разделителей и пробует резать по ним по порядку, пока куски не станут достаточно маленькими. Список по умолчанию: ["\n\n", "\n", " ", ""]. То есть сначала он старается сохранить абзацы целиком, потом строки, потом слова. Параметры, которые вы будете настраивать:
chunk_sizeзадаёт максимальный размер чанка, измеряемый функциейlength_function(по умолчаниюlen, то есть в символах);chunk_overlapзадаёт целевое перекрытие между соседними чанками, чтобы мысль на стыке не терялась;length_functionопределяет, чем измерять длину;separatorsпозволяет задать свои разделители.
pip install -qU langchain-text-splittersfrom langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=100,
separators=["\n\n", "\n", ". ", " ", ""],
)
with open("report.txt", encoding="utf-8") as f:
text = f.read()
parts = splitter.split_text(text) # список строк
docs = splitter.create_documents([text]) # список Document
print(len(parts), len(docs[0].page_content))split_text возвращает строки, create_documents объекты Document для векторных хранилищ, split_documents режет уже готовые документы. В список разделителей мы добавили ". ": для русского текста это граница предложения, а в списке по умолчанию её нет. Для отдельных форматов есть специальные сплиттеры, например MarkdownHeaderTextSplitter режет по заголовкам, а RecursiveCharacterTextSplitter.from_language(...) учитывает синтаксис языков программирования.
Почему символы не подходят
Токен не равен символу. Сколько символов приходится на токен, зависит от токенизатора модели и от языка текста, а у русского текста обычно другая плотность, чем у английского. Например, чанк в 1000 символов для английского текста и для русского займёт разное число токенов, и один и тот же chunk_size даст разные расходы. Поэтому для RAG и суммаризации размер удобнее задавать в токенах. Подробнее: почему русский текст считается иначе.
Подсчёт в токенах через tiktoken
В документации LangChain описан способ мерить размер токенизатором tiktoken: у сплиттеров есть конструктор from_tiktoken_encoder. Для жёсткого ограничения размера документация рекомендует именно RecursiveCharacterTextSplitter.from_tiktoken_encoder: каждый кусок, превысивший размер, режется рекурсивно.
pip install -qU tiktokensplitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base",
chunk_size=300,
chunk_overlap=40,
)
chunks = splitter.split_text(text)Здесь chunk_size=300 уже в токенах. Документация отдельно предупреждает: прямое использование TokenTextSplitter может разорвать многобайтовый символ между чанками (проблема для языков вроде китайского и японского; она возможна и для других алфавитов, если символ кодируется несколькими токенами); from_tiktoken_encoder у рекурсивного сплиттера этого избегает. И есть оговорка про точность: tiktoken это токенизатор OpenAI, и он лучше подходит для моделей OpenAI. Для Claude, Gemini, DeepSeek или GLM он даёт оценку, а не точное число.
Сверка с точным подсчётом через шлюз
У Zukko есть POST /v1/messages/count_tokens: он считает входные токены запроса в формате Anthropic Messages без генерации. По справочнику это платный запрос (токены оплачиваются по цене входа модели), для GLM подсчёт не поддерживается, а модель для замера берите из поддерживаемых. Поэтому разумно не считать каждый чанк, а один раз откалибровать коэффициент на образце:
import os
import requests
def exact_tokens(text: str, model: str = "claude-sonnet-5") -> int:
r = requests.post(
"https://api.zukko.pro/v1/messages/count_tokens",
headers={
"Authorization": f"Bearer {os.environ['ZUKKO_API_KEY']}",
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
json={"model": model, "messages": [{"role": "user", "content": text}]},
timeout=30,
)
r.raise_for_status()
return r.json()["input_tokens"]
sample = text[:20000]
chars_per_token = len(sample) / exact_tokens(sample)
print(f"символов на токен: {chars_per_token:.2f}")Пусть образец дал 8 000 токенов. Тогда замер стоит 8 000 × 0.4 / 1M = $0.0032 (цена claude-sonnet-5 на 18 октября 2026). Число включает несколько служебных токенов обёртки сообщения, для оценки это допустимо. Полученный коэффициент подставляем в символьный сплиттер:
TARGET_TOKENS, OVERLAP_TOKENS = 300, 40
splitter = RecursiveCharacterTextSplitter(
chunk_size=int(TARGET_TOKENS * chars_per_token),
chunk_overlap=int(OVERLAP_TOKENS * chars_per_token),
separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(text)
print(max(exact_tokens(c) for c in chunks[:10]))Последняя строка проверяет на десяти чанках, что максимум близок к целевому значению. Описание эндпоинта: count_tokens. Как использовать его для оценки стоимости запроса заранее, разобрано в статье посчитать стоимость запроса через count_tokens.
Какой размер и перекрытие выбрать
Универсального ответа нет, но есть отправные точки:
- RAG. Начните с нескольких сотен токенов на чанк. Мелкие чанки точнее находятся, крупные лучше сохраняют контекст. Проверьте на 20–30 реальных вопросах, лежит ли ответ в найденных фрагментах.
- Суммаризация (map-reduce). Чанки можно делать крупнее, до размера, при котором модель ещё хорошо справляется. Контекст у моделей Zukko от 128K (GLM) до 1M, так что для одного документа чанкование иногда вообще не нужно.
- Перекрытие. Ориентир 10–15% от размера чанка, это отправная точка, а не правило. Перекрытие стоит денег: повторяющийся текст оплачивается дважды.
Сколько стоит перекрытие: расчёт
Число чанков при длине текста N токенов, размере c и перекрытии o: примерно N − o, делённое на c − o, с округлением вверх. Пусть N = 100 000, c = 500, o = 50. Тогда (100 000 − 50) / (500 − 50) = 222.1, то есть 223 чанка. Общий объём чанков около 223 × 500 = 111 500 токенов, избыток около o / (c − o) = 11%. Допустим, на каждый чанк модель выдаёт резюме в 150 токенов (всего 33 450 выходных). Цены на 18 октября 2026, расчёт в миллионных долях доллара:
| Модель | Вход / выход, $ за 1M | Расчёт | Итого |
|---|---|---|---|
deepseek-v4.1-flash | 0.013 / 0.038 | 111 500 × 0.013 + 33 450 × 0.038 = 1 449.5 + 1 271.1 | $0.0027 |
gpt-6.1-sol | 0.27 / 1.32 | 111 500 × 0.27 + 33 450 × 1.32 = 30 105 + 44 154 | $0.074 |
Без перекрытия входных токенов было бы 100 000, то есть 11 500 токенов (около $0.003 на gpt-6.1-sol) уходят на дубли. Это нормальная цена за сохранённый смысл на стыках, но не стоит раздувать перекрытие без нужды. Для Claude в OpenAI-формате весь вход идёт по цене входа, а скидка за кэш возможна в Anthropic-формате. Свежие цены всегда в ответе GET https://api.zukko.pro/v1/models.
Частые вопросы
Как резать код и Markdown?
Используйте специализированные сплиттеры: MarkdownHeaderTextSplitter для заголовков и RecursiveCharacterTextSplitter.from_language для кода. Они сохраняют структуру лучше, чем простые разделители.
Нужно ли чанковать, если контекст модели миллион токенов?
Для суммаризации одного документа часто нет. Для RAG чанки нужны всё равно: вы отправляете модели только релевантные фрагменты и платите за них, а не за весь документ.
Почему размер чанков получился больше заданного?
Для некоторых сплиттеров документация предупреждает, что куски могут превышать заданный размер. Жёсткое ограничение даёт RecursiveCharacterTextSplitter.from_tiktoken_encoder.
Какой токенизатор выбрать для не-OpenAI моделей?
Документация LangChain советует использовать токенизатор той же модели, которую вы вызываете. Для моделей Claude точный счёт даёт count_tokens, для других tiktoken служит оценкой с запасом.
Создайте ключ в кабинете и сверьте свой коэффициент на образце. Для RAG на готовых чанках см. RAG-пайплайн своими руками.
zukko
Платите в 25×
меньше
за Claude Code & ChatGPT
Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.
Похожие статьи
- Память чат-бота: как хранить историю диалога и не платить за неё дваждыМодель не помнит предыдущих реплик: историю отправляете вы при каждом запросе. Разбираем, как растёт цена диалога и чем её сдерживать.
- LangChain RAG: рабочий пример с моделью через шлюзСтроим простой RAG на LangChain: документы, чанки, векторный поиск и ответ модели через шлюз. Считаем, из чего складывается цена вопроса, и режем лишние токены.
- Агенты opencode: как настроить роли, модели и инструментыСвой агент в opencode — это роль, модель и список разрешений. Показываем конфиг и пример, где рутину делает дешёвая модель, а код правит сильная.
- Plan mode в Claude Code: сначала план, потом правки в кодеВ plan mode Claude Code читает код и предлагает план, но ничего не правит, пока вы не согласитесь. Как включить, использовать и во сколько это обходится.