Logging d’agents IA (quoi logger, quoi redacter, sur quoi alerter)

Du logging qui aide en incident : trace IDs, événements de tool calls, stop reasons, redaction, alerting. Exemples Python + JS.
Sur cette page
  1. Problème (pourquoi tu lis ça)
  2. Pourquoi ça casse en prod
  3. Diagramme : pipeline d’événements minimal
  4. Code réel : instrumenter le tool gateway (Python + JS)
  5. Panne réelle (avec chiffres)
  6. Compromis
  7. Quand NE PAS faire ça
  8. Checklist copy-paste
  9. Config par défaut (YAML)
  10. Implémenter dans OnceOnly (optionnel)
  11. FAQ (3–5)
  12. Pages liées (3–6 liens)

Problème (pourquoi tu lis ça)

En dev, ton agent “marche”.

En prod, une fois sur 200 runs :

  • un client dit “il a envoyé le mauvais truc”
  • le coût explose pendant 15 minutes
  • il boucle sur une API flaky jusqu’au timeout

Et toi, tu n’as… presque rien :

  • une “réponse finale”
  • deux logs perdus
  • parfois une erreur tool sans contexte

Résultat : le pire debugging possible, au pif avec une CB branchée.

Cette page, c’est du logging qui rend les incidents ennuyeux (le rêve).

Pourquoi ça casse en prod

Les agents cassent comme des systèmes distribués parce que c’en sont :

  • le modèle est un planner peu fiable
  • les tools sont des side effects (HTTP/DB/tickets/email)
  • retries + timeouts créent des comportements émergents

Si tu ne logges pas la boucle, tu ne peux pas répondre à :

  • quels tool calls, dans quel ordre ?
  • quels args (ou au moins quel args_hash) ?
  • qu’est-ce que le tool a renvoyé (ou qu’est-ce qu’on a redigé) ?
  • pourquoi le run s’est arrêté (stop_reason) ?
  • quelle requête / quel user ?

Sans stop_reason, tu n’observes rien. Tu collectionnes des impressions.

Diagramme : pipeline d’événements minimal

Code réel : instrumenter le tool gateway (Python + JS)

Commence par la frontière. C’est là que l’argent (et les dégâts) se passent.

On log :

  • run_id, trace_id, tool_name
  • args_hash (pas les args bruts par défaut)
  • latence + statut
  • error_class (normalisée)

Et on force le passage par un gateway pour éviter le “oops j’ai oublié de logger”.

PYTHON
import hashlib
import json
import time
from dataclasses import dataclass
from typing import Any, Callable, Dict, Optional


def stable_hash(obj: Any) -> str:
  raw = json.dumps(obj, sort_keys=True, ensure_ascii=False).encode("utf-8")
  return hashlib.sha256(raw).hexdigest()


@dataclass(frozen=True)
class RunCtx:
  run_id: str
  trace_id: str
  user_id: Optional[str] = None
  request_id: Optional[str] = None


class Logger:
  def event(self, name: str, fields: Dict[str, Any]) -> None: ...


class ToolGateway:
  def __init__(self, *, impls: dict[str, Callable[..., Any]], logger: Logger):
      self.impls = impls
      self.logger = logger

  def call(self, ctx: RunCtx, name: str, args: Dict[str, Any]) -> Any:
      fn = self.impls.get(name)
      if not fn:
          self.logger.event("tool_call", {
              "run_id": ctx.run_id,
              "trace_id": ctx.trace_id,
              "tool": name,
              "args_hash": stable_hash(args),
              "ok": False,
              "error_class": "unknown_tool",
          })
          raise RuntimeError(f"unknown tool: {name}")

      t0 = time.time()
      self.logger.event("tool_call", {
          "run_id": ctx.run_id,
          "trace_id": ctx.trace_id,
          "tool": name,
          "args_hash": stable_hash(args),
      })

      try:
          out = fn(**args)
          self.logger.event("tool_result", {
              "run_id": ctx.run_id,
              "trace_id": ctx.trace_id,
              "tool": name,
              "latency_ms": int((time.time() - t0) * 1000),
              "ok": True,
          })
          return out
      except TimeoutError:
          self.logger.event("tool_result", {
              "run_id": ctx.run_id,
              "trace_id": ctx.trace_id,
              "tool": name,
              "latency_ms": int((time.time() - t0) * 1000),
              "ok": False,
              "error_class": "timeout",
          })
          raise
      except Exception as e:
          self.logger.event("tool_result", {
              "run_id": ctx.run_id,
              "trace_id": ctx.trace_id,
              "tool": name,
              "latency_ms": int((time.time() - t0) * 1000),
              "ok": False,
              "error_class": type(e).__name__,
          })
          raise
JAVASCRIPT
import crypto from "node:crypto";

export function stableHash(obj) {
const raw = JSON.stringify(obj);
return crypto.createHash("sha256").update(raw).digest("hex");
}

export class ToolGateway {
constructor({ impls = {}, logger }) {
  this.impls = impls;
  this.logger = logger;
}

call(ctx, name, args) {
  const fn = this.impls[name];
  const argsHash = stableHash(args);

  if (!fn) {
    this.logger.event("tool_call", {
      run_id: ctx.run_id,
      trace_id: ctx.trace_id,
      tool: name,
      args_hash: argsHash,
      ok: false,
      error_class: "unknown_tool",
    });
    throw new Error("unknown tool: " + name);
  }

  const t0 = Date.now();
  this.logger.event("tool_call", {
    run_id: ctx.run_id,
    trace_id: ctx.trace_id,
    tool: name,
    args_hash: argsHash,
  });

  try {
    const out = fn(args);
    this.logger.event("tool_result", {
      run_id: ctx.run_id,
      trace_id: ctx.trace_id,
      tool: name,
      latency_ms: Date.now() - t0,
      ok: true,
    });
    return out;
  } catch (e) {
    this.logger.event("tool_result", {
      run_id: ctx.run_id,
      trace_id: ctx.trace_id,
      tool: name,
      latency_ms: Date.now() - t0,
      ok: false,
      error_class: e?.name || "Error",
    });
    throw e;
  }
}

À combiner avec :

