- BrainTools - https://www.braintools.ru -
Всем привет, меня зовут Сергей Прощаев и в этой статье расскажу про то, почему выбор модели [1] по цене за миллион токенов — это самый быстрый способ получить неприятный счёт в конце месяца.
Я Tech Lead и руководитель направления Java | Kotlin разработки в FinTech & E‑commerce и преподаю на курсах разработки и архитектуры в ОТУС.
В апреле мы подключили агента к рутине, которую никто в команде не любил: разбор упавших интеграционных тестов и подготовка черновика фикса. Взяли топовую модель по принципу «пусть работает лучшее».
Через три недели финансовый контроллер прислал выписку и вежливо спросил, что это за строчка — годовая проекция перекрывала стоимость ещё одного разработчика. Я сделал то, что казалось очевидным: перешёл на модель подешевле.
Счёт упал примерно на треть, а количество задач, которые агент доводил до зелёных тестов, упало сильнее — и каждая закрытая задача стала обходиться дороже, чем была.
Дальше я сел считать. И выяснил, что считал всё это время неправильно — причём тремя разными способами сразу.
Пару лет назад вопрос решался просто: моделей, способных тянуть сложную задачу, было три штуки, стоили они одинаково, разница была видна невооружённым глазом. Сейчас иначе. Мне попалась точная формулировка в разборе июльской волны релизов: вопрос «какая модель лучшая» распался на «какая лучшая на доллар, на тип задачи, на бюджет токенов» (buildthisnow.com [2]).
За одну неделю июля вышло семь заметных моделей от пяти вендоров — перепробовать это руками невозможно.
Второе, что изменилось, — разброс цен. Тарифы на начало августа 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 показывает, где именно рождается правильная метрика.
Главное, что стоит вынести из схемы: обе ветки, и успешная, и провальная, сходятся в одном узле. Формула получается такая:
Цена закрытой задачи = (стоимость прогона + стоимость человеко‑времени на прогон) / доля успешных прогонов
Здесь спрятано допущение, которое я делаю сознательно: успешный и провальный прогон стоят одинаково. В жизни это не так — провал обычно дороже. Строгая форма выглядела бы вот так:
Цена закрытой задачи = (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% |
Именно этот расчёт заставил меня пересмотреть подход.
Если человеко‑время не учитывать, дешёвая модель почти непобедима: ей достаточно закрывать чуть больше четверти задач, чтобы выигрывать по деньгам. Эта логика [3] арифметически верна ровно до тех пор, пока за агентом никто не смотрит.
Но за ним смотрят. Каждый прогон кто‑то ставит, дожидается, читает диф и решает, годится ли результат. Как только вы добавляете в формулу десять минут инженера, порог подскакивает с 27% до 76%. А если вы ещё и настроили кэширование — то есть сделали ровно то, что советуют все гайды по экономии — порог поднимается до 82%.
Причина неочевидная, но простая: кэш уменьшает токенную часть счёта, а человеческая остаётся на месте.
По мере оптимизации токенных расходов человеко‑время занимает всё большую долю общей стоимости, и разница между тарифами перестаёт быть главным фактором.
Условие важное: это верно там, где человеческое время сопоставимо со стоимостью прогона или превышает её. В полностью автономном пайплайне тариф по‑прежнему решает всё.
Практический вывод: для полностью автономного пайплайна, где провал не стоит ничего кроме токенов, дешёвый тариф выигрывает почти всегда. Для сценария с человеком в контуре — почти никогда.
Оговорюсь про качество модели: в этой формуле оно не является отдельной переменной, но это не значит, что оно не важно.
Качество уже встроено в долю успешных прогонов, а косвенно влияет ещё и на объём контекста, число итераций и количество последующих правок. Формула не отменяет качество — она переводит его в деньги.
Формула упирается в одно число — долю успешных прогонов. И тут возникает соблазн не мерить её самому, а взять из бенчмарка. Я на этом обжёгся раньше всего.
Проблема даже не в том, что бенчмарк меряет другую задачу. Проблема в том, что публичные цифры часто несопоставимы между собой. официальный лидерборд [4], и там результат — это оценка пары «агент плюс модель», а не модели самой по себе: один и тот же бэкенд под разными обвязками показывает заметно разные числа.
Сравнивать строчки из вендорских анонсов с лидербордными можно только по направлению, но не по абсолютной величине.
Дальше хуже. Исследование корпоративных агентов зафиксировало разрыв в 37% между лабораторными оценками и поведением [5] в проде, а фреймворк kili‑technology.com [6]).
Пятипроцентный разрыв на бенчмарке может обернуться сорокапроцентным на вашем репозитории — в любую сторону.
Вывод простой: лидерборд — повод добавить модель в шорт‑лист, а не источник числа для расчёта. Долю успеха придётся померить руками, и это единственная часть работы, которую нельзя ни у кого списать.
Помню, как на внутреннем обсуждении коллега предложил раз в квартал перевыбирать модель по результатам замера. Идея звучала разумно ровно до момента, когда я посчитал темп релизов — примерно один в два дня.
Мой вариант, который я использую сейчас: модель перестаёт быть архитектурным решением и становится параметром конфигурации, а решением становится слой между приложением и провайдерами.
Команды, внедрившие настроенный слой digitalapplied.com [7]). Разброс огромный и не случайный: у нижней границы те, чей трафик однородно сложный и роутеру нечего отдавать вниз, у верхней — те, у кого преобладают короткие однотипные запросы. Где внутри этой вилки окажетесь вы, покажет только распределение ваших задач.
Из готовых решений в шорт‑листе обычно оказываются Portkey (с марта под Apache 2.0, больше 1600 моделей у 250+ провайдеров) и Model Router в Azure AI Foundry, где режим Balanced берёт самую дешёвую модель в пределах 1–2% качества от лучшей. Оговорюсь честно: ни то, ни другое я на боевой нагрузке не гонял, рекомендацией это не считаю — только ориентиром.
Полная конструкция — на рис. 3.
Главная мысль схемы: слои бьют по разным частям счёта и поэтому складываются. Сжатие уменьшает то, за что вы платите на входе. Кэш убирает повторную оплату одного и того же.
Маршрутизация меняет тариф. Batch API — отдельная ветка для всего, что не горит: провайдеры дают за отложенную обработку около половины цены, и ночная переиндексация или пакетная разметка туда переезжают безболезненно. А слой 4, eval‑проверка, держит ту самую долю успеха из формулы и не даёт дешёвой модели тихо утащить её вниз. Под eval здесь понимается не LLM‑as‑a‑judge, а тот же набор детерминированных проверок: сборка, тесты, линтер, пороги покрытия.
Судить модель другой моделью в денежном контуре я бы не стал — это ещё один источник ошибки [8] и ещё одна строчка в счёте.
Схема намеренно упрощена до денежного контура. В продовом шлюзе рядом живут ещё квоты и 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 [9]). Это прослойка между агентом и API модели, которая сжимает контекст шестью движками — AST‑осведомлённое сжатие кода для Python, JavaScript, Go, Rust, Java и C++, отдельный компрессор JSON, обученная на агентных трейсах модель для текста. Сжатие обратимо: оригиналы кэшируются локально, и модель может запросить полный фрагмент.
Числа впечатляют: сокращение объёма на 60–95% без измеримой потери качества (точность держалась на opensourceforu.com [10]).
Отдельно отмечу CacheAligner: он определяет, какие части контекста не изменились с прошлого вызова, чтобы не выбивать боль [11] — вставил в промпт текущее время, и весь кэш префикса пошёл насмарку.
Проект честно называют сыроватым. Но идея правильная в любом масштабе: сначала посмотреть, что вы отправляете, и только потом торговаться за тариф.
Обязан обозначить границы, иначе получится не разбор, а проповедь.
Первое и главное: всё выше — расчёт, а не замер. Я показываю механику и пороги, а не утверждаю, что у вас Flash закрывает ровно столько‑то процентов задач. Долю успеха придётся померить на своей задаче и своём репозитории, иначе формула останется красивой и бесполезной.
Второе: человеко‑время в формуле — константа, а в жизни это сумма слагаемых, распределённых неравномерно: провальный прогон почти всегда съедает больше времени, чем успешный. Более точная модель развела бы стоимость успеха и провала на два числа. Я этого не делал сознательно: усложнение не меняет направление вывода, а порог только повышает.
Третье: цены и механика кэширования двигаются. Claude Sonnet 5 стартовал по вводному тарифу $2/$10, а с сентября переходит на $3/$15. Скидки на чтение из кэша тоже различаются между провайдерами и поколениями моделей. Любые числа в статье — снимок на дату.
Четвёртое: профиль в 250 тысяч входных токенов я взял с потолка, пусть и консервативно. Если ваш агент потребляет вчетверо больше, соотношения между строчками сохранятся, но вес человеко‑времени упадёт — и пороги сдвинутся обратно в пользу дешёвой модели.
Пятое: маршрутизация не окупается, если весь ваш трафик действительно сложный — роутеру нечего оптимизировать. И включать его без eval нельзя: будете тихо деградировать, пока не пожалуются пользователи.
Шестое: self‑hosting открытых весов не дешевле автоматически. Вы меняете плату за токены на плату за GPU и за людей, которые это эксплуатируют.
Могу себе представить, как после такого текста хочется закрыть вкладку и вернуться к задачам. Поэтому сведу к минимуму:
Проверьте, где вы считаете токены. Если по финальному ответу — недосчитываете кратно; если не отделяете cached‑токены — наоборот, завышаете.
Ужесточите критерий успеха. Зелёные тесты — это минимум, а не приёмка.
Померьте долю успеха на своей задаче. Даже несколько прогонов дадут порядок величины — это несравнимо лучше, чем цифра из чужого бенчмарка.
Оцените стоимость человеко‑времени на один прогон. Именно она решает, есть ли смысл в дешёвом тарифе.
Подставьте всё это в формулу цены закрытой задачи. Возможно, вы удивитесь так же, как удивился я.
Заведите eval раньше роутера. Без него маршрутизация — это выключенный свет в комнате с граблями.
Составьте список путей и модулей, где экономия запрещена. У нас это платежи и учёт.
Модель — это не архитектурное решение, а параметр конфигурации, который вы обязаны уметь менять за один деплой: через две недели выйдет ещё семь моделей и половина ваших выводов устареет. Инженерное решение находится не в выборе строчки из прайса, а в том, умеете ли вы считать, во сколько вам обходится один закрытый тикет.
И вопрос к тем, кто уже гонял агентов в проде: вы учитываете человеко‑время в стоимости? Мне интересно, у скольких команд после такого пересчёта дешёвый тариф перестал быть дешёвым — напишите в комментариях свои цифры.

