Corrupción de respuestas de tools (schema drift + truncation) + código

  • Detecta el fallo temprano antes de que suba el gasto.
  • Entiende qué se rompe en producción y por qué.
  • Copia guardrails: budgets, stop reasons, validación.
  • Sabe cuándo esto no es la causa raíz.
Señales de detección
  • Tool calls por run suben (o repiten mismo args hash).
  • Gasto/tokens suben sin mejorar el resultado.
  • Retries pasan de raros a constantes (429/5xx).
Outputs corruptos o con drift terminan en acciones equivocadas. Valida outputs, impón límites de tamaño y falla cerrado para que el agente no actúe sobre basura.
En esta página
  1. El problema (en producción)
  2. Por qué esto se rompe en producción
  3. 1) Se trata el output del tool como confiable y bien formado
  4. 2) Las respuestas parciales pasan
  5. 3) El schema drift es constante
  6. 4) El modelo intenta “suavizar” la corrupción
  7. Ejemplo de implementación (código real)
  8. Incidente real (con números)
  9. Trade-offs
  10. Cuándo NO usarlo
  11. Checklist (copiar/pegar)
  12. Config segura por defecto (JSON/YAML)
  13. FAQ (3–5)
  14. Páginas relacionadas (3–6 links)
Flujo interactivo
Escenario:
Paso 1/2: Execution

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:

  1. límites de tamaño
  2. content-type
  3. schema + invariants
  4. fail-closed (o degrade) si no cuadra
PYTHON
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)
JAVASCRIPT
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:

  1. content-type + parse JSON estricto
  2. schema validation + enums
  3. fail closed + safe-mode (saltar writes si el perfil es inválido)
  4. 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)

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)

¿Validar outputs es redundante si el tool es interno?
No. Los tools internos también derivan y fallan. ‘Interno’ solo significa que el bug es tuyo.
¿Qué es lo peor si me lo salto?
Corrupción silenciosa: writes incorrectos que parecen éxito. Te enteras días después por humanos.
¿Necesito una librería completa de JSON schema?
No al principio. Empieza con parse estricto + invariants clave. Añade schemas donde el blast radius sea grande.
¿Cómo se relaciona con prompt injection?
Outputs corruptos suelen contener texto no confiable. Valida y trata outputs como datos, no como instrucciones.

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.

No sabes si este es tu caso?

Disena tu agente ->
⏱️ 6 min de lecturaActualizado Mar, 2026Dificultad: ★★☆
Implementar en OnceOnly
Guardrails for loops, retries, and spend escalation.
Usar en OnceOnly
# onceonly guardrails (concept)
version: 1
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
  max_usd: 1.00
policy:
  tool_allowlist:
    - search.read
    - http.get
controls:
  loop_detection:
    enabled: true
    dedupe_by: [tool, args_hash]
  retries:
    max: 2
    backoff_ms: [200, 800]
stop_reasons:
  enabled: true
logging:
  tool_calls: { enabled: true, store_args: false, store_args_hash: true }
Integrado: control en producciónOnceOnly
Guardrails para agentes con tool-calling
Lleva este patrón a producción con gobernanza:
  • Presupuestos (pasos / topes de gasto)
  • Kill switch y parada por incidente
  • Audit logs y trazabilidad
  • Idempotencia y dedupe
  • Permisos de herramientas (allowlist / blocklist)
Mención integrada: OnceOnly es una capa de control para sistemas de agentes en producción.
Ejemplo de policy (concepto)
# Example (Python — conceptual)
policy = {
  "budgets": {"steps": 20, "seconds": 60, "usd": 1.0},
  "controls": {"kill_switch": True, "audit": True},
}
Autor

Esta documentación está curada y mantenida por ingenieros que despliegan agentes de IA en producción.

El contenido es asistido por IA, con responsabilidad editorial humana sobre la exactitud, la claridad y la relevancia en producción.

Los patrones y las recomendaciones se basan en post-mortems, modos de fallo e incidentes operativos en sistemas desplegados, incluido durante el desarrollo y la operación de infraestructura de gobernanza para agentes en OnceOnly.