Перейти к содержимому
6 мин чтения

Почему стоимость делегирования агентов так быстро растет?

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

Почему стоимость делегирования агентов так быстро растет?

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

Такой отчет отвечает на вопрос «сколько мы потратили за день», но не отвечает на вопрос, который нужен владельцу продукта и инженеру: «какая исходная задача съела деньги и почему». Для этого цена должна подниматься от каждого вызова модели, инструмента и подагента к одному корневому запросу. Глубина делегирования при этом нужна не как украшение дашборда, а как способ увидеть, где система стала думать несколько раз об одном и том же.

Корневой запрос должен пережить всю работу

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

Не путайте его с HTTP request ID. HTTP-запрос может закончиться до того, как агент поставит задачу в очередь, дождется результата инструмента или передаст работу другому сервису. Если вы сделали корнем только входящий HTTP-спан, стоимость асинхронных потомков отвалится от исходной задачи именно там, где она часто становится самой большой.

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

Минимальный конверт для внутренней команды может выглядеть так:

{
  "root_request_id": "rr_01JX8A7QK6B2",
  "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
  "parent_work_id": "work_8c19",
  "delegation_depth": 2,
  "delegation_reason": "verify_sources",
  "budget_remaining_usd": "0.1840"
}

traceparent связывает техническую трассу. root_request_id делает бизнес-агрегацию устойчивой даже тогда, когда трассировщик сэмплирует часть спанов, когда работа уходит в другую очередь или когда один корень живет дольше обычного trace retention. Не выбирайте между этими полями. Используйте оба.

Спецификация W3C Trace Context описывает перенос контекста трассировки между границами сервисов. Документация OpenTelemetry отдельно напоминает, что при работе с асинхронными сообщениями отдельные единицы обработки должны сохранять возможность корреляции с отправителем. Для учета агентных расходов это не академическая деталь: без сохраненного контекста вы увидите дорогой дочерний вызов как сироту.

Цена корня складывается из потомков, а не из спанов-родителей

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

Храните два разных поля:

  • cost_direct: расход именно этой операции;
  • cost_subtree: cost_direct плюс расходы всех потомков;
  • cost_root_total: итог для корня, записанный после завершения дерева;
  • cost_attributed: доля расхода, если одну операцию делят несколько корней.

На практике путаница начинается с названия cost. Инженер записывает в спан координатора сумму поддерева, аналитик суммирует все строки в хранилище, а финансовая команда получает отчет в два или три раза выше счета провайдера. Это не ошибка SQL. Это ошибка модели данных.

Для обычного дерева формула проста:

cost_subtree(node) = cost_direct(node) + Σ cost_subtree(child)
cost_root_total(root) = cost_subtree(root)

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

Не смешивайте денежные расходы с оценочной стоимостью. Денежный расход - сумма, которую вы можете сверить с инвойсом поставщика. Оценочная стоимость - расчет по известной цене, токенам и правилам округления. Она нужна быстро, пока инвойс еще не пришел, но должна иметь метку estimated. Когда поставщик отдает фактический usage, сохраняйте и его, не затирая расчет. Разница между ними полезна: она показывает бесплатные токены, скидки, пакетное округление и ошибки в вашей таблице тарифов.

Один логический вызов может стоить нескольких физических попыток

Агент видит «получи ответ от модели». Сеть, SDK и маршрутизатор могут выполнить несколько физических обращений до результата. Для пользователя это один логический шаг. Для счета это несколько попыток, и скрывать их нельзя.

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

Создайте родительский спан логической операции llm.generate, а для каждой реальной отправки создавайте дочерний llm.attempt. На попытке фиксируйте модель, провайдера, маршрут, токены, прямую стоимость и причину завершения. На логической операции храните число попыток и ее итоговый статус.

{
  "span_name": "llm.attempt",
  "root_request_id": "rr_01JX8A7QK6B2",
  "logical_operation_id": "lop_4821",
  "attempt_number": 2,
  "attempt_reason": "retry_after_timeout",
  "model": "model-x",
  "provider": "provider-a",
  "input_tokens": 18420,
  "output_tokens": 912,
  "cached_input_tokens": 0,
  "cost_direct_usd": "0.071436",
  "status": "ok"
}

