Перейти к содержимому
15 дек. 2024 г.·7 мин чтения

Разные токенизаторы у провайдеров: почему цифры не сходятся

Разные токенизаторы у провайдеров меняют цену, лимиты и реальную длину контекста. Разберем, где расходятся расчеты и как это проверить заранее.

Разные токенизаторы у провайдеров: почему цифры не сходятся

Почему одна строка дает разные числа

Одна и та же фраза не имеет "своего" фиксированного числа токенов. Модель не читает текст целиком, как человек. Она разбивает строку на части по правилам токенизатора, а эти правила у провайдеров и моделей отличаются.

Из-за этого короткий запрос вроде "Проверь статус заказа 15482" у одного backend может занимать одно число токенов, а у другого - заметно больше. Особенно часто расхождение видно на русском, в смешанном тексте, JSON, коде, датах, эмодзи и строках с пробелами или плотной пунктуацией.

Проблема не сводится к самому тексту. Чат-модель почти никогда не получает "голую" строку. Провайдер оборачивает запрос служебными частями: ролями сообщений, системными инструкциями, форматом ответа, описанием tools, а иногда еще и схемой для structured output. Команда видит одну реплику в коде, а биллинг считает уже другой, более длинный пакет.

Обычно расхождение идет из трех мест: сам токенизатор по-разному режет слова, цифры и символы; API добавляет скрытую обвязку для chat, tools и response format; backend может по-своему сериализовать сообщения перед отправкой в модель.

Поэтому одинаковый prompt дает не только разные цифры в логах. Он еще и занимает разный объем контекста. Это важно, когда вы работаете близко к лимиту окна, передаете длинную историю диалога или прикладываете большой JSON. То, что "влезает" у одного провайдера, у другого может упереться в предел раньше.

Здесь команды и путаются. Разработчик смотрит на локальный счетчик токенов, видит 7 800, оставляет запас и считает, что все в порядке. Потом меняет только base_url на совместимый backend, например через AI Router, оставляет тот же SDK и те же промпты, а в реальном запросе получает уже другую цифру. Код не изменился, строка тоже. Изменился способ, которым система превратила ее в токены.

Тема быстро перестает быть теорией. Она влияет на длину контекста, предсказуемость запросов и на то, совпадут ли ваши расчеты с тем, что потом покажет счетчик у провайдера.

Откуда берется разница в токенах

Для модели строка не существует "как есть". Сначала токенизатор режет ее на части, с которыми модель и работает. И тут начинаются расхождения: один токенизатор может взять слово почти целиком, а другой разбить его на несколько кусков.

На русском это заметнее, чем многие ждут. Слова на кириллице, смешанный текст вроде "счет 2457 по договору", даты, кавычки, скобки и даже лишний пробел часто дают другой счет, чем похожая фраза на английском. Короткая строка для человека может оказаться длинной для модели просто из-за того, как она нарезается.

Что именно меняет счет

Итог зависит не только от слов. В общий объем обычно попадает все, что вы отправляете модели: системное сообщение, роли вроде system и user, JSON-обертка с названиями полей, schema для structured output или tool calling, а также служебные токены, которые добавляет сам провайдер.

Из-за этого команды часто смотрят только на текст промпта и недооценивают реальный размер запроса. На бумаге у вас 700 символов, а по факту модель видит заметно больше токенов.

Есть и еще один слой. У провайдеров различается не только словарь токенов, но и способ упаковки сообщений. Два API могут быть совместимы по формату запроса, но это не значит, что они одинаково посчитают объем. Совместимость OpenAI API означает только то, что запрос можно отправить похожим способом. Она не обещает одинаковый токенизатор, одинаковые служебные токены и одинаковый биллинг.

Это хорошо видно в мультипровайдерной схеме. Команда отправляет один и тот же OpenAI-совместимый запрос через AI Router на разные модели и получает разный usage, хотя код не менялся. Обычно это не баг. Просто каждый backend по-своему режет текст и собирает сообщения.

Самый неприятный случай - структурированные ответы. Чем строже schema и чем длиннее названия полей, тем сильнее растет объем. Иногда пара "удобных" полей в JSON стоит больше токенов, чем сам вопрос пользователя.

Как это бьет по цене и лимитам

Одна и та же строка может занимать 900 токенов у одного backend и 1100 у другого. На первый взгляд разница небольшая, но в рабочем сценарии она быстро съедает и бюджет, и запас по контексту.

