Практика · Экономика AI-систем

Как считать стоимость LLM не по токенам, а по завершённым бизнес-операциям

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

Уровень: средний Чтение: до 8 минут Результат: модель себестоимости проверяемой операции

Что именно нужно считать

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

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

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

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

Шаг 1. Зафиксируйте контракт операции

До сбора стоимости опишите четыре поля:

  1. Начало: событие, создающее операцию.
  2. Конец: наблюдаемое состояние, после которого работа завершена.
  3. Критерий успеха: машинная или подтверждённая человеком проверка.
  4. Граница затрат: какие компоненты относятся к операции.

Пример, а не отраслевой стандарт:

operation_type: document_registration
starts_when: document_received
succeeds_when:
  - required_fields_valid
  - record_persisted
  - persistence_ack_received
fails_when:
  - retry_budget_exhausted
  - validation_rejected
  - deadline_exceeded
cost_scope:
  - llm
  - embeddings
  - tool_calls
  - storage
  - human_review

Не меняйте критерий успеха задним числом ради улучшения метрики. Версионируйте контракт, например как document_registration:v3.

Шаг 2. Назначьте сквозной идентификатор

Каждый запуск получает operation_id. Все попытки, модельные запросы, вызовы инструментов и проверки должны ссылаться на него. Для повторной попытки используйте новый attempt_id, но сохраняйте исходный operation_id.

{
  "operation_id": "op_generated_id",
  "operation_type": "document_registration:v3",
  "attempt_id": "attempt_generated_id",
  "event_type": "llm_call",
  "component": "classifier",
  "status": "completed",
  "input_units": 0,
  "output_units": 0,
  "cost": 0,
  "currency": "billing_currency",
  "occurred_at": "timestamp"
}

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

Шаг 3. Записывайте стоимость как журнал событий

Удобнее хранить атомарные события, а итог собирать агрегацией. Минимальная схема:

Поле Назначение
operation_id Связывает все расходы одного бизнес-результата
attempt_id Разделяет первоначальный запуск и повторы
event_type LLM, инструмент, проверка, хранение, ручная работа
quantity Токены, секунды, вызовы или минуты работы
unit_price Цена единицы на момент события
cost Рассчитанная стоимость события
price_version Версия тарифной таблицы
status Результат события или операции

Стоимость события вычисляется по фактической единице тарификации:

event_cost = quantity × unit_price
operation_cost = Σ event_cost для одного operation_id

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

Шаг 4. Не теряйте повторы и незавершённые процессы

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

Используйте конечные статусы:

  • succeeded — критерий успеха подтверждён;
  • failed — процесс завершён без результата;
  • abandoned — продолжение прекращено внешним решением;
  • timed_out — превышён срок операции;
  • in_progress — финального состояния ещё нет.

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

Шаг 5. Рассчитайте три показателя

1. Полная стоимость успешной операции

cost_per_success =
  total_cost_of_all_finalized_operations
  / count(succeeded_operations)

Показатель распределяет расходы неудачных запусков между фактически полученными результатами.

2. Доля успешных операций

success_rate =
  count(succeeded_operations)
  / count(finalized_operations)

3. Стоимость потерь

failure_cost =
  sum(operation_cost where status != "succeeded")

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

Воспроизводимый расчёт в SQL

Ниже — безопасный запрос только на чтение. Он предполагает таблицу operation_events и отдельную таблицу operations с финальным статусом. Названия полей адаптируйте к своей схеме.

