Action is proposed as structured data (tool + args).
El problema (en producción)
Miras el uso del LLM y piensas: “todo bien”.
Luego llega la factura del vendor de browser/scraping/datos. Y te das cuenta: pusiste límite de velocidad… en una sola rueda.
Los cost limits no son glamorosos. Son la diferencia entre:
- “a veces sale caro”
- y “esto es un generador de costo sin límites”
Por qué esto se rompe en producción
1) Se limitan tokens y se olvidan los tools
En agentes reales:
- tokens son más previsibles
- tools son lo salvaje (retries, rate limits, trabajo variable)
Si un tool cuesta $0.20/call, 10 calls = $2. Eso pasa más rápido de lo que piensas.
2) Costo sin stop reason no es operable
Si solo ves “timeout”, nadie entiende que fue “demasiado caro”. El usuario reintenta y empeora.
3) Sin caps por tenant, un cliente puede romper tu mes
Multi-tenant = un solo tenant puede destruir el gráfico si no cappeas.
Ejemplo de implementación (código real)
Un cost guard simple:
- tokens + costo tools en un state
- check después de cada 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;
}Incidente real (con números)
Vimos una loop de investigación que usaba tools de browser.
El vendor estaba inestable ese día. Los tools retryan. El agente retrya. Y un request cuesta más que tu promedio diario.
Impacto:
- p95 gasto/request subió a $4.80
- la queue se llenó (runs más largos)
- soporte pasó ~2h con tickets “está lento”
Fix:
- cap duro
max_usd+ stop reason claro - circuit breaker a nivel tool cuando el vendor está flaky
- caching/dedupe para URLs/queries repetidas
Trade-offs
- Los caps cortan runs útiles a veces.
- El costo exacto por tool call es difícil; el aproximado sirve como guardrail.
- Cost limits sin budgets (steps/time/tool calls) es incompleto.
Cuándo NO usarlo
- Si no puedes estimar costo, pon al menos
max_secondsymax_tool_calls. - Aunque tengas tools internos “gratis”, cappea cuando haya vendors que cobren.
Checklist (copiar/pegar)
- [ ] cap
max_usdpor run - [ ] stop reason
max_usden logs + respuesta - [ ] costos tools aproximados (conservador)
- [ ] circuit breaker para vendor flaky
- [ ] caps por tenant / tiers
- [ ] alertas por spikes de stops
max_usd
Config segura por defecto (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)
Usado por patrones
Fallos relacionados
- AI Agent Infinite Loop (Detectar + arreglar, con código)
- Explosión de presupuesto (cuando un agente quema dinero) + fixes + código
- Tool Spam Loops (fallo del agente + fixes + código)
- Incidentes de exceso de tokens (prompt bloat) + fixes + código
- Corrupción de respuestas de tools (schema drift + truncation) + código
Gobernanza requerida
P: ¿Alcanza con cappear solo tools?
R: No. Los tokens también pueden explotar (context largo, retries). Trackea ambos.
P: ¿Cómo hago caps por tenant?
R: Con tiers. Y loggea spend por tenant, o lo ves recién a fin de mes.
P: ¿Qué cap por defecto recomiendas?
R: Lo suficientemente baja para evitar sorpresas y suficientemente alta para runs normales. Empieza en $1 y ajusta con datos.
P: ¿Por qué no elegir el modelo más barato?
R: Porque muchas veces el modelo no es lo más caro. Tool calls + retries suelen ser el driver real.
Páginas relacionadas (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