Danila (Dayfing)
Retour aux articles
2 246 mots10 min

Observabilité d’un agent IA : traces, latence, jetons, coût et erreurs

Pourquoi un agent demande un modèle d’observabilité différent

Une requête API classique possède un début, un gestionnaire et une réponse. Un agent IA ajoute une boucle. Il choisit un modèle, appelle parfois un outil, attend un système distant, lit le résultat et peut revenir au modèle. Une requête peut donc contenir plusieurs appels, recherches, retries et contrôles de politique. Une ligne disant « la requête a échoué » ne montre pas quelle branche a consommé le temps ou le budget.

L’observabilité rend ce parcours lisible grâce à des traces, métriques et journaux corrélés. OpenTelemetry décrit la trace comme le chemin d’une requête et le span comme une opération dans ce chemin. Utilisez un span racine pour l’exécution visible par l’utilisateur, puis des spans enfants pour l’inférence, la recherche, les outils, les garde-fous et la sérialisation. Gardez les noms de modèles et d’outils à faible cardinalité. Placez les identifiants propres à une requête dans le contexte de trace ou les journaux, jamais dans les labels de métrique. L’opérateur peut alors déterminer ce qui s’est passé dans une exécution, à quelle fréquence et quels workflows sont touchés.

Cet article emploie un schéma indépendant d’un fournisseur. La documentation du fournisseur reste la référence pour les champs d’usage et la facturation. Les conventions de spans GenAI d’OpenTelemetry et ses conventions de métriques GenAI fournissent un vocabulaire commun.

Un schéma de trace adapté à une boucle d’agent

Créez le span racine lorsque l’application accepte la requête, et non au premier appel de modèle. Utilisez invoke_agent et des attributs pour service, déploiement, environnement, version du workflow et classe de locataire non sensible. N’enregistrez l’identifiant de conversation que s’il est autorisé par la rétention. Ne mettez jamais le message, le prompt complet ou les paramètres d’un outil dans un label.

Chaque span enfant doit répondre à une question d’exploitation. Un minimum utile est le suivant :

Span Données à enregistrer
agent.run nom et version du workflow, résultat, nombre de tentatives, durée
gen_ai.inference fournisseur, modèle demandé et modèle utilisé, opération, streaming, fin, jetons
gen_ai.retrieval classe d’index ou de source, mode de requête, nombre de résultats, cache
gen_ai.tool nom et type d’outil, décision d’autorisation, délai, résultat
guardrail.check version de la politique, décision, code de raison, durée

Attribuez un statut au span et une valeur error.type lorsqu’une opération échoue. Pour un retry, ajoutez un événement horodaté avec son numéro, le délai d’attente et le code de raison. Un retry n’est pas une deuxième exécution racine. C’est une tentative de la même opération logique, avec un span client distinct lorsque la requête part sur le réseau. Cette distinction évite de compter deux requêtes utilisateur tout en montrant les tentatives du fournisseur.

Le JSON suivant illustre une forme exportée, pas le format imposé par un fournisseur. Il conserve des compteurs et des codes, sans contenu.

{
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "parent_span_id": "b7ad6b7169203331",
  "name": "chat model",
  "kind": "CLIENT",
  "status": "OK",
  "attributes": {
    "gen_ai.operation.name": "chat",
    "gen_ai.provider.name": "provider.example",
    "gen_ai.request.model": "model.example",
    "gen_ai.response.model": "model.example-2026-01",
    "gen_ai.usage.input_tokens": 820,
    "gen_ai.usage.output_tokens": 146,
    "gen_ai.response.finish_reasons": ["stop"],
    "app.agent.attempt": 1
  }
}

Le nom d’un span doit décrire une classe d’opération, jamais un ID ou du texte utilisateur. Conservez la version de la convention sémantique dans les métadonnées d’instrumentation. Si le fournisseur sépare les jetons facturables des jetons traités, conservez les deux dans la comptabilité privée et utilisez les facturables pour le coût. Ne mélangez pas les instrumentations client et serveur d’une même requête sans distinguer leur couche, sinon les jetons seront comptés deux fois.

Correlation IDs et propagation du contexte

Le trace ID relie les services. Le span ID identifie une opération. Un request ID applicatif reste pratique pour le support, mais ne remplace pas le contexte de trace. Propagez l’en-tête W3C traceparent par la passerelle API, le service agent, le service de recherche et les adaptateurs d’outils. Le guide OpenTelemetry sur la propagation décrit l’extraction du contexte distant et la création d’un span enfant. Le même contexte peut être injecté dans les journaux structurés pour relier une ligne à une trace.

Créez un request ID aléatoire pour le support. Conservez sa correspondance avec le trace ID dans les journaux. Pour une file, injectez le contexte et créez un span consommateur. Pour des tâches parallèles sans parent unique, utilisez des span links. Ne transmettez pas le baggage interne à un tiers. Validez la propagation à la frontière publique.

Les spans d’outils révèlent le comportement réel

