← Блог

Один и тот же JSON, счёт в 12 раз разный: разбор документов через API

· 7 минут чтения

Коротко

На формальной задаче - «вытащи из документа поля и верни JSON» - модели дают одинаковый результат, а счета отличаются в разы. Четыре модели вернули по моему тестовому счёту побайтово те же значения. Списание за один документ: 0,069 ₽ у самой дешёвой и 0,82 ₽ у самой дорогой - разница почти в двенадцать раз, и в тексте ответа она никак не видна.

Ниже - что я собрал, сколько это стоило, и три грабли, на которые я налетел в день замера:

  1. невидимые токены рассуждения, за которые платят как за обычные;
  2. max_tokens, который выставлен, но не соблюдён;
  3. половина каталога, которая в тот день просто не отвечала.

Все числа сняты 23.08.2026, курс ЦБ 82,9211 ₽/$ на 22.08.2026.

Задача: счета, которые никто не хочет вбивать руками

У меня в проекте копятся входящие счета: номер, дата, поставщик, ИНН, сумма, срок оплаты, позиции. Всё это надо превратить в строки в базе. Раньше это делал человек, и делал ровно так, как делают люди: быстро, пока их десять в месяц, и с ошибками, когда их сорок.

Требований было три:

  • на выходе строгий JSON, который парсер съедает без регулярок;
  • цена предсказуемая, потому что счетов будет не сорок, а тысячи;
  • никаких плейсхолдеров вроде <СУММА> - модель обязана либо дать число, либо null.

Задача формальная: рассуждать тут не о чем, нужно аккуратно переписать поля. Значит, дорогая модель не должна давать выигрыша - это я и пошёл проверять.

Как это собрано

Код целиком - двадцать строк. Один OpenAI-совместимый клиент, у которого меняется только имя модели.

import json, os
from openai import OpenAI

client = OpenAI(base_url="https://api.altrouter.ai/v1", api_key=os.environ["AR_KEY"])

PROMPT = """Извлеки из счёта поля и верни СТРОГО JSON без пояснений:
{"number": str, "date": "YYYY-MM-DD", "supplier": str, "inn": str,
 "total": float, "due_date": "YYYY-MM-DD", "items": [{"name": str, "sum": float}]}
Если поля нет - null. Никаких плейсхолдеров вида <ЗНАЧЕНИЕ>.

СЧЁТ:
"""

def parse(doc: str, model: str) -> dict:
    r = client.chat.completions.create(
        model=model, messages=[{"role": "user", "content": PROMPT + doc}])
    return json.loads(r.choices[0].message.content)

Дальше - три прогона на каждую модель одним и тем же счётом на 100 500 ₽ с двумя позициями. JSON распарсился с первого раза у всех четырёх моделей, и значения совпали до последнего поля: номер, дата, ИНН, обе позиции, сумма. Ни одного плейсхолдера, ни одного «вот ваш JSON:» перед скобкой.

Что списалось за один документ

МодельВремя, сТокенов выходаОдин счёт, ₽1000 счетов, ₽
gpt-5.6-mini4,8–5,2122–1260,069–0,071~70
gemini-3-pro11,3–13,11470,080~80
gpt-5.214,4–29,11020,142~142
grok-4.316,2–39,02327–44080,45–0,82~450–820

Токен - кусочек текста, которым модель меряет вход и выход; платят именно за них. Цены - то, что реально списалось с баланса на 23.08.2026, по три прогона на модель.

Вывод для моей задачи простой: самая дешёвая модель закрывает её полностью, и переплата ничего не покупает.

Столбчатая диаграмма, показывающая разницу в стоимости обработки 1000 счетов: 70 рублей против 820 рублей при идентичном результате.
Переплата за мощную модель на рутинной задаче не улучшает качество данных, но кратно увеличивает бюджет.
На тысяче счетов разница между первой и последней строкой - примерно 750 ₽ на ровном месте.

Грабля 1: токены, которых нет в ответе

Самое интересное - четвёртая строка. grok-4.3 вернул тот же самый JSON на 331 символ, что и gemini-3-pro. Но в usage у него от 2327 до 4408 токенов выхода против 147 у Gemini - при одинаковом видимом тексте.