С лимитами это чувствуется первым. Если вы рассчитывали, что в окно контекста поместятся системный промпт, история диалога, пять фрагментов из RAG и еще останется место на ответ, лишние 10-20% по токенам ломают план. Влезают уже не пять фрагментов, а три. Или приходится резать историю, и бот теряет важные детали разговора.

Даже при совместимости OpenAI API это никуда не исчезает. Команда может сменить только base_url и оставить тот же SDK, код и промпты, но подсчет токенов все равно будет зависеть от конкретной модели и провайдера.

По деньгам эффект еще заметнее. Биллинг почти всегда привязан к входным и выходным токенам, значит цена одного и того же сценария меняется без изменений в продукте. Если сервис делает 200 000 запросов в день, а новый backend добавляет в среднем 150 входных токенов на запрос, это уже 30 млн лишних токенов в день. За месяц набегает сумма, которую сложно списать на "погрешность".

Есть и менее очевидный удар по качеству ответа. Когда prompt почти заполняет лимиты контекста, на выход остается мало места. Модель отвечает короче, обрывает мысль на середине или не успевает выдать список шагов целиком. Со стороны кажется, что "модель стала хуже", хотя ей просто не хватило окна на ответ.

Чаще всего это всплывает в длинных диалогах с памятью на 10-20 сообщений, в RAG-сценариях с крупными фрагментами документов, в tool calling со схемами функций и в задачах, где ответ должен быть не короче заданного минимума, например в поддержке или комплаенсе.

Разница в токенизации меняет не только счетчик в логах. Она меняет то, сколько контекста реально помещается, сколько стоит запрос и где модель начнет резать ответ раньше, чем вы ожидали.

Пример с ботом поддержки

У бота поддержки интернет-магазина нет сложного промпта, но даже такой сценарий легко ломает ожидания. На каждый запрос он отправляет системную инструкцию с правилами ответа, сообщение клиента и три карточки товара, которые бот может предложить взамен.

Клиент пишет: "Нужен смартфон с eSIM и быстрой доставкой". Первая модель получает весь пакет целиком. Она видит все три карточки, сравнивает сроки, память и цену, а потом дает внятный ответ на несколько предложений.

Вторая модель получает почти тот же текст, но считает его иначе. Русские слова, артикулы, кавычки, числа, куски JSON и названия брендов она дробит на большее число токенов. В итоге в окно контекста уже не влезает последняя карточка. Приложение обрезает хвост запроса, и модель отвечает только по двум товарам. Со стороны кажется, что бот просто "забыл" часть каталога.

На глаз ничего не изменилось. Разработчик видит в логах те же строки. Менеджер видит тот же шаблон. Но по факту меняются сразу две вещи: вход стоит дороже, потому что провайдер насчитал больше токенов, и ответ становится короче, потому что после длинного входа остается меньше места на вывод.

Разница бывает неприятной даже на маленьком объеме. У одного провайдера запрос занимает, например, 1450 токенов, у другого - 1680. Если лимит рядом, эти 230 токенов съедают запас под ответ. Бот начинает писать суше, пропускает детали или перестает задавать уточняющий вопрос. Команда часто списывает это на "слабую" модель, хотя проблема в другом.

Так и проявляется эта история на практике: одинаковая строка дает разный расход, другой остаток контекста и другой результат. Спор в команде быстро уходит не туда. Один человек предлагает менять модель, другой хочет резать системную инструкцию, третий винит каталог товаров. А начать нужно с простого - сравнить реальный подсчет токенов у каждого backend.

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

Где команды ошибаются чаще всего

Сверьте токены на моделях
Прогоните один и тот же запрос через разные маршруты и сравните реальный расход

Первая ошибка проста: команда берет локальный скрипт для подсчета токенов, получает цифру и считает ее истиной. Для грубой оценки это нормально. Для продакшена этого мало, потому что провайдер считает запрос так, как его видит свой backend, а не ваш тестовый ноутбук.

Именно здесь привычные расчеты и ломаются. Один и тот же текст может занимать разное число токенов у двух моделей, даже если API снаружи выглядит одинаково.

Промах обычно начинается с того, что смотрят только на входной текст пользователя. Но счет почти всегда шире. В него попадают system prompt, история диалога, schema для structured output, описание tools, имена ролей и служебные поля, которые SDK или шлюз добавляет сам.

