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 позволяет задать свои разделители.
bash
pip install -qU langchain-text-splitters
bash
from 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: каждый кусок, превысивший размер, режется рекурсивно.

bash
pip install -qU tiktoken
bash
splitter = 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 подсчёт не поддерживается, а модель для замера берите из поддерживаемых. Поэтому разумно не считать каждый чанк, а один раз откалибровать коэффициент на образце:

bash
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). Число включает несколько служебных токенов обёртки сообщения, для оценки это допустимо. Полученный коэффициент подставляем в символьный сплиттер:

bash
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-flash0.013 / 0.038111 500 × 0.013 + 33 450 × 0.038 = 1 449.5 + 1 271.1$0.0027
gpt-6.1-sol0.27 / 1.32111 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.