Séparez la décision du modèle et l’exécution quand cette distinction est utile. Le span du modèle montre qu’un outil a été demandé. Le span de l’outil montre ce que l’application a réellement exécuté. Il doit contenir un nom tool.name stable, un type comme function, extension ou datastore, une décision de politique et l’opération externe. N’ajoutez la méthode ou la classe de recherche que si elle ne révèle rien de sensible. N’enregistrez ni jeton d’accès, ni paramètres SQL, ni contenu de document, ni URL complète avec query.

Pour chaque outil, enregistrez les temps, le délai, la classe de résultat, les retries et une taille bornée. Un timeout, un rejet métier, un refus de politique et une erreur upstream 5xx ont des codes différents. Propagez le contexte pour un appel à un autre service. Pour une commande locale, enregistrez seulement famille et classe de sortie.

Montrez aussi les attentes qui ne sont pas des appels d’outils. Ajoutez des spans pour l’attente en file, le sommeil imposé par une limite, un circuit breaker ouvert et le streaming. Sinon le temps d’un appel modèle peut cacher l’attente d’un créneau de concurrence. Dans un workflow multi-agents, donnez un nom à chaque workflow délégué et reliez-le à la trace parente. Ne créez pas une nouvelle trace pour chaque état interne.

Comptabiliser les jetons et le coût

Utilisez l’usage renvoyé par le fournisseur quand il existe. Les jetons d’entrée, de sortie, mis en cache ou de raisonnement peuvent avoir des prix distincts, tout comme les images et les unités d’outils. Le guide OpenAI sur les jetons rappelle que la tokenisation dépend du modèle et de la langue et que l’objet usage de la réponse est la base d’une requête terminée. Un tokenizer local estime un budget préalable, mais ne remplace pas l’usage fournisseur pour la facture.

Conservez par tentative le fournisseur, les modèles demandé et utilisé, la catégorie, la quantité, la devise, la version de prix et le centre de coût. Calculez ensuite :

cost = input_billable_tokens * input_price
     + cached_input_tokens * cached_input_price
     + output_billable_tokens * output_price
     + provider_units * unit_price

Les prix doivent être une configuration datée, et non une constante dans l’exporteur. Conservez la quantité brute et le montant calculé afin d’auditer une correction. Additionnez toutes les tentatives, car un appel échoué peut coûter des jetons. Marquez une charge estimée ou différée comme provisoire et rapprochez-la du rapport du fournisseur.

Exposez des dimensions modifiables : workflow, famille de modèle, environnement, classe de locataire et résultat. Excluez ID utilisateur, prompt et arguments d’outil des labels. Le tableau doit montrer total, coût par exécution, jetons, retries et modèles chers. Une hausse des jetons peut signaler une réponse non bornée ou une boucle.

Distributions de latence : p50, p95 et p99

Une moyenne masque la queue d’une file ou d’un outil lent. Enregistrez la durée du run racine et des spans importants dans un histogramme en secondes. Mesurez le time to first token, l’intervalle des chunks utile à l’affichage et le temps total. Séparez serveur, file et réseau quand c’est possible.

p50 décrit le cas typique, p95 les utilisateurs lents et p99 les cas rares mais graves. Ce sont des quantiles d’une distribution. Choisissez des buckets autour de l’objectif réel, de la fraction de seconde à plusieurs minutes, sans mélanger les unités. La fonction Prometheus histogram_quantile estime un quantile depuis les buckets. Pour un histogramme classique, agrégez par le label le avant le calcul.

histogram_quantile(
  0.95,
  sum by (le, workflow) (
    rate(agent_run_duration_seconds_bucket[10m])
  )
)

Ne mettez pas le trace ID dans un label. Ventilez par workflow, famille de modèle, région et résultat. Une trace explique alors le déplacement du p95. Ne faites pas la somme des durées de spans concurrents : utilisez le chemin critique.

Erreurs, retries et limites de débit

Définissez une taxonomie avant les alertes. Distinguez validation, refus de politique, authentification, rate limit, timeout, erreur upstream, réponse modèle mal formée, panne d’outil et annulation. Mappez les codes du fournisseur vers ces classes et conservez un code original borné. Marquez l’annulation volontaire séparément.

Chaque retry doit avoir une raison, un numéro, un backoff et un résultat. Utilisez un backoff exponentiel borné avec jitter seulement si le contrat le permet. Ne répétez pas validation, autorisation, politique ou erreur de schéma déterministe. Fixez une deadline au run et à chaque tentative. Le span racine rapporte retry_count, attempt_count et le résultat. Un taux élevé de retries peut augmenter le coût malgré un bon succès.

Enregistrez le choix d’un modèle de secours dans un événement ou un span. Le tableau doit distinguer rate limit, latence, politique de sécurité et test de capacité. Surveillez séparément les arguments d’outil mal formés et les boucles de réparation de schéma. Bornez les itérations et émettez loop_limit à la limite.

Confidentialité, redaction et sampling

