Un agente de demo recorre el camino feliz una sola vez, para ti, con la entrada perfecta. En producción te llegan desconocidos, casos límite y consecuencias, y casi toda la ingeniería de verdad vive en ese hueco entre los dos. Esta guía es el checklist que lo cierra, con el config y el código para conectar cada punto.

En resumen
- El fallo más común en producción no es una respuesta equivocada. Es una llamada que se queda colgada sin timeout y deja al usuario mirando un spinner. Lo primero: ponle un plazo límite a cada llamada externa.
- Fija las versiones de tu modelo y tu prompt. Un agente que cambia de comportamiento porque el proveedor movió un default no está en producción, está en un incidente en cámara lenta.
- Haz idempotente cada escritura. Los reintentos de red y los dobles clics van a pasar sí o sí; un 'enviar reembolso' que se reintenta y paga dos veces es un bug que tu demo nunca llegó a ver.
- Pon límites de tasa y de costo por usuario. Un bug en bucle o un usuario abusivo no debería poder dispararte la factura ni saturar una API externa.
- Si no puedes reconstruir una corrida a partir de los logs y una regresión logra pasar el CI, no tienes producción. Tienes un demo con más usuarios.
Un agente de demo recorre el camino feliz una sola vez, para ti, con la entrada perfecta que tú mismo escribiste. En producción los desconocidos le mandan basura, la red corta llamadas a mitad de camino, un proveedor cambia un default sin avisar, y cada acción que toma tiene una consecuencia real que alguien va a notar. Casi todo el trabajo de sacar un agente a producción está en ese hueco entre los dos mundos, y la buena noticia es que ese hueco es un checklist conocido y acotado, no un misterio. Esta guía recorre cada punto con el config y el código para conectarlo, un ejemplo resuelto de agente de reembolsos, y los modos de falla que te muerden a las 3am.
Nota
Los ejemplos asumen un agente sobre el SDK de Anthropic (modelo fijado a claude-sonnet-4-6) dentro de un route handler de Next.js, que llama a unas cuantas herramientas que escriben en Supabase. Los patrones no dependen del framework. Si estás en Python, FastAPI o un worker de cola, los mismos seis puntos aplican igual.
01 · Requisitos: qué da por sentado un agente "listo para producción"
Antes de que cualquiera de las medidas de endurecimiento de abajo valga la pena, tienen que cumplirse tres cosas. Sáltatelas y vas a estar puliendo las manijas de una casa sin cimientos.
- Un único punto de entrada invocable. Una sola función, llámala runAgent(input, ctx), que reciba un request y un contexto (el id de usuario, un id de request, una señal de abort) y devuelva el resultado completo del agente: el texto final, los tool calls que hizo, el uso de tokens y un stop reason. Si la lógica de tu agente está enredada en un loop de chat o en un route handler, extráela primero. No puedes agregarle timeouts, reintentos ni evals a algo que no puedes invocar de forma aislada.
- Los secretos, fuera del código y fuera de git. La clave del modelo, la clave service-role de la base, cualquier token de una API externa: léelos del entorno, nunca escritos a mano en el código, nunca en un commit. Una clave que entra al historial de git queda comprometida en ese mismo instante; borrar la línea en un commit nuevo no sirve de nada, porque sigue en el historial. Mejor rótala, y da por filtrado cualquier secreto que ya hayas subido a un commit.
- Una suite de evals, aunque sea mínima. Diez casos que ejerciten las entradas más feas, corriendo en cada cambio. Todo en esta guía da por sentado que puedes saber si un cambio mejoró o empeoró las cosas. Sin eso, cada punto de abajo es pura intuición. (Si todavía no la tienes, ármala antes de salir a producción, no después.)
El cambio de mentalidad de toda la guía: un demo optimiza para que la corrida funcione una vez. Producción optimiza para que cada corrida falle de forma segura. Son metas distintas, y la segunda es la mayor parte del trabajo.
02 · Plazos límite en todo lo que sale del proceso
El fallo de producción más común en sistemas de agentes no es una respuesta equivocada. Es una llamada que se queda colgada para siempre. Un request al modelo se traba, la API externa de una herramienta se queda muda, y sin un plazo límite el request entero se queda abierto: el usuario mira un spinner, una función serverless quema todo su timeout, las conexiones se van acumulando. Una respuesta equivocada al menos responde algo. Un cuelgue no responde nada y encima se lleva un recurso con él.
Envuelve cada llamada externa (llamadas al modelo, tool calls, queries a la base, fetches HTTP) en un timeout, y decide de antemano qué pasa cuando salta: reintentar, degradar a una respuesta más simple o pasarle el control a un humano. Decídelo ahora, en código, no a las 3am mientras todo está ardiendo.
async function withDeadline<T>(
work: (signal: AbortSignal) => Promise<T>,
ms: number,
): Promise<T> {
const ctrl = new AbortController()
const timer = setTimeout(() => ctrl.abort(), ms)
try {
return await work(ctrl.signal)
} finally {
clearTimeout(timer)
}
}
// Pasa la señal hasta el fondo para que el abort de verdad cancele la llamada.
const reply = await withDeadline(
(signal) => client.messages.create(
{ model: "claude-sonnet-4-6", max_tokens: 1024, messages },
{ signal },
),
30_000,
)
El detalle que casi todos pasan por alto: un timeout que no propaga un AbortSignal es puro teatro. Resuelve tu promesa a los 30 segundos, pero el request de abajo sigue corriendo, todavía ocupando una conexión y todavía cobrándote. Lleva la señal hasta la llamada del SDK para que el abort sea de verdad.
Atención
Un agente de varios pasos puede ir multiplicando timeouts sin que te des cuenta. Cinco tool calls de 30 segundos cada uno son un request de 150 segundos que el usuario nunca va a esperar. Ponle un plazo total a toda la corrida del agente y chéquealo entre pasos, no solo uno por llamada, o tu "timeout de 30 segundos" se convierte en dos minutos y medio en la práctica.
03 · Fija versiones y haz idempotente cada escritura
Dos modos de falla que los demos nunca sacan a la luz, porque un demo corre una sola vez, con un solo usuario, el día que lo armaste.
Fija el modelo y el prompt
Un agente cuyo comportamiento cambia cuando el proveedor rota un default no es lo bastante determinista como para operar. Fija el modelo a un string de versión exacto (claude-sonnet-4-6, no un alias flotante tipo "latest") y versiona tus prompts igual que versionas el código, para que un cambio de comportamiento sea siempre algo que tú publicaste y puedas señalar en un diff. Cuando sí actualices el modelo, trátalo como lo que es, un cambio: corre la suite completa de evals, cuenta con tener que reafinar el prompt y haz el rollout a conciencia. El mejor prompt para un modelo rara vez es el mejor para el siguiente.
Haz seguras las acciones repetidas
Los reintentos de red pasan. Los dobles clics pasan. Un usuario recarga la página con el request todavía en vuelo. Si tu agente emite un reembolso, manda un correo o crea un registro, esa acción va a dispararse dos veces tarde o temprano, y ese segundo disparo es un bug que tu demo nunca vio. La solución es una idempotency key: un id estable que manda el caller y que tú chequeas antes de hacer la escritura.
// El caller genera una key estable por cada acción lógica.
async function issueRefund(orderId: string, amount: number, idemKey: string) {
const { error } = await supabase
.from("refunds")
.insert({ order_id: orderId, amount, idem_key: idemKey })
// Un constraint UNIQUE sobre idem_key convierte el duplicado en un no-op,
// no en un segundo pago.
if (error?.code === "23505") return { ok: true, duplicate: true }
if (error) throw error
return { ok: true, duplicate: false }
}
Que lo imponga la base de datos, no la aplicación. Un constraint UNIQUE sobre la columna de idempotencia hace que ni siquiera dos requests concurrentes puedan ganar los dos. Hacer el chequeo en código de la app con un lee-y-luego-escribe deja una race condition abierta de par en par entre la lectura y la escritura.
04 · Límites, para que un solo usuario no hunda el barco
En un demo eres el único caller, y lo tratas bien. En producción un bug en bucle, una tormenta de reintentos impacientes o un usuario realmente abusivo puede llevarte la factura del modelo a cuatro cifras de la noche a la mañana, o saturar una API externa hasta que les ponga rate limit a todos. Necesitas dos tipos de tope.
- Límites de tasa por usuario, no solo globales. Un límite global protege al proveedor; uno por usuario te protege a ti de un único mal actor y de tu propio bug que llama al agente en bucle. Aplica el límite sobre el id del usuario autenticado, y prefiere un store chico y rápido (una tabla de Postgres con un contador por ventana, o Redis si ya lo tienes corriendo) antes que un contador en memoria que se reinicia en cada deploy y no existe entre instancias.
- Un tope de costo estricto por request y por usuario al día. Lleva la cuenta del uso de tokens de cada respuesta del modelo (viene en el resultado de la API) y detén al agente cuando una corrida o el gasto diario de un usuario cruce un umbral. Un agente que puede llamar herramientas en bucle puede, en principio, gastar hasta dejarte en la quiebra. Un tope de presupuesto convierte la "factura sorpresa de cuatro cifras" en "request rechazado, alerta disparada".
// Tras cada llamada al modelo, acumula el uso real e impón un tope por corrida.
spent += reply.usage.input_tokens + reply.usage.output_tokens
if (spent > RUN_TOKEN_BUDGET) {
throw new BudgetExceeded(`la corrida superó ${RUN_TOKEN_BUDGET} tokens`)
}
Importante
El bucle más peligroso es un agente que reintenta un tool call que falla, recibe la misma falla y vuelve a reintentar, quemando tokens en cada vuelta. Ponle un tope al número de iteraciones de tool call por corrida (un entero fijo, p. ej. 10), aparte de tu presupuesto de tokens. Dos topes, para dos modos de descontrol distintos.
05 · Observabilidad: no puedes arreglar lo que no puedes reconstruir
Cuando un usuario reporta que "el agente hizo algo raro", tus únicas opciones son reproducirlo o leer qué fue lo que pasó. En producción casi nunca puedes reproducirlo: no tienes su input exacto, ni su estado, ni la respuesta exacta del modelo. Así que tienes que poder leerlo, y eso significa registrar lo suficiente como para reconstruir cualquier corrida después de que ocurrió.
Por cada corrida del agente, guarda un registro estructurado identificado por un request id que generas en el punto de entrada y arrastras por cada llamada:
- El input (redacta lo sensible), el id de usuario y el request id.
- Cada tool call: nombre, argumentos, resultado o error, y cuánto tardó.
- El uso de tokens, y el modelo y la versión de prompt exactos que se usaron.
- El resultado final: éxito, degradado, timeout, rechazado o error, y por qué.
log.info({
requestId: ctx.requestId,
userId: ctx.userId,
model: "claude-sonnet-4-6",
promptVersion: "refund-agent@7",
toolCalls: trace.map((t) => ({ name: t.name, ms: t.ms, ok: t.ok })),
tokens: spent,
outcome,
})
Registra JSON estructurado, no texto en prosa. Cuando algo se rompa vas a querer filtrar por usuario, por resultado, por versión de modelo, y con texto libre no llegas a "todas las corridas con timeout del usuario X de ayer" ni con grep. El request id es lo que hace que un ticket de soporte, una línea de log y un error en tu tracker apunten todos a la misma corrida.
Consejo
Agrega una alerta antes de lanzar: que te avise a ti mismo cuando la tasa de error-o-timeout en una ventana de 5 minutos cruce un umbral. No vas a estar mirando el dashboard a las 3am, pero sí quieres ser tú el que se entera, no el usuario que lo tuitea.
06 · Un ejemplo resuelto, y el orden en que conviene hacerlo
Júntalo todo con el agente de reembolsos. Un usuario pide un reembolso; el agente decide si es válido y, si lo es, llama a issueRefund. Hecho con forma de producción, ese único flujo toca cada punto de arriba:
- El punto de entrada genera un request id y un abort controller, y lee el id de usuario de la sesión.
- Chequeo de tasa + presupuesto antes de la primera llamada al modelo, rechazando temprano si el usuario superó su ventana o su tope diario.
- Llamada al modelo fijada a claude-sonnet-4-6, versión de prompt refund-agent@7, envuelta en un plazo por llamada y contrastada contra el plazo total de la corrida entre pasos.
- Tool call a issueRefund con una idempotency key provista por el caller, para que un reintento no pague dos veces.
- Ante un timeout o una falla de la herramienta, degrada: no falles en silencio. Devuelve un claro "no pudimos procesar esto automáticamente, una persona le va a dar seguimiento" y mételo en una cola, en vez de dejar al usuario con un spinner o un 500 opaco.
- Registra el trace estructurado completo, haya salido bien o no, identificado por el request id.
El orden importa, porque los puntos baratos previenen los incidentes caros. Si solo vas a hacer una cosa, haz los plazos del paso 3, porque el cuelgue es la falla que te saca de la cama. Después la idempotencia, porque el doble pago es la que cuesta dinero. Luego los límites, después la observabilidad, y al final la fijación de versiones. Cada capa son unas pocas horas de trabajo y te quita de encima toda una categoría de page a las 3am.
Nada de esto es del otro mundo; es la plomería sin gloria que separa algo que funcionó una vez en una grabación de pantalla de algo que dejarías que toque usuarios y dinero de verdad. Arma el punto de entrada y después agrega las capas en orden, apoyándote en tu suite de evals en cada paso para poder demostrar que cada cambio ayudó en lugar de solo cruzar los dedos. El demo, mándatelo a ti mismo; el checklist, mándaselo a tus usuarios.
Puntos clave
- Envuelve cada llamada externa en un plazo y propaga el AbortSignal; un cuelgue sin timeout es el fallo de producción más común y más grave, y el primero que toca arreglar.
- Fija las versiones de modelo y prompt para que el comportamiento solo cambie cuando publicas un diff, y trata cada actualización de modelo como un cambio deliberado que pasa por tus evals.
- Haz idempotente cada escritura con una key provista por el caller e impuesta por un constraint UNIQUE, para que los reintentos y los dobles clics no paguen ni envíen dos veces.
- Ponle tope a la tasa, al costo y a las iteraciones de tool call por usuario y por corrida, en un store durable; un mal actor o un bug en bucle no debería hundirte la factura ni una API externa.
- Registra traces estructurados identificados por el request id y agrega una alerta de tasa de error; si no puedes reconstruir una corrida después de que pasó, no puedes arreglarla.
Preguntas frecuentes
Voy a sacar una herramienta interna chica. ¿De verdad necesito los seis puntos?
Dos no son negociables ni siquiera puertas adentro: los plazos (punto 02) y la idempotencia en cualquier escritura que toque dinero, correo o sistemas externos (punto 03). Un request interno colgado igual te quema una función serverless y deja a tu compañero confundido; una acción interna que se dispara dos veces igual cobra o envía doble. Los límites de tasa y un stack completo de observabilidad pueden ser más livianos con un público interno de confianza, así que una sola alerta de error y unos logs estructurados básicos rinden bastante. Pero no te saltes la fijación de versiones: un default del modelo que se mueve por debajo de una herramienta interna es igual de confuso, y mucho más difícil de depurar, justo porque nadie la está mirando de cerca.
Mi framework ya trae reintentos y timeouts. ¿No alcanza con eso?
Los reintentos integrados son justamente la razón por la que necesitas idempotencia, no un sustituto de ella. Un framework que reintenta en silencio un 'enviar reembolso' que falló es exactamente la forma en que terminas pagando dos veces. Y la mayoría de los timeouts integrados son por llamada, lo que no te protege de un agente de varios pasos que apila cinco llamadas de 30 segundos en un request de 150. Usa las primitivas del framework, pero agrégale un plazo total de corrida chequeado entre pasos y una idempotency key en cada escritura. El framework resuelve la mitad fácil; la mitad específica del agente te toca a ti.
¿Por qué fijar a una versión exacta del modelo en vez de usar siempre el más nuevo y mejor?
Porque 'latest' significa que el comportamiento de tu agente puede cambiar un día en que no desplegaste nada, y cuando pasa no tienes ningún diff que señalar ni manera de hacer rollback. Lo más nuevo suele ser mejor, pero 'mejor en el benchmark' no es lo mismo que 'mejor para tu prompt fijado'. El mejor prompt para un modelo rara vez es el mejor para el siguiente, así que una actualización casi siempre exige reafinar el prompt. Fija la versión, trata cada actualización como un cambio deliberado que pasa por tu suite de evals, y te quedas con las mejoras del modelo nuevo sin los incidentes de cambio en silencio.
¿Dónde guardo los contadores de rate limit y de presupuesto si tengo un solo VPS?
En un solo VPS con un solo proceso, un contador en memoria técnicamente funciona, pero se reinicia en cada deploy y te miente en silencio apenas levantas dos instancias o reinicias. Una tabla de Postgres con un contador por ventana es la opción durable y sin infraestructura extra: sobrevive a los reinicios, la puedes consultar para depurar y casi seguro ya tienes la base ahí. Recurre a Redis solo si ya lo tienes corriendo para otra cosa; no metas una dependencia nueva entera solo para llevar unos contadores. Lo que hay que evitar es ese contador en memoria del que te olvidas que está en memoria hasta que un deploy le reinicia el límite a todos sin que te enteres.
Si solo tengo una tarde, ¿cuál es el único punto que más me rinde?
Los plazos, con el AbortSignal realmente propagado (punto 02). El cuelgue sin timeout es el fallo de producción más común y el que da la peor experiencia de usuario, un spinner congelado sin salida, y encima quema tiempo serverless y conexiones en silencio. Envolver cada llamada externa y agregar un plazo total de corrida es una tarde de trabajo, y te quita de encima el fallo que con más probabilidad te va a hacer el primer page. Haz eso, y luego la idempotencia, porque el segundo fallo más caro es la acción que se disparó dos veces.
¿Prefieres que lo hagamos por ti?
Esto mismo lo construimos para negocios como el tuyo. La primera conversación es gratis y sin compromiso.
Escríbenos por WhatsAppPrimera conversación gratis. Te responde el fundador.
Recursos relacionados

