Normal path: execute → tool → observe.
Проблема (з реального продакшену)
Ти шипиш агента.
У тестах це “кілька центів”.
Потім він ловить прод‑трафік і хтось пише в Slack:
“Чому ми витратили $900 на агента вчора?”
Budget explosions майже ніколи не одна велика помилка. Це смерть від тисячі дрібних:
- токени дрейфують вгору
- ретраї множаться
- tool calls стають лупами
- промпти ростуть “ще трошки, і все”
Якщо ти не міряєш і не капаєш бюджети — ти дізнаєшся про spend від фінансів. Фінанси — не моніторинг.
Чому це ламається в продакшені
В агентних системах витрати множаться.
1) Токени масштабуються контекстом, а не інтенцією
Інтенція: “підсумуй це”. Реалізація: “встав останні 40 повідомлень + 6 tool outputs + 2 runbooks”.
Вартість залежить від того, що ти годуєш моделлю, а не від того, що попросив користувач.
2) Ретраї множать cost
Якщо падає модель‑кол і ти ретраїш:
- платиш двічі
- додаєш лейтенсі
Якщо падає tool‑кол і ти ретраїш:
- платиш у tool cost
- часто ще й платиш більше токенів, бо пояснюєш фейл
Ретраї не безкоштовні. У loop’ах вони мультиплікативні.
3) “Planning” — чистий overhead
Planning‑важкі агенти палять токени ще до того, як зроблять щось корисне. Це ок, якщо це зупиняє tool spam. Це не ок, якщо це просто “ще подумай”.
4) Tool spam робить будь-який budget безсенсовним
Якщо ти не капнеш tool calls, агент може витратити $0.01 на токени і $5 на tools. Твій “token budget” не захистив. Бо це був не той бюджет.
5) Ти не бачиш spend, якщо не логиш його
Якщо в логах немає:
- model tokens in/out
- tool calls count
- per-run cost estimate
- stop reason
…ти не зловиш drift алертами.
Приклад реалізації (реальний код)
Мінімальний per-run budget tracker:
- зупинка по часу, кроках, tool calls
- груба оцінка cost і стоп по spend
- stop reason, на який можна повісити алерт
from dataclasses import dataclass
import time
@dataclass(frozen=True)
class Budget:
max_steps: int = 25
max_seconds: int = 60
max_tool_calls: int = 12
max_usd: float = 1.00
@dataclass
class Usage:
tool_calls: int = 0
model_tokens_in: int = 0
model_tokens_out: int = 0
estimated_usd: float = 0.0
class BudgetExceeded(RuntimeError):
pass
def estimate_usd(tokens_in: int, tokens_out: int) -> float:
# Replace with real pricing for your model(s).
# This is intentionally simple: budgets need to be approximate, not perfect.
return (tokens_in + tokens_out) * 0.000002 # $/token (placeholder)
class BudgetGuard:
def __init__(self, budget: Budget) -> None:
self.budget = budget
self.usage = Usage()
self.started = time.time()
self.steps = 0
def on_step(self) -> None:
self.steps += 1
if self.steps > self.budget.max_steps:
raise BudgetExceeded("step budget exceeded")
if time.time() - self.started > self.budget.max_seconds:
raise BudgetExceeded("time budget exceeded")
if self.usage.tool_calls > self.budget.max_tool_calls:
raise BudgetExceeded("tool budget exceeded")
if self.usage.estimated_usd > self.budget.max_usd:
raise BudgetExceeded("cost budget exceeded")
def on_tool_call(self) -> None:
self.usage.tool_calls += 1
def on_model_call(self, *, tokens_in: int, tokens_out: int) -> None:
self.usage.model_tokens_in += tokens_in
self.usage.model_tokens_out += tokens_out
self.usage.estimated_usd = estimate_usd(
self.usage.model_tokens_in, self.usage.model_tokens_out
)
def run(task: str, *, budget: Budget) -> str:
guard = BudgetGuard(budget)
while True:
guard.on_step()
# model call (pseudo)
action, tokens_in, tokens_out = llm_decide(task) # (pseudo)
guard.on_model_call(tokens_in=tokens_in, tokens_out=tokens_out)
if action.kind == "tool":
guard.on_tool_call()
result = call_tool(action.name, action.args) # (pseudo)
task = update_state(task, action, result) # (pseudo)
else:
return action.final_answerexport class BudgetExceeded extends Error {}
export class BudgetGuard {
constructor(budget) {
this.budget = budget;
this.started = Date.now();
this.steps = 0;
this.usage = { toolCalls: 0, tokensIn: 0, tokensOut: 0, estimatedUsd: 0 };
}
estimateUsd(tokensIn, tokensOut) {
// Replace with real pricing. Approximate is fine for guards.
return (tokensIn + tokensOut) * 0.000002;
}
onStep() {
this.steps += 1;
const elapsedS = (Date.now() - this.started) / 1000;
if (this.steps > this.budget.maxSteps) throw new BudgetExceeded("step budget exceeded");
if (elapsedS > this.budget.maxSeconds) throw new BudgetExceeded("time budget exceeded");
if (this.usage.toolCalls > this.budget.maxToolCalls) throw new BudgetExceeded("tool budget exceeded");
if (this.usage.estimatedUsd > this.budget.maxUsd) throw new BudgetExceeded("cost budget exceeded");
}
onToolCall() {
this.usage.toolCalls += 1;
}
onModelCall({ tokensIn, tokensOut }) {
this.usage.tokensIn += tokensIn;
this.usage.tokensOut += tokensOut;
this.usage.estimatedUsd = this.estimateUsd(this.usage.tokensIn, this.usage.tokensOut);
}
}Ключова деталь: budgets перевіряються постійно, а не “під кінець”. Треба зупинитись до того, як ти впадеш зі скелі.
Реальний інцидент (з цифрами)
У нас був агент, який у деві стабільно працював на ~3k tokens/request.
Потім ми додали “helpful context”:
- останні 20 повідомлень користувача
- повні tool outputs (включно з HTML)
- шматок runbook
Розмір промпта поплив. Ніхто не помітив.
Impact за 48 годин:
- median tokens/request: 3k → 16k
- p95 latency: 2.4s → 8.9s
- spend: +$740 відносно baseline
Fix:
- hard budgets (tokens, tool calls, time, spend)
- prompt builder з caps + summarization
- алерти на tokens/request і spend/run
- safe-mode fallback, коли budget hit
Це не “модель стала гіршою”. Ми дали їй більше тексту і сподівались, що рахунок не помітить.
Компроміси
- Tight budgets підвищують “stopped early” відповіді. Ок — краще, ніж runaway spend.
- Spend estimation приблизний. Йому не треба бути ідеальним, щоб бути корисним.
- Саммарі економлять токени, але можуть втратити нюанс. Використовуй там, де безпечно.
Коли НЕ варто
- Якщо ти взагалі не можеш оцінити cost (кілька моделей/tools) — почни з time/tool budgets і додай cost потім.
- Якщо ворклоад детермінований — workflow із фіксованим cost кращий.
- Якщо тобі потрібно long-context reasoning — плануй більший бюджет і роби це явно.
Чекліст (можна копіювати)
- [ ] Budgets: steps, tool calls, seconds, USD
- [ ] Track tokens in/out per run
- [ ] Оцінюй spend/run і алерть на спайки
- [ ] Капни retries (model + tool)
- [ ] Капни untrusted text (HTML/tool dumps)
- [ ] Summarize або truncate over-budget контекст
- [ ] Повертай stop reason (не мовчи таймаутом)
Безпечний дефолтний конфіг (JSON/YAML)
budgets:
max_steps: 25
max_seconds: 60
max_tool_calls: 12
max_usd: 1.0
llm:
retries: { max_attempts: 2 }
context:
max_prompt_tokens: 2500
summarize_when_over_budget: true
FAQ (3–5)
Використовується в патернах
Пов’язані відмови
Q: Потрібна точна калькуляція вартості для budgets?
A: Ні. Guards можуть бути приблизні. Мета — зупиняти runaway runs до того, як це стане інвойсом.
Q: З якого бюджету почати?
A: Час + tool calls. Потім додай token/spend, коли зможеш їх міряти.
Q: Як обробляти запити, яким треба більше бюджету?
A: Ескалюй: попроси підтвердження, переключи на більший tier або запусти async із видимим статусом.
Q: Можна поставити величезний budget і забути?
A: Можна. Тоді знову дізнаєшся про проблеми від фінансів і on-call.
Пов’язані сторінки (3–6 лінків)
- Foundations: Як ліміти LLM впливають на агентів · Production-ready агент
- Failure: Tool spam loops · Infinite loop
- Governance: Tool permissions (allowlists)
- Production stack: Production agent stack