Я открыла счёт за языковые модели и увидела цифру больше привычной. При этом — ни новых пользователей, ни релизов, ни роста нагрузки. Система просто «жила своей жизнью» и списывала деньги.
Это история про мою агентную систему (не клиентский проект). Агент здесь — программа, которая по расписанию или событию сама решает, что делать дальше, и на каждом шаге обращается к языковой модели. У такой системы есть неприятная черта: она задаёт вопросы даже тогда, когда отвечать не на что.
Когда я докопалась до причин, выяснилось неожиданное: 45% всех моих расходов на модели за всё время ушло на один вопрос, который агент задавал сам себе: «нет ли для меня работы?». Не на работу — на вопрос о работе.
Ниже — почему в агентных системах главная статья расхода часто не полезное действие, а холостой ход, и какие метрики/учёт нужны, чтобы отвечать на «куда ушло», а не только на «сколько осталось».
Симптом: счёт вырос, а пользователей больше не стало
Обычная реакция на выросший счёт — искать причину в нагрузке: кто-то много писал агенту, кто-то загрузил большой документ, где-то пошёл ретрай. Но нагрузки не было: пользователей столько же, задач столько же, функциональность та же.
Второй симптом я заметила почти случайно: контекст, который агент отправлял в модель, рос сам по себе. За два с половиной часа наблюдения размер входа увеличился с 29 643 до 29 707 токенов. Токены — это то, за что в итоге выставляют счёт.
64 токена за пару часов — мелочь. Важна не величина, а знак. Система дорожала в простое. А система, которая дорожает в простое, будет дорожать всегда — рост встроен в её поведение.
Одиннадцать дней вслепую: почему причина не находилась
Между «счёт вырос» и «вот причина» прошло 11 дней. Деньги были видны идеально — в долларах. Но причины были невидимы полностью.
Потому что мой мониторинг отвечал на вопрос «сколько осталось»: остаток лимита, потрачено за период, приближаемся ли к порогу. Это честно предупреждает, что деньги кончаются — и не говорит вообще ничего про то, за что они кончаются.
Мне нужен был другой вопрос: «куда ушло» — разбивка по проектам, моделям, агентам и дням.
И ещё хуже: учёт жил в двух слоях, которые не видели друг друга. Разбор был по одному слою, а деньги горели в другом. Пока эти слои не сведены в одну таблицу, любой анализ превращается в уверенное гадание: объясняешь ту часть, которая у тебя под руками, и не подозреваешь, что есть ещё одна.
flowchart LR A["Счёт провайдера"] --> D["Одна таблица:<br/>проект × модель × агент × день"] B["Логи агентов"] --> D C["Выгрузка по проектам"] --> D D --> Q["Вопрос «какой агент<br/>съел эти деньги»"] A -.->|"поодиночке"| X["Объяснима только<br/>часть счёта"] B -.-> X C -.-> X
Что показал разбор: цена вопроса «мне есть чем заняться?»
Когда я наконец свела всё в одну таблицу, крупнейшая строка расходов оказалась не работой, а опросом.
- 1007 вызовов
- $13.83
- 45% всех расходов на модели за всё время жизни системы
Механика простая: опрос шёл каждые 30 минут, круглосуточно, при пустом списке задач. 48 раз в сутки система будила агента и спрашивала, нет ли для него работы. Работы не было ни разу.
Самое интересное — соотношение внутри вызова. Каждый такой запрос — около 29 700 входных токенов при ответе примерно в 90 токенов.
То есть каждые полчаса в модель уезжал полный контекст агента: системные инструкции, описания инструментов, накопленное состояние — всё ради фразы «мне нечего делать».
И отсюда главный вывод, который я теперь держу в голове, когда проектирую агента: в агентных системах вы платите в основном за вход, а не за выход. Ответ почти всегда короткий. Дорогой — контекст, который приходится отправлять целиком при каждом вызове. Даже когда решение заранее известно и равно «ничего не делать».
Ловушка промежуточной оптимизации
До того как я нашла причину, я сделала ровно то, что делают все: перевела вызов на модель подешевле. Экономия получилась красивой — в 7 раз на той же операции.
Только я удешевила то, что надо было отключить.
Семикратная экономия на бесполезном действии — это всё равно трата, просто «аккуратнее». И есть ещё один неприятный эффект: такая оптимизация психологически закрывает вопрос. График пошёл вниз — значит, «я сделала работу» — и можно идти дальше. А расход остаётся, просто перестаёт раздражать.
Порядок действий должен быть обратным:
- «нужен ли этот вызов вообще?»
- «какая модель должна его делать?»
Оптимизировать модель, не проверив нужность самого вызова, — это реально экономить на спичках.
Документация описывает намерение, а не поведение
Следующая попытка была «по-честному» выключить опрос.
У платформы есть служебный файл со списком периодических задач, и документация обещала: держите файл пустым — вызовов не будет.
Я оставила в файле только комментарии. Опрос отработал снова — ровно по расписанию.
Фактический вывод: polling включён на уровне платформы и не гасится содержимым этого файла.
Методический вывод важнее: документация часто описывает намерение авторов, а не поведение системы. Это не обязательно обман. Просто текст мог быть верным «на момент замысла», а потом поведение разъехалось, и никто не вернулся переписать абзац.
Практическое следствие: выключение считается выключением только после того, как вы посмотрели в логи или в счёт и убедились, что вызовов действительно нет. Проверка по документации — это проверка чужих намерений. А платите вы всегда за поведение.
flowchart LR
subgraph BEFORE["До разбора"]
B1["Опрос каждые 30 минут,<br/>круглосуточно"] --> B2["1007 вызовов"] --> B3["13,83 $ — 45%<br/>всех расходов"]
end
subgraph AFTER["После выключения"]
A1["Опроса нет"] --> A2["0 вызовов"] --> A3["0 $"]
end
BEFORE ==>|"проверено по счёту,<br/>а не по документации"| AFTERКак считать по‑честному: инструмент разбора
Чтобы не повторять эти 11 дней вслепую, я написала скрипт, который сводит все слои учёта в одну таблицу: проект → модель → агент → день.
Одна плоская таблица, в которой можно спросить: «какой агент, в какой модели и в какой день съел эти деньги?»
Три вещи, которые для такого инструмента принципиальны:
- Ноль вызовов модели. Инструмент разбора расходов не должен сам расходовать. Это чистый парсинг логов, без ИИ внутри.
- Быстро. У меня — около 3 секунд на 125 МБ логов. Если анализ долго — вы запустите его раз в месяц. А нужно — в момент, когда «что-то не так».
- Сходимость с биллингом. У меня расхождение 4%. Если не сходится со счётом провайдера, вы объясняете не тот расход.
И три принципа из практики наблюдаемости:
- Цифра от провайдера важнее пересчёта токенайзером.
- Пересчитывать токенайзером только там, где провайдер цифру не вернул (это заплатка, не источник истины).
- Где цены нет — писать «без цены», а не ноль. Ноль молча складывается и даёт правдоподобную, но неправильную сумму.
Применить у себя
Что проверить у себя (короткий чек‑лист)
- Найдите вызовы, которые происходят строго по расписанию, а не в ответ на человека/событие. Сортируйте по количеству — самая частая строка часто и самая дорогая.
- Посмотрите отношение входных токенов к выходным. Соотношение в сотни раз — сигнал, что вы возите «полный контекст» ради односложного ответа.
- Посчитайте долю счёта, которая уходит на вызовы, после которых система ничего не сделала. Это цена холостого хода.
- Проверьте, сколько у вас «слоёв учёта», и видят ли они друг друга. Если разбор идёт по одному источнику, а счёт приходит по другому — сведите в одну таблицу.
- Выключите один периодический вызов тем способом, который обещает документация, и дождитесь следующего расписания. Смотрите не в конфиг, а в логи.
- Прежде чем менять модель на более дешёвую, ответьте письменно: что сломается, если этот вызов не делать вообще? Если ответ «ничего» — удешевлять нечего, надо удалять.
Как понять, что получилось
Критерий не «счёт стал меньше». Счёт может уменьшиться просто потому, что вы перевели мусор на дешёвую модель.
Считайте, что вы реально закрыли проблему, если выполняются три условия:
- За минуту, не открывая консоль провайдера, вы можете назвать три самые дорогие строки расхода за неделю — с точностью до агента и дня.
- Ваш разбор сходится со счётом с понятной вам погрешностью.
- В счёте не осталось строк, за которыми не стоит ни одного действия. Каждый периодический вызов либо приводит к работе, либо выключен — и это проверено по логам.
Пока эти три вещи не выполняются, «сколько осталось» будет продолжать заслонять «куда ушло», а холостой ход будет выдавать себя только медленно растущей цифрой.