Pas de monitoring (Anti-Pattern) + Quoi logger + Code

  • Repère le piège avant qu’il arrive en prod.
  • Vois ce qui casse quand le modèle est sûr de lui.
  • Copie des defaults sûrs : permissions, budgets, idempotence.
  • Sache quand il ne faut pas d’agent.
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).
Si tu ne peux pas répondre 'qu’est-ce que l’agent a fait ?', tu ne peux pas l’opérer en production. Minimum : traces, stop reasons, spend, logs de tool calls.
Sur cette page
  1. Problème (d’abord)
  2. Le moment 03:00
  3. Pourquoi ça casse en production
  4. 1) Les agents sont des systèmes distribués avec plus d’étapes
  5. 2) Le “success rate” cache les failures intéressantes
  6. 3) Tu ne peux pas corriger ce que tu ne peux pas rejouer
  7. 4) Le monitoring fait partie de la gouvernance
  8. Hard invariants (non négociables)
  9. Implémentation (vrai code)
  10. Example failure case (concret)
  11. 🚨 Incident: “Tout est lent” (et on ne savait pas pourquoi)
  12. Dashboards + alerts (exemples)
  13. PromQL (Grafana)
  14. SQL (style Postgres/BigQuery)
  15. Alert rules (plain English)
  16. Compromis
  17. Quand NE PAS l’utiliser
  18. Checklist (copier-coller)
  19. Safe default config
  20. FAQ
  21. Related pages
  22. Production takeaway
  23. What breaks without this
  24. What works with this
  25. Minimum to ship
En bref

En bref: Sans observabilité, chaque incident devient “le modèle était bizarre” — impossible à tester et à corriger. Il te faut : traces de tools, stop reasons, suivi des coûts, replay. Ce n’est pas optionnel.

Tu vas apprendre : monitoring minimum • un schéma d’événements unique • taxonomie de stop reasons • replay basique • un incident concret

Concrete metric

Sans monitoring : les utilisateurs signalent en premier • debug “au feeling” • pas de replay
Avec monitoring minimal : drift détecté tôt • debug via traces + stop reasons • replay des derniers runs
Impact : réponse incident plus rapide + moins de récidive (tu fixes vraiment la cause)


Problème (d’abord)

Un run d’agent part en vrille.

Un utilisateur dit : “il a envoyé le mauvais email.”

Tu ouvres les logs :

  • le texte final (peut-être)
  • une stack trace (peut-être)
  • des vibes (oui)

Ce que tu n’as pas : Si tu ne peux pas répondre à ces 5 questions, le système n’est pas opérable :

Incident questions
  1. Quels tools ont été appelés (et dans quel ordre) ?
  2. Avec quels args (ou au moins des hashes) ?
  3. Qu’est-ce qui est revenu (ou au moins des snapshot hashes) ?
  4. Quelle version (model/prompt/tools) tournait ?
  5. Pourquoi ça s’est arrêté ?
Truth

Ce n’est pas “il manque des dashboards”. C’est “ce système n’est pas opérable”.

Le moment 03:00

TEXT
03:12 — Support: "Agent emailed the wrong customer. Please stop it."

Tu greppes et tu trouves… rien de joinable.

TEXT
2026-02-07T03:11:58Z INFO sent email to customer@example.com
2026-02-07T03:11:59Z INFO sent email to customer@example.com
2026-02-07T03:12:01Z WARN http.get 429
2026-02-07T03:12:03Z INFO Agent completed task

Pas de run_id. Pas de trace par step. Pas de stop reason. Pas de hash d’args. Pas de version.


Pourquoi ça casse en production

Failure analysis

1) Les agents sont des systèmes distribués avec plus d’étapes

Tools = dépendances + modes d’échec + retries. Sans logs par step, tu expliques des histoires.

2) Le “success rate” cache les failures intéressantes

Le drift se voit par :

  • plus de tool calls / run
  • plus de tokens / request
  • plus de latence
  • stop reasons qui changent

3) Tu ne peux pas corriger ce que tu ne peux pas rejouer

Si tu ne peux pas rejouer (ou au moins reconstruire) un run à partir des logs, tu ne peux pas faire confiance à un “fix”. Tu devines.

4) Le monitoring fait partie de la gouvernance

Budgets, allowlists et kill switch ne servent à rien si tu ne vois pas quand ça trigger.


Hard invariants (non négociables)

  • Chaque run a un run_id.
  • Chaque step a un step_id.
  • Chaque tool call log : tool, hash d’args, durée, status, classe d’erreur.
  • Chaque run se termine par un stop event : stop_reason.
  • Sans replay (même partiel), tu ne peux pas faire confiance à un fix.

Implémentation (vrai code)