Не сваливайте в одну категорию все повторы. Повтор после таймаута, повтор после 429, повтор из-за невалидного JSON и новый вызов, который агент сделал потому, что не поверил ответу, требуют разных решений. Первый может требовать настройки таймаута. Второй - ограничения параллелизма. Третий часто указывает на плохой контракт инструмента или неясный prompt. Четвертый может быть продуктовым требованием, но тогда его нужно заложить в бюджет задачи.

Есть и неприятный случай: провайдер вернул ошибку после того, как начал генерацию. Ваша система может не получить usage, хотя поставщик учтет часть работы. Помечайте такую попытку как cost_status=unknown, а не как нулевую. Затем сверяйте ее с данными биллинга и правилами провайдера. Ноль в отчете выглядит успокаивающе, но ломает прогноз расходов.

Глубина делегирования объясняет форму, но не доказывает причину

Глубина - это количество переходов от корневой задачи до текущей работы. Корневой агент имеет глубину 0, его подагент - 1, вызванный им проверяющий агент - 2. Считать ее стоит при создании работы, а не восстанавливать задним числом из названий спанов.

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

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

Пример, который встречается постоянно. Агент-координатор получает запрос «сравни условия трех поставщиков». Он вызывает трех исследователей параллельно. Каждый исследователь передает полный исходный запрос, историю диалога и список всех поставщиков в подагент проверки. Затем проверяющий агент вызывает поиск и анализ документа. Стоимость растет не из-за глубины 2. Она растет потому, что три ветви несут одинаковые 20 тысяч входных токенов и повторяют общую работу.

В карточке каждого делегирования фиксируйте причину. Поле delegation_reason с ограниченным набором значений дает больше пользы, чем свободный текст: retrieve, verify, transform, review, execute_tool, fallback_model. Если команда записывает туда произвольные фразы, через месяц вы получите тысячи почти одинаковых значений и ни одной нормальной группировки.

Отдельно записывайте delegation_fanout. Координатор, который породил десять работников, может иметь глубину 0, но уже создал главный риск расходов. Лимит только на максимальную глубину не защитит вас от такого веера.

Модель учета должна отличать модель, инструмент и оркестрацию

Проверяйте цену каждой попытки
Тарификация по ставкам провайдеров без наценки упрощает проверку стоимости физических попыток.

Вызов модели, вызов инструмента и работа оркестратора имеют разные источники цены. Смешать их в одном поле означает потерять возможность что-то исправить.

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

Практичная запись расхода содержит тип начисления:

{
  "charge_type": "llm_tokens",
  "unit": "token",
  "quantity_input": 18420,
  "quantity_output": 912,
  "unit_price_version": "provider-a-2026-06",
  "currency": "USD",
  "amount": "0.071436",
  "pricing_source": "rate_card",
  "cost_status": "estimated"
}

Для инструмента эта же схема может использовать charge_type=search_request или charge_type=compute_second. Не пытайтесь загнать все в токены. Поисковый запрос не становится токеном от того, что его вызвал агент.

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

У AI Router можно передавать тот же OpenAI-совместимый клиентский трафик через единый эндпоинт, но учет корней лучше строить в вашем приложении и трассировке. Шлюз видит обращение к модели, а только оркестратор знает, зачем эта ветвь была создана и к какой бизнес-задаче ее отнести.

Дорогой корень ищут по вкладу, а не по среднему чеку

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

Рабочий запрос к хранилищу трасс должен возвращать не только топ дорогих задач, но и их состав:

SELECT
  root_request_id,
  route_version,
  root_type,
  max(delegation_depth) AS max_depth,
  count(*) FILTER (WHERE operation_kind = 'llm_attempt') AS llm_attempts,
  sum(cost_direct_usd) AS root_cost_usd,
  sum(input_tokens) AS input_tokens,
  sum(output_tokens) AS output_tokens
FROM agent_cost_events
WHERE finished_at >= :start
  AND finished_at < :end
