Stack de producción para agentes de IA (lo que hay entre tu agente y el desastre)

Tu agente no es un prompt. Es un stack: budgets, tools, estado, logs y controles. Esto es el pegamento que evita incidentes.
En esta página
  1. El problema
  2. Por qué pasa en sistemas reales
  3. Qué se rompe si lo ignoras
  4. El stack (lo que de verdad corremos)
  5. Diagrama (dónde se coloca la capa de control)
  6. Capa por capa: lo que aprendimos a golpes
  7. Punto de entrada
  8. Orchestrator
  9. Model layer
  10. Tool layer
  11. State layer
  12. Observability
  13. Control layer
  14. Código: skeleton de orquestación (TypeScript)
  15. Qué medimos (porque “parece que va” no es una métrica)
  16. Taxonomía de stop reasons (para debugear sin vibes)
  17. Realidad multi‑tenant (donde se esconden los incidentes)
  18. Rate limits y circuit breakers (porque los agentes amplifican outages)
  19. El rollout que usamos (porque un agente no merece confianza el día 1)
  20. Dónde debería vivir la “memory” (pista: no en los prompts)
  21. Respuesta a incidentes (lo que quieres listo antes del primer pager)
  22. Testing y replay (porque “funciona con mi prompt” no es un test)
  23. Orden de construcción “boring first”
  24. Fallo real
  25. Compromisos
  26. Cuándo NO construir un stack de agente
  27. Enlaces

El problema

En dev, tu agente “funciona”.

En prod:

  • se queda en loop con una API flaky
  • hace 200 tool calls porque “solo uno más”
  • no puedes explicar qué pasó porque tu único log es el final answer

Eso no es un problema del LLM. Es un problema de stack.

Por qué pasa en sistemas reales

Un agente es, básicamente:

  • un planner (LLM)
  • una runtime (tu código)
  • side effects (tools)
  • state (memory/artifacts)
  • restricciones (budgets/policy)
  • observabilidad (logs/audit)

Si solo construyes el planner, te van a paginar.

Qué se rompe si lo ignoras

  • Sin audit = sin postmortem (o el postmortem es “fue el modelo”)
  • Sin budgets = coste sin límites
  • Sin frontera de policy = writes accidentales con credenciales de prod
  • Sin state = trabajo repetido, tool calls duplicados, prompt bloat

El stack (lo que de verdad corremos)

  1. Punto de entrada: UI/API, auth, request id
  2. Orchestrator: routing, retries, budgets, tracing
  3. Capa de modelo: llamadas al LLM (con spend tracking)
  4. Capa de tools: APIs, browser, DB (con allowlists)
  5. State: memory, artifacts, caches, claves de idempotencia
  6. Observabilidad: logs estructurados, trazas, eventos de auditoría
  7. Capa de control: policy engine, kill switch, stop de incidentes

Diagrama (dónde se coloca la capa de control)

Este es el modelo mental que usamos:

Si metes el “control” dentro de un prompt, no tienes una capa de control. Tienes una sugerencia.

Capa por capa: lo que aprendimos a golpes

Punto de entrada

El punto de entrada es donde decides el blast radius.

Buenos defaults:

  • autenticar antes de que corra el agente
  • generar un request id
  • atar tenant/entorno a ese request id
  • fijar el budget upfront (no dejes que el modelo “negocie” budgets)

Si dejas que el modelo elija el tenant o el entorno, tarde o temprano vas a escribir en el equivocado.

Orchestrator

Esta es la runtime que mantiene a tu agente “honesto”:

  • step loop
  • timeouts
  • retry policy
  • tool allowlists
  • trace collection
  • stop reasons

Si no construyes esto, cada agente se vuelve una snowflake que falla distinto. Las snowflakes son monas… hasta que tienes que operarlas.

Model layer

Tu capa de modelo va, sobre todo, de:

  • provider fallbacks (if you have them)
  • spend tracking
  • predictable output formats (tool actions)

