Un agente autónomo (WhatsApp + Gmail + herramientas internas vía MCP) que decide qué tool llamar y con qué parámetros, corriendo sin supervisión humana en cada mensaje. El LLM no es determinístico por naturaleza, pero el sistema que lo rodea sí tiene que serlo, o rompes integraciones reales cada vez que el modelo decide ser "creativo" con el formato de salida.
El problema real
Un LLM sin restricciones responde distinto ante el mismo input: a veces devuelve JSON limpio, a veces lo envuelve en markdown con backticks, a veces agrega una frase de cortesía antes del JSON, a veces cambia el nombre de un campo por un sinónimo. Si tu código de downstream hace JSON.parse() directo sobre eso, falla de forma intermitente e impredecible — el peor tipo de bug porque no reproduce siempre.
Por qué "prompt mejor redactado" no alcanza
La tentación es escribir un prompt más detallado pidiendo "responde solo JSON". Funciona la mayoría de las veces, pero "la mayoría de las veces" no sirve en un sistema que corre miles de veces al día sin supervisión. Necesitas garantías estructurales, no buenas intenciones del modelo.
La solución: contrato de esquema + validación + reintento
Tres capas, cada una atrapa lo que la anterior deja pasar:
1. Esquema JSON explícito en el system prompt, con tipos y valores permitidos enumerados, no descritos en prosa:
Responde ÚNICAMENTE con un objeto JSON, sin texto adicional, sin markdown, sin backticks.
Esquema exacto:
{
"accion": "enviar_mensaje" | "consultar_estado" | "escalar_humano",
"destinatario": string,
"contenido": string,
"prioridad": "alta" | "media" | "baja"
}
No agregues campos fuera de este esquema. No omitas ninguno.
2. Validación estructural antes de tocar el resultado, nunca confiar en que el modelo cumplió:
public class ToolCallValidator {
public ValidatedCall validate(String rawOutput) {
String cleaned = stripMarkdownFences(rawOutput);
JsonNode node;
try {
node = mapper.readTree(cleaned);
} catch (JsonProcessingException e) {
throw new InvalidLLMOutputException("No es JSON válido", rawOutput);
}
if (!node.has("accion") || !ACCIONES_VALIDAS.contains(node.get("accion").asText())) {
throw new InvalidLLMOutputException("Acción fuera de contrato", rawOutput);
}
// ... validar resto de campos requeridos y tipos
return new ValidatedCall(node);
}
}
3. Reintento con el error como contexto, no un reintento ciego:
int intentos = 0;
while (intentos < 2) {
String raw = llmClient.complete(prompt);
try {
return validator.validate(raw);
} catch (InvalidLLMOutputException e) {
prompt = prompt + "\n\nTu respuesta anterior no cumplió el esquema: "
+ e.getMessage() + ". Corrige y responde de nuevo.";
intentos++;
}
}
throw new ToolCallFailedException("El modelo no cumplió el contrato tras reintentos");
Dos intentos suele bastar. Si el segundo también falla, mejor escalar a un humano (o loggear y responder con un mensaje genérico) que dejar que un JSON corrupto siga corriendo por el pipeline.
Un catálogo de tools acotado ayuda más que un prompt largo
En vez de darle al modelo acceso libre a "cualquier función", defino un catálogo cerrado con nombre, parámetros y ejemplos de uso correcto e incorrecto para cada tool. Menos superficie de decisión, menos variabilidad en la salida.
Resultado
El agente lleva meses en producción manejando WhatsApp y Gmail sin intervención manual constante. Los fallos de formato bajaron a prácticamente cero después de meter la capa de validación — el prompt solo redujo la frecuencia, la validación estructural fue lo que lo hizo confiable.
Resumen para copiar y pegar
- Esquema JSON explícito y cerrado en el prompt, nunca descripción en prosa.
- Nunca confíes el output crudo del LLM — valida estructura y tipos siempre.
- Reintento con el error específico inyectado en el prompt, no reintento ciego.
- Catálogo de tools acotado, no acceso libre a funciones.
Top comments (0)