Когда ИИ уже встроен в разработку, быстро выясняется: выбрать модель и подключить API недостаточно. Нужно понимать, где агент теряет деньги, почему ошибается, как проверять его результат и в каких задачах локальная или более дорогая модель действительно окупается.
Если хочется перейти от экспериментов с LLM к управляемому рабочему процессу, важно разобраться и в самих моделях, и в сценариях их применения — тогда выбор перестаёт быть гаданием по прайсу и превращается в инженерное решение.
Ближайшие бесплатные открытые уроки по теме:
3 сентября, 20:00. «Локальные LLM модели для разработки». Записаться [12]
8 сентября, 20:00. «Что надо знать про работу LLM моделей». Записаться [13]
8 сентября, 20:00. «ИИ против бага: как разобрать инцидент в Python‑проекте от логов до исправления». Записаться [14]
Полный список бесплатных уроков августа можно найти в дайджесте. [15]
Автор: sproshchaev
Источник [16]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/34740
URLs in this post:
[1] модели: https://otus.pw/8Poa/
[2] buildthisnow.com: http://buildthisnow.com
[3] логика: http://www.braintools.ru/article/7640
[4] официальный лидерборд: https://www.tbench.ai/leaderboard/terminal-bench/2.1
[5] поведением: http://www.braintools.ru/article/9372
[6] kili‑technology.com: http://kili-technology.com
[7] digitalapplied.com: http://digitalapplied.com
[8] ошибки: http://www.braintools.ru/article/4192
[9] github.com/chopratejas/headroom: https://github.com/chopratejas/headroom
[10] opensourceforu.com: http://opensourceforu.com
[11] боль: http://www.braintools.ru/article/9901
[12] Записаться: https://otus.pw/HPU4/
[13] Записаться: https://otus.pw/Stqe/
[14] Записаться: https://otus.pw/FfXS/
[15] в дайджесте.: https://otus.pw/m1b7/
[16] Источник: https://habr.com/ru/companies/otus/articles/1067744/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1067744
Нажмите здесь для печати.