Le piège classique : deux formats de logs :

  • tool events structurés
  • stop events “spéciaux”

Résultat : impossible à joindre.

Ce sample utilise un seul schéma d’événements pour tool results et stop events.

PYTHON
from __future__ import annotations

from dataclasses import dataclass, asdict
import hashlib
import json
import time
from typing import Any, Literal


EventKind = Literal["tool_result", "stop"]


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


@dataclass(frozen=True)
class Event:
  run_id: str
  kind: EventKind
  ts_ms: int

  # optional fields
  step_id: int | None = None
  tool: str | None = None
  args_sha: str | None = None
  duration_ms: int | None = None
  status: Literal["ok", "error"] | None = None
  error: str | None = None

  stop_reason: str | None = None
  usage: dict[str, Any] | None = None


def log_event(ev: Event) -> None:
  print(json.dumps(asdict(ev), ensure_ascii=False))


def call_tool(run_id: str, step_id: int, tool: str, args: dict[str, Any]) -> Any:
  started = time.time()
  try:
      out = tool_impl(tool, args=args)  # (pseudo)
      dur = int((time.time() - started) * 1000)
      log_event(
          Event(
              run_id=run_id,
              kind="tool_result",
              ts_ms=int(time.time() * 1000),
              step_id=step_id,
              tool=tool,
              args_sha=sha(args),
              duration_ms=dur,
              status="ok",
              error=None,
          )
      )
      return out
  except Exception as e:
      dur = int((time.time() - started) * 1000)
      log_event(
          Event(
              run_id=run_id,
              kind="tool_result",
              ts_ms=int(time.time() * 1000),
              step_id=step_id,
              tool=tool,
              args_sha=sha(args),
              duration_ms=dur,
              status="error",
              error=type(e).__name__,
          )
      )
      raise


def stop(run_id: str, *, reason: str, usage: dict[str, Any]) -> dict[str, Any]:
  log_event(
      Event(
          run_id=run_id,
          kind="stop",
          ts_ms=int(time.time() * 1000),
          stop_reason=reason,
          usage=usage,
      )
  )
  return {"status": "stopped", "stop_reason": reason, "usage": usage}
JAVASCRIPT
import crypto from "node:crypto";

export function sha(obj) {
const raw = JSON.stringify(obj, Object.keys(obj || {}).sort());
return crypto.createHash("sha256").update(raw, "utf8").digest("hex").slice(0, 24);
}

export function logEvent(ev) {
console.log(JSON.stringify(ev));
}

export async function callTool(runId, stepId, tool, args) {
const started = Date.now();
try {
  const out = await toolImpl(tool, { args }); // (pseudo)
  logEvent({
    run_id: runId,
    kind: "tool_result",
    ts_ms: Date.now(),
    step_id: stepId,
    tool,
    args_sha: sha(args),
    duration_ms: Date.now() - started,
    status: "ok",
    error: null,
  });
  return out;
} catch (e) {
  logEvent({
    run_id: runId,
    kind: "tool_result",
    ts_ms: Date.now(),
    step_id: stepId,
    tool,
    args_sha: sha(args),
    duration_ms: Date.now() - started,
    status: "error",
    error: e?.name || "Error",
  });
  throw e;
}
}

export function stop(runId, { reason, usage }) {
logEvent({
  run_id: runId,
  kind: "stop",
  ts_ms: Date.now(),
  stop_reason: reason,
  usage,
});
return { status: "stopped", stop_reason: reason, usage };
}

Example failure case (concret)

Incident

🚨 Incident: “Tout est lent” (et on ne savait pas pourquoi)

Date: 2024-10-08
Duration: 3 jours sans alerte, ~2 heures de debug quand on a eu de la visibilité
System: agent de support client


What actually happened

Le tool http.get renvoyait des 429/503 intermittents.

Notre layer tool a retry jusqu’à 8× par call (au lieu de 2×) sans jitter. L’agent interprétait ça comme “essaie une autre requête”, donc plus de tool calls par run.

Sur 3 jours (chiffres illustratifs, mais le pattern est classique) :

  • avg tool calls/run : 4.3 → 11.7
  • latence p95 : 2.1s → 8.4s
  • spend/run : ~2×

Rien n’a “crashé”. Le success rate restait ~91%, donc le drift ressemblait à “les users sont impatients” jusqu’à l’escalade support.


Root cause (version boring)

  • retries sans jitter → thundering herd
  • pas de stop reasons dans les logs → “success” masque le drift
  • pas de tool-call trace → impossible de prouver où partaient temps et spend

Fix

  1. logs structurés (run_id, step_id, tool, args hash, durée, status)
  2. stop reasons retournés à l’UI
  3. dashboards + alerting sur signaux de drift (tool calls/run, latence P95, stop reasons)

Dashboards + alerts (exemples)

Tu n’as pas besoin d’une plateforme parfaite. Tu as besoin d’une observabilité utile.

PromQL (Grafana)

PROMQL
# Tool calls per run (p95)
histogram_quantile(0.95, sum(rate(agent_tool_calls_bucket[5m])) by (le))

# Stop reasons over time
sum(rate(agent_stop_total[10m])) by (stop_reason)

# Latency p95
histogram_quantile(0.95, sum(rate(agent_run_latency_ms_bucket[5m])) by (le))

SQL (style Postgres/BigQuery)

SQL
-- Alert: tool_calls/run spike vs baseline
SELECT
  date_trunc('hour', created_at) AS hour,
  avg(tool_calls) AS avg_tool_calls
FROM agent_runs
WHERE created_at > now() - interval '7 days'
GROUP BY 1
HAVING avg(tool_calls) > 2 * (
  SELECT avg(tool_calls)
  FROM agent_runs
  WHERE created_at BETWEEN now() - interval '14 days' AND now() - interval '7 days'
);

Alert rules (plain English)

  • Si tool_calls_per_run_p95 fait 2× la baseline sur 10 minutes → investigate (et pense à couper les writes).
  • Si stop_reason=loop_detected augmente → investigate (tool spam / outage / prompt).
  • Si stop_reason=tool_timeout spike → upstream, pas “model weirdness”.

Compromis

Trade-offs
  • Logger coûte (stockage, indexation). Toujours moins que des incidents aveugles.
  • Évite de logger PII/secrets en clair. Hash + redaction.
  • Le replay exige une politique de rétention + contrôles d’accès.

Quand NE PAS l’utiliser

Don’t
  • Ne construis pas une grosse plateforme de tracing avant d’avoir des logs structurés. Start small.
  • Ne logge jamais des args bruts si ça contient PII/secrets.
  • Ne shippe pas sans stop reasons. Tu fabriques des retry loops.

Checklist (copier-coller)

Production checklist
  • [ ] run_id / step_id pour chaque run
  • [ ] schéma d’événements unique (tool results + stop events)
  • [ ] logs tool-call : tool, args_hash, durée, status, classe d’erreur
  • [ ] stop reason retournée + loggée
  • [ ] metrics tokens/tool calls/spend par run
  • [ ] dashboards : latence P95, tool_calls/run, stop_reason distribution
  • [ ] replay : snapshot hashes (rétention + accès)

Safe default config

YAML
logging:
  events:
    enabled: true
    schema: "unified"
    store_args: false
    store_args_hash: true
    include: ["run_id", "step_id", "tool", "duration_ms", "status", "error", "stop_reason"]
metrics:
  track: ["tokens_per_request", "tool_calls_per_run", "latency_p95", "spend_per_run", "stop_reason"]
retention:
  tool_snapshot_days: 14
  logs_days: 30

FAQ

FAQ
C’est quoi le minimum de monitoring ?
Logs de tool calls + stop reasons + metrics de base. Si tu ne peux pas dire “qu’est-ce qu’il a fait ?”, tu ne peux pas l’opérer.
On peut logger les args bruts ?
En général non. Hash + redaction, et raw uniquement dans des systèmes très contrôlés si vraiment nécessaire.
On a besoin de distributed tracing ?
Plus tard. Commence par des logs structurés avec run_id, step_id et durées.
Comment monitorer le drift ?
Tokens, tool calls, latence, stop reasons. Ça bouge avant les plaintes de qualité.

Related

Production takeaway

Production takeaway

What breaks without this

  • ❌ incidents inexplicables
  • ❌ drift = “model weirdness”
  • ❌ dérive de coûts détectée trop tard

What works with this

  • ✅ runs joignables, rejouables, debuggables
  • ✅ drift = un graph, pas un débat
  • ✅ kill switch déclenché sur des signaux réels

Minimum to ship

  1. Logs structurés
  2. Stop reasons
  3. Metrics + dashboards
  4. Alerting sur drift

Pas sur que ce soit votre cas ?

Concevez votre agent ->
⏱️ 9 min de lectureMis à jour Mars, 2026Difficulté: ★★★
Implémenter dans OnceOnly
Safe defaults for tool permissions + write gating.
Utiliser dans OnceOnly
# onceonly guardrails (concept)
version: 1
tools:
  default_mode: read_only
  allowlist:
    - search.read
    - kb.read
    - http.get
writes:
  enabled: false
  require_approval: true
  idempotency: true
controls:
  kill_switch: { enabled: true, mode: disable_writes }
audit:
  enabled: 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)
  • 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.