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:
- ein Agent ist eine Loop, und Loops hören nicht aus Höflichkeit auf
- 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_secondsmax_tool_callsmax_usdloop_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
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.",
}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:
- harte Budgets in einem Choke Point
- stop reasons in Logs + Alerts
- dedupe/caching für wiederholte Tool Args
- 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)
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)
Von Patterns genutzt
Verwandte Failures
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“).
Verwandte Seiten (3–6 Links)
- Foundations: What makes an agent production-ready · Why agents fail in production
- Failure: Budget explosion · Tool spam loops
- Governance: Step limits · Kill switch design
- Production stack: Production agent stack