Les prompts et données d’outils peuvent contenir des informations personnelles, confidentielles ou sensibles. Par défaut, collectez métadonnées, compteurs, empreintes et codes, sans contenu. Si le débogage exige des exemples, utilisez un stockage séparé avec consentement, rétention courte, chiffrement, journaux d’accès et redaction par champ. Rédigez avant l’export.

Utilisez une allowlist d’attributs. Supprimez les headers d’autorisation, cookies, clés API, coordonnées, numéros de compte, URL avec query et texte documentaire. Le hachage n’est pas automatiquement anonyme. Séparez l’empreinte du prompt du prompt et documentez les personnes autorisées à les relier. Testez la redaction avec des secrets réalistes et des données multilingues.

Le sampling réduit le volume, mais ne doit pas cacher les incidents. Utilisez un sampling parent-based pour garder une trace cohérente. Dans le Collector, un tail sampling peut conserver erreurs, timeouts, exécutions coûteuses et traces lentes, tout en échantillonnant les succès ordinaires. La spécification OpenTelemetry du sampling distingue enregistrement et export. Gardez les métriques non échantillonnées et utilisez les traces pour les exemplars.

OpenTelemetry, Grafana et Sentry

Instrumentez l’agent avec les API OpenTelemetry et les conventions sémantiques, envoyez OTLP au Collector, puis laissez-le gérer batch, mémoire, redaction, sampling et routage. Exportez les trois signaux vers leurs backends avec les mêmes attributs de service, version, environnement, région et déploiement. Vérifiez une trace complète en staging avant le sampling de production.

Grafana fournit une vue opérationnelle partagée. Créez des panneaux pour volume, succès, p50/p95/p99, premier token, jetons, coût, retries, outils et fournisseur. Ajoutez des liens vers la recherche et le runbook. La documentation Grafana des alert rules couvre requêtes, conditions, évaluation et notifications. Sentry ajoute regroupement, contexte d’erreur et inspection. Son Trace API expose les spans et erreurs d’une trace. Envoyez seulement des données redactées et configurez le sampling.

SLO et alertes actionnables

Un SLO doit représenter une promesse faite à l’utilisateur, pas la santé du Collector. Définissez la disponibilité comme la part des runs sans erreur classée serveur, fournisseur ou outil. Définissez la latence comme la part des runs sous un seuil. Suivez une contrainte de coût avec un SLI budget séparé.

Choisissez les objectifs selon une base mesurée et le besoin produit. Séparez les workflows dont les attentes diffèrent. Montrez l’error budget sur une fenêtre longue et une vue courte pour les déploiements. La documentation Grafana SLO décrit les SLI, la consommation de budget et les alertes fast-burn et slow-burn. Une page n’existe que lorsqu’une action est possible. Un trend lent peut créer un ticket.

Alertez sur la hausse durable des erreurs racines, la consommation rapide du budget, un p95 au-dessus du contrat, un pic de rate limit, la progression des retries, des usages manquants, un coût inattendu ou une file bloquée. Ajoutez workflow, région, déploiement, valeur, seuil, lien de trace, propriétaire et runbook. Un pending period évite une page sur un seul point. Groupez par service et sévérité. Sans action immédiate, préférez un tableau de bord.

Du symptôme à la cause

Commencez par le SLO ou le retour utilisateur et sélectionnez une trace représentative. Vérifiez les enfants du span racine et la propagation à la passerelle, dans la file et à la frontière d’outil. Si des spans manquent, examinez d’abord l’exporteur, le sampling et l’injection du contexte.

Pour une exécution lente, comparez attente de file, premier token, durée de sortie, recherche et chemins critiques des outils. Un p99 haut avec p50 normal indique souvent une dépendance de queue, une limite de concurrence ou un retry. Un déplacement du p50 suggère un déploiement, modèle, prompt ou région différent. Pour un coût en hausse, regroupez par modèle réellement utilisé, catégorie de jeton, version de workflow et tentatives, puis rapprochez l’usage du rapport fournisseur.

Pour une erreur, partez du premier span en échec, pas de l’exception finale. Vérifiez status, error.type, code fournisseur, budget de timeout et événements de retry. Distinguez rejet d’outil, panne fournisseur, réponse mal formée et bug de parseur. Vérifiez que la redaction a conservé un code de diagnostic sûr. Comptez un blocage de politique attendu comme résultat produit et n’alertez que si sa fréquence change de façon inattendue.

Conservez des requêtes synthétiques à fixtures déterministes et non sensibles. Après une modification de l’instrumentation, du modèle, du prompt ou du routage, vérifiez que traces, métriques, enregistrements de jetons et erreurs partagent le même correlation ID. Pour le contexte d’architecture, consultez production AI agent architecture, AI agent evaluations, prompt injection and MCP security et hybrid RAG with pgvector. Ces sujets déterminent les spans et le SLO acceptable, tandis que l’observabilité reste la couche de preuve.

Plus d’articles