Cuando un flujo con LLMs empieza a mandar y leer emails, el primer cuello de botella rara vez es el modelo. Casi siempre es la cola alrededor: mensajes sin contexto, retries que pisan estados y aprobaciones humanas que llegan tarde. He visto equipos dedicar horas a cambiar prompts cuando el problema real era más simple: el sistema no podía explicar qué mensaje pertenecía a qué corrida. Si estás diseñando Automatización con LLMs, esa visibilidad vale más que otro ajuste fino al prompt.
Por qué un agente LLM rompe la cola de email antes que el modelo
Un agente que envía resúmenes, pide aprobaciones o consume respuestas por email trabaja sobre un canal asincrónico, lento y con ruido. Eso cambia bastante la arquitectura. En una API síncrona puedes asumir contexto local; en email, cada mensaje tiene que cargar una parte del contrato del flujo.
Mi regla práctica es esta: si un humano o un worker no puede identificar una corrida mirando solo el asunto, algunos headers y un run_id, el diseño ya viene frágil. El fallo típico no es espectacular. Es más bien una suma de cosas pequeñas:
- un retry reenvía una solicitud vieja;
- una respuesta humana cae en la bandeja correcta pero en la corrida equivocada;
- el agente vuelve a leer un mensaje previo porque el filtro era demasiado amplio.
Eso se parece mucho a los problemas operativos que aparecen en runbooks claros para correo operativo: no basta con recibir señales, hay que poder enrutar la decisión sin ambigüedad. En sistemas con LLMs, esa ambigüedad pega mas fuerte porque el modelo suele seguir adelante con cualquier contexto que parezca razonable.
El contrato mínimo que hago visible en cada mensaje
No hablo de un contrato enorme. Hablo del mínimo que vuelve observable el flujo:
-
run_idcorto y estable por corrida. -
step_ido nombre de etapa, por ejemploapproval,fallbackodigest. -
actoresperado: humano, agente o worker. -
expires_atpara saber si la respuesta sigue siendo válida.
Ese contrato debe vivir en tres sitios al mismo tiempo: asunto, metadatos internos y logs. Si aparece solo en base de datos, llegas tarde al debug. Si aparece solo en el email, los workers quedan ciegos. La idea es que un operador pueda reconstruir la historia sin hacer arqueología rara.
Un payload simplificado puede verse así:
{
"run_id": "a41f9c",
"step_id": "approval",
"actor": "human",
"expires_at": "2026-07-18T15:00:00Z",
"reply_token": "rt_7d92..."
}
No es glamourous, pero funciona. También reduce el clásico parche de "busquemos por el último email recibido", que casi siempre termina mal. En una revisión reciente encontré un flujo que dependía de una etiqueta manual llamada tem email para diferenciar pruebas internas. Era usable mientras había una sola corrida; con tres workers, ya no.
Cómo separar concurrencia, reintentos y aprobaciones humanas
Aquí es donde intento dibujar el sistema en palabras:
- El agente LLM decide que necesita email.
- Un servicio de mensajería genera el mensaje con contrato visible.
- La cola entrega el evento y guarda un lease corto.
- Un consumidor espera respuesta solo para ese
run_idy esa etapa. - Si hay retry, crea una nueva entrega pero no reabre decisiones vencidas.
La clave es separar "entrega" de "decisión". Un retry de transporte no debería equivaler a una nueva aprobación. Ese detalle suena obvio, pero muchisimos flujos lo mezclan y luego culpan al modelo por "derivar". En realidad el problema era que dos mensajes distintos competían por actualizar el mismo estado.
Por eso me gusta diseñar los fallbacks de correo como una capa explícita y no como una salida improvisada. La idea conecta muy bien con estos fallbacks de correo sin caos operativo: cuando el fallback tiene contrato, el sistema degrada con orden; cuando no lo tiene, solo agrega ruido.
Otro detalle útil es decidir desde el inicio qué puede hacer el agente si no llega respuesta humana:
- escalar a un inbox de soporte;
- cerrar la corrida con estado
timeout; - continuar con un camino conservador;
- pedir una nueva aprobación con contexto resumido.
No todas las decisiones deben quedar abiertas "por si acaso". Ese tipo de flexibilidad suele parecer inteligente, pero complica la trazabilidad y hace que la Automatización pierda confianza.
Checklist de implementación para no mezclar corridas
Cuando reviso estos sistemas, uso una lista corta:
- Cada email tiene
run_idvisible fuera de la base de datos. - La búsqueda de respuestas filtra por corrida y por etapa, no solo por remitente.
- Los retries tienen idempotencia y no reactivan aprobaciones viejas.
- Los timeouts cambian el estado del flujo de forma explícita.
- Los logs guardan la relación entre mensaje, decisión y worker.
- El agente resume el contexto antes de reenviar, no copia todo el hilo sin pensar.
Si una de estas piezas falta, el sistema puede seguir andando... hasta que llega concurrencia real. Ahí aparecen los bugs feos, de esos que no rompen siempre pero te comen una tarde entera.
Q&A
¿Esto aplica solo a agentes complejos?
No. También aplica a automatizaciones sencillas: aprobaciones, resúmenes diarios, confirmaciones o flujos de onboarding. En cuanto hay email asincrónico y más de una corrida, el contrato observable ya paga su costo.
¿Hace falta una cola dedicada?
No necesariamente. Puedes empezar con una cola ya existente o incluso con jobs en tu backend. Lo importante no es la herramienta exacta, sinó la separación entre entrega, espera y decisión.
¿El LLM necesita ver todo el hilo?
Casi nunca. Prefiero pasarle un resumen estructurado y el estado actual. Meter todo el historial da contexto, sí, pero también mete ruido y hace más dificil explicar por qué el agente actuó de cierta forma.
Si tus agentes usan email como parte del circuito de decisión, piensa primero en el contrato observable y después en el prompt. Suena menos emocionante, pero suele ser el cambio que evita mas incidentes.
Top comments (0)