Normal path: execute → tool → observe.
El problema (en producción)
Shipeas un agente.
En testing cuesta “unos céntimos”.
Luego entra tráfico real y alguien escribe en Slack:
“¿Por qué gastamos $900 en el agente ayer?”
Las explosiones de presupuesto rara vez son un bug enorme. Son muerte por mil cortes:
- el uso de tokens deriva hacia arriba
- los retries se multiplican
- las tool calls se convierten en loops
- los prompts crecen “solo una vez más”
Si no mides y capás budgets, te enteras del gasto por finanzas. Finanzas no es un sistema de monitoreo.
Por qué esto se rompe en producción
En sistemas con agentes, el coste compone.
1) Los tokens escalan con el contexto, no con la intención
Intención: “resume esto”. Implementación: “pega los últimos 40 mensajes + 6 outputs de tools + 2 runbooks”.
El coste escala con lo que le das al modelo, no con lo que pidió el usuario.
2) Los retries multiplican coste
Si falla una llamada al modelo y retry:
- pagas dos veces
- sube la latencia
Si falla un tool y retry:
- pagas en coste del tool
- y a menudo pagas más tokens explicando el fallo
Los retries no son gratis. En loops de agentes son multiplicativos.
3) “Planning” es overhead puro
Agentes con mucho planning queman tokens antes de hacer algo útil. Eso está bien cuando previene tool spam. No está bien cuando es solo “más pensamiento”.
4) El tool spam vuelve inútil cualquier budget
Si no capás tool calls, el agente puede gastar $0.01 en tokens y $5 en tools. Tu “token budget” no te protegió. Porque no era el budget que necesitabas.
5) Si no loggeas gasto, no lo ves
Si tus logs no incluyen:
- tokens in/out
- conteo de tool calls
- estimación de coste por run
- stop reason
…no puedes alertar por drift de gasto.
Ejemplo de implementación (código real)
Un tracker mínimo por run:
- frena por tiempo, pasos, tool calls
- estima coste (aprox) y frena por gasto
- devuelve stop reason para alertar
from dataclasses import dataclass
import time
@dataclass(frozen=True)
class Budget:
max_steps: int = 25
max_seconds: int = 60
max_tool_calls: int = 12
max_usd: float = 1.00
@dataclass
class Usage:
tool_calls: int = 0
model_tokens_in: int = 0
model_tokens_out: int = 0
estimated_usd: float = 0.0
class BudgetExceeded(RuntimeError):
pass
def estimate_usd(tokens_in: int, tokens_out: int) -> float:
# Replace with real pricing for your model(s).
# This is intentionally simple: budgets need to be approximate, not perfect.
return (tokens_in + tokens_out) * 0.000002 # $/token (placeholder)
class BudgetGuard:
def __init__(self, budget: Budget) -> None:
self.budget = budget
self.usage = Usage()
self.started = time.time()
self.steps = 0
def on_step(self) -> None:
self.steps += 1
if self.steps > self.budget.max_steps:
raise BudgetExceeded("step budget exceeded")
if time.time() - self.started > self.budget.max_seconds:
raise BudgetExceeded("time budget exceeded")
if self.usage.tool_calls > self.budget.max_tool_calls:
raise BudgetExceeded("tool budget exceeded")
if self.usage.estimated_usd > self.budget.max_usd:
raise BudgetExceeded("cost budget exceeded")
def on_tool_call(self) -> None:
self.usage.tool_calls += 1
def on_model_call(self, *, tokens_in: int, tokens_out: int) -> None:
self.usage.model_tokens_in += tokens_in
self.usage.model_tokens_out += tokens_out
self.usage.estimated_usd = estimate_usd(
self.usage.model_tokens_in, self.usage.model_tokens_out
)
def run(task: str, *, budget: Budget) -> str:
guard = BudgetGuard(budget)
while True:
guard.on_step()
# model call (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()
result = call_tool(action.name, action.args) # (pseudo)
task = update_state(task, action, result) # (pseudo)
else:
return action.final_answerexport class BudgetExceeded extends Error {}
export class BudgetGuard {
constructor(budget) {
this.budget = budget;
this.started = Date.now();
this.steps = 0;
this.usage = { toolCalls: 0, tokensIn: 0, tokensOut: 0, estimatedUsd: 0 };
}
estimateUsd(tokensIn, tokensOut) {
// Replace with real pricing. Approximate is fine for guards.
return (tokensIn + tokensOut) * 0.000002;
}
onStep() {
this.steps += 1;
const elapsedS = (Date.now() - this.started) / 1000;
if (this.steps > this.budget.maxSteps) throw new BudgetExceeded("step budget exceeded");
if (elapsedS > this.budget.maxSeconds) throw new BudgetExceeded("time budget exceeded");
if (this.usage.toolCalls > this.budget.maxToolCalls) throw new BudgetExceeded("tool budget exceeded");
if (this.usage.estimatedUsd > this.budget.maxUsd) throw new BudgetExceeded("cost budget exceeded");
}
onToolCall() {
this.usage.toolCalls += 1;
}
onModelCall({ tokensIn, tokensOut }) {
this.usage.tokensIn += tokensIn;
this.usage.tokensOut += tokensOut;
this.usage.estimatedUsd = this.estimateUsd(this.usage.tokensIn, this.usage.tokensOut);
}
}El detalle clave: los budgets se chequean continuamente, no solo al final. Quieres parar antes del precipicio.
Incidente real (con números)
Teníamos un agente que iba bien en dev con ~3k tokens/request.
Luego añadimos “contexto útil”:
- últimos 20 mensajes de usuario
- outputs completos de tools (incluyendo HTML)
- un snippet de runbook
El tamaño del prompt derivó. Nadie lo vio.
Impacto en 48 horas:
- mediana tokens/request: 3k → 16k
- p95 latencia: 2.4s → 8.9s
- gasto: +$740 vs baseline
Fix:
- budgets duros (tokens, tool calls, tiempo, gasto)
- prompt builder con caps + summarization
- alertas en tokens/request y spend/run
- safe-mode cuando se llegue al budget
Esto no fue “el modelo empeoró”. Le dimos más texto y esperamos que la factura no se diera cuenta.
Trade-offs
- Budgets tight aumentan respuestas “cortadas temprano”. Bien — mejor que gasto infinito.
- La estimación de gasto es aproximada. No necesita ser perfecta para ser útil.
- Los resúmenes ahorran tokens pero pueden perder matices. Úsalos donde sea seguro.
Cuándo NO usarlo
- Si no puedes estimar coste (múltiples modelos/tools), empieza con budgets de tiempo/tools primero.
- Si el workload es determinista, un workflow con coste fijo es mejor.
- Si necesitas razonamiento de contexto largo, planea un budget mayor y hazlo explícito.
Checklist (copiar/pegar)
- [ ] Budgets: steps, tool calls, segundos, USD
- [ ] Track tokens in/out por run
- [ ] Estima gasto por run y alerta por spikes
- [ ] Capa retries (modelo + tool)
- [ ] Capa tamaño de texto no confiable (HTML/tool dumps)
- [ ] Resume o trunca contexto over-budget
- [ ] Devuelve stop reason (no timeouts silenciosos)
Config segura por defecto (JSON/YAML)
budgets:
max_steps: 25
max_seconds: 60
max_tool_calls: 12
max_usd: 1.0
llm:
retries: { max_attempts: 2 }
context:
max_prompt_tokens: 2500
summarize_when_over_budget: true
FAQ (3–5)
Usado por patrones
Fallos relacionados
Gobernanza requerida
Q: ¿Necesito contabilidad exacta para budgets?
A: No. Los guards pueden ser aproximados. El objetivo es parar runs desbocados antes de que sean facturas.
Q: ¿Con qué budget empiezo?
A: Tiempo + tool calls. Luego añade token/spend cuando puedas medirlo.
Q: ¿Qué hago con requests que necesitan más budget?
A: Escala: pide confirmación, sube a un tier mayor o corre async con estado visible.
Q: ¿Puedo poner un budget enorme y olvidarme?
A: Puedes, pero vuelves a enterarte de fallos por finanzas y on-call.
Páginas relacionadas (3–6 links)
- Foundations: Cómo afectan los límites del LLM a los agentes · Agente listo para producción
- Failure: Tool spam loops · Infinite loop
- Governance: Tool permissions (allowlists)
- Production stack: Production agent stack