El modelo no es la única parte “unreliable”. Pero es la única a la que todo el mundo culpa, porque es más fácil que admitir que falta la runtime.

Tool layer

Los tools son donde viven los side effects. Aquí es donde impones:

  • allowlists (what can be called)
  • permissions (what can be written)
  • idempotency keys (what can be repeated safely)
  • timeouts (what can’t hang)
  • rate limits (what can’t DDoS your dependencies)

La capa de tools no debería aceptar “do the thing” como input. Debería aceptar args estructurados con validación.

State layer

El state no es un solo bucket.

Lo separamos en:

  • scratch: notas por run, de vida corta (pequeñas, estructuradas)
  • artifacts: outputs que necesitas luego (drafts, extractos, planes)
  • memory: lo que quieres cargar entre runs (con cuidado)
  • cache: deduplicar lecturas caras (URLs, lookups de KB)

Si metes todo en “memory”, obtienes prompt bloat y peores respuestas. Si no cargas nada, obtienes trabajo repetido y tool calls duplicados.

Observability

Si no puedes responder “¿qué hizo?”, no puedes correrlo en producción.

Observabilidad mínima:

  • action trace (steps, tool calls, stop reason)
  • structured tool logs (args hash, duration, status)
  • spend/cost estimation
  • per-tenant usage metrics

Si vas en serio, añade tracing (spans del modelo, spans de tools). Pero incluso logs estructurados simples ganan a “el modelo dijo”.

Control layer

La capa de control es la parte que quieres que exista cuando estás dormido:

  • budgets (hard limits)
  • tool permissions (least privilege)
  • approvals (for writes)
  • kill switch (operator stop)
  • incident stop (circuit breakers)

No es “security theater”. Es lo que convierte una demo de LLM en un sistema que puedes dejar corriendo.

Código: skeleton de orquestación (TypeScript)

No necesitas un framework enorme. Sí necesitas puntos de control explícitos.

TS
type Budget = { maxSteps: number; maxSeconds: number; maxUsd: number };
type ToolName = "web.search" | "http.get" | "ticket.create";

type Policy = {
  allowTools: ToolName[];
  budget: Budget;
  requireApprovalFor: ToolName[];
};

type AuditEvent =
  | { type: "tool.call"; tool: ToolName; args: unknown; ms: number }
  | { type: "budget.stop"; reason: string }
  | { type: "kill"; reason: string };

export async function runAgent(input: string, policy: Policy) {
  const started = Date.now();
  const events: AuditEvent[] = [];

  for (let step = 0; step < policy.budget.maxSteps; step++) {
    if (Date.now() - started > policy.budget.maxSeconds * 1000) {
      events.push({ type: "budget.stop", reason: "time" });
      break;
    }
    if (await killSwitchIsOn()) {
      events.push({ type: "kill", reason: "operator" });
      break;
    }

    const action = await llmDecideNext(input); // returns {tool, args} or {finish}
    if (action.type === "finish") return { output: action.text, events };

    if (!policy.allowTools.includes(action.tool)) {
      throw new Error(`tool not allowed: ${action.tool}`);
    }
    if (policy.requireApprovalFor.includes(action.tool)) {
      await waitForHumanApproval(action); // (pseudo)
    }

    const t0 = Date.now();
    const obs = await callTool(action.tool, action.args); // must enforce timeouts + idempotency
    events.push({ type: "tool.call", tool: action.tool, args: action.args, ms: Date.now() - t0 });

    input = updateState(input, action, obs); // keep state small, structured
  }

  return { output: "stopped", events };
}

Qué medimos (porque “parece que va” no es una métrica)

Si quieres operar agentes en producción, mide lo aburrido:

  • completion rate (¿terminó o chocó con el budget?)
  • p50/p95 runtime
  • p50/p95 tool calls per run
  • cost per run (tokens + tool credits)
  • loop rate (runs stopped by loop guard)
  • policy deny rate (how often your allowlist blocks it)

