Дешёвая LLM оказалась дороже: считаем cost per successful task, а не цену токена
Низкая цена за миллион токенов ещё не означает экономию. Если модель чаще ошибается, запускает retries и требует ручной проверки, стоимость выполненной задачи растёт. Покажу, как это измерить и использовать в роутинге.
Почему цена токена вводит в заблуждение
В таблице провайдеров легко выбрать модель, которая дешевле за миллион входных и выходных токенов. Но пользователь покупает не токены. Ему нужен корректно выполненный запрос: извлечённые данные, классификация, ответ поддержки или завершённый шаг агента.
Если дешёвая модель чаще возвращает невалидный JSON, выбирает неверный инструмент или требует повторной генерации, экономия исчезает. Поэтому базовая метрика для production — стоимость успешной задачи, а не стоимость одного вызова.
Простой пример
Допустим, модель A стоит 0,20 рубля за средний вызов, а модель B — 0,35 рубля. На первый взгляд A дешевле почти вдвое.
Но из 100 задач модель A успешно завершает 70. Ещё 20 проходят после retry, а 10 уходят человеку. Модель B успешно закрывает 92 задачи с первой попытки, шесть — после retry, две — вручную.
Если ручная проверка стоит 50 рублей, итог может выглядеть так:
• модель A: 100 вызовов + 30 повторов + 10 ручных проверок;
• модель B: 100 вызовов + 8 повторов + 2 ручные проверки.
Более дорогая по токенам модель оказывается дешевле на уровне процесса.
Что включать в полную стоимость
Я считаю пять компонентов:
• стоимость input и output tokens;
• стоимость повторных LLM-вызовов;
• вызовы embeddings, reranker и внешних инструментов;
• инфраструктуру: CPU, очередь, хранение и observability;
• ручную проверку и исправление ошибок.
Для агента добавляется стоимость побочных эффектов. Неверно созданная сделка в CRM или отправленное письмо может быть дороже всех токенов за месяц.
Формула cost per successful task
Минимальный вариант:
cost_per_success = total_cost / successful_tasks
Но для сравнения моделей полезно считать отдельно прямые и операционные расходы:
CODE:
from dataclasses import dataclass
@dataclass
class ModelStats:
tasks: int
successful: int
llm_cost: float
tool_cost: float
infra_cost: float
manual_reviews: int
manual_review_cost: float
@property
def total_cost(self) -> float:
return (
self.llm_cost
+ self.tool_cost
+ self.infra_cost
+ self.manual_reviews * self.manual_review_cost
)
@property
def cost_per_success(self) -> float:
if self.successful == 0:
return float("inf")
return self.total_cost / self.successful
Успех нужно определить заранее
Самая сложная часть — не формула, а определение successful. HTTP 200 не означает, что задача решена. Валидный JSON тоже не гарантирует корректный результат.
Для каждого сценария нужен собственный критерий:
• extraction: обязательные поля заполнены и проходят бизнес-валидацию;
• support: ответ соответствует базе знаний и не требует эскалации;
• tool calling: выбран правильный инструмент и достигнуто ожидаемое состояние;
• code generation: тесты прошли, а изменение принято;
• RAG: ответ содержит подтверждённые источники и проходит groundedness check.
Одна общая метрика качества для всех маршрутов скрывает проблемы. Считать нужно по модели, маршруту, классу задачи и версии prompt.
Почему retries и repair-loop надо учитывать отдельно
Retry после timeout и repair после ошибки схемы — разные причины дополнительных расходов. Если объединить их в один счётчик, непонятно, что исправлять: сеть, provider или качество модели.
Я сохраняю для каждого запуска:
• initial model call;
• число retries и причина;
• число repair-вызовов;
• использованные tools;
• итоговый статус задачи;
• факт ручной проверки;
• полную стоимость цепочки.
Тогда можно увидеть, что модель дешёвая на первом вызове, но дорогая из-за repair rate.
Пример агрегации событий
CODE:
def aggregate_task(events):
result = {
"llm_cost": 0.0,
"tool_cost": 0.0,
"retries": 0,
"repairs": 0,
"manual_review": False,
"success": False,
}
for event in events:
if event["type"] == "llm_call":
result["llm_cost"] += event["cost"]
elif event["type"] == "tool_call":
result["tool_cost"] += event["cost"]
elif event["type"] == "retry":
result["retries"] += 1
elif event["type"] == "repair":
result["repairs"] += 1
elif event["type"] == "manual_review":
result["manual_review"] = True
elif event["type"] == "task_completed":
result["success"] = event["accepted"]
return result
Не сравнивайте модели на разных задачах
Если модель A получает простые запросы, а модель B — сложные, средний cost per success будет бесполезен. Нужен одинаковый набор задач или стратификация по сложности.
Практичный вариант — shadow evaluation. Новый кандидат получает копию production-запроса, но не выполняет внешние действия. Затем результаты обеих моделей проходят одинаковую автоматическую и выборочную ручную проверку.
Для tool calls необходимо блокировать побочные эффекты: использовать sandbox, mock-инструменты или режим dry-run.
Как учитывать latency
Самая дешёвая успешная задача может отвечать 30 секунд. Поэтому стоимость нельзя рассматривать отдельно от latency и SLA.
Я сравниваю модели по трём координатам:
• cost per successful task;
• success rate без ручного вмешательства;
• p95 end-to-end latency.
Модель проходит в production только тогда, когда укладывается во все три ограничения. Выигрыш в цене не компенсирует нарушение SLA.
Маршрутизация по сложности
Выбирать одну модель для всего трафика необязательно. Простые задачи можно отправлять дешёвой модели, сложные — более сильной.
Но router тоже ошибается и стоит денег. Его нужно включать в общий расчёт, а fallback считать частью стоимости задачи.
CODE:
async def run_with_fallback(task):
route = await classify_complexity(task)
primary = cheap_model if route == "simple" else strong_model
result = await primary(task)
verdict = validate(result)
if verdict.ok:
return result
if verdict.retryable:
return await strong_model(task)
raise TaskFailed(verdict.reason)
В метриках такой запуск должен быть одной задачей с двумя модельными вызовами, а не двумя независимыми запросами.
Не оптимизируйте только среднее
Средний cost per success может выглядеть хорошо, пока небольшая группа запросов создаёт огромные расходы. Полезно смотреть p50, p95 и p99 стоимости задачи.
Длинный prompt, циклический agent loop или несколько неудачных tool calls формируют тяжёлый хвост. Для него нужны ограничения:
• максимальный токен-бюджет;
• лимит шагов агента;
• максимум retries и repairs;
• отдельный бюджет на tools;
• kill switch при превышении стоимости.
Эксперимент без риска для бюджета
Безопасный rollout можно провести так:
1. Зафиксировать определение успеха для каждого маршрута.
2. Собрать baseline стоимости, качества и latency текущей модели.
3. Прогнать кандидата в shadow-режиме на том же трафике.
4. Включить его на 1–5% production-запросов.
5. Ограничить retries, ручные проверки и максимальную стоимость задачи.
6. Сравнить доверительные интервалы, а не один средний день.
7. Расширять трафик только при устойчивом выигрыше.
Какие метрики вывести на дашборд
• tasks и successful_tasks;
• first_pass_success_rate;
• retry_rate и repair_rate;
• manual_review_rate;
• cost_per_success p50/p95/p99;
• end-to-end latency p50/p95/p99;
• tool error rate;
• стоимость по модели, маршруту и версии prompt.
Итог
Цена токена удобна для прайс-листа, но плохо описывает production. Правильная единица экономики — завершённая и принятая задача с учётом повторов, инструментов, инфраструктуры и ручного труда.
Иногда более сильная и дорогая модель оказывается дешевле, потому что быстрее приходит к правильному результату. А иногда дешёвая модель действительно выигрывает — но это должно подтверждаться на одинаковых задачах и полной цепочке расходов.
Вы уже считаете cost per successful task или пока сравниваете модели по цене токенов и стоимости одного API-вызова?