Action is proposed as structured data (tool + args).
Problem (aus der Praxis)
Du schaust auf deine LLM-Usage und denkst: „Alles okay.“
Dann kommt die Rechnung vom Browser-Vendor / Scraper / Datenanbieter. Und plötzlich ist klar: du hast „Speed Limits“ auf einem Rad gebaut.
Cost Limits sind nicht sexy. Sie sind der Unterschied zwischen:
- „Agent ist gelegentlich teuer“
- und „Agent ist ein unbounded Cost-Generator“
Warum das in Production bricht
1) Teams limitieren Tokens und vergessen Tools
In realen Agenten:
- Tokens sind predictable-ish
- Tools sind der wilde Teil (Retries, Rate Limits, variable Work)
Wenn dein Tool $0.20 pro Call kostet, brauchst du 10 Calls, um $2 zu verbrennen. Das passiert schneller, als ein Promptrun fertig tippt.
2) Cost ohne Stop-Reason ist nicht operierbar
Wenn du nur „timeout“ hast, sieht niemand, dass es eigentlich „zu teuer“ war. Und dann retryen Leute. Und es wird noch teurer.
3) Ohne per-tenant Caps eskaliert ein einzelner Kunde deinen Monat
Multi-tenant heißt: ein Customer kann den Cost-Graphen für alle ruinieren, wenn du nicht capst.
Implementierungsbeispiel (echter Code)
Ein einfacher „Cost Guard“:
- token + tool costs in einem State
- checkt nach jedem Step und Tool Call
- stoppt sauber mit
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;
}Echter Incident (mit Zahlen)
Wir haben eine „simple“ Research-Loop gesehen, die Browser-Tools benutzt hat.
Ein Vendor hatte an dem Tag Instabilität. Tools retryen. Agent retry’t. Und plötzlich kostet ein einziger User-Request mehr als dein durchschnittlicher Tageswert.
Impact:
- p95 spend/request stieg auf $4.80
- Queue wurde voll (weil Runs länger wurden)
- Support hat 2 Stunden lang „es ist langsam“ Tickets triagiert
Fix:
- harte Cost Cap (
max_usd) + Stop-Reason - Tool-level Circuit Breaker bei Vendor Instabilität
- Caching/Dedupe bei gleichen URLs/Queries
Abwägungen
- Cost Caps schneiden manchmal „nützliche“ Runs ab.
- Exakte Kosten pro Tool Call sind schwer; Approx ist okay für Guardrails.
- Cost Limits ohne Budgets (Steps/Time/Tool Calls) sind unvollständig.
Wann du es NICHT nutzen solltest
- Wenn du wirklich keine Kosten approximieren kannst, nutz wenigstens Time + Tool-call budgets.
- Für read-only, free internal tools: du brauchst trotzdem Caps, sobald ein Vendor Geld kostet.
Checkliste (Copy/Paste)
- [ ]
max_usdcap pro run - [ ] Stop-Reason
max_usdin Logs + Response - [ ] Tool costs approximieren (konservativ)
- [ ] Circuit breaker für flaky vendors
- [ ] per-tenant caps / tiers
- [ ] Alert: spike in max_usd stops
Sicheres Default-Config-Snippet (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)
Von Patterns genutzt
Verwandte Failures
Q: Reicht es, nur Tool Costs zu cappen?
A: Nein. Tokens können auch eskalieren (lange Kontext-Threads, Retries). Track beides.
Q: Wie setze ich per-tenant Cost Limits?
A: Über Tiers. Und logge spend per tenant, sonst merkst du’s erst am Monatsende.
Q: Was ist eine gute Default-Cap?
A: So niedrig, dass du nicht überrascht wirst, aber hoch genug für „normal“. Starte z. B. bei $1 und adjust nach Daten.
Q: Warum nicht einfach den günstigsten Model nehmen?
A: Model kostet selten am meisten. Tool-Calls + Retries sind oft der echte Cost-Treiber.
Verwandte Seiten (3–6 Links)
- 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