Практика · AI-агенты
Юнит-экономика AI-агента: как распределять стоимость по задачам и инструментам
Итоговый счёт провайдера отвечает на вопрос «сколько потрачено», но не объясняет, какая задача, модель, интеграция или повторная попытка создали расход. Исправим это с помощью единого журнала событий и понятных правил атрибуции.
Что считать единицей
Юнит-экономика AI-агента начинается не с токенов, а с выбора полезной единицы. Для агента такой единицей обычно служит завершённая пользовательская задача: обработать обращение, подготовить сводку, проверить документ или обновить запись.
Один запрос пользователя не всегда равен одной задаче. Агент может несколько раз обратиться к модели, вызвать поиск, повторить упавший инструмент и запустить проверку результата. Поэтому расходы нужно собирать на двух уровнях:
- task — бизнес-задача, ради которой запущен агент;
- attempt и step — попытки и отдельные действия внутри неё.
Базовая формула выглядит так:
стоимость задачи =
вызовы моделей
+ вызовы платных инструментов
+ вычисления и хранение
+ распределённая общая инфраструктура
Отдельно сохраняйте стоимость неуспешных попыток. Она уже понесена и не должна исчезать из отчёта после успешного повтора.
Минимальная модель атрибуции
Каждое расходное событие должно отвечать как минимум на пять вопросов: для какой задачи оно возникло, кто её инициировал, что выполнялось, какой ресурс использовался и чем завершилась попытка.
| Поле | Назначение | Пример значения |
|---|---|---|
task_id |
Связывает все шаги одной задачи | task_01J... |
task_type |
Группирует одинаковые сценарии | document_summary |
user_id |
Показывает потребителя расходов | Псевдонимизированный идентификатор |
attempt_id |
Отделяет первоначальный запуск от повторов | attempt_2 |
step_id |
Идентифицирует конкретное действие | step_validate |
resource_type |
Различает модель, инструмент и инфраструктуру | model, tool, compute |
resource_name |
Указывает модель или интеграцию | model_standard |
quantity |
Хранит измеренный объём | Число токенов, секунд или вызовов |
unit_price |
Фиксирует применённый тариф | 0.000002 |
cost |
Хранит рассчитанную стоимость события | 0.004210 |
status |
Разделяет успехи и потери | succeeded, failed, cancelled |
Значения в последнем столбце — только иллюстрации структуры. Они не описывают тарифы конкретного провайдера.
Шаг 1. Создайте контекст задачи
Сгенерируйте task_id на входе в систему и передавайте его через оркестратор, вызовы моделей, очереди и инструменты. Не создавайте новый идентификатор после ошибки: для повтора меняется attempt_id, а задача остаётся прежней.
from dataclasses import dataclass
from uuid import uuid4
@dataclass(frozen=True)
class TaskContext:
task_id: str
task_type: str
user_id: str
attempt_id: str
def new_task(task_type: str, pseudonymous_user_id: str) -> TaskContext:
return TaskContext(
task_id=f"task_{uuid4().hex}",
task_type=task_type,
user_id=pseudonymous_user_id,
attempt_id="attempt_1",
)
Не записывайте в user_id электронную почту, имя или содержимое запроса. Для финансовой группировки обычно достаточно внутреннего псевдонимизированного идентификатора.
Шаг 2. Записывайте одно событие на один расход
Журнал должен хранить измеренные величины и рассчитанную сумму. Это позволит пересчитать историю при исправлении тарифной таблицы и одновременно воспроизвести исходный отчёт.
{
"event_id": "evt_01",
"occurred_at": "2026-01-15T10:30:00Z",
"task_id": "task_example",
"task_type": "document_summary",
"user_id": "usr_pseudonymous",
"attempt_id": "attempt_1",
"step_id": "step_extract",
"resource_type": "model",
"resource_name": "model_standard",
"pricing_version": "prices_2026_01",
"quantity": {
"input_tokens": 2400,
"output_tokens": 320
},
"cost": {
"currency": "USD",
"amount": "0.000000"
},
"status": "succeeded"
}
Это пример схемы, а не реальный расчёт. Нулевую сумму заменяет функция, которая использует фактические данные об объёме и вашу версионированную таблицу цен.
Для денежных величин применяйте десятичный тип, а не число с плавающей точкой. Храните больше знаков после запятой на уровне события и округляйте только итог отчёта.
Шаг 3. Разделите измерение и ценообразование
Ответ модели сообщает объём использования, инструмент — число вызовов или переданных единиц, инфраструктура — время и потреблённые ресурсы. Преобразование этих величин в деньги должно происходить в одном компоненте.
pricing_version: prices_2026_01
currency: USD
resources:
model_standard:
input_token: ${MODEL_STANDARD_INPUT_PRICE}
output_token: ${MODEL_STANDARD_OUTPUT_PRICE}
web_search:
call: ${WEB_SEARCH_CALL_PRICE}
worker:
compute_second: ${WORKER_SECOND_PRICE}
Конфигурация намеренно не содержит выдуманных тарифов. Значения передаются через переменные окружения из контролируемого хранилища конфигурации. Секретные ключи в журнал стоимости не попадают.
Версия цен обязательна: без неё изменение тарифа задним числом сделает старый отчёт невоспроизводимым.
Шаг 4. Учитывайте инструменты и повторы
Перед вызовом инструмента создайте шаг, после завершения запишите статус, измеренный объём и стоимость. Если инструмент вернул ошибку, событие всё равно сохраняется. Следующий запуск получает новый attempt_id или номер повтора.
def run_tool_with_attribution(tool, payload, context, ledger):
step_id = f"tool_{uuid4().hex}"
try:
result = tool(payload)
ledger.record(
context=context,
step_id=step_id,
resource_type="tool",
resource_name=tool.name,
quantity={"calls": 1},
status="succeeded",
)
return result
except Exception:
ledger.record(
context=context,
step_id=step_id,
resource_type="tool",
resource_name=tool.name,
quantity={"calls": 1},
status="failed",
)
raise
В рабочей системе запись события должна быть идемпотентной: повторная доставка одного event_id не создаёт второй расход. Для этого задайте уникальное ограничение на идентификатор события.
Шаг 5. Распределите общую инфраструктуру
Не вся стоимость имеет прямой task_id. Общие воркеры, база наблюдаемости и хранение обслуживают множество задач. Для них выберите прозрачный драйвер распределения:
- вычисления — по времени выполнения задачи;
- хранение — по объёму сохранённых данных и сроку хранения;
- общий сервис — по числу задач или активных пользователей;
- зарезервированная мощность — по фактическому потреблению либо по согласованной квоте.
доля задачи =
драйвер задачи / сумма драйвера всех задач периода
распределённая стоимость =
стоимость общего ресурса × доля задачи
Не смешивайте прямые и распределённые расходы в одном поле. Сохраняйте, например, direct_cost и allocated_cost, чтобы выбор драйвера не скрывал реальную стоимость вызовов.
Шаг 6. Постройте отчёт по задачам
Ниже — воспроизводимый запрос для таблицы cost_events. Он использует только чтение данных и не изменяет записи.
SELECT
task_type,
user_id,
resource_type,
resource_name,
COUNT(DISTINCT task_id) AS tasks,
COUNT(*) AS cost_events,
SUM(cost_amount) AS total_cost,
SUM(
CASE WHEN status <> 'succeeded'
THEN cost_amount ELSE 0 END
) AS unsuccessful_cost,
SUM(cost_amount)
/ NULLIF(COUNT(DISTINCT task_id), 0) AS cost_per_task
FROM cost_events
WHERE occurred_at >= :period_start
AND occurred_at < :period_end
GROUP BY
task_type,
user_id,
resource_type,
resource_name
ORDER BY total_cost DESC;
Следующий срез показывает сценарии, где повторы заметно влияют на стоимость:
SELECT
task_type,
COUNT(DISTINCT task_id) AS tasks,
COUNT(DISTINCT attempt_id) AS attempts,
SUM(cost_amount) AS total_cost,
SUM(CASE WHEN status = 'failed'
THEN cost_amount ELSE 0 END) AS failed_cost
FROM cost_events
WHERE occurred_at >= :period_start
AND occurred_at < :period_end
GROUP BY task_type
ORDER BY failed_cost DESC;
Если attempt_id уникален только внутри задачи, считайте составной ключ из task_id и attempt_id, иначе одинаковые номера попыток сольются.
Как проверить результат
- Трассировка одной задачи. Выберите тестовую задачу в собственной среде и убедитесь, что каждый вызов модели и инструмента содержит одинаковый
task_id. - Повтор после ошибки. На безопасном стенде используйте инструмент-заглушку, который предсказуемо возвращает ошибку. Проверьте, что первая попытка имеет статус
failed, повтор — новыйattempt_id, а обе стоимости входят в итог задачи. - Защита от дублей. Повторно отправьте то же событие с тем же
event_id. Число событий и общая сумма не должны увеличиться. - Сверка объёмов. Сравните агрегированные токены, вызовы и вычислительное время с доступными отчётами поставщиков за одинаковый период и в одной временной зоне.
- Сверка денег. Сумма прямых событий плюс распределённая инфраструктура должна объяснять внутренний итог периода. Расхождение фиксируйте отдельной строкой, а не скрывайте округлением.
Метрики, которые помогают принимать решения
- Стоимость успешной задачи
- Все расходы успешных и предшествовавших неуспешных попыток, разделённые на число завершённых задач.
- Доля стоимости ошибок
- Расходы событий со статусом ошибки относительно общей стоимости сценария.
- Стоимость по модели
- Показывает, где дорогая модель используется чаще или дольше ожидаемого.
- Стоимость инструментов на задачу
- Помогает обнаружить циклы поиска, повторное чтение одного ресурса и слишком мелкие вызовы.
- Стоимость по пользователю или команде
- Отделяет рост использования от удорожания самого сценария.
- Распределение, а не только среднее
- Медиана и верхние перцентили показывают редкие дорогие задачи, которые средняя стоимость может скрыть.
Финансовая метрика без показателя качества опасна. Снижение стоимости полезно только при сохранении требуемого результата: успешности, полноты, времени ответа или оценки человеком.
Типовые ошибки
- Агрегация только по API-ключу. Ключ показывает приложение или среду, но не конкретный сценарий и пользователя.
- Запись только успешного ответа. Тайм-ауты, отмены и ошибки инструментов исчезают, хотя ресурсы уже могли быть потрачены.
- Один идентификатор на каждый повтор. Связь между попытками теряется, и стоимость исходной задачи занижается.
- Текущий тариф для старых событий. Исторические отчёты начинают меняться после обновления цен.
- Округление каждого шага до копеек или центов. Множество малых вызовов превращается в систематическую погрешность.
- Двойной учёт кэша. Нельзя одновременно записывать полную стоимость исходного вызова и такую же стоимость для попадания в кэш.
- Смешение валют. Сначала храните исходную валюту и курс с датой, затем формируйте общий отчёт.
- Содержимое запросов в финансовом журнале. Для атрибуции обычно достаточно идентификаторов и счётчиков; тексты повышают риск утечки данных.
- Стоимость без версии агента. Без
agent_versionтрудно понять, какое изменение промпта или маршрутизации повлияло на расход.
Ограничения подхода
Атрибуция не всегда совпадает со счётом до последнего знака. Поставщик может применять пакетное округление, скидки, минимальные платежи, бесплатные квоты или начисления с задержкой. Поэтому различайте три величины:
- расчётную стоимость события — оперативная оценка по вашей таблице цен;
- распределённую стоимость — управленческое правило для общих ресурсов;
- фактический счёт — внешняя финансовая сумма за период.
Распределение общей инфраструктуры неизбежно зависит от выбранного драйвера. Оно полезно для сравнения сценариев, но не превращается от этого в прямое измерение.
Также стоимость задачи не равна её ценности. Для решений о маршрутизации модели или отключении сценария сопоставляйте расход с качеством, временем выполнения и бизнес-результатом.
Практический итог
Рабочая система атрибуции строится вокруг стабильного task_id, отдельных попыток, событий расходов и версионированных цен. Она сохраняет неуспешные шаги, защищается от дублей и явно отделяет прямую стоимость от распределённой.
Начните с одного сценария и одного отчёта: стоимость задачи в разрезе пользователя, модели, инструмента и статуса. Когда данные сходятся на этом уровне, добавляйте общую инфраструктуру, версии агента и показатели качества.
Другие практические материалы собраны в разделе «Гайды», а определения терминов — в глоссарии Agent Lab Journal.