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

Я Tech Lead и руководитель направления Java | Kotlin разработки в FinTech & E‑commerce и преподаю на курсах разработки и архитектуры в ОТУС.

В апреле мы подключили агента к рутине, которую никто в команде не любил: разбор упавших интеграционных тестов и подготовка черновика фикса. Взяли топовую модель по принципу «пусть работает лучшее».

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

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

Дальше я сел считать. И выяснил, что считал всё это время неправильно — причём тремя разными способами сразу.

Рис. 1. Цена или скорость: компромисс, который приходится выбирать на каждом запросе
Рис. 1. Цена или скорость: компромисс, который приходится выбирать на каждом запросе

Почему прайс перестал быть ориентиром

Пару лет назад вопрос решался просто: моделей, способных тянуть сложную задачу, было три штуки, стоили они одинаково, разница была видна невооружённым глазом. Сейчас иначе. Мне попалась точная формулировка в разборе июльской волны релизов: вопрос «какая модель лучшая» распался на «какая лучшая на доллар, на тип задачи, на бюджет токенов» (buildthisnow.com).

За одну неделю июля вышло семь заметных моделей от пяти вендоров — перепробовать это руками невозможно.

Второе, что изменилось, — разброс цен. Тарифы на начало августа 2026, здесь и далее сверяйте с прайс‑страницами провайдеров перед тем, как что‑то на них строить:

Модель

Вход, $/1M токенов

Выход, $/1M токенов

GPT-5.6 Sol

5.00

30.00

Claude Opus 5

5.00

25.00

Kimi K3

3.00

15.00

Grok 4.5

2.00

6.00

Gemini 3.6 Flash

1.50

7.50

DeepSeek V4 Flash

0.14

0.28

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

Третье, самое неприятное для нас, бэкендеров. Агент — это не один запрос. В бенчмарке ProjDevBench, где шесть агентов строят проекты с нуля по двадцати задачам из восьми категорий, среднее потребление составило 138 ходов и 4,81 млн токенов на задачу при доле принятых решений 27,38% (arXiv:2602.01655). Это среднее, а не медиана, и распределение с тяжёлым хвостом: зациклившийся агент вытягивает его вверх. Каждый шаг перечитывает всю накопленную историю — прайс за миллион токенов вам об этом не скажет ни слова.

Ошибка первая: считать токены не в том месте

Первое, обо что я споткнулся, — расхождение между моими расчётами и биллингом ровно вдвое. Причина оказалась банальной: я суммировал usage по финальному ответу агента.

А платите вы за каждый шаг. Агент прочитал файл — это запрос. Запустил тесты, получил лог — ещё запрос, и в него уехала вся предыдущая история. К десятому шагу вы оплачиваете первый файл в десятый раз.

Второй нюанс всплыл позже и дал расхождение уже в другую сторону. Провайдеры тарифицируют повторно отправленный префикс дешевле: Anthropic и новые модели OpenAI берут за чтение из кэша около 0,1 обычной входной ставки, Google на неявном кэшировании — порядка 75%. Простая сумма входных токенов такой скидки не видит и завышает счёт. Cached‑токены нужны отдельной строкой (Kotlin):

/**
 * ВАЖНО: здесь inputTokens — ПОЛНЫЙ вход, включая прочитанное из кэша.
 * Провайдеры отдают эти поля по-разному: у одних cached-токены входят
 * в общий счётчик, у других лежат отдельным полем и в него не входят.
 * Приводите к единой семантике на границе с API, иначе вычитание ниже
 * даст либо двойной учёт, либо отрицательное значение.
 */
data class Usage(
    val inputTokens: Long,
    val cachedInputTokens: Long,
    val outputTokens: Long
) {
    init {
        require(inputTokens >= 0 && cachedInputTokens >= 0 && outputTokens >= 0) {
            "usage не может быть отрицательным"
        }
        require(cachedInputTokens <= inputTokens) {
            "cachedInputTokens ($cachedInputTokens) > inputTokens ($inputTokens): " +
            "похоже, семантика полей провайдера не приведена к общей"
        }
    }
    val uncachedInputTokens: Long get() = inputTokens - cachedInputTokens
}

