Natalia Vegman
← Все статьи

45% бюджета уходило на то, что агент спрашивал «нет ли для меня работы»

2026-07-31Article

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

Это история про мою агентную систему (не клиентский проект). Агент здесь — программа, которая по расписанию или событию сама решает, что делать дальше, и на каждом шаге обращается к языковой модели. У такой системы есть неприятная черта: она задаёт вопросы даже тогда, когда отвечать не на что.

Когда я докопалась до причин, выяснилось неожиданное: 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
45% всех расходов на модели — это опрос «нет ли для меня работы» при пустом списке задач

Как считать по‑честному: инструмент разбора

Чтобы не повторять эти 11 дней вслепую, я написала скрипт, который сводит все слои учёта в одну таблицу: проект → модель → агент → день.

Одна плоская таблица, в которой можно спросить: «какой агент, в какой модели и в какой день съел эти деньги?»

Три вещи, которые для такого инструмента принципиальны:

  • Ноль вызовов модели. Инструмент разбора расходов не должен сам расходовать. Это чистый парсинг логов, без ИИ внутри.
  • Быстро. У меня — около 3 секунд на 125 МБ логов. Если анализ долго — вы запустите его раз в месяц. А нужно — в момент, когда «что-то не так».
  • Сходимость с биллингом. У меня расхождение 4%. Если не сходится со счётом провайдера, вы объясняете не тот расход.

И три принципа из практики наблюдаемости:

  • Цифра от провайдера важнее пересчёта токенайзером.
  • Пересчитывать токенайзером только там, где провайдер цифру не вернул (это заплатка, не источник истины).
  • Где цены нет — писать «без цены», а не ноль. Ноль молча складывается и даёт правдоподобную, но неправильную сумму.

Применить у себя

Что проверить у себя (короткий чек‑лист)

  • Найдите вызовы, которые происходят строго по расписанию, а не в ответ на человека/событие. Сортируйте по количеству — самая частая строка часто и самая дорогая.
  • Посмотрите отношение входных токенов к выходным. Соотношение в сотни раз — сигнал, что вы возите «полный контекст» ради односложного ответа.
  • Посчитайте долю счёта, которая уходит на вызовы, после которых система ничего не сделала. Это цена холостого хода.
  • Проверьте, сколько у вас «слоёв учёта», и видят ли они друг друга. Если разбор идёт по одному источнику, а счёт приходит по другому — сведите в одну таблицу.
  • Выключите один периодический вызов тем способом, который обещает документация, и дождитесь следующего расписания. Смотрите не в конфиг, а в логи.
  • Прежде чем менять модель на более дешёвую, ответьте письменно: что сломается, если этот вызов не делать вообще? Если ответ «ничего» — удешевлять нечего, надо удалять.

Как понять, что получилось

Критерий не «счёт стал меньше». Счёт может уменьшиться просто потому, что вы перевели мусор на дешёвую модель.

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

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

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

← All posts

45% of My Model Spend Went to One Question the Agent Asked Itself

2026-07-31Article

I opened my bill for language models and saw a number bigger than usual. Over that same period not a single new user had shown up, I hadn't shipped any new features, and load hadn't grown. The system was sitting there servicing itself — and that cost money.

This is about my own agent system, not a client project. An agent here is a program that decides on its own what to do next, on a schedule or in response to an event, and calls a language model at every step. Such a program has an unpleasant property: it asks questions even when there's nothing to answer.

When I finished the analysis, it turned out that 45% of everything I had ever spent on models went to one single question the agent asked itself: "is there any work for me?" Not on the work — on the question about the work.

By the end of this article you'll understand why the main cost line in agent systems is idling rather than useful action, and how to build accounting that answers "where did it go" instead of only "how much is left."

The symptom: the bill grew, the user count didn't

The usual reaction to a bigger bill is to look for the cause in load. Somebody wrote a lot to the agent, somebody uploaded a big document, a retry loop kicked in somewhere. But there was no load: same number of users, same number of tasks, same functionality.

I noticed the second symptom almost by accident. The context the agent was sending to the model was growing on its own. Over two and a half hours of observation, the input size went from 29,643 to 29,707 tokens. A token is a chunk of text — model input and output are counted in tokens, and that's what the provider bills you for.

Sixty-four tokens over two and a half hours is trivial in itself. What matters isn't the size, it's the sign. Spend was growing without a single new user. A system that gets more expensive while idle will keep getting more expensive: the growth mechanism is built in, and it doesn't depend on whether anyone is using it.

Eleven days blind: why the cause stayed hidden

Eleven days passed between "the bill grew" and "here's the cause." The whole time, the spend was perfectly visible in dollars and completely invisible in causes. I knew the amount and didn't know what it was for.

The reason is annoying. My monitoring answered the question "how much is left": remaining limit, spent this period, are we approaching the threshold. That kind of monitoring honestly warns you that the money is running out and says nothing at all about where exactly it's going. What I needed was a different question — "where did it go": a breakdown by project, model, agent, and day.

The other half of the problem was that accounting lived in two layers that can't see each other. My analysis ran over one layer while the money was burning in the other. Until those layers are pulled into a single table, any analysis turns into guesswork: you confidently explain the part of the spend that happened to land in your hands, and you don't suspect the rest exists. For eleven days I was explaining the smaller half of the bill to myself.