Из-за этого короткий вопрос вроде "Где мой заказ?" почти ничего не говорит о реальной нагрузке. Если рядом лежит длинный system prompt и JSON schema на пару экранов, фактический размер запроса может вырасти в разы.

Вторая частая ошибка - считать только input и забывать про output. Команда видит, что вход помещается в лимит контекста, и успокаивается. Потом модель отвечает длиннее, чем ожидали, и запрос либо дорожает, либо упирается в лимит, либо обрезает ответ в неудобном месте.

Это особенно заметно в поддержке. Допустим, бот получает сообщение на русском, артикулы на английском, адрес с цифрами и потом вызывает tool с большим JSON. На коротких тестах все выглядит нормально. На длинном диалоге цифры уже другие: кириллица, смешанный текст и большие блоки JSON часто режутся на токены менее предсказуемо.

Типичные промахи повторяются:

  • сверяют цену и лимиты только по локальному токенизатору, без проверки на стороне провайдера;
  • меряют один запрос без ответа модели;
  • не включают в расчет system prompt, tools и schema;
  • тестируют фразы на 1-2 строки, а потом переносят выводы на диалоги из 20 сообщений;
  • гоняют только чистый текст и не проверяют кириллицу, JSON, таблицы и смешанный язык.

Если вы работаете через слой совместимости, такой как AI Router, соблазн еще выше: endpoint один, а backend у моделей разный. Поэтому проверять нужно не "в среднем по API", а на реальных моделях, с реальными payload и ожидаемой длиной ответа. Иначе стоимость и доступный контекст начнут расходиться с планом уже в первую неделю.

Как проверить все до запуска

Проверка на тестовой фразе почти всегда дает ложное спокойствие. Сильнее всего расхождения видны не на коротких примерах, а на реальных запросах, где есть системный промпт, история диалога, JSON schema и длинный формат ответа.

Начните не с модели, а со своего потока. Возьмите 10-20 живых запросов из логов или из ожидаемых сценариев. Набор должен быть неровным: короткие вопросы, длинные обращения, текст с цифрами, таблицами, именами, кодами заказов и смешением русского с английским.

В каждый тест включайте все, что пойдет в прод: системный текст целиком, инструкции по роли и стилю ответа, schema или строгий формат JSON, историю сообщений, если она есть в продукте, и ожидаемый размер ответа, а не только входной текст.

Именно здесь чаще всего ломается оценка. Команда считает только сообщение пользователя, а потом удивляется, почему лимит контекста заканчивается раньше. На практике значимую часть объема съедают системные инструкции и служебная обвязка.

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

Хороший простой тест - запрос в поддержку на 700-900 символов, системный промпт на 1-2 страницы и ответ в JSON с полями status, reason и next_step. На одной модели такой пакет может пройти с запасом, на другой - почти упереться в лимит еще до генерации ответа.

Перед релизом заложите запас. Обычно хватает 15-25% по контексту и отдельного потолка по бюджету на запрос. Если средний расход получился приемлемым, но 2-3 длинных кейса выбиваются, не игнорируйте их. Именно такие запросы потом дают обрывы ответа, лишние повторы и неожиданный счет.

Если вы работаете через единый шлюз, такой как AI Router, эту проверку проще повторять на нескольких провайдерах без смены SDK и кода. Но сама дисциплина важнее инструмента: тестируйте не модель в вакууме, а полный запрос таким, каким он уйдет в прод.

Как держать разброс под контролем

Смените маршрут без правок
Оставьте SDK, код и промпты, поменяйте только эндпоинт

Полностью убрать разброс нельзя. Один и тот же текст все равно будут резать по-разному. Но разброс можно сделать предсказуемым, если держать форму запросов стабильной.

Чаще всего токены раздувает не сама задача, а шаблон вокруг нее. Команды дублируют правила в system prompt, потом повторяют их в developer message, а затем еще раз вставляют в инструкцию пользователя. Если правило уже есть один раз и модель его соблюдает, второй и третий повторы обычно только съедают контекст.

С RAG похожая история. В индекс легко положить длинные фрагменты, но в запрос стоит отправлять только то, что реально помогает ответу. Вместо пяти больших кусков текста часто лучше дать два коротких с точными фактами. Для бота поддержки это видно сразу: одна лишняя вставка из базы знаний иногда стоит больше, чем сам ответ.

Еще одна простая мера - не менять формат system prompt между тестами. Если одна модель проверяется с короткой инструкцией, а другая с длинной и переписанной, сравнение уже ломается. Вы не поймете, где разница идет из-за backend, а где из-за шаблона.