Si no mides los policy denies, vas a “arreglar” el agente ampliando permisos en vez de arreglar la tarea.

Taxonomía de stop reasons (para debugear sin vibes)

Si shipeas sin stop reasons explícitos, tus dashboards van a ser 100% vibes: “it didn’t work” → “it timed out” → “maybe the model was bad”.

Logeamos un único stop_reason por run y lo tratamos como un contrato. Es la diferencia entre:

  • “agent feels flaky”
  • “60% of runs stop on tool_timeout:http.get because the upstream is dying”

Stop reasons comunes que vemos de verdad:

  • finish
  • max_steps, max_seconds, max_usd
  • policy_deny:<tool>
  • approval_timeout
  • tool_timeout:<tool>
  • tool_error_exhausted:<tool>
  • loop_detected
  • operator_kill

Ejemplo de evento (este tipo de línea aburrida te salva un día después):

JSON
{
  "request_id": "req_9f2c",
  "tenant": "acme-prod",
  "steps": 25,
  "tool_calls": 17,
  "usd_estimate": 1.03,
  "stop_reason": "max_usd"
}

Sí, puedes ponerte fancy con “partial success” y “degraded mode”. Empieza con un stop reason. Hazlo consistente. Tu yo de guardia te lo va a agradecer.

Realidad multi‑tenant (donde se esconden los incidentes)

Los sistemas multi‑tenant fallan de formas bastante predecibles:

  • contexto de tenant equivocado
  • caches cruzados entre tenants
  • credenciales compartidas
  • tools “globales” que acceden a todo en silencio

Controles:

  • el tenant id lo fija el punto de entrada, nunca el modelo
  • los caches van keyeados por tenant + entorno
  • las credenciales van scopeadas por tenant + entorno
  • los audit logs siempre incluyen tenant id

Si falta cualquiera de esos, en algún momento vas a filtrar datos.

Rate limits y circuit breakers (porque los agentes amplifican outages)

Si una dependencia está flaky, un agente es un amplificador de fallos: reintenta, busca alternativas, vuelve a intentar, “verifica”, y vuelve a intentar.

Así conviertes:

  • “la API upstream devuelve 500 durante 2 minutos” en
  • “enviamos 80k requests y nos rate‑limitearon una hora”

Hacemos tres cosas aburridas:

  1. Caps de concurrencia por tool (por tenant). Ejemplo: tool de browser max 2 runs concurrentes. Más es auto‑DDoS.
  2. Rate limiting en la frontera del tool. No dentro del modelo.
  3. Circuit breakers que hacen fail fast cuando el error rate se dispara.

Pseudocódigo:

TS
const httpGet = rateLimit({ perTenantRps: 5 }, async (url: string) => {
  return fetch(url, { signal: AbortSignal.timeout(8000) });
});

const breaker = new CircuitBreaker({
  windowMs: 30_000,
  failureRate: 0.5,
  cooldownMs: 60_000,
});

const res = await breaker.exec(() => httpGet("https://api.example.com/health"));

Cuando el breaker está abierto, paramos el run con una razón clara (tool_unhealthy:http.get), y no fingimos que el modelo pueda “razonar” a través de un outage. No puede. Solo va a quemar budget.

El rollout que usamos (porque un agente no merece confianza el día 1)

Shipear a prod no es un interruptor binario.

Shipeamos así:

  1. solo usuarios internos
  2. tools solo lectura
  3. porcentaje canary pequeño
  4. expandir permisos gradualmente (con aprobaciones para writes)
  5. y solo entonces considerar comportamiento “autónomo”

Y sí: mantenemos el kill switch a mano todo el tiempo.

Dónde debería vivir la “memory” (pista: no en los prompts)

Si guardas todo en el prompt, consigues:

  • context windows que se inflan
  • peores respuestas (el modelo se ahoga en ruido)
  • más coste

Preferimos:

  • scratchpad pequeño y estructurado por run
  • artifacts guardados fuera (drafts, notas, citas)
  • memory a largo plazo opcional, con scoping estricto + TTL

