Budget Controls для AI агентів (кроки, час, $) + Код

Якщо агент може витрачати час і гроші без ліміту — він так і зробить. Бюджетна policy, яка зупиняє run’и безпечно і дає stop reasons.
На цій сторінці
  1. Проблема (з реального продакшену)
  2. Чому це ламається в продакшені
  3. 1) Команди лімітують одну метрику і забувають про інші
  4. 2) Retries стають мультиплікатором у loop
  5. 3) Бюджети без stop reasons — невидимі
  6. 4) Бюджетний контроль “по всьому коду” не працює
  7. Приклад реалізації (реальний код)
  8. Реальний інцидент (з цифрами)
  9. Компроміси
  10. Коли НЕ варто
  11. Чекліст (можна копіювати)
  12. Безпечний дефолтний конфіг (JSON/YAML)
  13. FAQ (3–5)
  14. Пов’язані сторінки (3–6 лінків)
Інтерактивний флоу
Сценарій:
Крок 1/3: Execution

Action is proposed as structured data (tool + args).

Проблема (з реального продакшену)

Агент “працює” у staging.

Потім приходить прод і ти дізнаєшся дві речі:

  1. агент — це loop, а loop не зупиняється з ввічливості
  2. фінанси — не monitoring система (але вони тебе точно знайдуть)

Ми бачили це багато разів:

  • flaky tool додає retries
  • retries множать tool calls
  • tool calls множать tokens (“ось що сталося… спробуй ще раз”)
  • і твій “кілька центів” агент раптом робить $8–$20 за один run

У масштабі це не “баг”. Це сюрприз-підписка, яку CFO не підписував.

Бюджети — це не “оптимізація”. Це safety controls. Вони визначають, що робити, якщо агент не може завершити задачу.

Якщо ти не вирішиш — вирішить агент. І зазвичай його рішення: “ще одна спроба”.

Чому це ламається в продакшені

1) Команди лімітують одну метрику і забувають про інші

Класика: “у нас є token budget”.

Ок. Агент витратив $0.04 на токени і $6 на browser automation.

У проді мінімально потрібні:

  • max_steps
  • max_seconds
  • max_tool_calls
  • max_usd

2) Retries стають мультиплікатором у loop

Один retry не проблема. Retries всередині loop + retries tool’ів = множник вартості.

3) Бюджети без stop reasons — невидимі

Якщо run закінчується timeout’ом, користувачі просто повторять. Це створює більше run’ів.

Хочеш явні stop reasons:

  • max_seconds
  • max_tool_calls
  • max_usd
  • loop_detected

Stop reasons = observability.

4) Бюджетний контроль “по всьому коду” не працює

Якщо бюджети перевіряються:

  • інколи в агенті
  • інколи в tool wrapper’і
  • інколи ніде

…ти пропустиш шлях.

Поклади бюджети в choke point: run loop + tool gateway.

Приклад реалізації (реальний код)

Production-shaped budget guard:

  • постійні перевірки
  • трекінг model + tool costs (approx ок)
  • typed stop reason для логів/алертів
PYTHON
from dataclasses import dataclass, field
import time
from typing import Any


TOOL_USD = {
  "search.read": 0.00,
  "http.get": 0.00,
  "browser.run": 0.20,  # placeholder
}


@dataclass(frozen=True)
class BudgetPolicy:
  max_steps: int = 25
  max_seconds: int = 60
  max_tool_calls: int = 12
  max_usd: float = 1.00


@dataclass
class BudgetState:
  started_at: float = field(default_factory=time.time)
  steps: int = 0
  tool_calls: int = 0
  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:
  # Replace with your real pricing model(s). Approximate is fine for guards.
  return (tokens_in + tokens_out) * 0.000002


class BudgetExceeded(RuntimeError):
  def __init__(self, stop_reason: str, *, state: BudgetState):
      super().__init__(stop_reason)
      self.stop_reason = stop_reason
      self.state = state


