Leçon 3 — Instrumenter

Tracer un agent ADK, ses sessions et ses utilisateurs

7 min de lecture

Tracer un agent ADK, ses sessions et ses utilisateurs

La seconde version de l'assistant d'Arvea est un agent ADK : le modèle décide lui-même de chercher une procédure ou d'ouvrir un ticket. Un conseiller rapporte que l'agent a affirmé à un client que « la mutuelle ne propose pas ce service ». Est-ce écrit dans une fiche, ou le modèle l'a-t-il ajouté ? Sans trace, la question reste ouverte. Avec une trace, la réponse prend dix secondes : on lit ce que l'outil a renvoyé.

Le mécanisme : un agent, c'est une boucle de spans

ADK exécute une boucle : appel au modèle, éventuel appel d'outil, retour au modèle, jusqu'à une réponse finale. L'instrumentation OpenInference pour ADK (openinference-instrumentation-google-adk, celle que documente Langfuse) transforme chaque étape en span OpenTelemetry typé :

flowchart TB
  I["CHAIN invocation [assistant-conseillers]<br/>user_id, session_id"] --> A["AGENT agent_run [assistant_conseillers]"]
  A --> L1["GENERATION call_llm<br/>décide : rechercher_procedure"]
  A --> T1["TOOL rechercher_procedure<br/>renvoie : aucune fiche"]
  A --> L2["GENERATION call_llm<br/>décide : ouvrir_ticket"]
  A --> T2["TOOL ouvrir_ticket<br/>TCK-00001"]
  A --> L3["GENERATION call_llm<br/>réponse finale"]

Le user_id et le session_id passés à runner.run_async() sont recopiés par l'instrumentation : Langfuse range la trace sous le bon conseiller et la bonne conversation sans code supplémentaire.

La mise en œuvre

1. Des versions qui s'accordent

Premier essai d'installation, avec les dernières versions de tout :

$ pip install langfuse==5.0.1 google-adk==2.10.0 ...
ERROR: ResolutionImpossible

Laissé libre, pip a ensuite « résolu » en installant google-adk 1.14.1, une version majeure plus ancienne, sans erreur. La cause est dans les métadonnées des paquets, relevées le 2026-10-15 :

PaquetContrainte OpenTelemetry
google-adk 2.10 et 2.12opentelemetry-sdk >=1.39, <=1.42.1
langfuse 4.18 et 5.xopentelemetry-sdk >=1.45.0
langfuse 4.15.6opentelemetry-sdk >=1.33.1

requirements.txt épingle donc langfuse==4.15.6 et google-adk==2.10.0, avec la raison en commentaire. Le SDK 4.x reste supporté par le serveur v4.59. Revérifiez ce tableau à chaque mise à jour d'ADK : le jour où ADK relève son plafond, passez au SDK v5 (qui exige aussi Python 3.11).

2. Une seule instrumentation

files/src/assistant/observabilite.py (extrait)
@lru_cache(maxsize=1)
def initialiser() -> Langfuse:
    from openinference.instrumentation.google_adk import GoogleADKInstrumentor
    langfuse = get_client()              # enregistre l'exportateur OTLP vers Langfuse
    GoogleADKInstrumentor().instrument() # spans ADK -> Langfuse
    return langfuse

lru_cache garantit un seul appel : instrumenter deux fois produit des spans en double.

3. run_async, pas run

files/src/assistant/agent_adk.py (extrait)
async def demander(self, question: str, *, user_id: str, session_id: str) -> str:
    ...
    with propagate_attributes(trace_name="agent-conseiller", tags=["agent-adk"],
                              user_id=user_id, session_id=session_id):
        async for ev in self.runner.run_async(user_id=user_id, session_id=session_id,
                                              new_message=message):
            if ev.is_final_response() and ev.content and ev.content.parts:
                final = "".join(p.text or "" for p in ev.content.parts).strip()

runner.run() (synchrone) exécute l'agent dans un thread séparé : le contexte OpenTelemetry ne le suit pas, et le nom de trace ou les tags posés par propagate_attributes n'atteignent pas les spans ADK. run_async s'exécute dans le contexte courant.

4. Les outils, typés et bornés

Les outils sont des fonctions Python ordinaires ; leur docstring sert de description au modèle. ouvrir_ticket refuse une priorité inconnue au lieu de l'accepter en silence : l'erreur apparaît dans le span TOOL et le modèle peut se corriger.