Что считать отдельно

Сценарии с tools, JSON и длинной историей лучше мерить не вместе, а по отдельности. Именно там чаще всего сильнее расходятся цифры по токенам и лимитам.

Полезно держать небольшой постоянный набор тестов: короткий вопрос без истории, диалог на 10-15 сообщений, вызов tools с аргументами, ответ в строгом JSON и запрос с RAG-вставками.

После каждой смены модели или маршрута фиксируйте общий рост токенов на этом наборе. Смотрите не только на среднее число, но и на крайние случаи. Если новый backend дал плюс 7-10% к входным токенам, этого уже может хватить, чтобы сдвинуть месячный бюджет или вызвать обрезание длинных диалогов.

Если команда переключает модели через единый OpenAI-совместимый шлюз вроде AI Router, такой замер удобно делать сразу после смены маршрута. Тогда видно, изменился ли только ответ модели или вырос еще и служебный расход на prompt, tools и формат вывода. Это скучная дисциплина, но она экономит много денег и нервов на проде.

Быстрый чек перед релизом

Упростите B2B расчеты
Получайте ежемесячный B2B инвойсинг в тенге и держите расходы в одном месте

Перед запуском смотрят не на средний запрос, а на самый тяжелый. Один длинный диалог с вложенным JSON, историей чата и описанием tool может внезапно упереться в лимит, хотя обычные сообщения проходят без проблем.

Хорошая практика проста: соберите 10-20 самых длинных сценариев из реального продукта и прогоните их через тот backend, который пойдет в прод. Не берите только короткие тесты из песочницы. Они почти всегда дают слишком спокойную картину.

Проверьте не только обычный текст. На токены сильно влияют кириллица, длинные числа, артикулы, таблицы, куски кода и JSON-структуры. Сообщение "номер заказа 000184750029" и та же мысль в разговорной фразе могут занимать разный объем, хотя человеку это не видно.

Отдельно посчитайте все, что команда часто забывает: system prompt и скрытые служебные поля, schema для tools и function calling, формат ответа, если вы просите JSON, историю переписки, которую приложение добавляет само, и текст ошибок, ретраев и служебных вставок.

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

Запас по окну контекста тоже нужен. Обычно хватает 10-20%, а для сложных цепочек лучше держать больше. Если запрос в тесте занимает 115 тысяч токенов при лимите 128 тысяч, это уже тесно. Любая лишняя таблица, новое поле в JSON или длинный ответ инструмента съедят остаток.

Если вы меняете backend через совместимый OpenAI API, проверьте все заново, даже если код не менялся. В AI Router можно сохранить тот же SDK и тот же запрос, но фактический подсчет у конкретной модели все равно может отличаться.

После запуска не полагайтесь на ручную проверку. Поставьте метрики на входные и выходные токены, долю запросов у границы контекста и резкий рост расхода по модели. Алерт в первый день полезнее, чем разбор инцидента через неделю.

Нормальный финальный тест выглядит скучно, и это хорошо. Если длинные запросы проходят, JSON не раздувает бюджет, а график токенов не прыгает без причины, релиз обычно идет спокойно.

Что сделать следующим шагом

Сначала договоритесь внутри команды, чем вы меряете токены. Иначе разработчик смотрит на локальный токенизатор, финансист - на счет от провайдера, а SRE - на ошибки по лимиту контекста. У всех цифры разные, и спорить тут бессмысленно.

Обычно лучше взять один эталон и закрепить его в работе. Для биллинга это почти всегда фактический usage от того backend, который обработал запрос. Для ранней оценки можно держать локальный подсчет, но рядом с ним стоит прямо писать: это прогноз, а не итоговая цифра.

Если у вас уже несколько провайдеров, не пытайтесь свести все к одному "идеальному" числу. Лучше ввести простое правило: решения по цене и лимитам команда принимает по данным реального ответа модели и по логам продакшена.

Нужен и короткий рабочий чек. Выберите один эталон подсчета и зафиксируйте его в документации. После смены модели прогоняйте один и тот же набор запросов и сравнивайте usage. После смены провайдера отдельно проверяйте цену, обрезание ответа и запас по контексту. И держите дашборд по input tokens, output tokens и ошибкам лимита, чтобы раз в неделю смотреть на самые дорогие сценарии.