data class Pricing(
    val inputPerMillion: Double,
    val outputPerMillion: Double,
    val cacheReadMultiplier: Double = 0.1   // сверяйте с документацией провайдера
)

fun Usage.costUsd(p: Pricing): Double =
    uncachedInputTokens / 1_000_000.0 * p.inputPerMillion +
    cachedInputTokens / 1_000_000.0 * p.inputPerMillion * p.cacheReadMultiplier +
    outputTokens / 1_000_000.0 * p.outputPerMillion

Проверка в init выглядит избыточной ровно до первого прогона на новом провайдере: одно из API отдавало cached‑токены отдельным полем, не включая их в общий счётчик, и наивное вычитание уводило часть значений в минус.

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

Теперь сбор метрик по прогону целиком:

data class RunMetrics(
    val model: String,
    val wallClockMs: Long,
    val toolCalls: Int,
    val usage: Usage,
    val evaluation: EvaluationResult
)

class MeteredAgent(private val agent: Agent) {
    fun runTask(model: String, task: Task): RunMetrics {
        // Монотонные часы: на измерении интервалов настенное время
        // может дать отрицательную дельту при коррекции NTP
        val started = TimeSource.Monotonic.markNow()
        val trace = agent.execute(model, task)
        return RunMetrics(
            model = model,
            wallClockMs = started.elapsedNow().inWholeMilliseconds,
            toolCalls = trace.steps.count { it.isToolCall },
            // Суммируем по КАЖДОМУ шагу, а не по финальному ответу
            usage = Usage(
                inputTokens = trace.steps.sumOf { it.usage.inputTokens },
                cachedInputTokens = trace.steps.sumOf { it.usage.cachedInputTokens },
                outputTokens = trace.steps.sumOf { it.usage.outputTokens }
            ),
            evaluation = Evaluator.check(trace.workdir)
        )
    }
}

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

/** Детерминированные проверки: воспроизводимы, гоняются в CI без человека. */
data class AutomatedChecks(
    val buildSucceeded: Boolean,
    val testsPassed: Boolean,
    val lintPassed: Boolean,
    val coverageNotDropped: Boolean
) {
    val passed: Boolean get() =
        buildSucceeded && testsPassed && lintPassed && coverageNotDropped
}

data class EvaluationResult(
    val automated: AutomatedChecks,
    val humanReviewAccepted: Boolean
) {
    val isSuccess: Boolean get() = automated.passed && humanReviewAccepted
}

Машинные проверки и человеческое решение разделены намеренно: первые воспроизводимы и гоняются в CI, второе — суждение, и отказ здесь может быть чисто архитектурным. Ситуация «CI зелёный, но ревьюер не согласен с решением» — не баг метрики, а сигнал: если такие отказы копятся, проблема не в надёжности модели, а в том, что вы не описали ей ограничения проекта. Разделив поля, вы это увидите; слепив в одно — нет.

Чем строже критерий, тем ниже доля успеха и тем честнее дальнейшая арифметика. Проверять надо независимо, даже если модель отчиталась об успехе: модели регулярно объявляют задачу решённой при красной сборке.

Ошибка вторая: считать цену прогона вместо цены результата

Вот это и есть главная подмена. Цена прогона — не та величина, которая вам нужна. Вам нужна цена одной закрытой задачи.

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

Схема на рис. 2 показывает, где именно рождается правильная метрика.

Рис. 2. Схема принципиальная: где рождается честная метрика стоимости
Рис. 2. Схема принципиальная: где рождается честная метрика стоимости

Главное, что стоит вынести из схемы: обе ветки, и успешная, и провальная, сходятся в одном узле. Формула получается такая:

Цена закрытой задачи = (стоимость прогона + стоимость человеко‑времени на прогон) / доля успешных прогонов

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

Цена закрытой задачи = (p × стоимость успешного прогона + (1 − p) × стоимость провального) / p

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

Слагаемое с человеко‑временем я поначалу не учитывал вообще. Зря — сейчас покажу, почему именно оно всё решает.

Считаем: сколько стоит закрытая задача

Дальше — открытая арифметика на публичных тарифах. Это не замер, а расчётная модель: я фиксирую профиль нагрузки и смотрю, как ведут себя деньги. Свои доли успеха вы подставите сами, механика от этого не меняется.