class BudgetGuard:
  def __init__(self, policy: BudgetPolicy):
      self.policy = policy
      self.state = BudgetState()

  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.state.steps > self.policy.max_steps:
          raise BudgetExceeded("max_steps", state=self.state)
      if self.state.elapsed_s() > self.policy.max_seconds:
          raise BudgetExceeded("max_seconds", state=self.state)
      if self.state.tool_calls > self.policy.max_tool_calls:
          raise BudgetExceeded("max_tool_calls", state=self.state)
      if self.total_usd() > self.policy.max_usd:
          raise BudgetExceeded("max_usd", state=self.state)

  def on_step(self) -> None:
      self.state.steps += 1
      self.check()

  def on_model_call(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_call(self, *, tool: str) -> None:
      self.state.tool_calls += 1
      self.state.tool_usd += float(TOOL_USD.get(tool, 0.0))
      self.check()


def run_agent(task: str, *, policy: BudgetPolicy) -> dict[str, Any]:
  guard = BudgetGuard(policy)

  try:
      while True:
          guard.on_step()

          # model decides next action (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(tool=action.name)
              obs = call_tool(action.name, action.args)  # (pseudo)
              task = update_state(task, action, obs)  # (pseudo)
              continue

          return {"status": "ok", "answer": action.final_answer, "usage": guard.state.__dict__}

  except BudgetExceeded as e:
      return {
          "status": "stopped",
          "stop_reason": e.stop_reason,
          "usage": e.state.__dict__,
          "partial": "Stopped by budget. Return partial results + a reason users can understand.",
      }
JAVASCRIPT
const TOOL_USD = {
"search.read": 0.0,
"http.get": 0.0,
"browser.run": 0.2, // placeholder
};

export class BudgetExceeded extends Error {
constructor(stopReason, { state }) {
  super(stopReason);
  this.stopReason = stopReason;
  this.state = state;
}
}

export class BudgetGuard {
constructor(policy) {
  this.policy = policy;
  this.state = {
    startedAtMs: Date.now(),
    steps: 0,
    toolCalls: 0,
    tokensIn: 0,
    tokensOut: 0,
    toolUsd: 0,
  };
}

elapsedS() {
  return (Date.now() - this.state.startedAtMs) / 1000;
}

totalUsd() {
  return estimateModelUsd(this.state.tokensIn, this.state.tokensOut) + this.state.toolUsd;
}

check() {
  if (this.state.steps > this.policy.maxSteps) throw new BudgetExceeded("max_steps", { state: this.state });
  if (this.elapsedS() > this.policy.maxSeconds) throw new BudgetExceeded("max_seconds", { state: this.state });
  if (this.state.toolCalls > this.policy.maxToolCalls) throw new BudgetExceeded("max_tool_calls", { state: this.state });
  if (this.totalUsd() > this.policy.maxUsd) throw new BudgetExceeded("max_usd", { state: this.state });
}

onStep() {
  this.state.steps += 1;
  this.check();
}

onModelCall({ tokensIn, tokensOut }) {
  this.state.tokensIn += tokensIn;
  this.state.tokensOut += tokensOut;
  this.check();
}

onToolCall({ tool }) {
  this.state.toolCalls += 1;
  this.state.toolUsd += Number(TOOL_USD[tool] ?? 0);
  this.check();
}
}

function estimateModelUsd(tokensIn, tokensOut) {
return (tokensIn + tokensOut) * 0.000002;
}

Реальний інцидент (з цифрами)

Ми мали “невинного” агента, який шукав по внутрішній документації.

Tool інколи повертав 5xx. Tool layer ретраїла (логічно). Агент ретраїв теж (логічно). У loop “логічно” швидко перетворюється на “чому горить бюджет”.

Імпакт за один день:

  • p95 cost/request з $0.06 → $2.10
  • 429/5xx зросли (агент наполягав)
  • сапорт отримав “повільно” тікети (бо так, повільно)

Fix:

  1. жорсткі бюджети в choke point
  2. stop reasons у логах + алертах
  3. dedupe/caching для повторних args
  4. read-only degrade mode замість “хай крутиться”

Компроміси

  • Бюджети інколи відрізають легітимні run’и. Це краще, ніж нескінченні loops.
  • Потрібні stop reasons + UX, інакше користувачі просто повторять.
  • Точний cost tracking складний; approximate достатньо як guardrail.

Коли НЕ варто

  • Якщо ти взагалі не можеш оцінити cost — почни хоча б зі steps/time/tool-calls.
  • Навіть у детермінованих workflow бюджети корисні (менш жорсткі, але є).

Чекліст (можна копіювати)

  • [ ] Steps budget (max_steps)
  • [ ] Time budget (max_seconds)
  • [ ] Tool-call budget (max_tool_calls)
  • [ ] Spend budget (max_usd) або approximate
  • [ ] Stop reasons логувати + показувати
  • [ ] Enforcement у tool gateway + run loop
  • [ ] Alerting на сплеск budget stops

Безпечний дефолтний конфіг (JSON/YAML)

YAML
budgets:
  max_steps: 25
  max_seconds: 60
  max_tool_calls: 12
  max_usd: 1.00
stop_reasons:
  log: true
  surface_to_user: true

FAQ (3–5)

Token budget достатньо?
Ні. Tools часто дорожчі за tokens. Лімітуй steps/time/tool-calls і $.
Потрібні бюджети per-tenant?
Так. Tiers працюють добре. Бюджети — частина продукту.
Наскільки точним має бути cost tracking?
Не ідеальним. Для guardrails вистачає приблизного. Точна білінг-модель може бути пізніше.
Що відповідати користувачу при budget stop?
Partial результат + stop reason + що робити далі (звузити scope тощо).

Q: Token budget достатньо?
A: Ні. Tools часто дорожчі за tokens. Лімітуй steps/time/tool-calls і $.

Q: Потрібні бюджети per-tenant?
A: Так. Tiers працюють добре. Бюджети — частина продукту.

Q: Наскільки точним має бути cost tracking?
A: Не ідеальним. Для guardrails вистачає приблизного. Точна білінг-модель може бути пізніше.

Q: Що відповідати користувачу при budget stop?
A: Partial результат + stop reason + що робити далі (звузити scope тощо).

Пов’язані сторінки (3–6 лінків)

Не впевнені, що це ваш кейс?

Спроєктувати агента →
⏱️ 6 хв читанняОновлено Бер, 2026Складність: ★★★
Реалізувати в OnceOnly
Budgets + permissions you can enforce at the boundary.
Використати в OnceOnly
# onceonly guardrails (concept)
version: 1
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
  max_usd: 1.00
policy:
  tool_allowlist:
    - search.read
    - http.get
writes:
  require_approval: true
  idempotency: true
controls:
  kill_switch: { enabled: true }
Інтегровано: продакшен-контрольOnceOnly
Додай guardrails до агентів з tool-calling
Зашип цей патерн з governance:
  • Бюджетами (кроки / ліміти витрат)
  • Дозволами на інструменти (allowlist / blocklist)
  • Kill switch та аварійна зупинка
  • Ідемпотентність і dedupe
  • Audit logs та трасування
Інтегрована згадка: OnceOnly — контрольний шар для продакшен агент-систем.
Автор

Цю документацію курують і підтримують інженери, які запускають AI-агентів у продакшені.

Контент створено з допомогою AI, із людською редакторською відповідальністю за точність, ясність і продакшн-релевантність.

Патерни та рекомендації базуються на постмортемах, режимах відмов і операційних інцидентах у розгорнутих системах, зокрема під час розробки та експлуатації governance-інфраструктури для агентів у OnceOnly.