GROUP BY root_request_id, route_version, root_type
ORDER BY root_cost_usd DESC
LIMIT 50;

После этого не останавливайтесь на таблице. Откройте дерево одного дорогого корня и ответьте на четыре вопроса:

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

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

Считайте также «стоимость без результата». Это сумма попыток и ветвей, которые не внесли данные в финальный ответ, завершились ошибкой или были отброшены контроллером. Здесь нужна аккуратность: не вся промежуточная работа бесполезна. Проверка может подтвердить отсутствие риска. Но ветвь, которую отменили после того, как соседняя ветвь уже нашла тот же ответ, обычно показывает слабое управление параллелизмом.

Контекст чаще раздувает счет сильнее, чем выбор модели

Сдерживайте веер вызовов ключами
Rate-limits на уровне ключа ограничивают поток обращений до того, как веер подагентов разрастется.

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

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

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

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

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

Бюджет должен останавливать ветвь до нового расхода

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

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

available = root_budget
            - root_cost_committed
            - root_cost_reserved

if estimated_next_cost > available:
    return partial_result_or_escalate()

root_cost_committed включает завершенные попытки. root_cost_reserved защищает вас от параллельной гонки, когда три работника одновременно считают, что денег еще достаточно. Резерв создается до отправки вызова и снимается или уточняется после получения usage.

У бюджета должно быть действие, а не только красная линия на графике. Для разных типов задач оно отличается: вернуть частичный ответ с пометкой, перейти на короткий режим, отменить низкоприоритетные ветви, передать задачу оператору или поставить ее в очередь на пакетную обработку. Ответ «лимит превышен» без полезного результата часто превращает экономию API в дорогую ручную операцию.

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

Трассировка без правил хранения быстро становится еще одной проблемой

Маскируйте PII перед вызовами
Маскирование PII помогает не отправлять чувствительные данные в модельные обращения без необходимой защиты.

Полная телеметрия агента соблазнительна: хочется записать каждый prompt, каждый ответ и каждый результат инструмента. В production такой подход быстро становится дорогим и опасным. Содержимое может включать персональные данные, договоры, медицинские сведения, фрагменты исходного кода и внутренние инструкции.

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

Разделите данные на три уровня. В основном хранилище держите агрегаты расходов и безопасные атрибуты. В трассировке держите технические связи, статусы, версии prompt и размеры полезной нагрузки. В отдельном защищенном контуре держите редкие отладочные образцы, если они вообще нужны.

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

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

Учет нужно сверять с инвойсом и с результатом для пользователя

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

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

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

Начните с одного типа корневой работы, который уже создает заметный расход. Введите root_request_id, прямую стоимость каждой попытки, причину делегирования и итоговую сумму поддерева. Через несколько дней у вас появится не абстрактный «расход агентов», а список конкретных задач, ветвей и повторов, за которые вы платите. Именно с такого списка и начинается нормальная оптимизация.

Часто задаваемые вопросы

Что считать корневым запросом в многоагентной системе?

Корневым запросом считайте бизнес-задачу, принятую вашим приложением: сообщение пользователя, событие очереди или запуск фоновой задачи. Ему нужен неизменяемый root_request_id, который переживает все переходы между агентами, очередями и сервисами. HTTP request ID недостаточен, если работа продолжается асинхронно.

Нужен ли отдельный спан для каждого подагента?

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

Почему цена верхнего вызова модели не равна цене задачи?

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

Какие данные нужны для расчета стоимости вызова LLM?

Токены сами по себе не объясняют расход. Нужны модель, провайдер, входные и выходные токены, цена за единицу, кэшированные токены, число попыток и стоимость инструментов. Храните рассчитанную сумму вместе с исходными величинами, иначе после изменения прайса вы не сможете проверить старые отчеты.

Всегда ли большая глубина делегирования означает большую стоимость?

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

Как учитывать ретраи и неудачные обращения к модели?

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

Можно ли хранить промпты в трассировках затрат?

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

Когда нужен бюджет на один корневой запрос?

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

Как быстро найти источник внезапного роста расходов?

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

Можно ли внедрить такой учет через OpenAI-совместимый API-шлюз?

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