MAATRIX / Блог / Как токенизация превращает ваш текст в числа для LLM

Как токенизация превращает ваш текст в числа для LLM

MAATRIX

Счётчик токенов в ответе API или ошибка «context length exceeded» упираются в механику, которую большинство пользователей LLM никогда не видели: модель не читает буквы и не читает слова — она читает числа, полученные разбиением текста на куски по заранее обученному словарю. Пока это чёрный ящик, непонятно, почему короткая фраза на русском стоит дороже похожей на английском, а аккуратный JSON съедает лимит быстрее, чем сплошной текст того же размера. Разберём, как токенизация устроена внутри и что из этого следует на практике — без придуманных точных коэффициентов, которые не проверял.

Токен — это не буква и не слово, а кусок из словаря

Прежде чем текст попадёт в модель, его прогоняют через токенизатор — отдельную программу, никак не связанную с самой нейросетью по смыслу того, что она делает. Токенизатор не понимает язык и не разбирает грамматику: у него есть готовый словарь (vocabulary) из нескольких десятков тысяч кусков — обычно от 30 000 до примерно 100 000+ элементов в зависимости от модели — и задача токенизатора одна: разрезать входную строку на кусочки из этого словаря и заменить каждый на его номер. Дальше модель работает уже только с числами — векторными представлениями (эмбеддингами) этих номеров, а не с текстом как таковым.

Ключевая путаница, из-за которой оценки «на глаз» промахиваются: токен — не символ и не слово. Это подслово (subword) — фрагмент произвольной длины, который словарь посчитал достаточно частым, чтобы выделить его в отдельную единицу. Частое короткое слово вроде «the» или «и» почти всегда попадает в словарь целиком и превращается в один токен. Редкое, составное или иностранное слово словарь как целую единицу не хранит — тогда токенизатор режет его на несколько кусков подряд, каждый из которых сам по себе есть в словаре. Отсюда для одного и того же текста итоговое число токенов может отличаться в разы просто в зависимости от того, какие слова и в какой форме в нём встретились.

Словарь у каждой модели свой и обучается заранее, до основного обучения самой сети, на большом корпусе текста. Поэтому токенизация GPT-подобной модели и токенизация Llama или Qwen дают разное число токенов на одном и том же тексте — это не единый стандарт, а конкретный артефакт, привязанный к конкретной модели.

Как устроен BPE: словарь строится слияниями частых пар

Самый распространённый подход — byte-pair encoding (BPE) и его варианты (byte-level BPE, WordPiece, Unigram — механика отличается в деталях, идея близкая). Схема обучения словаря выглядит так:

  1. Стартовая единица — отдельные байты или символы: разбиваем весь обучающий корпус на них, это самый мелкий возможный алфавит.
  2. Считаем, какая пара соседних единиц встречается в корпусе чаще всего.
  3. Объединяем эту пару в один новый составной токен, добавляем его в словарь.
  4. Повторяем шаги 2–3 много тысяч раз, каждый раз укрупняя всё более частые сочетания, пока словарь не достигнет заданного размера (например, 50 000 записей).

Результат — иерархия из очень частых длинных кусков (целые распространённые слова и приставки) и очень редких, оставшихся почти на уровне отдельных символов или байтов. Условная иллюстрация механики, а не дамп реального токенизатора: слово «маленький» при таком подходе скорее распадётся на что-то вроде «мал» + «еньк» + «ий», потому что целиком в словарь оно не попало, а вот частая приставка и суффикс — попали как отдельные куски. Точное разбиение зависит от конкретного словаря конкретной модели и от того, какие пары были частыми именно в её обучающем корпусе — проверять нужно на самом токенизаторе, а не по аналогии.

Важное следствие этой механики: для одного и того же текста нет единого «правильного» способа посчитать токены в уме — есть только то, что выдаёт конкретный токенизатор на конкретном словаре. Отсюда и следующий пункт: почему словари, обученные в основном на английских текстах и коде, дают разную цену русскому и английскому.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть ИИ на сервере

Почему одно слово — 1 токен, а другое — 3–5

Частота слова в обучающем корпусе токенизатора — единственный фактор, который решает, попадёт ли оно в словарь целиком. Это значит:

  • Частые служебные и распространённые слова языка, на котором обучен словарь, почти всегда — один токен: артикли, предлоги, короткие частые глаголы.
  • Редкие, специализированные или составные слова режутся на несколько кусков: медицинские и юридические термины, малоупотребительные формы, неологизмы.
  • Слова на языке, слабо представленном в обучающем корпусе словаря, систематически режутся мельче, чем аналогичные по смыслу слова доминирующего языка — просто потому что для них не набралось статистики на слияние в крупные куски.
  • Опечатки, редкие имена собственные, транслитерация и смешение языков в одном слове — почти гарантированно несколько токенов, потому что словарь такую комбинацию целиком не видел.
  • Числа часто режутся не привычным для человека образом, а произвольными группами цифр — токенизатор не знает, что «12345» — это одно число, для него это просто ещё одна строка символов.

