Токенизация — это решение системного дизайна, а не этап предобработки
Как байтовый BPE строит словарь, который видит LLM, почему число токенов зависит от языка, какие сбои это вызывает и что измерить, прежде чем винить модель.
Четыре симптома с одной причиной
Рано или поздно большинство тех, кто строит продукты на LLM, сталкивается с чем-то из этого:
- Модель с заявленным контекстным окном в 128k упирается в лимит гораздо раньше ожидаемого, если вход не на английском или в нём много кода.
- Одна и та же функция заметно дороже за запрос для пользователей, пишущих на японском, хинди или арабском, чем для англоязычных.
- Модель уверенно не может посчитать буквы или развернуть слово, как ни формулируй промпт.
- Две команды получают разное число токенов для текста, который выглядит одинаково, и никто не может объяснить почему.
Переформулировка промпта ничего из этого не решит, и дело не в том, что модель просто глупая. Виновник — токенизация, этап пайплайна, который инженеры обычно знают хуже всего. Она живёт между вашим кодом и моделью, работает незаметно и привлекает внимание, только когда ломается. Дальше — как устроен механизм, где он стоит в реальной системе, какой вред может причинить, и чек-лист, который стоит пройти, прежде чем винить модель.
Что на самом деле получает модель
Трансформер не умеет обрабатывать текст напрямую, поэтому первым делом текст превращается в целые числа. Это превращение и есть токенизация, и опирается оно на словарь, зафиксированный заранее. Ничто в вашем промпте не может изменить его во время работы модели.
Представьте фиксированный набор деталей Lego. Частые английские слова вроде «the», «is» и «running» обычно — одна деталь. Редкие слова разбиваются, так что «tokenization» может стать «token» плюс «ization». Текст, которого токенизатор видел мало, например редкий язык, необычный символ Unicode или малоизвестное название продукта, распадается на большее число мелких деталей.
Ваши исходные символы до модели не доходят. Доходит то разбиение, которое выбрал токенизатор, и биллинг, расход контекста и некоторые ошибки рассуждений — всё это следствие того, где он резал. Хороший пример — слово «strawberry»: модель видит несколько токенов, а не десять букв.
Как строится словарь
Большинство словарей получается с помощью Byte Pair Encoding (BPE). Корни у него в сжатии данных, а не в языке: Филип Гейдж описал его в 1994 году как способ уменьшить данные, заменяя новым символом ту пару символов, которая чаще всего стоит рядом.
Зеннрих, Хэддоу и Бёрч перенесли его в нейронный машинный перевод в 2016 году в статье «Neural Machine Translation of Rare Words with Subword Units». Словари на уровне слов плохо справляются с редкими словами, опечатками и словоформами, и их ответом стал открытый словарь, где редкие слова собираются из частых, более мелких подсловных кусочков.
Работа OpenAI над GPT-2 (Radford et al., 2019, «Language Models are Unsupervised Multitask Learners») опустила идею до уровня байтов. Слияния начинаются с сырых байтов UTF-8, а не с символов, что даёт стартовый алфавит ровно из 256 символов. Одно это изменение позволяет современному токенизатору представить любую строку на любом языке, включая битый текст и эмодзи, не упираясь в неизвестный токен. Почти все крупные семейства моделей сегодня, в том числе GPT и Llama, используют какой-то его вариант.
Обучение происходит один раз, офлайн, по таким шагам:
- Начать с базовых единиц: 256 байтов или символов в исходной формулировке.
- Пройти по большому корпусу и посчитать, как часто встречается каждая соседняя пара символов.
- Взять самую частую пару, слить её в один новый символ и добавить его в словарь.
- Заменить каждое вхождение этой пары в корпусе новым символом.
- Повторять шаги 2–4, пока словарь не достигнет целевого размера — обычно от десятков тысяч до пары сотен тысяч токенов.
После обучения кодирование быстрое и полностью детерминированное. К новому тексту применяются выученные слияния в том же порядке, в каком они были выучены, и куски жадно склеиваются, пока сливать больше нечего. Никакого понимания тут нет. Это поиск и слияние, и один и тот же вход всегда даёт одни и те же токены.
SentencePiece, отдельная библиотека от Google (2018), идёт другим путём, и его стоит знать. Она обучается прямо на сыром тексте и считает пробел обычным символом, а не особым разделителем. Это важно для китайского, японского и тайского, где слова не разделяются пробелами: там предтокенизатор, основанный на пробелах, разваливается, а SentencePiece справляется единообразно.
Где она стоит в запросе
Токенизация выполняется до трансформера. Путь реального запроса выглядит так:
User Text
↓
Pretokenizer (split on whitespace/punctuation rules, handle Unicode)
↓
BPE Encoder (apply learned merges → token IDs)
↓
Token IDs (integers)
↓
Embedding Lookup Table
↓
Transformer (attention + feedforward layers)
↓
Output Token IDs
↓
Decoder (token IDs → text)
↓
Response Text
О двух компонентах легко забыть. Во-первых, таблица эмбеддингов индексируется по ID токена, так что размер словаря задаёт размер слоя эмбеддингов и выходной проекции. Чем больше словарь, тем больше параметров уходит на отображение токенов в векторы ещё до того, как заработает хоть один слой рассуждений. Во-вторых, декодер — это токенизатор наоборот. Потоковая выдача ответа токен за токеном и превращение его обратно в читаемый текст опираются на тот же фиксированный словарь, что и кодирование.
Смотрим на это через tiktoken
Чтобы не принимать всё на веру, можно посмотреть самому с помощью tiktoken, библиотеки токенизатора от OpenAI:
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4o")
samples = {
"english": "The invoice was approved yesterday.",
"japanese": "請求書は昨日承認されました。",
}
for label, text in samples.items():
tokens = enc.encode(text)
print(f"{label}: {len(tokens)} tokens -> {tokens}")
print([enc.decode([t]) for t in tokens])
При таком сравнении стоит проверить две вещи. Во-первых, число токенов на символ или слово сильно зависит от языка. Одна и та же информация может стоить заметно больше токенов в зависимости от того, насколько хорошо язык был представлен при обучении токенизатора. Это видно и в документации OpenAI по токенизатору: короткая фраза распадается на больше кусков в старых кодировках, ориентированных на английский, чем в новых, а нелатинские письменности в целом тратят больше токенов на символ, чем английская проза. Во-вторых, границы токенов редко совпадают с символами или даже слогами. Именно поэтому LLM по умолчанию с трудом считают буквы и переворачивают строки: рабочее представление не даёт отдельные символы как полноценную единицу.
Пример многоязычного бюджета
Представьте агента поддержки, который обслуживает носителей английского, японского и арабского при жёстком лимите токенов на запрос. Ограничивать число входных и выходных токенов на вызов, чтобы держать в рамках стоимость и задержку, — нормальная практика в продакшене. Если подобрать этот бюджет, тестируя только на английском, вы систематически обделите всех остальных. Та же история диалога и тот же системный промпт съедают больше бюджета на японском или арабском и оставляют меньше места для ответа.
Ничего экзотического тут нет. BPE-токенизаторы, обученные в основном на английском, ведут себя так из коробки, и результат — тихая потеря качества из-за обрезанного контекста именно у тех международных пользователей, ради которых продукт и делается. Хитрый промпт этого не исправит. Помогает другое: на тестах считать реальные токены для каждого языка, а потом выбирать бюджеты, правила обрезки или суммаризации для каждого отдельно.
Дешёвый эксперимент
Прежде чем доверять предположениям о поведении токенизатора, попробуйте вот что. Возьмите 20–30 предложений с одинаковым смыслом, профессионально переведённых на английский, японский, арабский и хинди (хорошо подходят расшифровки обращений в поддержку). Закодируйте каждое одним токенизатором, например o200k_base через tiktoken, и измерьте число токенов на предложение и на символ для каждого языка.
Гипотеза для проверки (это не установленный факт): у японского, арабского и хинди соотношение токенов к символам окажется заметно выше, чем у английского. Это значило бы, что то же контекстное окно и тот же бюджет на запрос оставляют этим пользователям меньше полезного места для того же содержания. Точное соотношение зависит от версии токенизатора и языков, так что измеряйте на своих данных.
Сбои, которые встретятся в продакшене
Токенизация порождает конкретный, повторяющийся набор багов.
- Сбои на задачах уровня символов. Подсчёт букв, разворот строк, побуквенное написание и часть многозначной арифметики не получаются, потому что модель рассуждает над подсловными токенами, а не над символами. Симптом — модель уверенно ошибается в том, что для человека тривиально. Одним улучшением промпта это не исправить. Лучше работает просьба выписать слово через разделители, что принудительно даёт токены уровня символов, или передача задачи на выполнение кода.
- Несовпадение токенизатора и модели. Если оценивать стоимость или расход контекста токенизатором другой модели, цифры будут систематически неверны. Вы увидите, что локальные подсчёты токенов расходятся со счётом за API. Используйте кодировку, задокументированную для той модели, которую вызываете.
- Переполнение, которого никто не замечает. Промпт, который легко помещается на английском, после перевода может превысить лимит; то же касается контента, полного кода, JSON или странных символов. Симптом — обрезанный контекст и ответы хуже, а в некоторых пайплайнах без явной ошибки. Измеряйте реальное число токенов на реальном продакшен-контенте.
- Чувствительность к пробелам и форматированию. Ведущие пробелы, повторяющиеся пробельные символы и непоследовательное форматирование могут сдвигать границы и число токенов, иногда сильно, так что два промпта, одинаковых на вид для человека, различаются по токенам и стоимости.
- Несовпадение словарей после дообучения или смены модели. Переход на другое семейство моделей с другим токенизатором без перепроверки бюджетов промптов и длины few-shot примеров может тихо сломать допущения, на которые опиралась система. То, что прекрасно работало, деградирует после якобы бесшовной замены.
Что делать в продакшене
- Стоимость: токены — настоящая единица биллинга, поэтому многоязычным продуктам стоит планировать бюджет по языкам, а не исходить из одного глобального среднего.
- Планирование контекста: размер RAG-чанков, обрезку истории диалога и few-shot примеры считайте в токенах конкретного используемого токенизатора, а не в словах или символах.
- Скорость: сама токенизация быстрая и редко бывает узким местом, но оценка длины промпта неправильной кодировкой перед вызовом может приводить к лишним повторам или обрезке, и то и другое добавляет задержку.
- Наблюдаемость: логируйте фактическое число входных и выходных токенов на запрос, с разбивкой по языку, если обслуживаете несколько локалей. Добавить это дёшево, а проблемы со стоимостью и контекстом ловятся раньше, чем о них сообщат пользователи.
- Согласованность: используйте одну и ту же версию токенизатора для локальных тестов, оценки стоимости и продакшен-вызова API. Несовпадающая кодировка даёт уверенно неверные цифры.
Когда всё это можно игнорировать
Для чисто англоязычного приложения с короткими текстами и малым объёмом, например внутреннего инструмента, прототипа или малонагруженного чат-бота, вопросы уровня токенизатора вроде разницы в стоимости между языками не стоят инженерного времени. Значения библиотеки по умолчанию подойдут, а силы лучше потратить на другое.
Точно так же, если продукту никогда не нужна работа на уровне символов вроде побуквенного написания, подсчёта букв или точного разворота строк, обходить здесь нечего. Это важно, только когда сценарий действительно требует от модели таких задач, и тогда обычное решение — передать их шагу с выполнением кода, а не бороться с токенизатором через промпты.
Коротко
Модели никогда не видят символов или слов — только токены из фиксированного словаря, построенного заранее, обычно с помощью байтового BPE. BPE появился в 1994 году как алгоритм сжатия, пришёл в NLP в 2016-м и был расширен до байтов командой GPT-2 в 2019-м, поэтому современные токенизаторы практически никогда не выдают неизвестных токенов. Реальную стоимость и расход контекста задаёт число токенов, а не слов или символов. Одна и та же информация на некоторых языках стоит больше токенов, и это тихо ставит неанглоязычных пользователей в невыгодное положение, если не планировать бюджет по языкам. Сбои при подсчёте символов, побуквенном написании и развороте строк идут от представления токенизатора, а не от недостатка рассуждений, а несовпадающие токенизаторы вызывают баги в продакшене, которых можно было избежать. Измеряйте токенизацию и продолжайте следить за ней как за частью дизайна, а не забывайте о ней после первой интеграции.
Источники:
- Gage (1994), «A New Algorithm for Data Compression», The C Users Journal — исходный алгоритм сжатия BPE.
- Sennrich, Haddow и Birch (2016), «Neural Machine Translation of Rare Words with Subword Units», ACL 2016, DOI 10.18653/v1/P16-1162.
- Radford et al. (2019), «Language Models are Unsupervised Multitask Learners» — статья о GPT-2, в которой появился байтовый BPE.
- Kudo и Richardson (2018) — статья Google о SentencePiece, подсловном токенизаторе и детокенизаторе, не зависящем от языка.
- Руководство OpenAI Cookbook по подсчёту токенов с tiktoken и документация
tiktokenпо кодировкам cl100k_base и o200k_base.