Guardrails para Agentes IA: Valida el Output en Producción
Tu agente funcionó perfectamente en staging. Las primeras semanas en producción, también. Luego un martes por la tarde el modelo devolvió un precio negativo. O un email inventado. O una acción que no estaba en la lista de opciones válidas.
No hubo excepción. El código procesó la respuesta, la envió al flujo del cliente y siguió adelante. Llevas tres días sin saberlo.
Este es el problema central que los guardrails resuelven: el LLM puede devolver algo que parece correcto estructuralmente pero es semánticamente imposible o directamente peligroso. Un número válido fuera de rango. Una dirección de email con formato correcto pero de un dominio ficticio. Una acción que tu sistema no debería ejecutar jamás.
Si estás construyendo agentes IA para automatizar procesos de empresa, esta arquitectura no es opcional — es la diferencia entre un agente que funciona y uno que eventualmente falla de silencio.
Capa 1: Validación de esquema con Zod
La primera capa es la más simple y la más ignorada. Antes de usar el output del LLM en cualquier parte de tu código, parsea contra un schema estricto.
import { z } from 'zod'
const ActionEnum = z.enum(['send_quote', 'schedule_call', 'escalate', 'close'])
const AgentOutputSchema = z.object({
action: ActionEnum,
price: z.number().positive().max(100_000),
customer_email: z.string().email(),
message: z.string().min(10).max(500),
confidence: z.number().min(0).max(1),
})
async function runAgentWithGuardrail(input: string) {
const rawOutput = await callLLM(input)
const parsed = AgentOutputSchema.safeParse(rawOutput)
if (!parsed.success) {
await logGuardrailFailure({
input,
rawOutput,
errors: parsed.error.issues,
})
return fallbackResponse()
}
return parsed.data
}
El error más común que veo en proyectos ajenos: usar schema.parse() en lugar de schema.safeParse(). La primera lanza una excepción que no siempre se captura limpiamente. La segunda devuelve { success: boolean; data | error } — manejas el fallo sin sorpresas.
Una regla práctica: cuanto más crítica sea la acción del agente, más estricto debe ser el schema. Un agente que solo genera texto puede tolerar más flexibilidad que uno que ejecuta transacciones, envía emails o modifica registros en base de datos.
Capa 2: Guardrails de contenido
La validación de esquema no detecta todo lo que puede salir mal. Un agente de atención al cliente puede responder con el formato perfecto y aun así:
- Inventar una política de devoluciones que no existe
- Ofrecer un descuento que no está autorizado
- Responder en un idioma diferente al del cliente
- Incluir datos de otro cliente en la respuesta
Para esto necesitas una segunda capa de validación semántica. Hay dos enfoques prácticos:
Reglas deterministas — baratas y rápidas:
function contentGuardrails(
output: AgentOutput,
context: RequestContext
): { passed: boolean; issues: string[] } {
const issues: string[] = []
// Frases que el agente nunca debería inventar
const BANNED_PHRASES = [
'según nuestra política',
'tienes derecho a',
'te garantizamos',
]
for (const phrase of BANNED_PHRASES) {
if (output.message.toLowerCase().includes(phrase)) {
issues.push(`Banned phrase: "${phrase}"`)
}
}
// Precio fuera del margen permitido
if (output.price < MINIMUM_PRICE || output.price > MAXIMUM_PRICE) {
issues.push(`Price ${output.price} out of allowed range`)
}
// Coherencia de idioma
const detected = detectLanguage(output.message)
if (detected !== context.customerLanguage) {
issues.push(`Language mismatch: expected ${context.customerLanguage}`)
}
return { passed: issues.length === 0, issues }
}
LLM juez — más flexible, más caro:
async function llmJudge(
input: string,
agentOutput: string,
businessContext: BusinessContext
): Promise<{ approved: boolean; reason: string }> {
const judgePrompt = `
Evalúa si esta respuesta del agente es segura para enviar al cliente.
Input del usuario: ${input}
Respuesta del agente: ${agentOutput}
Contexto del negocio: ${JSON.stringify(businessContext)}
Rechaza si: contiene políticas inventadas, descuentos no autorizados,
datos de otros clientes, contenido ofensivo o inconsistencias graves.
Responde en JSON: { "approved": boolean, "reason": string }
`.trim()
const result = await callLLM(judgePrompt, { model: 'claude-haiku-4-5' })
return JSON.parse(result)
}
Usa el modelo más barato disponible para el juez — Haiku en lugar de Sonnet. El coste de esta capa baja a un 10-15% del coste de la llamada principal. Para muchos casos de uso, las reglas deterministas son suficientes; el LLM juez aporta valor cuando el contenido es abierto y difícil de validar con reglas fijas.
Capa 3: Circuit breaker por fallos repetidos
Las dos capas anteriores actúan respuesta a respuesta. Pero hay un patrón más peligroso: el agente falla de forma consistente durante un periodo de tiempo — por un cambio silencioso en el comportamiento del modelo, por un input edge case que se volvió recurrente, o por un bug en el contexto que inyectas al prompt.
Sin circuit breaker, ese agente sigue en marcha, sigue acumulando fallos y nadie lo nota hasta que un cliente escala.
class AgentCircuitBreaker {
private failures = 0
private lastFailureTime = 0
private state: 'closed' | 'open' | 'half-open' = 'closed'
private readonly THRESHOLD = 5
private readonly RESET_AFTER = 300_000 // 5 minutos
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
const elapsed = Date.now() - this.lastFailureTime
if (elapsed > this.RESET_AFTER) {
this.state = 'half-open'
} else {
throw new Error('Circuit open: agent temporarily disabled')
}
}
try {
const result = await fn()
if (this.state === 'half-open') this.reset()
return result
} catch (err) {
this.recordFailure()
throw err
}
}
private recordFailure() {
this.failures++
this.lastFailureTime = Date.now()
if (this.failures >= this.THRESHOLD) {
this.state = 'open'
alertOps(`Circuit OPEN after ${this.failures} consecutive guardrail failures`)
}
}
private reset() {
this.failures = 0
this.state = 'closed'
}
}
Cuando el circuit breaker abre, el sistema deja de llamar al agente y activa el fallback: respuesta predefinida, escalado a humano, o encolado para procesamiento posterior. El equipo recibe la alerta y puede investigar antes de que el impacto escale.
Este patrón es especialmente crítico en agentes con integración IA conectada a sistemas externos — CRMs, ERPs, plataformas de facturación — donde un fallo en cascada puede propagar datos incorrectos a varios sistemas a la vez.
Lo que cambia en producción real
La mayoría de proyectos con los que trabajo llegan sin ninguna de estas tres capas. El agente funciona bien en demo y bien durante las primeras semanas. El primer incident grave llega cuando el volumen escala o cuando el modelo tiene un comportamiento inesperado en un input edge case que nunca apareció en staging.
Los proyectos donde implementamos las tres capas desde el primer sprint tienen tasas de error de agente por debajo del 0,3% en producción. Los que llegan sin guardrails suelen tener entre 2-8% de respuestas incorrectas que pasan sin ser detectadas.
La diferencia no está en qué modelo usas. Está en la arquitectura de validación alrededor del modelo.
Si estás construyendo un agente IA para producción y quieres que la arquitectura sea sólida desde el principio, cuéntame en qué estás trabajando — revisamos la arquitectura juntos antes de que el primer bug llegue a producción.