Практический вывод: не пытайтесь оценивать стоимость текста в токенах по числу слов. Два текста с одинаковым числом слов на одном языке могут отличаться по числу токенов в полтора-два раза просто из-за того, какая лексика в них встретилась — частая или редкая.

Почему русский текст обходится дороже английского

Большинство массовых токенизаторов (в семействах GPT, Llama, Mistral и близких) обучены на корпусах с сильным перекосом в сторону английского текста и кода — просто потому что таких данных в открытом вебе исторически больше. Из этого следует прямое и проверяемое на практике наблюдение: кириллица в такой словарь входит менее эффективными кусками, чем латиница, — часто отдельными буквами или короткими сочетаниями букв, а не целыми частыми словами, как получается с распространённой английской лексикой.

Насколько именно дороже — вопрос, на который нет единого честного числа: разброс зависит от конкретной модели, конкретного словаря и конкретного текста (насколько в нём формальная лексика против разговорной, насколько текст насыщен именами собственными и т. д.). Готовые правила вида «один токен — это N символов» из англоязычных гайдов для русского текста систематически занижают реальный расход — не берите их как факт, а проверяйте на своём тексте и своей модели, способ описан в следующем разделе.

Практическое следствие для тех, кто работает с LLM из России или пишет для них промпты и системные инструкции: длинный русскоязычный системный промпт съедает заметно больше бюджета контекста, чем такой же по смыслу английский. Если контекстное окно тесное, иногда выгоднее держать техническую часть промпта (инструкции, формат вывода, служебные пометки) на английском, а на русском оставлять только то, что реально нужно писать по-русски — пользовательский контент и ответ. Подробнее о том, как вообще устроено контекстное окно и что происходит при его нехватке, — в статье «Контекстное окно и память: как считать».

Токены, контекстное окно и лимиты API

Токен — единица учёта не только для памяти модели, но и для всего, что вокруг неё построено:

  • Контекстное окно — это общий бюджет токенов на весь обмен: системный промпт, история диалога, новый вопрос и место под ответ модели делят один и тот же лимит, а не считаются отдельно.
  • Лимиты запроса в API — большинство провайдеров ограничивают не число символов, а именно число токенов на входе (max_input_tokens или аналог) и на выходе (max_tokens, max_completion_tokens) — превышение возвращает ошибку ещё до того, как модель начала генерацию.
  • Оплата по API почти везде считается по токенам, отдельно за вход и за выход, а не по символам и не по запросам — отсюда прямая связь между тем, насколько «дорого» токенизируется ваш язык и текст, и счётом за месяц использования. Как прикидывать эту стоимость заранее на своём трафике — отдельная тема, разобранная в статье «Как считать стоимость LLM-запросов на своём сервере».
  • Rate limits у многих API считаются в токенах в минуту (TPM), а не только в запросах в минуту (RPM) — упереться можно даже при небольшом числе запросов, если каждый несёт длинный контекст.

Практически любой ответ от API-совместимого эндпоинта (OpenAI-совместимый формат, который поддерживают многие шлюзы) несёт в JSON поле usage с точным числом использованных токенов — это не оценка, а фактический расход, посчитанный тем же токенизатором, что применялся к запросу:

curl -s https://api.example.com/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"<model>","messages":[{"role":"user","content":"Тест"}]}' \
  | jq '.usage'

# {
#   "prompt_tokens": 12,
#   "completion_tokens": 84,
#   "total_tokens": 96
# }

Если модель развёрнута локально через Ollama, аналогичное число видно в --verbose при интерактивном запуске или в полях prompt_eval_count / eval_count JSON-ответа — механика та же самая, просто без сетевого биллинга поверх неё.

Как прикинуть число токенов без специнструмента

Точный подсчёт токенов без запуска настоящего токенизатора конкретной модели невозможен по определению — это не формула, а результат работы конкретного алгоритма на конкретном словаре. Но для прикидки «влезет — не влезет» есть рабочие способы, от грубых к точным:

  1. Самый грубый ориентир — отталкивайтесь от порядка величины «несколько символов на токен» для латиницы и учитывайте, что для кириллицы число токенов на тот же текст обычно заметно выше — насколько именно, заранее не скажет никто, это не постоянный коэффициент, а прикидка порядка величины.
  2. Отправить реальный текст и посмотреть usage — самый надёжный способ: одно тестовое обращение к API или к локальной модели с куском вашего текста даёт точное число, посчитанное настоящим токенизатором, а не оценку.
  3. Токенизатор модели офлайн — если нужен подсчёт до отправки запроса (например, чтобы заранее обрезать историю диалога под лимит), берётся родной токенизатор модели: библиотека transformers загружает его по имени модели (AutoTokenizer.from_pretrained(...)) и считает точно, без сетевого запроса. Обязательно под правильный словарь — счётчик, обученный под чужую модель, ошибается на десятки процентов из-за другого набора слияний.
  4. Не смешивайте языки в одной оценке. Если текст на русском с вкраплениями кода и английских терминов, единого коэффициента для всего текста не существует — куски разного языка и типа (проза, код, идентификаторы) токенизируются по-разному внутри одного документа, подробнее — в разделе про код и JSON ниже.