Diseñar un sistema multiagente que no se derrumbe
La mayoría de los proyectos 'multiagente' deberían ser un solo agente con buenas herramientas. En esta guía aprendes a decidir cuándo de verdad necesitas varios agentes, a elegir una topología, a conectarlos con contratos tipados y a ponerles los topes de turnos, los desempates y los presupuestos que evitan que un panel se enrede discutiendo en círculos o te infle la factura sin que te des cuenta.

Observabilidad en apps de LLM: registrar lo que de verdad importa
Cuando una función de LLM falla a las 2am, "dio una respuesta rara" no te dice nada. Esta guía te muestra cómo instrumentar cada llamada al modelo como una traza estructurada (el prompt resuelto, la respuesta cruda, la secuencia de tool calls, tokens y latencia) para que puedas reconstruir con exactitud qué vio e hizo el modelo, y luego enmascarar lo sensible para que tu tabla de trazas no termine siendo tu próxima filtración de datos.

Un modelo de amenazas para agentes que usan herramientas
Un agente que solo conversa es de bajo riesgo. Un agente que puede llamar herramientas es software con un núcleo no determinista que hace llamadas privilegiadas, y te toca modelar sus amenazas como tal. Esta guía recorre los ataques que de verdad importan cuando un agente puede actuar (prompt injection, confused deputy, exfiltración de datos) y después arma la contención de mayor a menor eficacia: mínimo privilegio, aprobación humana en las acciones peligrosas y un muro sólido entre el contexto confiable y el no confiable.