Résultat obtenu

make run du 2026-10-15, partie agent :

[agent ADK] Selon la procédure (PROC-047), la Mutuelle Arvea doit accuser réception d'une réclamation sous
10 jours ouvrés et y répondre dans un délai maximal de 2 mois. En cas de dépassement, l'adhérent a la possibilité
de saisir le médiateur de la mutualité.

[agent ADK] Aucune procédure interne ne couvre l'assurance automobile, car la mutuelle ne propose pas ce service.
Un ticket (TCK-00001) a été ouvert auprès du back-office pour traiter cette demande spécifique.

Observations de la seconde trace (API Observations v2) : CHAIN invocation, AGENT agent_run, GENERATION call_llm ×3, TOOL rechercher_procedure, TOOL ouvrir_ticket. Coûts des trois appels : 0,000187, 0,000228 et 0,000286 USD.

La trace répond à la question du conseiller : rechercher_procedure a renvoyé {"fiches": []}. « La mutuelle ne propose pas ce service » ne vient d'aucune fiche ; c'est une affirmation du modèle. Le prompt de l'agent doit l'interdire, et un évaluateur doit la détecter (leçon 5).

Vue session dans Langfuse : deux questions d'un même appel, réponses de l'assistant

Capture réelle : la session appel-971fc328 du conseiller cons-0412 regroupe les deux questions de la même conversation.

Ce qui casse en production

SymptômeCauseParade
Chaque appel de l'agent apparaît deux fois (call_llm et AsyncGenerateContent), coût doubléinstrumentation ADK et instrumentation google-genai actives : ADK appelle google-genai en interne (constaté pendant la rédaction)une seule instrumentation par chemin d'appel
Traces de l'agent sans nom ni tagsrunner.run() synchrone : contexte OTel perdu dans le threadrun_async
ADK passe d'une version majeure à l'autre sans prévenirrésolution de dépendances libreversions épinglées, test qui vérifie la version d'ADK
Sessions géantes de plusieurs jourssession_id réutilisé (identifiant du conseiller au lieu de l'appel)une session = une conversation ; l'identité va dans user_id
Identifiants personnels dans les filtresuser_id = e-mail ou nomidentifiant interne pseudonyme (cons-0412), table de correspondance hors Langfuse

La règle de décision

Pour un framework d'agents, utilisez l'instrumentation que documente l'éditeur de l'outil d'observabilité (ici OpenInference pour ADK) plutôt que des spans écrits à la main : la structure agent / outil / appel est déjà là. Ajoutez par-dessus, avec propagate_attributes, ce que le framework ne connaît pas : nom métier de la trace, tags, version de l'application. Surveillez le nombre d'appels au modèle par tour : une hausse signale une boucle d'outils.

Pratique

Une trace de l'agent montre six appels call_llm et cinq appels rechercher_procedure avec la même question pour un seul tour. Que vérifiez-vous en premier ?

Le DPO d'Arvea demande que l'on puisse supprimer toutes les traces d'un conseiller qui quitte l'entreprise. Quelle convention adoptez-vous dès maintenant ?

Exercice. Ajoutez la version de l'application (ASSISTANT_VERSION, par exemple 2026.10.2) à toutes les traces de l'agent et de l'application directe, pour comparer les coûts avant et après un déploiement.

Voir la solution

propagate_attributes accepte un paramètre version ; il suffit de l'ajouter aux deux blocs existants :

import os
VERSION = os.environ.get("ASSISTANT_VERSION", "dev")

with propagate_attributes(trace_name="agent-conseiller", tags=["agent-adk"],
                          user_id=user_id, session_id=session_id, version=VERSION):
    ...

Autre option : Langfuse(release=VERSION) à la création du client, qui pose la valeur sur toutes les observations. Dans l'interface, filtrez ensuite par version et comparez coût moyen et latence par trace.

  • Je sais instrumenter un agent ADK et lire son arbre agent / outils / appels.
  • Je sais vérifier qu'une affirmation de l'agent vient d'un outil, ou non.
  • Je sais diagnostiquer un conflit de versions OpenTelemetry avant qu'il ne rétrograde un framework.
  • Je sais choisir user_id et session_id compatibles avec le RGPD.
Suivant