WITH event_costs AS (
  SELECT
    operation_id,
    SUM(cost) AS operation_cost
  FROM operation_events
  WHERE occurred_at >= :period_start
    AND occurred_at < :period_end
  GROUP BY operation_id
),
finalized AS (
  SELECT
    o.operation_id,
    o.status,
    COALESCE(e.operation_cost, 0) AS operation_cost
  FROM operations AS o
  LEFT JOIN event_costs AS e
    ON e.operation_id = o.operation_id
  WHERE o.finished_at >= :period_start
    AND o.finished_at < :period_end
    AND o.status IN ('succeeded', 'failed', 'abandoned', 'timed_out')
)
SELECT
  COUNT(*) AS finalized_operations,
  SUM(CASE WHEN status = 'succeeded' THEN 1 ELSE 0 END)
    AS succeeded_operations,
  SUM(operation_cost) AS total_cost,
  SUM(CASE WHEN status != 'succeeded' THEN operation_cost ELSE 0 END)
    AS failure_cost,
  SUM(operation_cost)
    / NULLIF(
        SUM(CASE WHEN status = 'succeeded' THEN 1 ELSE 0 END),
        0
      ) AS cost_per_success
FROM finalized;

Параметры :period_start и :period_end передавайте через механизм параметризации вашей базы данных. Не подставляйте пользовательские строки конкатенацией.

Условный пример расчёта

Это иллюстрация формулы, а не реальные данные компании или поставщика.

Операция Статус Попытки Условная стоимость
A succeeded 1 0,18
B succeeded 3 0,51
C failed 2 0,27
D timed_out 1 0,12

Всего потрачено 1,08 условной денежной единицы, успешных операций — две. Следовательно:

1,08 / 2 = 0,54 на одну успешную операцию

Если считать только успешные строки, получилось бы (0,18 + 0,51) / 2 = 0,345. Такая оценка занижает реальную себестоимость, потому что исключает оплаченные неудачи.

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

  1. Сверьте сумму. Стоимость событий за период должна объяснимо сходиться с детализацией счёта поставщика. Разницу фиксируйте отдельно: округление, скидки, бесплатные лимиты или задержка биллинга.
  2. Найдите события без операции. У каждого оплачиваемого события должен быть operation_id либо явно обозначенная категория общих расходов.
  3. Проверьте повторы. Несколько attempt_id должны собираться под одним operation_id.
  4. Проверьте нулевые успехи. Деление должно возвращать NULL или специальное состояние, а не ноль.
  5. Проверьте зависшие операции. Сумма затрат in_progress должна отображаться отдельно.
  6. Выберите несколько операций вручную. Восстановите их цепочки событий и пересчитайте сумму независимо.

Полезный контроль инварианта:

общая стоимость =
  стоимость успешных
  + стоимость неуспешных
  + стоимость незавершённых
  + явно распределяемые общие расходы

Как распределять общие расходы

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

  • по числу операций — если нагрузка примерно одинакова;
  • по времени выполнения — если доминируют вычислительные ресурсы;
  • по прямой стоимости — если инфраструктурная нагрузка следует за объёмом вызовов;
  • не распределять — если показатель нужен для сравнения только переменных затрат.

Формула с накладными расходами:

fully_loaded_cost_per_success =
  (direct_cost + allocated_shared_cost)
  / succeeded_operations

Типовые ошибки

Считать цену одного модельного ответа

Это стоимость технического шага, а не бизнес-результата. Она полезна для оптимизации промпта, но недостаточна для оценки процесса.

Удалять неуспешные операции из знаменателя и числителя

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

Считать повтор новой операцией

Так теряется цена достижения исходного результата. Разделяйте идентификаторы операции и попытки.

Признавать успех по ответу модели

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

Использовать текущий тариф для старых событий

Храните цену или версию прайс-листа на момент вызова. Иначе исторические отчёты будут меняться задним числом.

Смешивать валюты и единицы

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

Ограничения модели

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

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

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

Минимальный план внедрения

  1. Выберите одну массовую операцию с проверяемым финалом.
  2. Опишите контракт успеха и конечные статусы.
  3. Проведите operation_id через модель, инструменты и валидатор.
  4. Записывайте атомарные затраты с версией цены.
  5. Собирайте стоимость по операции и включайте неудачи в общий расход.
  6. Сверяйте агрегат с биллингом и отдельно показывайте незавершённые затраты.
  7. Только после проверки расширяйте схему на другие процессы.

Итоговая модель отвечает не на вопрос «сколько стоит запрос», а на более полезный: «сколько мы действительно тратим, чтобы получить один подтверждённый бизнес-результат».