Возьмём скромный по нынешним меркам профиль: 250 тысяч входных токенов и 25 тысяч выходных на один прогон. Это заметно меньше средних 4,81 млн из ProjDevBench, так что оценка консервативная.

Модель

Прогон без кэша

Прогон при 80% попаданий в кэш

GPT-5.6 Sol

$2.00

$1.10

Claude Opus 5

$1.88

$0.98

Kimi K3

$1.12

$0.59

Grok 4.5

$0.65

$0.29

Gemini 3.6 Flash

$0.56

$0.29

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

Порог считается в несколько строк, так что подставить свои числа можно прямо сейчас (Kotlin):

/**
 * Минимальная доля успеха, при которой дешёвая модель
 * выгоднее дорогой с учётом стоимости человеко-времени.
 */
fun breakEvenRate(
    cheapRunCost: Double,      // стоимость прогона дешёвой модели
    strongRunCost: Double,     // стоимость прогона дорогой модели
    strongSuccessRate: Double, // её доля успеха
    humanCostPerRun: Double    // стоимость времени инженера на один прогон
): Double {
    require(cheapRunCost >= 0 && strongRunCost >= 0) { "стоимость прогона не может быть отрицательной" }
    require(humanCostPerRun >= 0) { "стоимость человеко-времени не может быть отрицательной" }
    require(strongSuccessRate > 0.0 && strongSuccessRate <= 1.0) { "доля успеха вне диапазона (0, 1]" }
    return strongSuccessRate *
        (cheapRunCost + humanCostPerRun) / (strongRunCost + humanCostPerRun)
}

// breakEvenRate(0.56, 1.88, 0.9, 0.0)  -> 0.27
// breakEvenRate(0.56, 1.88, 0.9, 6.70) -> 0.76
// breakEvenRate(0.29, 0.98, 0.9, 6.70) -> 0.82   // тот же расчёт с кэшем

Пусть Opus закрывает задачу в 90% прогонов. Пороги для Flash при разных допущениях:

Стоимость человеко‑времени на прогон

Без кэша

С кэшем

$0 (полностью автономный пайплайн)

27%

27%

$6.70 (10 минут инженера по ставке $40/час)

76%

82%

$20 (полчаса senior‑инженера)

85%

87%

Именно этот расчёт заставил меня пересмотреть подход.

Если человеко‑время не учитывать, дешёвая модель почти непобедима: ей достаточно закрывать чуть больше четверти задач, чтобы выигрывать по деньгам. Эта логика арифметически верна ровно до тех пор, пока за агентом никто не смотрит.

Но за ним смотрят. Каждый прогон кто‑то ставит, дожидается, читает диф и решает, годится ли результат. Как только вы добавляете в формулу десять минут инженера, порог подскакивает с 27% до 76%. А если вы ещё и настроили кэширование — то есть сделали ровно то, что советуют все гайды по экономии — порог поднимается до 82%.

Причина неочевидная, но простая: кэш уменьшает токенную часть счёта, а человеческая остаётся на месте.

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

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

  • Практический вывод: для полностью автономного пайплайна, где провал не стоит ничего кроме токенов, дешёвый тариф выигрывает почти всегда. Для сценария с человеком в контуре — почти никогда.

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

Качество уже встроено в долю успешных прогонов, а косвенно влияет ещё и на объём контекста, число итераций и количество последующих правок. Формула не отменяет качество — она переводит его в деньги.

Ошибка третья: брать долю успеха из лидерборда

Формула упирается в одно число — долю успешных прогонов. И тут возникает соблазн не мерить её самому, а взять из бенчмарка. Я на этом обжёгся раньше всего.

Проблема даже не в том, что бенчмарк меряет другую задачу. Проблема в том, что публичные цифры часто несопоставимы между собой. Terminal‑Bench 2.1 ведёт официальный лидерборд, и там результат — это оценка пары «агент плюс модель», а не модели самой по себе: один и тот же бэкенд под разными обвязками показывает заметно разные числа.

Сравнивать строчки из вендорских анонсов с лидербордными можно только по направлению, но не по абсолютной величине.

Дальше хуже. Исследование корпоративных агентов зафиксировало разрыв в 37% между лабораторными оценками и поведением в проде, а фреймворк CLEAR показал 50-кратный разброс стоимости при сопоставимой точности на одних и тех же задачах (kili‑technology.com).