Практический сценарий: вы собираете RAG-пайплайн (retrieval-augmented generation) и должны заранее прикинуть, сколько токенов займут top_k найденных фрагментов документов, прежде чем добавлять к ним историю диалога и вопрос — если сумма подходит вплотную к лимиту, ответ обрежется или запрос не пройдёт. Правильный порядок — прогнать реальные фрагменты через токенизатор модели один раз при подготовке пайплайна, а не оценивать на глаз при каждом запросе.

Почему код и JSON обходятся дороже обычного текста

Одинаковое число символов прозы и структурированных данных (код, JSON, XML, YAML) почти никогда не дают одинаковое число токенов — и разница обычно не в пользу структурированных данных. Причины связаны с той же механикой BPE:

  • Пунктуация и служебные символы (фигурные и квадратные скобки, кавычки, двоеточия, запятые) в прозе встречаются реже и не образуют длинных повторяющихся последовательностей — а в JSON и коде они плотные и часто соседствуют с символами, которые в словаре словосочетаниями не закреплены, поэтому режутся мельче.
  • Идентификаторы в camelCase и snake_case (getUserById, max_input_tokens) для токенизатора, обученного в основном на естественном тексте, не выглядят частыми целыми словами — они режутся на составные части, а иногда и посимвольно, если комбинация букв в таком виде в обучающем корпусе была редкой.
  • Отступы и повторяющаяся структура — в JSON с вложенными объектами одни и те же ключи, скобки и кавычки повторяются много раз подряд; каждое повторение расходует токены заново, потому что токенизатор не «сжимает» смысловые повторы — это не архиватор, у него нет памяти о том, что было раньше в этом же тексте.
  • Экранирование и форматирование (\n, \", лишние пробелы для читаемости) добавляют символы, которые в прозе не встречаются вовсе, — а токенизатор всё равно должен их куда-то деть, обычно отдельными короткими токенами.

Практическое следствие для промптинга: если модели нужно вернуть структурированные данные, компактный JSON без лишних пробелов и отступов почти всегда обходится дешевле по токенам, чем тот же JSON, красиво отформатированный для человеческого глаза, — а на выходе, где токены оплачиваются так же, как на входе, эта разница уже прямые деньги при большом объёме запросов. Если вам нужен именно надёжный структурированный вывод от модели, а не просто «текст, похожий на JSON» — подробный разбор способов в статье «Structured output в LLM: как получить JSON».

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть ИИ на сервере

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли узнать точное число токенов текста без обращения к модели?

Да, если у вас есть офлайн-токенизатор именно той модели, с которой вы работаете (например, через transformers и AutoTokenizer.from_pretrained с именем модели) — тогда подсчёт точный и без сетевого запроса. Без родного токенизатора — только грубая прикидка, а не точное число.

Почему число токенов у одного и того же текста разное в разных моделях?

Потому что у каждой модели свой словарь, обученный на своём корпусе своим алгоритмом (BPE или его варианты) — токенизация не стандартизирована между моделями разных разработчиков, и цифра из одной модели не переносится на другую.

Правда ли, что русский язык всегда токенизируется примерно вдвое дороже английского?

Такой универсальный коэффициент выдумывать не стоит — перекос в сторону английского в обучающих корпусах большинства массовых токенизаторов реален и системный, но точная разница зависит от конкретной модели и конкретного текста. Надёжный способ узнать цифру для вашего случая — прогнать реальный текст через API или токенизатор и посмотреть на usage.prompt_tokens в ответе.

Стоит ли переписывать промпты покороче, чтобы сэкономить токены?

Если контекстное окно или бюджет по API — узкое место, да, особенно для служебной, повторяющейся в каждом запросе части промпта (системные инструкции, формат ответа): здесь выгода накапливается на каждом обращении. Для пользовательского контента сокращать в ущерб смыслу не стоит — экономия на порядок ощутимее там, где текст статичен и повторяется, а не там, где он одноразовый.

JSON от модели теряет форматирование и превращается в одну строку — это баг?

Нет, это ожидаемое поведение части моделей и настроек генерации: компактный вывод без отступов и переносов реально дешевле по токенам на выходе, и модель (или обвязка вокруг неё) может быть настроена выдавать именно такой вид по умолчанию, а не ради экономии на вашей стороне.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →