Corruption de réponse tool (schema drift + truncation) + code

  • Repère la panne tôt, avant que la facture grimpe.
  • Comprends ce qui casse en prod, et pourquoi.
  • Copie des garde-fous : budgets, stop reasons, validation.
  • Sache quand ce n’est pas la vraie cause.
Signaux de détection
  • Tool calls/run explosent (ou se répètent avec args hash).
  • Spend/tokens montent sans amélioration des outputs.
  • Retries passent de rares à constants (429/5xx).
Des outputs de tools corrompus ou driftés mènent à de mauvaises actions. Validation stricte, size limits et fail-closed évitent d’agir sur du garbage.
Sur cette page
  1. Le problème (côté prod)
  2. Pourquoi ça casse en prod
  3. Exemple d’implémentation (code réel)
  4. Incident réel (avec chiffres)
  5. Compromis
  6. Quand NE PAS l’utiliser
  7. Checklist (copier-coller)
  8. Config par défaut sûre (JSON/YAML)
  9. FAQ (3–5)
  10. Pages liées (3–6 liens)
Flux interactif
Scénario:
Étape 1/2: Execution

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 :

  1. size limit
  2. content-type
  3. parse strict
  4. schema + invariants
  5. fail closed / degrade
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;
}

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 :

  1. checks content-type + parse strict
  2. validation schema + enums
  3. fail closed + safe-mode (pas de write si profil invalide)
  4. 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)

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)

Validation output même en interne ?
Oui. ‘Interne’ veut dire ‘tu devras le corriger’.
Le pire si je skip ?
Corruption silencieuse : des writes faux qui ont l’air OK. Tu le découvres trop tard.
Schema lib obligatoire ?
Pas au début. Parse strict + invariants d’abord. Ajoute du schema là où le blast radius est grand.
Lien avec prompt injection ?
Un output corrompu contient souvent du texte non fiable. Traite-le comme data et valide.

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)

Pas sur que ce soit votre cas ?

Concevez votre agent ->
⏱️ 4 min de lectureMis à jour Mars, 2026Difficulté: ★★☆
Implémenter dans OnceOnly
Guardrails for loops, retries, and spend escalation.
Utiliser dans 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 }
Intégré : contrôle en productionOnceOnly
Ajoutez des garde-fous aux agents tool-calling
Livrez ce pattern avec de la gouvernance :
  • Budgets (steps / plafonds de coût)
  • Kill switch & arrêt incident
  • Audit logs & traçabilité
  • Idempotence & déduplication
  • Permissions outils (allowlist / blocklist)
Mention intégrée : OnceOnly est une couche de contrôle pour des systèmes d’agents en prod.
Exemple de policy (concept)
# Example (Python — conceptual)
policy = {
  "budgets": {"steps": 20, "seconds": 60, "usd": 1.0},
  "controls": {"kill_switch": True, "audit": True},
}
Auteur

Cette documentation est organisée et maintenue par des ingénieurs qui déploient des agents IA en production.

Le contenu est assisté par l’IA, avec une responsabilité éditoriale humaine quant à l’exactitude, la clarté et la pertinence en production.

Les patterns et recommandations s’appuient sur des post-mortems, des modes de défaillance et des incidents opérationnels dans des systèmes déployés, notamment lors du développement et de l’exploitation d’une infrastructure de gouvernance pour les agents chez OnceOnly.