Это токены рассуждения: модель думает «про себя», в ответ эти рассуждения не попадают, а в счёт попадают.

Схема в виде айсберга, где видимая часть ответа составляет 147 токенов, а скрытые токены рассуждения — 4408.
Итоговая стоимость запроса зависит не от длины финального текста, а от объема скрытых токенов рассуждения, которые генерирует модель.
И плавают они сильно: три одинаковых запроса подряд дали 0,45 ₽, 0,62 ₽ и 0,82 ₽ - разброс в 1,8 раза на идентичном входе. Планировать бюджет по одному замеру такой модели нельзя.

Практический вывод: сравнивая модели, смотрите не на текст ответа, а на usage - поле, которое API возвращает вместе с ответом. Оно одно объясняет, почему два одинаковых JSON стоят по-разному.

Грабля 2: max_tokens выставлен, но не соблюдён

Логичная защита от такого - поставить потолок на длину ответа. Я поставил max_tokens=40 и повторил все четыре запроса.

Результат на 23.08.2026: лимит проигнорирован 4 раза из 4 - 124, 147, 1281 и 102 токена выхода, причём finish_reason во всех случаях stop, то есть ответ считается нормально завершённым, а не обрезанным.

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

Грабля 3: в день замера четыре модели просто не ответили

Честная часть. Изначально в замере были Claude Haiku и Sonnet, но в тот день ни одна модель Claude мне не ответила: 503, All providers failed (last: kie 500), три попытки из трёх. Две Gemini-Flash отвалились по таймауту через 60 секунд с тем же All providers failed.

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

Блок-схема перенаправления запроса при ошибке 503 на резервную модель.
При падении всей инфраструктуры конкретной модели единственный способ сохранить работу сервиса — переключиться на модель от другого производителя.
Поэтому в коде, который разбирает счета в фоне, у меня стоит не «повторить тот же запрос», а «взять запасную модель из другого семейства» - и как раз поэтому в замере остались четыре, а не шесть строк.

С чего начать

  1. Возьмите свой документ, а не пример из статьи, и один промпт.
  2. Прогоните его на самой дешёвой подходящей модели три раза подряд.
  3. Посмотрите не на ответ, а на usage: completion_tokens и есть ваш будущий счёт.
  4. Умножьте на реальный поток документов в месяц - и только потом решайте, нужна ли модель дороже.
  5. Заложите запасную модель другого семейства: провайдеры падают, и падают целиком.

Если у вас есть зарубежная карта и свой прокси - берите официальные ключи вендоров напрямую, в коде выше не изменится ни строчки: base_url подставляется любой.

Дальше по теме: как заставить модель отдавать JSON надёжно - в разборе структурированного ответа; как раздавать задачи разным моделям по цене - в статье о маршрутизации; текущие цены и скидки по каждой модели - на странице моделей.

FAQ

Какая модель дешевле всего разбирает документы в JSON?

На замере 23.08.2026 - gpt-5.6-mini: 0,069–0,071 ₽ за счёт из семи полей и двух позиций. Результат при этом совпал с моделями в 2–12 раз дороже. Для формальных задач извлечения полей дорогая модель выигрыша не даёт.

Почему одинаковые ответы стоят по-разному?

Из-за токенов рассуждения: модель может «думать» сотнями и тысячами токенов, которые не попадают в текст ответа, но попадают в счёт. Видно это только в поле usage ответа API.

Помогает ли max_tokens ограничить расходы?

Не гарантированно. В моём замере лимит в 40 токенов был проигнорирован во всех четырёх случаях, ответы вышли на 102–1281 токен. Это защита от бесконечной генерации, а не потолок счёта.

Что делать, если модель отвечает 503?

Проверить, лежит ли она у всех провайдеров сразу: если в ошибке All providers failed, повторять тот же запрос бессмысленно. Рабочая стратегия - заранее выбранная запасная модель другого семейства.

Сколько стоит разобрать тысячу счетов?

По ценам на 23.08.2026 - от ~70 ₽ на самой дешёвой модели до ~820 ₽ на самой дорогой из проверенных, при одинаковом результате. Точную цифру считайте на своём документе: длина счёта прямо влияет на число токенов входа.