Пятипроцентный разрыв на бенчмарке может обернуться сорокапроцентным на вашем репозитории — в любую сторону.

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

Что с этим делать: не выбирать модель, а строить слой

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

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

Команды, внедрившие настроенный слой маршрутизации, отчитываются о снижении счёта на 40–85% (digitalapplied.com). Разброс огромный и не случайный: у нижней границы те, чей трафик однородно сложный и роутеру нечего отдавать вниз, у верхней — те, у кого преобладают короткие однотипные запросы. Где внутри этой вилки окажетесь вы, покажет только распределение ваших задач.

Из готовых решений в шорт‑листе обычно оказываются Portkey (с марта под Apache 2.0, больше 1600 моделей у 250+ провайдеров) и Model Router в Azure AI Foundry, где режим Balanced берёт самую дешёвую модель в пределах 1–2% качества от лучшей. Оговорюсь честно: ни то, ни другое я на боевой нагрузке не гонял, рекомендацией это не считаю — только ориентиром.

Полная конструкция — на рис. 3.

Рис. 3. План действий: четырёхслойная схема управления стоимостью запроса
Рис. 3. План действий: четырёхслойная схема управления стоимостью запроса

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

Маршрутизация меняет тариф. Batch API — отдельная ветка для всего, что не горит: провайдеры дают за отложенную обработку около половины цены, и ночная переиндексация или пакетная разметка туда переезжают безболезненно. А слой 4, eval‑проверка, держит ту самую долю успеха из формулы и не даёт дешёвой модели тихо утащить её вниз. Под eval здесь понимается не LLM‑as‑a‑judge, а тот же набор детерминированных проверок: сборка, тесты, линтер, пороги покрытия.

Судить модель другой моделью в денежном контуре я бы не стал — это ещё один источник ошибки и ещё одна строчка в счёте.

Схема намеренно упрощена до денежного контура. В продовом шлюзе рядом живут ещё квоты и rate limiting, circuit breaker на падающего провайдера, failover между вендорами и сквозной трейсинг по маршрутам. Без них конструкция красиво считает деньги ровно до первого инцидента у провайдера.

Конфиг роутера в простейшем виде (YAML):

routes:
  - name: simple-lookup
    match: { intent: [ "explain", "summarize", "lint" ] }
    model: gemini-3.6-flash
    max_retries: 1
    fallback: grok-4.5

  - name: code-change
    match: { intent: [ "refactor", "bugfix" ], touches_tests: true }
    model: grok-4.5
    eval: gradle-test-green
    fallback: claude-opus-5      # эскалация только после провала eval

  - name: critical
    match: { paths: [ "payments/**", "ledger/**" ] }
    model: claude-opus-5
    routing: disabled            # здесь экономия не окупает риск

Тут сразу возникает резонный вопрос: откуда берётся intent, готовым он не приходит. Вариантов четыре, по возрастанию цены: правила на метаданных запроса, классификация по эмбеддингам, отдельный вызов дешёвой модели‑классификатора (гибко, но это лишний round trip на каждый запрос) и дообученный классификатор. Мы начали с правил на путях в репозитории: скучно, зато отлаживается за пять минут и не добавляет к счёту ни цента.

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

Реальная история: сколько токенов вообще лишние

Тут стоит рассказать про кейс, который заставил меня пересмотреть первый слой схемы.

Старший инженер Netflix Теджас Чопра копнул не в сторону выбора модели, а в сторону того, что именно уезжает в контекст. По его оценке, до 90% отправляемых в модель токенов избыточны.

Причина знакома любому, кто подключал MCP‑инструменты: ответы инструментов написаны для человека. Модели нужны три поля, а прилетает 200 строк JSON.

Решение он оформил в проект Headroom и в январе 2026 выложил под Apache 2.0 (github.com/chopratejas/headroom). Это прослойка между агентом и API модели, которая сжимает контекст шестью движками — AST‑осведомлённое сжатие кода для Python, JavaScript, Go, Rust, Java и C++, отдельный компрессор JSON, обученная на агентных трейсах модель для текста. Сжатие обратимо: оригиналы кэшируются локально, и модель может запросить полный фрагмент.

