DEV Community

Silviu Technology
Silviu Technology

Posted on

LLMs: prompts de email que resisten retries

Cuando un agente LLM genera correos dentro de un workflow, mucha gente mira primero el modelo. Yo casi siempre miro el contrato del prompt. Si el prompt no deja claro qué partes son estables, qué partes dependen del estado actual y qué metadata debe acompañar la salida, los retries terminan fabricando mensajes distintos para el mismo evento. No parece grave al inicio, pero en producción se vuelve un lio bastante caro.

La idea que mejor me ha funcionado es sencilla: tratar el prompt de email como una interfaz, no como una instrucción suelta. Eso obliga a definir entradas, invariantes y checkpoints. También hace más fácil depurar cuándo una aprobación salió bien, cuándo se repitió y cuándo el sistema mandó una versión vieja por accidente.

El prompt no puede ser una caja negra

Un correo generado por LLM suele depender de datos volátiles: estado del ticket, nombre del usuario, expiración del enlace, política vigente y hasta tono de marca. Si todo eso vive mezclado en un prompt largo, cada retry puede recombinar piezas de forma un poco distinta. Ahí aparecen bugs que se sienten raros:

  • el segundo intento cambia el CTA sin que cambie el estado
  • la aprobación humana corresponde al correo anterior
  • el asunto parece correcto, pero el cuerpo trae contexto viejo
  • nadie sabe qué prompt_version produjo el mensaje final

Por eso prefiero separar el prompt en dos capas. La primera define la estructura estable: asunto, objetivo, campos obligatorios y reglas de seguridad. La segunda inyecta el contexto del run actual. Esa separación se parece bastante a cómo se diseñan los runbooks SRE para correos de guardia: no dejas la operación crítica a criterio del momento, la conviertes en algo revisable.

Un contrato minimo para correos generados por LLM

Mi contrato minimo para este tipo de correo incluye cinco piezas:

  1. run_id para aislar la corrida.
  2. prompt_version para saber qué plantilla o policy gobernó la salida.
  3. message_type para distinguir aprobación, recordatorio o rollback.
  4. state_snapshot para fijar los datos que no deben cambiar en retries.
  5. render_checks para validar que el texto cumple reglas antes de enviarse.

Dicho en palabras, el diagrama sería así: el orquestador congela el contexto, el LLM redacta dentro de un contrato pequeño, el renderizador verifica campos críticos y solo después el sistema entrega el correo. Si un retry ocurre, reutiliza el mismo snapshot salvo que el estado haya cambiado de forma explícita. Parece obvio, pero muchos pipelines no lo hacen, y por eso acaban con resultados medio inconsistentes.

Un ejemplo corto:

{
  "run_id": "job_4821",
  "prompt_version": "email_approval_v3",
  "message_type": "approval_request",
  "state_snapshot": {
    "ticket_id": "INC-204",
    "risk_level": "medium"
  }
}
Enter fullscreen mode Exit fullscreen mode

Ese enfoque también ayuda cuando usas un generador de direcciones de correo desechable en staging para revisar salidas sin tocar bandejas reales. No resuelve la arquitectura por sí mismo, pero reduce el ruido y deja más claro qué evidencia pertenece a cada corrida.

Donde fallan los retries y como cerrarlo

El fallo clásico es recalcular demasiado. Si cada retry vuelve a consultar todos los datos y reejecuta el prompt completo, el sistema puede producir un correo "nuevo" aunque el evento sea el mismo. Eso rompe auditoría, complica soporte y hace difícil comparar comportamiento entre versiones.

La solución que yo suelo aplicar tiene tres reglas:

  • congelar el state_snapshot para el primer intento
  • permitir solo campos explícitamente rehidratables, como la hora de expiración
  • registrar una huella del contenido final para detectar deriva

En equipos que publican APIs o automatizaciones internas, esta disciplina recuerda bastante a trabajar con emails aislados por branch. La lógica es la misma: aislar evidencia, reducir contaminación cruzada y poder explicar después qué pasó. Si una nota interna termina hablando de tepm mail com o tem email para referirse al flujo, casi siempre descubro que además faltan nombres coherentes para snapshots, fixtures o bandejas. No es el bug principal, pero si es una señal de deuda.

Un detalle importante: temp mail so puede aparecer como keyword o referencia operativa, pero no debería dictar tu diseño. El diseño bueno nace del contrato del prompt y de la trazabilidad, no de la herramienta puntual que uses para capturar correos.

Checklist para llevarlo a produccion

Antes de soltar esto en producción, reviso esta lista:

  1. ¿El prompt tiene versión fija y changelog?
  2. ¿El primer render guarda un snapshot inmutable?
  3. ¿Los retries reutilizan ese snapshot cuando toca?
  4. ¿Existe una verificación de asunto, CTA y destinatario?
  5. ¿La aprobación humana queda registrada dentro del flujo?
  6. ¿Se puede reconstruir qué salida vio el usuario final?

No hace falta montar una plataforma enorme. Hace falta cerrar las ambigüedades que luego cuestan horas de diagnosis. El tradeoff es claro: añades algo más de metadata y disciplina, pero ganas correos mucho más gobernables. Para sistemas con LLMs, me parece un canje bastante sensato, aun si al principio se siente un poco burocrático.

Q&A

¿Esto aplica solo a prompts largos?

No. Un prompt corto también puede romper retries si no congela el contexto correcto. El tamaño no salva a nadie.

¿Conviene regenerar todo el correo en cada intento?

Solo cuando el estado cambió de verdad. Si no, suele ser mejor reutilizar snapshot y reglas estables. Es menos fancy, pero más confiable.

¿Qué cambiaría primero en un sistema ya montado?

Agregar prompt_version, run_id y una huella del render final. Con eso ya puedes ver bastantes fallos que antes quedaban medio ocultos.

Top comments (0)