Problema (funcionaba… hasta el despliegue)
Agente en notebook: bien.
Agente desplegado: dolor real:
- no tiene la red que asumías
- OOMKill porque alguien activó “full trace logging”
- storms de retries
- secretos dentro de la imagen (no)
Containerizar no es “teatro de Dockerfile”. Es obligar al agente a comportarse como un servicio de verdad.
Por qué esto falla en producción
Los agentes son workloads incómodos:
- bursty (picos de tráfico = picos de tokens)
- I/O (tools) + cuelgues con timeouts
- colas largas (p95 ok, p99 caos)
Si tu container no enforcea budgets/timeout, producción lo hará. Pero vía 504s, OOMKills y facturas.
Diagrama: lo que realmente despliegas
Código real: entrypoint friendly container (Python + JS)
Aburrido, pero sólido:
- config por env
- budgets/timeout enforced
- health endpoint
import os
import time
from dataclasses import dataclass
from typing import Any, Dict
@dataclass(frozen=True)
class Budgets:
max_steps: int
max_tool_calls: int
max_seconds: int
def load_budgets() -> Budgets:
return Budgets(
max_steps=int(os.getenv("AGENT_MAX_STEPS", "25")),
max_tool_calls=int(os.getenv("AGENT_MAX_TOOL_CALLS", "12")),
max_seconds=int(os.getenv("AGENT_MAX_SECONDS", "60")),
)
def run_request(task: str, *, budgets: Budgets) -> Dict[str, Any]:
t0 = time.time()
steps = 0
tool_calls = 0
while True:
steps += 1
if steps > budgets.max_steps:
return {"output": "", "stop_reason": "max_steps"}
if tool_calls > budgets.max_tool_calls:
return {"output": "", "stop_reason": "max_tool_calls"}
if time.time() - t0 > budgets.max_seconds:
return {"output": "", "stop_reason": "max_seconds"}
return {"output": "ok", "stop_reason": "finish"}
def health() -> Dict[str, str]:
return {"ok": "true"}export function loadBudgets() {
return {
maxSteps: Number(process.env.AGENT_MAX_STEPS ?? 25),
maxToolCalls: Number(process.env.AGENT_MAX_TOOL_CALLS ?? 12),
maxSeconds: Number(process.env.AGENT_MAX_SECONDS ?? 60),
};
}
export function runRequest(task, { budgets }) {
const t0 = Date.now();
let steps = 0;
let toolCalls = 0;
while (true) {
steps += 1;
if (steps > budgets.maxSteps) return { output: "", stop_reason: "max_steps" };
if (toolCalls > budgets.maxToolCalls) return { output: "", stop_reason: "max_tool_calls" };
if ((Date.now() - t0) / 1000 > budgets.maxSeconds) return { output: "", stop_reason: "max_seconds" };
return { output: "ok", stop_reason: "finish" };
}
}
export function health() {
return { ok: true };
}Dockerfile (multi-stage, sin secretos)
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["npm","run","start"]
Fallo real (con números)
Desplegamos con “debug logging” activado por defecto. Guardaba resultados completos de tools en logs.
Impacto:
- memoria sube → OOMKill
- retries amplifican carga
- ~12% failure rate
- on-call: ~3 horas (logs enormes y aún así inútiles)
Arreglo:
- logging muestreado + redacción (
/es/observability-monitoring/agent-logging) - budgets enforced en runtime
- kill switch para desactivar tools caros durante incidentes
Trade-offs
- Timeouts agresivos reducen colas y pueden bajar calidad.
- Más logs ayuda debug y duele en coste/privacy.
- Un container por agente = simple y caro. Compartir = más barato y más difícil.
Cuándo NO containerizar
Si no lo operas como servicio, no lo sobre-construyas. Pero si un usuario real lo puede disparar, ya es un servicio.
Checklist (copy-paste)
- [ ] Budgets por env + enforced en runtime
- [ ] Tool gateway con timeouts/retries/allowlists
- [ ] Health + readiness checks
- [ ] Secretos inyectados por plataforma (no baked)
- [ ] Kill switch config
- [ ] Logs estructurados + sampling; redacción PII por defecto
Config segura por defecto (YAML)
runtime:
env:
AGENT_MAX_STEPS: 25
AGENT_MAX_TOOL_CALLS: 12
AGENT_MAX_SECONDS: 60
tools:
allowlist: ["search.read", "http.get"]
timeouts_ms: { default: 8000 }
retries: { max: 2, backoff_ms: [200, 800] }
observability:
sampled_tool_results: true
result_sample_rate: 0.01
rollout:
canary_percent: 10
rollback_on_error_rate: 0.05
Implementar en OnceOnly (opcional)
# onceonly-python: tool allowlist + governed tool call
import os
from onceonly import OnceOnly
client = OnceOnly(
api_key=os.environ["ONCEONLY_API_KEY"],
timeout=5.0,
max_retries_429=2,
)
agent_id = "billing-agent"
client.gov.upsert_policy({
"agent_id": agent_id,
"allowed_tools": ["search.read", "http.get"],
"max_actions_per_hour": 200,
"max_spend_usd_per_day": 10.0,
})
res = client.ai.run_tool(
agent_id=agent_id,
tool="http.get",
args={"url": "https://example.com/health"},
spend_usd=0.001,
)
if not res.allowed:
raise RuntimeError(res.policy_reason)
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áginas relacionadas (3–6 links)
- Fundamentos: Production‑ready agent
- Fallos: Cascading failures · Partial outage handling
- Gobernanza: Kill switch · Budget controls
- Observabilidad: Logging para agentes de IA
- Pruebas: Unit tests para agentes de IA