Числа впечатляют: сокращение объёма на 60–95% без измеримой потери качества (точность держалась на GSM8K и TruthfulQA), в демонстрации контекст ужимался с 10 144 токенов до 1 260 — и в нём по‑прежнему находилась та же критическая ошибка в логе. На Open Source Summit Чопра озвучил накопленный эффект: около $700 000 сэкономленных пользователям и порядка 200 миллиардов высвобожденных токенов (opensourceforu.com).

Отдельно отмечу CacheAligner: он определяет, какие части контекста не изменились с прошлого вызова, чтобы не выбивать KV‑кэш целиком из‑за одной новой даты или UUID. Узнаваемая боль — вставил в промпт текущее время, и весь кэш префикса пошёл насмарку.

Проект честно называют сыроватым. Но идея правильная в любом масштабе: сначала посмотреть, что вы отправляете, и только потом торговаться за тариф.

Где эта модель не работает

Обязан обозначить границы, иначе получится не разбор, а проповедь.

  • Первое и главное: всё выше — расчёт, а не замер. Я показываю механику и пороги, а не утверждаю, что у вас Flash закрывает ровно столько‑то процентов задач. Долю успеха придётся померить на своей задаче и своём репозитории, иначе формула останется красивой и бесполезной.

  • Второе: человеко‑время в формуле — константа, а в жизни это сумма слагаемых, распределённых неравномерно: провальный прогон почти всегда съедает больше времени, чем успешный. Более точная модель развела бы стоимость успеха и провала на два числа. Я этого не делал сознательно: усложнение не меняет направление вывода, а порог только повышает.

  • Третье: цены и механика кэширования двигаются. Claude Sonnet 5 стартовал по вводному тарифу $2/$10, а с сентября переходит на $3/$15. Скидки на чтение из кэша тоже различаются между провайдерами и поколениями моделей. Любые числа в статье — снимок на дату.

  • Четвёртое: профиль в 250 тысяч входных токенов я взял с потолка, пусть и консервативно. Если ваш агент потребляет вчетверо больше, соотношения между строчками сохранятся, но вес человеко‑времени упадёт — и пороги сдвинутся обратно в пользу дешёвой модели.

  • Пятое: маршрутизация не окупается, если весь ваш трафик действительно сложный — роутеру нечего оптимизировать. И включать его без eval нельзя: будете тихо деградировать, пока не пожалуются пользователи.

  • Шестое: self‑hosting открытых весов не дешевле автоматически. Вы меняете плату за токены на плату за GPU и за людей, которые это эксплуатируют.

Что сделать у себя на этой неделе

Могу себе представить, как после такого текста хочется закрыть вкладку и вернуться к задачам. Поэтому сведу к минимуму:

  1. Проверьте, где вы считаете токены. Если по финальному ответу — недосчитываете кратно; если не отделяете cached‑токены — наоборот, завышаете.

  2. Ужесточите критерий успеха. Зелёные тесты — это минимум, а не приёмка.

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

  4. Оцените стоимость человеко‑времени на один прогон. Именно она решает, есть ли смысл в дешёвом тарифе.

  5. Подставьте всё это в формулу цены закрытой задачи. Возможно, вы удивитесь так же, как удивился я.

  6. Заведите eval раньше роутера. Без него маршрутизация — это выключенный свет в комнате с граблями.

  7. Составьте список путей и модулей, где экономия запрещена. У нас это платежи и учёт.

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

И вопрос к тем, кто уже гонял агентов в проде: вы учитываете человеко‑время в стоимости? Мне интересно, у скольких команд после такого пересчёта дешёвый тариф перестал быть дешёвым — напишите в комментариях свои цифры.

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

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

Ближайшие бесплатные открытые уроки по теме:

  • 3 сентября, 20:00. «Локальные LLM модели для разработки». Записаться

  • 8 сентября, 20:00. «Что надо знать про работу LLM моделей». Записаться

  • 8 сентября, 20:00. «ИИ против бага: как разобрать инцидент в Python‑проекте от логов до исправления». Записаться

Полный список бесплатных уроков августа можно найти в дайджесте.

Комментарии (1)


  1. Dhwtj
    22.08.2026 10:07

    Качество/цена выросли за год в таких задачах?

    И что там с альтернативной в виде одной ставки кожаного?