La memory es una feature de producto. Trátala como tal. Testéala. Audítala. Scópela.

Respuesta a incidentes (lo que quieres listo antes del primer pager)

Los agentes van a fallar. La pregunta es si puedes parar el daño rápido.

Antes de shipear, asegúrate de poder:

  • desactivar un tool (browser, email, payments) sin desplegar código
  • desactivar un tenant sin tumbar a los demás
  • encontrar un run por request id
  • hacer replay de un run en un entorno seguro
  • responder “¿qué tool calls ocurrieron?” en menos de un minuto

Si no puedes hacer eso, el primer incidente va a ser lento y doloroso.

Testing y replay (porque “funciona con mi prompt” no es un test)

La verdad molesta: el comportamiento del agente cambia cuando cambias cualquier cosa. Versión del modelo. Prompt. Schema del tool. Respuestas de la API upstream. Incluso timeouts.

Así que testeamos el stack, no solo el prompt:

  • grabar/reproducir (record/replay) respuestas de tools en un sandbox (inputs iguales, outputs estables)
  • correr una suite pequeña de tareas “golden” en cada deploy
  • hacer asserts sobre traces, no solo sobre el texto final (steps, tools, stop_reason)

Esto nos pilló regresiones reales:

  • un rename del schema del tool hizo que el agente loopeara por errores de validación
  • un tweak de retries duplicó los tool calls (coste ~2× de un día a otro)

Si no puedes hacer replay determinista de un run, el debugging se vuelve arqueología.

Orden de construcción “boring first”

Si empiezas desde cero, construye en este orden:

  1. tool wrapper (allowlist + timeouts + idempotency)
  2. budgets (steps/time) + stop reasons
  3. eventos de auditoría (tool calls con hash de args)
  4. kill switch
  5. y solo entonces: planning fancy, memory, routing multi‑agent

La mayoría de equipos lo hace al revés porque las demos premian lo “smart”. Producción premia “para cuando se pone raro”.

Fallo real

Una vez shipeamos un agente “funcionando” sin eventos de auditoría estructurados. Luego hizo algo raro en producción.

Timeline del postmortem:

  • “llamó muchísimo al tool”
  • “creemos que reintentó”
  • “no podemos saber qué argumentos usó”

Eso costó ~medio día de ingeniería, sobre todo discutiendo qué había pasado.

Arreglo:

  • cada tool call emite un evento estructurado (tool, hash de args, duración, estado)
  • el request id se arrastra a través de todo
  • el kill switch es un clic, no un deploy

Compromisos

  • Más instrumentación = más código.
  • Más policy = más casos de “agent refused”.
  • Aun así, es más barato que debugear a ciegas.

Cuándo NO construir un stack de agente

Si esto es un script interno one‑off que corre una vez por semana, no lo sobre‑ingenierices. Pero si toca sistemas de producción o dinero real, necesitas el stack. Punto.

Enlaces

No sabes si este es tu caso?

Disena tu agente ->
⏱️ 11 min de lecturaActualizado Mar, 2026Dificultad: ★★★
Integrado: control en producciónOnceOnly
Guardrails para agentes con tool-calling
Lleva este patrón a producción con gobernanza:
  • Presupuestos (pasos / topes de gasto)
  • Permisos de herramientas (allowlist / blocklist)
  • Kill switch y parada por incidente
  • Idempotencia y dedupe
  • Audit logs y trazabilidad
Mención integrada: OnceOnly es una capa de control para sistemas de agentes en producción.
Autor

Esta documentación está curada y mantenida por ingenieros que despliegan agentes de IA en producción.

El contenido es asistido por IA, con responsabilidad editorial humana sobre la exactitud, la claridad y la relevancia en producción.

Los patrones y las recomendaciones se basan en post-mortems, modos de fallo e incidentes operativos en sistemas desplegados, incluido durante el desarrollo y la operación de infraestructura de gobernanza para agentes en OnceOnly.