  • budgets (/fr/governance/budget-controls)
  • dedupe pour éviter le spam (/fr/failures/tool-spam)
  • unit tests qui verrouillent les stop reasons (/fr/testing-evaluation/unit-testing-agents)

Panne réelle (avec chiffres)

On a livré un agent de recherche “read-only” qui faisait http.get.

Un jour, un partenaire a commencé à renvoyer des 200 avec un payload d’erreur (oui). Notre wrapper tool a traité “200 == ok” et on loggait uniquement “success”.

Impact :

  • ~18% des runs ont produit des résumés faux mais confiants pendant ~2 heures
  • ~30 tickets
  • astreinte : ~4 heures pour prouver que ce n’était pas juste “le modèle qui hallucine”

Fix :

  1. log d’une error_class normalisée + échecs de validation
  2. args_hash + latence pour repérer les hot spots
  3. alerte : validation_fail_rate > 2% pendant 5 minutes

Tu n’as pas besoin de logs parfaits. Tu as besoin de logs qui répondent “qu’est-ce qui s’est passé ?” en <10 minutes.

Compromis

  • Logger les args bruts aide… et leak de la PII. Par défaut : args_hash.
  • Stocker les résultats complets rend le debug simple et la conformité pénible. Sampling + redaction.
  • Trop de logs peut devenir une panne. Commence par ce que tu alertes.

Quand NE PAS faire ça

  • Si l’agent tourne seulement en local/trusté, tu peux être plus light (un temps).
  • Si tu changes la boucle tous les jours : garde les logs légers mais cohérents (IDs + stop reasons).
  • Ne construis pas un système de tracing maison si tu ne peux pas l’opérer.

Checklist copy-paste

  • [ ] run_id, trace_id, request_id, user_id sur chaque event
  • [ ] tool_call + tool_result (name, args_hash, latency, ok, error_class)
  • [ ] stop_reason + budgets en fin de run
  • [ ] policy de redaction (PII/secrets) + hashes par défaut
  • [ ] alertes : tool calls/run, timeouts, validation fails
  • [ ] une query “incident” par failure fréquente (dashboard / saved search)

Config par défaut (YAML)

YAML
logging:
  ids:
    run_id: required
    trace_id: required
    request_id: required
  tool_calls:
    enabled: true
    store_args: false
    store_args_hash: true
    store_results: "sampled"
    result_sample_rate: 0.01
  pii:
    redact_fields: ["email", "phone", "token", "authorization", "cookie"]
  stop_reasons:
    enabled: true
alerts:
  tool_calls_per_run_p95: { warn: 10, critical: 20 }
  timeout_rate: { warn: 0.02, critical: 0.05 }
  validation_fail_rate: { warn: 0.02, critical: 0.05 }

Implémenter dans OnceOnly (optionnel)

Implémenter dans OnceOnly
Logger les tool calls avec args_hash + stop reasons (safe par défaut).
Utiliser dans OnceOnly
# onceonly-python: governed audit logs + metrics
import os
from onceonly import OnceOnly

client = OnceOnly(api_key=os.environ["ONCEONLY_API_KEY"])
agent_id = "support-bot"

# Pull last 50 actions (includes args_hash + decisions)
for e in client.gov.agent_logs(agent_id, limit=50):
    print(e.ts, e.tool, e.decision, e.args_hash, e.spend_usd, e.reason)

# Rollups for dashboards/alerts
m = client.gov.agent_metrics(agent_id, period="day")
print("spend_usd=", m.total_spend_usd, "blocked=", m.blocked_actions)

FAQ (3–5)

Je dois logger les args bruts des tools ?
Par défaut non. Log args_hash + champs non sensibles. Active les args bruts uniquement pendant une fenêtre d’incident, avec redaction.
Le champ le plus utile ?
Un run_id/trace_id stable sur chaque event. Sans ça, impossible de reconstruire un run.
Comment détecter une boucle vite ?
Alerte sur tool_calls/run et répétitions de (tool, args_hash). Combine ça avec une taxonomie stop_reason.
Il faut du distributed tracing ?
Si tes tools touchent d’autres services, oui. Commence par propager trace_id + quelques spans autour des tool calls.

Q: Je dois logger les args bruts des tools ?
A: Par défaut non. Log args_hash + champs safe. Active les args bruts seulement pendant un incident, puis coupe.

Q: Le champ le plus utile ?
A: Un run_id/trace_id stable sur chaque event.

Q: Comment détecter une boucle vite ?
A: Alerte sur tool_calls/run + répétitions (tool, args_hash). Va lire /failures/tool-spam.

Q: Il faut du distributed tracing ?
A: Si tes tools touchent d’autres services, oui. Trace IDs + spans tool, puis on reparle.

Pages liées (3–6 liens)

⏱️ 7 min de lectureMis à jour Mars, 2026Difficulté: ★★★
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)
  • Permissions outils (allowlist / blocklist)
  • Kill switch & arrêt incident
  • Idempotence & déduplication
  • Audit logs & traçabilité
Mention intégrée : OnceOnly est une couche de contrôle pour des systèmes d’agents en prod.
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.