Idée en 30 secondes
L'agent tracing montre le chemin complet d'exécution d'un run.
Un trace est composé de spans (spans) : chaque span est une étape distincte, par exemple reasoning, tool call ou génération LLM.
Cela donne une visibilité au niveau des étapes et simplifie fortement le debugging en production.
Problème principal
Dans beaucoup de systèmes, on logge seulement le début et la fin d'un run.
Pour les agents, ce n'est pas suffisant : entre le démarrage et la réponse finale, il peut y avoir des dizaines d'étapes. Sans tracing, il est difficile de comprendre ce que l'agent a réellement fait et à quelle étape le problème est apparu.
Une même requête peut suivre des chemins différents : nombre d'étapes différent, outils différents, latency différente.
Sans tracing, même des questions de base sont difficiles :
- Quelle étape a été la plus lente ?
- Pourquoi l'agent a-t-il rappelé un outil ?
- Où exactement l'erreur est-elle apparue ?
- Pourquoi les tokens ont-ils augmenté dans ce run précis ?
C'est pour cela que le tracing est important : il montre tout le chemin d'exécution du run, pas seulement le résultat final.
Comment ça fonctionne
Le tracing repose sur deux entités de base :
trace— le chemin complet d'un runspan— une étape à l'intérieur de ce trace
En pratique, un step runtime correspond souvent à un span, mais pas toujours.
Une étape complexe peut contenir des spans imbriqués, par exemple un appel d'outil qui exécute plusieurs requêtes HTTP ou accès base de données.
Chaque span contient généralement des champs de base :
trace_idetrun_idpour la corrélationspan_id(etparent_span_idsi nécessaire)step_type(reasoning,tool_call,llm_generate)latency_msetstatus(ok/error)
Cette structure (trace_id, span_id) s'appuie sur le standard OpenTelemetry (OTel), base de la plupart des systèmes de monitoring modernes.
Le champ parent_span_id fait partie du modèle OTel des spans hiérarchiques, qui permet de construire l'arbre d'exécution (trace tree).
Il existe des outils spécialisés pour le tracing d'agents (par exemple LangSmith, Langfuse, Arize Phoenix), mais ces principes restent identiques quelle que soit la plateforme.
À quoi ressemble le trace d'un run
Le plus simple pour comprendre le tracing est un exemple concret de requête.
Dans les systèmes réels, chaque span-event contient trace_id, span_id et souvent parent_span_id.
Dans l'exemple ci-dessous, ces champs sont réduits pour la lisibilité.
trace_id: tr_9fd2
run_id: run_9fd2
user_query: "Find recent research about battery recycling"
span 1 llm_reasoning 320ms status=ok
span 2 tool_call: search 410ms status=ok
span 3 llm_reasoning 180ms status=ok
span 4 tool_call: fetch 260ms status=error
stop_reason: tool_error
Un tel trace montre immédiatement :
- quelles étapes l'agent a exécutées ;
- quels outils ont été appelés ;
- combien de temps chaque étape a pris ;
- où exactement la latence ou l'erreur est apparue.
Les traces servent aussi au-delà du debugging. Ils sont importants pour les evaluations et la validation automatique des étapes intermédiaires : sans traces, il est difficile de vérifier si l'agent a bien agi, et pas seulement s'il a donné la bonne réponse finale.
Quand l'utiliser
Le tracing n'est pas toujours nécessaire.
Pour un scénario simple — un seul appel LLM sans tools et sans boucle d'exécution — un logging de base suffit souvent.
Mais si un run contient plusieurs étapes, des appels d'outils ou des itérations répétées, sans tracing il devient difficile de :
- debugger le comportement de l'agent ;
- contrôler la latency et les coûts ;
- expliquer pourquoi le système a pris une décision donnée.
Exemple d'implémentation
Ci-dessous, un exemple simplifié d'instrumentation runtime pour trace et spans. Cette approche est utilisée dans LangGraph, CrewAI et des runtimes agentiques custom. Dans cet exemple, tout le run est aussi représenté comme un root span, et les étapes de l'agent sont loggées comme spans enfants.
import contextvars
import logging
import time
import uuid
logger = logging.getLogger("agent")
trace_id_ctx = contextvars.ContextVar("trace_id", default=None)
def start_span(run_id, step_type, tool=None, parent_span_id=None):
span_id = str(uuid.uuid4())
started_at = time.time()
logger.info(
"span_started",
extra={
"trace_id": trace_id_ctx.get(),
"run_id": run_id,
"span_id": span_id,
"parent_span_id": parent_span_id,
"step_type": step_type,
"tool": tool,
},
)
return span_id, started_at
def finish_span(
run_id,
span_id,
step_type,
started_at,
status,
tool=None,
parent_span_id=None,
error=None,
):
logger.info(
"span_finished",
extra={
"trace_id": trace_id_ctx.get(),
"run_id": run_id,
"span_id": span_id,
"parent_span_id": parent_span_id,
"step_type": step_type,
"tool": tool,
"status": status,
"latency_ms": int((time.time() - started_at) * 1000),
"error": error,
},
)
def run_agent(agent, task):
trace_id = str(uuid.uuid4())
run_id = str(uuid.uuid4()) # dans les systèmes multi-agents, un trace_id peut contenir plusieurs run_id
token = trace_id_ctx.set(trace_id)
logger.info("trace_started", extra={"trace_id": trace_id, "run_id": run_id, "task": task})
stop_reason = "max_steps"
step_count = 0
root_span_id, root_started_at = start_span(run_id, "run", parent_span_id=None)
try:
# dans cet exemple, toutes les étapes sont enfants du root span (sans imbrication profonde)
for step in agent.iter(task): # step: reasoning ou tool execution
step_count += 1
step_type = step.type # reasoning | tool_call | llm_generate
tool_name = getattr(step, "tool_name", None)
span_id, started_at = start_span(
run_id,
step_type,
tool=tool_name,
parent_span_id=root_span_id,
)
try:
result = step.execute()
finish_span(
run_id,
span_id,
step_type,
started_at,
status="ok",
tool=tool_name,
parent_span_id=root_span_id,
)
except Exception as error:
finish_span(
run_id,
span_id,
step_type,
started_at,
status="error",
tool=tool_name,
parent_span_id=root_span_id,
error=str(error),
)
stop_reason = "tool_error"
raise
if result.is_final:
stop_reason = "completed"
break
finally:
if stop_reason == "completed":
root_status = "ok"
elif stop_reason == "max_steps":
root_status = "error" # simplifié ici
else:
root_status = "error"
finish_span(
run_id,
root_span_id,
"run",
root_started_at,
status=root_status,
error=None if root_status == "ok" else stop_reason,
)
logger.info(
"trace_finished",
extra={
"trace_id": trace_id,
"run_id": run_id,
"steps": step_count,
"stop_reason": stop_reason,
},
)
trace_id_ctx.reset(token)
Dans les systèmes réels, trace_id et run_id doivent être propagés dans toute la chaîne d'appels.
En Python, on utilise souvent contextvars pour éviter de passer l'identifiant à chaque fonction.
Par exemple, un span en log structuré peut ressembler à ceci :
{
"timestamp": "2026-03-21T15:17:00Z",
"event": "span_finished",
"trace_id": "tr_9fd2",
"run_id": "run_9fd2",
"span_id": "sp_21ab",
"parent_span_id": "sp_root_01",
"step_type": "tool_call",
"tool": "search_docs",
"latency_ms": 410,
"status": "ok"
}
Erreurs typiques
Même avec du tracing en place, les systèmes restent souvent difficiles à diagnostiquer à cause des erreurs typiques ci-dessous.
Trace uniquement au niveau run, sans spans
Si seuls le début et la fin du run sont loggés, le tracing perd presque toute sa valeur : les étapes intermédiaires sont invisibles, et il devient presque impossible de localiser une latence ou une erreur.
trace_id absent sur une partie des événements
Quand une partie des logs n'a pas trace_id ou run_id, les événements ne se recollent pas en une seule chaîne.
Le debugging prend alors beaucoup plus de temps, même pour des incidents simples.
Les appels d'outils ne sont pas tracés
Les tools sont souvent la partie la plus lente du run. Si les tool calls n'entrent pas dans le trace, il est difficile de trouver la cause des retards et répétitions. En production, cela peut masquer échec d'outil ou spam d'outils.
Absence de stop_reason et de statut de span
Sans stop_reason et status, il est difficile de savoir si le run s'est terminé correctement ou s'est arrêté à cause d'une limite ou d'une erreur.
Résultat : plus difficile de reproduire un incident et de régler les alertes.
Auto-vérification
Ci-dessous, une checklist courte de tracing de base avant release.
Progression: 0/9
⚠ L'observability de base manque
Le système sera difficile à déboguer en production. Commencez par run_id, structured logs et tracing des tool calls.
FAQ
Q : Quelle différence entre un trace et des logs classiques ?
R : Les logs répondent à « que s'est-il passé ». Un trace montre la séquence des étapes d'un run et aide à comprendre « comment exactement cela s'est passé ».
Q : Que faut-il implémenter en premier pour le tracing d'agent ?
R : Minimum : trace_id, run_id, span_id, type d'étape, latency, status et stop_reason. C'est déjà suffisant pour un debugging de base.
Q : Faut-il connecter un outil de tracing externe immédiatement ?
R : Non. Vous pouvez démarrer avec votre instrumentation et des logs JSON. Les plateformes externes deviennent particulièrement utiles quand le volume de runs et le nombre d'équipes augmentent.
Q : Quand le tracing complet peut-il être excessif ?
R : Pour des scénarios single-shot simples sans tools et sans boucle d'exécution, un logging de base suffit souvent. Le tracing complet devient surtout utile quand un run contient plusieurs étapes, des outils externes ou des itérations répétées.
Pages liées
Suite du sujet :
- Observability pour agents IA — modèle de base de traces, logs et metrics.
- Tracing distribué d'agents — comment relier les traces entre plusieurs services.
- Debugging des runs d'agent — comment analyser un run problématique étape par étape.
- Logging d'agent — quels événements enregistrer dans les logs.
- Métriques d'agents — quels indicateurs suivre en production.