Особенно часто проблемы всплывают после тихой замены backend. Вчера бот поддержки укладывался в 12 тысяч токенов, а сегодня на другом провайдере тот же диалог внезапно подходит к лимиту. Код не менялся, текст тоже, но токенизация уже другая. Такие вещи лучше ловить на тестовом наборе из реальных запросов, а не после жалоб пользователей.

Если вы гоняете трафик через несколько провайдеров, удобно держать один OpenAI-совместимый endpoint и сравнивать расход по каждому маршруту в одном месте. Например, AI Router позволяет прогонять одинаковые payload через разные маршруты без смены SDK и кода, а для компаний в Казахстане дает ежемесячный B2B-инвойсинг в тенге. Это не заменяет проверку на своих запросах, но делает сравнение между backend заметно проще.

Самый полезный следующий шаг простой: возьмите 20-30 реальных промптов, прогоните их через все модели, которые хотите использовать, и сохраните по каждому запуску четыре числа - input, output, общую цену и запас до лимита контекста. После этого спорных мест обычно не остается.

Часто задаваемые вопросы

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

Потому что число токенов зависит не только от текста. У каждой модели свой токенизатор, а провайдер еще добавляет служебную обвязку: роли, system prompt, schema, tools и формат ответа.

В итоге одна и та же строка у двух backend превращается в разный пакет, и биллинг считает уже его.

Почему локальный токенизатор часто не совпадает с цифрами провайдера?

Локальный счетчик обычно видит только ваш текст или упрощенную версию запроса. Провайдер считает то, что реально ушло в модель после упаковки SDK, API и backend.

Для прикидки локальный расчет годится, но финальную цену и размер контекста лучше брать из фактического usage в ответе провайдера.

Что провайдер добавляет к запросу помимо текста пользователя?

Чаще всего в счет попадают роли сообщений, system prompt, история диалога, JSON-обертка, описания tools и schema для structured output. Иногда backend еще меняет сериализацию сообщений перед отправкой.

Поэтому короткий вопрос пользователя сам по себе почти ничего не говорит о полном размере запроса.

На каких типах текста расхождение видно сильнее всего?

Сильнее всего разница видна на русском, смешанном русском и английском, JSON, коде, датах, длинных числах, артикулах, кавычках, скобках и плотной пунктуации. Эмодзи и лишние пробелы тоже легко меняют счет.

Если у вас поддержка, RAG или tool calling, проверяйте именно такие данные, а не только чистый текст на пару строк.

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

Очень просто: тот же запрос занимает больше места в окне контекста. Если вы работали близко к лимиту, часть истории, RAG-фрагментов или JSON может уже не влезть.

Из-за этого приложение режет хвост запроса раньше, чем вы ждали, хотя сам промпт в коде не менялся.

Может ли из-за этого модель отвечать короче?

Да, и это частый случай. Когда вход занимает больше токенов, на ответ остается меньше места, и модель пишет суше или обрывает мысль.

Со стороны это легко принять за слабую модель, хотя причина часто в том, что запрос съел слишком большой кусок окна.

Как проверить реальные цифры до запуска?

Берите не тестовую фразу, а 10–20 живых сценариев из продукта. Прогоняйте каждый полный payload: system prompt, историю, tools, schema и ожидаемую длину ответа.

Сравнивайте по каждой модели входные токены, выходные токены, общую цену и запас до лимита. Тогда разница станет видна до релиза, а не после жалоб.

Какой запас по контексту лучше держать?

Для обычных сценариев часто хватает запаса 15–25% по контексту. Если у вас длинные диалоги, RAG с крупными фрагментами или tools с большим JSON, держите больше.

Когда тест уже упирается почти в предел окна, любой новый кусок данных быстро ломает расчет.

Что делать, если я сменил только base_url, а код не трогал?

Проверьте все заново на том же наборе реальных запросов. Совместимый API сохраняет код и SDK, но не обещает одинаковый токенизатор, одинаковую упаковку сообщений и одинаковый usage.

Даже если приложение работает без ошибок, цена и доступная длина ответа могут заметно сдвинуться.

Как уменьшить разброс токенов в проде?

Сделайте форму запросов стабильной. Не дублируйте одни и те же правила в нескольких местах, сокращайте лишний JSON и отправляйте в RAG только те фрагменты, которые реально помогают ответу.

Еще помогает постоянный тестовый набор. После каждой смены модели или маршрута сравнивайте расход на одних и тех же payload и ставьте метрики на input, output и ошибки лимита.