Normal path: execute → tool → observe.
El problema (en producción)
Tu tool devuelve JSON.
Hasta que no.
Quizá un proxy inyecta HTML. Quizá la respuesta llega truncada. Quizá cambiaste versión y un campo se renombró.
El modelo ve “algo” e intenta seguir. Así obtienes:
- decisiones equivocadas
- writes equivocados
- y el peor tipo de bug: “no crasheó, solo hizo lo incorrecto”
La corrupción del output es un fallo de producción porque el output del tool es la realidad del agente. Si la realidad está corrupta, las decisiones también.
Por qué esto se rompe en producción
1) Se trata el output del tool como confiable y bien formado
La mayoría valida inputs e ignora outputs. En agentes es al revés:
- los outputs son lo que el agente usa para decidir
- y el output es el lugar más fácil para corrupción silenciosa
2) Las respuestas parciales pasan
Los timeouts no siempre fallan “limpio”. A veces obtienes:
- medio JSON
- un 200 con payload de error
- body vacío con status “success”
3) El schema drift es constante
APIs internas cambian. Vendors cambian. Tus tools cambian.
Si no validas outputs, te enteras por errores de usuario.
4) El modelo intenta “suavizar” la corrupción
Un humano ve JSON inválido y para. Un modelo ve “casi” y rellena huecos con alucinaciones.
Eso es feature para texto. Es bug para acciones mediadas por tools.
Ejemplo de implementación (código real)
Patrón seguro:
- límites de tamaño
- content-type
- schema + invariants
- fail-closed (o degrade) si no cuadra
import json
from typing import Any
class ToolOutputInvalid(RuntimeError):
pass
def parse_json_strict(raw: str, *, max_chars: int = 200_000) -> Any:
if len(raw) > max_chars:
raise ToolOutputInvalid("tool output too large")
try:
return json.loads(raw)
except Exception as e:
raise ToolOutputInvalid(f"invalid JSON: {type(e).__name__}")
def validate_user_profile(obj: Any) -> dict[str, Any]:
if not isinstance(obj, dict):
raise ToolOutputInvalid("expected object")
if "user_id" not in obj or not isinstance(obj["user_id"], str):
raise ToolOutputInvalid("missing user_id")
if "plan" in obj and obj["plan"] not in {"free", "pro", "enterprise"}:
raise ToolOutputInvalid("invalid plan enum")
return obj
def get_user_profile(user_id: str) -> dict[str, Any]:
raw = http_get(f"https://api.internal/users/{user_id}") # (pseudo)
obj = parse_json_strict(raw)
return validate_user_profile(obj)export class ToolOutputInvalid extends Error {}
export function parseJsonStrict(raw, { maxChars = 200_000 } = {}) {
if (raw.length > maxChars) throw new ToolOutputInvalid("tool output too large");
try {
return JSON.parse(raw);
} catch (e) {
throw new ToolOutputInvalid("invalid JSON: " + (e && e.name ? e.name : "Error"));
}
}
export function validateUserProfile(obj) {
if (!obj || typeof obj !== "object") throw new ToolOutputInvalid("expected object");
if (typeof obj.user_id !== "string") throw new ToolOutputInvalid("missing user_id");
if ("plan" in obj && !["free", "pro", "enterprise"].includes(obj.plan)) {
throw new ToolOutputInvalid("invalid plan enum");
}
return obj;
}Esto es estrictamente a propósito. Si el output está corrupto, lo correcto suele ser:
- parar
- devolver partial
- y loggear la clase de fallo
No: “adivinar qué quiso decir el tool”.
Incidente real (con números)
Teníamos un agente que actualizaba notas de CRM basado en perfil de usuario + eventos recientes.
El tool user.profile empezó a devolver a veces páginas HTML de error con status 200 (proxy mal configurado).
El modelo “leyó” el HTML y sacó un “plan = enterprise” de un banner cualquiera.
Impacto:
- 23 registros de CRM marcados con el plan incorrecto
- el equipo de ventas perdió ~3 horas persiguiendo “leads enterprise” que no lo eran
- tuvimos que hacer cleanup y restaurar desde logs (que además estaban incompletos)
Fix:
- content-type + parse JSON estricto
- schema validation + enums
- fail closed + safe-mode (saltar writes si el perfil es inválido)
- métricas: rate de
tool_output_invalid
Los tools mienten de formas aburridas. Tu agente tiene que señalarlo.
Trade-offs
- Validación estricta aumenta fallos duros durante drift (bien: lo ves).
- Fail-closed baja success rate a corto plazo, previene corrupción silenciosa.
- Mantener schemas cuesta. Corrupción silenciosa cuesta más.
Cuándo NO usarlo
- Si el tool ya es fuertemente tipado end-to-end y lo controlas, puedes validar menos (pero mantén límites de tamaño).
- Si el tool devuelve texto libre por diseño, envuélvelo con un extractor que produzca output estructurado.
- Si no toleras parar, necesitas tools de fallback, no validación más floja.
Checklist (copiar/pegar)
- [ ] Límite máximo de tamaño por respuesta
- [ ] Check de content-type (JSON vs HTML)
- [ ] Parse estricto (sin “best effort”)
- [ ] Schema validation + constraints de enum
- [ ] Invariant checks (ids, counts, rangos)
- [ ] Fail closed o degrade (sin writes con output inválido)
- [ ] Métricas/alertas en invalid output rate
- [ ] Log de args hash + tool version + clase de error
Config segura por defecto (JSON/YAML)
validation:
tool_output:
fail_closed: true
max_chars: 200000
require_content_type: "application/json"
enforce_enums: true
safe_mode:
on_invalid_output: "skip_writes"
metrics:
track: ["tool_output_invalid_rate"]
FAQ (3–5)
Usado por patrones
Fallos relacionados
- Fallos en cascada de tools (cómo un agente amplifica outages) + código
- Prompt Injection en agentes (fallo + defensas + código)
- Incidentes de exceso de tokens (prompt bloat) + fixes + código
- AI Agent Infinite Loop (Detectar + arreglar, con código)
- Explosión de presupuesto (cuando un agente quema dinero) + fixes + código
Gobernanza requerida
Q: ¿Validar outputs es redundante si el tool es interno?
A: No. Los tools internos también derivan y fallan. “Interno” solo significa que el bug es tuyo.
Q: ¿Qué es lo peor si me lo salto?
A: Corrupción silenciosa: writes incorrectos que parecen éxito. Te enteras días después por humanos.
Q: ¿Necesito una librería completa de JSON schema?
A: No al principio. Empieza con parse estricto + invariants clave. Añade schemas donde el blast radius sea grande.
Q: ¿Cómo se relaciona con prompt injection?
A: Outputs corruptos suelen contener texto no confiable. Valida y trata outputs como datos, no como instrucciones.
Páginas relacionadas (3–6 links)
- Foundations: Cómo usan tools los agentes · Agente listo para producción
- Failure: Prompt injection · Fuentes alucinadas
- Governance: Tool permissions (allowlists)
- Production stack: Production agent stack