Action is proposed as structured data (tool + args).
Проблема (з реального продакшену)
Дивишся на LLM usage і думаєш: “норм”.
Потім прилітає рахунок від browser vendor / scraper / data provider. І стає ясно: ти зробив speed limit на одному колесі.
Cost limits не гламурні. Але це різниця між:
- “інколи дорого”
- і “це генератор витрат без меж”
Чому це ламається в продакшені
1) Лімітують tokens і забувають про tools
У реальних агентів:
- tokens більш-менш передбачувані
- tools — хаос (retries, rate limits, варіативна робота)
Якщо tool коштує $0.20 за call, 10 calls = $2. Це трапляється дуже швидко.
2) Вартість без stop reason не операбельна
Якщо ти бачиш тільки “timeout”, ніхто не розуміє, що причина — “занадто дорого”. Користувачі повторюють і роблять гірше.
3) Без per-tenant caps один клієнт може спалити місяць
Multi-tenant без caps — це коли один tenant ламає бюджет усім.
Приклад реалізації (реальний код)
Простий cost guard:
- tokens + tool costs в одному state
- check після кожного step/tool call
- stop reason
max_usd
from dataclasses import dataclass, field
import time
TOOL_USD = {
"browser.run": 0.20,
"search.read": 0.00,
}
@dataclass(frozen=True)
class CostPolicy:
max_usd: float = 1.00
max_seconds: int = 60
@dataclass
class CostState:
started_at: float = field(default_factory=time.time)
tokens_in: int = 0
tokens_out: int = 0
tool_usd: float = 0.0
def elapsed_s(self) -> float:
return time.time() - self.started_at
def estimate_model_usd(tokens_in: int, tokens_out: int) -> float:
return (tokens_in + tokens_out) * 0.000002
class CostExceeded(RuntimeError):
def __init__(self, stop_reason: str, *, state: CostState):
super().__init__(stop_reason)
self.stop_reason = stop_reason
self.state = state
class CostGuard:
def __init__(self, policy: CostPolicy):
self.policy = policy
self.state = CostState()
def total_usd(self) -> float:
return estimate_model_usd(self.state.tokens_in, self.state.tokens_out) + self.state.tool_usd
def check(self) -> None:
if self.total_usd() > self.policy.max_usd:
raise CostExceeded("max_usd", state=self.state)
if self.state.elapsed_s() > self.policy.max_seconds:
raise CostExceeded("max_seconds", state=self.state)
def on_model(self, *, tokens_in: int, tokens_out: int) -> None:
self.state.tokens_in += tokens_in
self.state.tokens_out += tokens_out
self.check()
def on_tool(self, *, tool: str) -> None:
self.state.tool_usd += float(TOOL_USD.get(tool, 0.0))
self.check()const TOOL_USD = { "browser.run": 0.2, "search.read": 0.0 };
export class CostExceeded extends Error {
constructor(stopReason, { state }) {
super(stopReason);
this.stopReason = stopReason;
this.state = state;
}
}
export class CostGuard {
constructor(policy) {
this.policy = policy;
this.state = { startedAtMs: Date.now(), tokensIn: 0, tokensOut: 0, toolUsd: 0 };
}
totalUsd() {
return estimateModelUsd(this.state.tokensIn, this.state.tokensOut) + this.state.toolUsd;
}
check() {
if (this.totalUsd() > this.policy.maxUsd) throw new CostExceeded("max_usd", { state: this.state });
if ((Date.now() - this.state.startedAtMs) / 1000 > this.policy.maxSeconds) {
throw new CostExceeded("max_seconds", { state: this.state });
}
}
onModel({ tokensIn, tokensOut }) {
this.state.tokensIn += tokensIn;
this.state.tokensOut += tokensOut;
this.check();
}
onTool({ tool }) {
this.state.toolUsd += Number(TOOL_USD[tool] ?? 0);
this.check();
}
}
function estimateModelUsd(tokensIn, tokensOut) {
return (tokensIn + tokensOut) * 0.000002;
}Реальний інцидент (з цифрами)
Ми бачили research loop, яка використовувала browser tools.
Вендор був нестабільний того дня. Tools ретраять. Агент ретраїть. І один request коштує більше, ніж твоя середня денна вартість.
Імпакт:
- p95 spend/request підскочив до $4.80
- queue забилась (runs стали довші)
- сапорт ~2 години розгрібав “повільно”
Fix:
- жорсткий cap
max_usd+ stop reason - circuit breaker на рівні tool при нестабільному vendor
- caching/dedupe для однакових URLs/queries
Компроміси
- Caps інколи відрізають корисні run’и.
- Точна ціна per tool call складна; approximate ок як guardrail.
- Cost limits без budgets (steps/time/tool calls) — неповно.
Коли НЕ варто
- Якщо взагалі не можеш оцінити cost — став хоча б
max_secondsіmax_tool_calls. - Навіть для “безкоштовних” внутрішніх tools cap потрібен, якщо поруч є платні вендори.
Чекліст (можна копіювати)
- [ ] cap
max_usdна run - [ ] stop reason
max_usdу логах і відповіді - [ ] conservative tool costs (approx)
- [ ] circuit breaker для flaky vendors
- [ ] per-tenant caps / tiers
- [ ] alert на spikes
max_usdstops
Безпечний дефолтний конфіг (JSON/YAML)
cost_limits:
max_usd: 1.00
max_seconds: 60
tool_costs_usd:
browser.run: 0.20
search.read: 0.00
stop_reasons:
log: true
surface_to_user: true
FAQ (3–5)
Використовується в патернах
Пов’язані відмови
Q: Достатньо лімітувати тільки tools?
A: Ні. Tokens теж можуть “вибухнути” (довгий контекст, retries). Трекай обидва.
Q: Як зробити caps per-tenant?
A: Через tiers. І логуйте spend per tenant, інакше дізнаєтесь наприкінці місяця.
Q: Який default cap нормальний?
A: Достатньо низький, щоб не було сюрпризів, але достатньо високий для нормальних run’ів. Стартуй з $1 і тюнінгуй по даних.
Q: Чому не просто брати найдешевшу модель?
A: Бо модель часто не найдорожча частина. Tool calls + retries — реальний драйвер витрат.
Пов’язані сторінки (3–6 лінків)
- Foundations: How agents use tools · How LLM limits affect agents
- Failure: Budget explosion · Token overuse incidents
- Governance: Budget controls · Step limits
- Production stack: Production agent stack