Normal path: execute → tool → observe.
Le problème (côté prod)
Ton tool renvoie du JSON. Jusqu’au jour où il renvoie de l’HTML, ou un JSON tronqué, ou un schema qui a changé.
Le modèle voit “presque” et complète. Et tu obtiens le pire bug : ça ne crashe pas, ça fait une action fausse.
Pourquoi ça casse en prod
- outputs traités comme fiables
- réponses partielles
- schema drift constant
- le modèle “smooth” les trous
Exemple d’implémentation (code réel)
Pattern :
- size limit
- content-type
- parse strict
- schema + invariants
- fail closed / degrade
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;
}Incident réel (avec chiffres)
Agent qui met à jour des notes CRM.
Le tool user.profile a commencé à renvoyer des pages d’erreur HTML avec un 200 (proxy mal configuré).
Le modèle a “trouvé” plan=enterprise dans un banner.
Impact :
- 23 enregistrements CRM taggés à tort
- ~3h perdues côté sales
- nettoyage + restauration (logs incomplets)
Fix :
- checks content-type + parse strict
- validation schema + enums
- fail closed + safe-mode (pas de write si profil invalide)
- métriques sur
tool_output_invalid
Compromis
- plus de failures “dures” quand un tool drift (c’est l’objectif)
- maintenance de schema
- moins de “réponses” court terme, beaucoup moins de corruption silencieuse
Quand NE PAS l’utiliser
- tool déjà typé et stable : validation légère (size limits quand même)
- tool free-text : wrap avec extractor structuré
- besoin de continuer coûte que coûte : il te faut un fallback, pas de la validation plus loose
Checklist (copier-coller)
- [ ] Max response size
- [ ] content-type check
- [ ] parse strict
- [ ] schema + enums
- [ ] invariants
- [ ] fail closed / degrade
- [ ] métriques + alerting
- [ ] logs: args hash + version + classe d’erreur
Config par défaut sûre (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)
Utilisé par les patterns
Pannes associées
- Pannes en cascade (comment un agent amplifie une outage) + code
- Attaques de prompt injection sur des agents (failure + défenses + code)
- Incidents de surconsommation de tokens (prompt bloat) + code
- AI Agent Infinite Loop (Détecter + corriger, avec code)
- Budget explosion (quand un agent brûle de l’argent) + fixes + code
Gouvernance requise
Q : Validation output même en interne ?
R : Oui. “Interne” veut dire “tu devras le corriger”.
Q : Le pire si je skip ?
R : Corruption silencieuse : des writes faux qui ont l’air OK. Tu le découvres trop tard.
Q : Schema lib obligatoire ?
R : Pas au début. Parse strict + invariants d’abord. Ajoute du schema là où le blast radius est grand.
Q : Lien avec prompt injection ?
R : Un output corrompu contient souvent du texte non fiable. Traite-le comme data et valide.
Pages liées (3–6 liens)
- Foundations: Comment les agents utilisent des tools · Un agent prêt pour la prod
- Failure: Prompt injection · Sources hallucinées
- Governance: Tool permissions
- Production stack: Production stack