Budget Controls für AI Agents (Steps, Time, $) + Code

Wenn dein Agent unbegrenzt Zeit und Geld ausgeben kann, wird er’s tun. Eine Budget-Policy, die Runs sauber stoppt und Stop-Reasons liefert.
Auf dieser Seite
  1. Problem (aus der Praxis)
  2. Warum das in Production bricht
  3. 1) Teams budgetieren *eine* Sache und vergessen den Rest
  4. 2) Retries sind multiplikativ in Loops
  5. 3) Budgets ohne Stop-Reasons sind unsichtbar
  6. 4) Budget-Enforcement überall verteilt funktioniert nicht
  7. Implementierungsbeispiel (echter Code)
  8. Echter Incident (mit Zahlen)
  9. Abwägungen
  10. Wann du es NICHT nutzen solltest
  11. Checkliste (Copy/Paste)
  12. Sicheres Default-Config-Snippet (JSON/YAML)
  13. FAQ (3–5)
  14. Verwandte Seiten (3–6 Links)
Interaktiver Ablauf
Szenario:
Schritt 1/3: Execution

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

Problem (aus der Praxis)

Dein Agent „funktioniert“ in Staging.

Dann trifft er Production-Traffic und du lernst zwei Dinge:

  1. ein Agent ist eine Loop, und Loops hören nicht aus Höflichkeit auf
  2. Finance ist kein Monitoring-System (aber sie pagen dich trotzdem)

Wir haben das Muster oft genug gesehen:

  • ein flaky Tool triggert Retries
  • Retries erhöhen Tool Calls
  • Tool Calls erhöhen Tokens („hier ist was passiert… versuch’s nochmal“)
  • und plötzlich kostet ein „ein paar Cent“-Agent $8–$20 pro Run

Bei Scale ist das kein „Bug“. Das ist ein Surprise-Abo, das dein CFO nicht unterschrieben hat.

Budgets sind keine „Cost Optimization“. Sie sind Safety Controls. Sie entscheiden, was passiert, wenn der Agent nicht fertig wird.

Wenn du’s nicht entscheidest, entscheidet der Agent. Und der Agent entscheidet meistens: „one more try“.

Warum das in Production bricht

Budget-Failures sind langweilig. Genau deshalb shippen sie.

1) Teams budgetieren eine Sache und vergessen den Rest

Der Klassiker: „Wir haben ein Token-Budget.“

Cool. Dein Agent hat $0.04 an Tokens verbrannt und $6 an Browser-Automation.

Production-Budgets brauchen mindestens:

  • max_steps (Loop-Länge)
  • max_seconds (Wall-Clock)
  • max_tool_calls (Blast Radius)
  • max_usd (die „nope“-Linie)

2) Retries sind multiplikativ in Loops

Ein Retry ist nicht das Problem. Retries in einer Agent-Loop (plus Tool-Retries) sind ein Cost-Multiplier.

3) Budgets ohne Stop-Reasons sind unsichtbar

Wenn ein Run einfach „timeout“ endet, retryen User. Das erzeugt mehr Runs.

Du willst explizite Stop-Reasons:

  • max_seconds
  • max_tool_calls
  • max_usd
  • loop_detected

Stop-Reasons sind Observability.

4) Budget-Enforcement überall verteilt funktioniert nicht

Wenn Budgets gecheckt werden:

  • manchmal im Agent
  • manchmal im Tool Wrapper
  • manchmal gar nicht

…verpasst du einen Pfad.

Leg Budgets in einen Choke Point: Run Loop + Tool Gateway.

Implementierungsbeispiel (echter Code)

Ein production-shaped Budget-Guard:

  • checkt Budgets kontinuierlich (nicht „am Ende“)
  • trackt Model + Tool Cost (Approx ist okay)
  • wirft einen typed Stop-Reason, den du loggen + alarmieren kannst
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;
}

Echter Incident (mit Zahlen)

Wir hatten einen „harmlosen“ Agenten, der interne Doku durchsuchen sollte.

Ein Tool war flaky und lieferte manchmal 5xx. Die Tool-Layer hat retryed (verständlich). Der Agent hat ebenfalls retryed (auch verständlich). Und in der Loop multipliziert sich „verständlich“ zu „warum brennt unser Budget“.