flowchart LR
  A["Счёт провайдера"] --> D["Одна таблица:<br/>проект × модель × агент × день"]
  B["Логи агентов"] --> D
  C["Выгрузка по проектам"] --> D
  D --> Q["Вопрос «какой агент<br/>съел эти деньги»"]
  A -.->|"поодиночке"| X["Объяснима только<br/>часть счёта"]
  B -.-> X
  C -.-> X
Пока слои учёта лежат врозь, каждый отвечает на свой вопрос — и ни один не отвечает на «куда ушло»

What the analysis showed: the price of "do I have anything to do?"

When I finally pulled everything into one table, the largest cost line turned out to be not work but polling. 1007 calls, $13.83 — 45% of all model spend over the entire life of the system.

The mechanics are simple. The poll ran every 30 minutes, around the clock, against an empty task list. Forty-eight times a day the system woke the agent and asked whether there was any work for it. There never was.

The most interesting part is the ratio inside each call. Every such request was roughly 29,700 input tokens against a response of about 90 tokens. That is, every half hour the agent's full context went off to the model: system instructions, tool descriptions, accumulated state — all for the phrase "I have nothing to do." The input is hundreds of times bigger than the answer.

Which leads to a conclusion worth taking away separately from my story. In agent systems you pay mostly for input, not output. The answer is short, while the context the agent needs to make a decision is long, and it gets sent in full on every call — including the ones where the decision is known in advance and equals "do nothing."

The intermediate-optimization trap

Before I found the cause, I did exactly what everyone does: I moved the call to a cheaper model. The savings came to 7x on that same call. A pretty number; it would have made a great report.

Except I made cheaper what I should have turned off. A sevenfold savings on a useless action is still spending, just tidier. Worse, that kind of optimization closes the question psychologically: the chart went down, so you did your job, so you can move on to something else. The spend stayed, but it stopped being irritating — and therefore stopped being investigated.

The order of operations should be the reverse. First "is this call needed at all," then "which model makes it." Optimizing the model without checking whether the call is necessary is penny-pinching.

Documentation describes intent, not behavior

Next came an attempt to turn the polling off the honest way. The platform has a service file listing periodic tasks, and the documentation flatly promised: keep the file empty and there will be no calls.

I reduced the file to nothing but comments. The poll ran again, right on schedule.

The factual conclusion: polling is enabled at the platform level and isn't suppressed by the contents of that file. The methodological conclusion matters more. The documentation described the authors' intent, not the system's behavior. That's not deception and usually not even a mistake: it was written that way when that was the plan, then the behavior drifted away from the text, and the text stayed.

The practical implication for you: turning something off counts as turned off only after you've looked at the logs or the bill and confirmed the calls really aren't happening. Checking against the documentation is checking someone else's intentions — and what you pay for is behavior.

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
45% всех расходов на модели — это опрос «нет ли для меня работы» при пустом списке задач

How to count honestly: the analysis tool

To avoid repeating eleven days of blindness, I wrote a script that pulls every accounting layer into one table: project by model by agent by day. One flat table you can query with "which agent, in which model, on which day ate this money."

The characteristics that are essential for a tool like this:

  • Zero model calls. A spend-analysis tool shouldn't spend anything itself. This is pure log parsing, with no AI inside.
  • About 3 seconds for 125 megabytes of logs. If the analysis takes a long time, you'll run it once a month, when what you need is to run it the moment the question comes up.
  • 4% divergence from the provider's billing. Billing is the bill the model provider issues. If your analysis doesn't reconcile with the bill, you're explaining the wrong spend.

And three principles from observability practice — that is, from the practice of structuring logs so they can answer a question you didn't ask in advance:

  • The provider's number beats a tokenizer recount. A tokenizer is a library that splits text into tokens the same way the model does. It gives you a similar number, but that's not what you're billed on.
  • Recount with a tokenizer only where the provider didn't return a number. That's a patch over a hole in the data, not a source of truth.
  • Where there's no price, write "no price," not zero. Zero silently adds into the total and makes the report wrong but plausible. "No price" is visible to the eye, and it forces you to go figure it out.

Put it to work

What to check in practice

  • Find the calls in your logs that happen strictly on a schedule rather than in response to a human action or an event. Sort them by count, not by cost — the most frequent line is usually also the most expensive one.
  • For each periodic call, look at the ratio of input tokens to output tokens. A ratio in the hundreds is a sign that you're hauling the full context around for a one-word answer.
  • Calculate what share of your bill consists of calls after which the system did nothing. That's the price of idling.
  • Check how many accounting layers you have and whether they can see each other. If your analysis runs on one source and the bill comes from another, pull them into a single table of the form project — model — agent — day.
  • Turn off one periodic call using the method the documentation promises, and wait for the next scheduled firing. Look at the logs, not at the config.
  • Before switching to a cheaper model, answer in writing: what breaks if this call isn't made at all. If the answer is "nothing," there's nothing to make cheaper — it needs to be deleted.

How to know it worked

The criterion isn't "the bill got smaller." The bill can shrink just because you moved garbage onto a cheap model.

It worked if three things are true.

First: within a minute, without opening the provider's console, you can name the three most expensive lines of your spend for the past week — down to the agent and the day. Not the total, the composition.

Second: your analysis reconciles with the provider's bill within a margin you understand. Mine is 4%, and I know what it's made of. If the divergence is unknown, you're still looking at part of the picture.

Third: there are no lines left in your bill with not a single action behind them. Every periodic call either leads to work, or is turned off and verified against the logs as actually turned off.

Until those three things are true, "how much is left" will keep blocking the view of "where did it go" — and idling won't give itself away by anything except a slowly growing number.