Impact über einen Nachmittag:

  • p95 Requests von $0.06 → $2.10
  • 429s/5xx stiegen, weil der Agent immer mehr nachgelegt hat
  • Support hat Tickets bekommen: „es ist langsam“ (weil es wirklich langsam war)

Fix:

  1. harte Budgets in einem Choke Point
  2. stop reasons in Logs + Alerts
  3. dedupe/caching für wiederholte Tool Args
  4. ein „read-only degrade“-Mode, statt es einfach laufen zu lassen

Abwägungen

  • Budgets cutten gelegentlich legitime Runs ab. Das ist besser als unendliche Loops.
  • Du brauchst Stop-Reasons + UX, sonst retryen User einfach.
  • Genaues Cost-Tracking ist schwer; Approx reicht als Guardrail.

Wann du es NICHT nutzen solltest

  • Wenn du keine Tool Costs hast und nichts approximieren kannst, fang wenigstens mit Steps/Time/Tool-Calls an.
  • Wenn du deterministische Workflows hast: budgetier trotzdem, aber du wirst weniger brauchen.

Checkliste (Copy/Paste)

  • [ ] Steps budget (max_steps)
  • [ ] Time budget (max_seconds)
  • [ ] Tool-call budget (max_tool_calls)
  • [ ] Spend budget (max_usd) oder Approx
  • [ ] Stop reasons in Logs + User Response
  • [ ] Budgets enforced im Tool Gateway + Run Loop
  • [ ] Alerting auf Budget Stops (spike)

Sicheres Default-Config-Snippet (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)

Reicht ein Token-Budget?
Nein. Tools kosten oft mehr als Tokens. Budgetiere Steps/Time/Tool-Calls und $.
Soll ich Budgets pro Tenant setzen?
Ja. Tiers funktionieren gut: free/standard/enterprise. Budgets sind Teil des Produkts.
Wie genau muss Cost-Tracking sein?
Nicht perfekt. Guards brauchen grobe Richtung. Exakte Abrechnung kann später kommen.
Was antworte ich dem User bei Budget-Stop?
Partial Result + Stop-Reason, und was als Nächstes zu tun ist (z. B. ‚try again with narrower scope‘).

Q: Reicht ein Token-Budget?
A: Nein. Tools kosten oft mehr als Tokens. Budgetiere Steps/Time/Tool-Calls und $.

Q: Soll ich Budgets pro Tenant setzen?
A: Ja. Tiers funktionieren gut: free/standard/enterprise. Budgets sind Teil des Produkts.

Q: Wie genau muss Cost-Tracking sein?
A: Nicht perfekt. Guards brauchen grobe Richtung. Exakte Abrechnung kann später kommen.

Q: Was antworte ich dem User bei Budget-Stop?
A: Partial Result + Stop-Reason, und was als Nächstes zu tun ist (z. B. „try again with narrower scope“).

Nicht sicher, ob das dein Fall ist?

Agent gestalten ->
⏱️ 7 Min. LesezeitAktualisiert Mär, 2026Schwierigkeit: ★★★
In OnceOnly umsetzen
Budgets + permissions you can enforce at the boundary.
In OnceOnly nutzen
# 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 }
Integriert: Production ControlOnceOnly
Guardrails für Tool-Calling-Agents
Shippe dieses Pattern mit Governance:
  • Budgets (Steps / Spend Caps)
  • Tool-Permissions (Allowlist / Blocklist)
  • Kill switch & Incident Stop
  • Idempotenz & Dedupe
  • Audit logs & Nachvollziehbarkeit
Integrierter Hinweis: OnceOnly ist eine Control-Layer für Production-Agent-Systeme.
Autor

Diese Dokumentation wird von Engineers kuratiert und gepflegt, die AI-Agenten in der Produktion betreiben.

Die Inhalte sind KI-gestützt, mit menschlicher redaktioneller Verantwortung für Genauigkeit, Klarheit und Produktionsrelevanz.

Patterns und Empfehlungen basieren auf Post-Mortems, Failure-Modes und operativen Incidents in produktiven Systemen, auch bei der Entwicklung und dem Betrieb von Governance-Infrastruktur für Agenten bei OnceOnly.