Lab 050 : Observabilité multi-agents avec les conventions sémantiques GenAI¶
Ce que vous apprendrez¶
- Appliquer les conventions sémantiques GenAI aux systèmes multi-agents : spans d'agent, spans de modèle, spans d'outil
- Tracer les transferts entre agents, les décisions de routage et les patterns de relance
- Distinguer les span kinds
INTERNAL(logique de l'agent) vsCLIENT(appels LLM/outils) - Analyser les scores de qualité, les coûts en tokens et la latence dans un pipeline multi-agents
- Construire des métriques de tableau de bord d'observabilité à partir de données de spans brutes
- Comprendre comment les conventions standardisent la télémétrie entre Foundry, Semantic Kernel, LangChain, AutoGen
Prérequis
Complétez d'abord le Lab 049 : Foundry IQ — Traçage des agents. Ce lab suppose une familiarité avec les spans OpenTelemetry, les attributs et les conventions GenAI.
Introduction¶
Le traçage d'un seul agent est difficile. Le traçage multi-agents est exponentiellement plus complexe. Quand un Router transfère à un Spécialiste, qui appelle des outils, qui transmet les résultats à un Réviseur — vous avez besoin d'un moyen standard pour capturer chaque étape afin de pouvoir reconstituer le flux d'exécution complet.
Les conventions sémantiques GenAI d'OpenTelemetry résolvent ce problème avec trois types de spans :
| Type de span | Kind | Attributs clés | Exemple |
|---|---|---|---|
| Span d'agent | INTERNAL |
gen_ai.agent.name, gen_ai.agent.id |
Router, ProductSpec, Reviewer |
| Span de modèle | CLIENT |
gen_ai.request.model, gen_ai.usage.*_tokens |
chat gpt-4o |
| Span d'outil | CLIENT |
gen_ai.tool.name |
search_products |
Le scénario¶
OutdoorGear Inc. a migré vers un système multi-agents avec 4 agents spécialistes orchestrés par un Router :
- Router Agent — classifie les requêtes entrantes et les dispatche au bon spécialiste
- Product Specialist — gère la recherche de produits et les recommandations
- Order Specialist — traite les statuts de commandes et les requêtes d'expédition
- Support Specialist — gère les plaintes et les sujets sensibles
- Reviewer Agent — vérifie chaque réponse pour la qualité et la conformité aux politiques
Vous disposez de 5 traces complexes avec 46 spans montrant le pipeline complet des agents, y compris une trace avec un échec de revue et une relance.
Prérequis¶
| Prérequis | Pourquoi |
|---|---|
| Python 3.10+ | Exécuter les scripts d'analyse |
pandas |
Analyser les données de spans |
| Lab 049 complété | Compréhension des bases d'OpenTelemetry |
Démarrage rapide avec GitHub Codespaces
Toutes les dépendances sont préinstallées dans le devcontainer.
📦 Fichiers d'accompagnement¶
Téléchargez ces fichiers avant de commencer le lab
Enregistrez tous les fichiers dans un dossier lab-050/ dans votre répertoire de travail.
| Fichier | Description | Télécharger |
|---|---|---|
broken_conventions.py |
Exercice de correction de bugs (3 bugs + auto-tests) | 📥 Télécharger |
dashboard_builder.py |
Script de démarrage avec TODOs | 📥 Télécharger |
multi_agent_spans.csv |
Jeu de données | 📥 Télécharger |
Étape 1 : Comprendre la structure des traces multi-agents¶
Dans un système multi-agents, la trace forme un arbre :
root: router_agent (INTERNAL)
├── classify_query (CLIENT, gpt-4o-mini)
├── product_specialist (INTERNAL)
│ ├── search_reasoning (CLIENT, gpt-4o)
│ ├── search_products (CLIENT, tool)
│ └── format_response (CLIENT, gpt-4o)
└── reviewer (INTERNAL)
└── quality_check (CLIENT, gpt-4o-mini)
Conventions clés :
- Les spans d'agent sont
INTERNAL— ils représentent la logique propre et l'orchestration de l'agent - Les appels LLM sont
CLIENT— requêtes sortantes vers les points de terminaison de modèles - Les appels d'outils sont
CLIENT— requêtes sortantes vers les outils/API - Les relations parent-enfant montrent la chaîne de transfert
gen_ai.agent.nameest défini UNIQUEMENT sur les spans d'agent, pas sur les spans LLM/outils
Pourquoi INTERNAL pour les agents ?
La prise de décision d'un agent se fait localement (routage, planification, récupération en mémoire). Elle ne franchit pas de frontière réseau — donc c'est INTERNAL. L'appel LLM que l'agent effectue est CLIENT car il passe par le réseau vers une API.
Étape 2 : Charger et explorer les données de traces¶
Le jeu de données contient 46 spans répartis dans 5 traces :
import pandas as pd
spans = pd.read_csv("lab-050/multi_agent_spans.csv")
print(f"Total spans: {len(spans)}")
print(f"Traces: {spans['trace_id'].nunique()}")
print(f"\nSpans per trace:")
print(spans.groupby("trace_id")["span_id"].count())
Attendu :
| Trace | Spans | Scénario |
|---|---|---|
| A001 | 8 | Recherche de produit (simple) |
| A002 | 10 | Requête de commande complexe |
| A003 | 9 | Traitement de plainte |
| A004 | 5 | FAQ (pas de réviseur) |
| A005 | 14 | Remboursement avec échec de revue + relance |
Étape 3 : Analyse des spans d'agent¶
Extrayez et analysez les spans d'agent :
agent_spans = spans[(spans["kind"] == "INTERNAL") & (spans["agent_name"].notna())]
print(f"Total agent spans: {len(agent_spans)}")
print(f"Unique agents: {sorted(agent_spans['agent_name'].unique())}")
print(f"\nSpans per agent:")
print(agent_spans["agent_name"].value_counts().sort_index())
Attendu :
Total agent spans: 16
Unique agents: ['FAQSpec', 'OrderSpec', 'ProductSpec', 'RefundSpec', 'Reviewer', 'Router', 'SupportSpec']
Reviewer 5
Router 5
RefundSpec 2
...
Observation
Router apparaît dans les 5 traces — c'est le point d'entrée. Reviewer apparaît dans 4 traces (pas A004, la FAQ simple). RefundSpec apparaît deux fois dans la trace A005 car la première tentative a échoué à la revue et a été relancée.
Étape 4 : Analyse de l'utilisation des tokens LLM¶
Analysez la consommation de tokens dans tous les appels de modèle :
llm_spans = spans[spans["model"].notna()]
print(f"Total LLM calls: {len(llm_spans)}")
by_model = llm_spans.groupby("model").agg(
calls=("span_id", "count"),
total_input=("input_tokens", "sum"),
total_output=("output_tokens", "sum"),
).reset_index()
by_model["total_tokens"] = by_model["total_input"] + by_model["total_output"]
print(by_model.to_string(index=False))
total_tokens = int(llm_spans["input_tokens"].sum() + llm_spans["output_tokens"].sum())
print(f"\nGrand total: {total_tokens:,} tokens")
Attendu :
| Modèle | Appels | Entrée | Sortie | Total |
|---|---|---|---|---|
| gpt-4o | 12 | 3 830 | 1 890 | 5 720 |
| gpt-4o-mini | 10 | 1 045 | 177 | 1 222 |
| Total | 22 | 4 875 | 2 067 | 6 942 |
Observation sur les coûts
gpt-4o gère le raisonnement complexe (82 % des tokens) tandis que gpt-4o-mini effectue la classification légère et les vérifications de qualité (18 %). C'est un pattern économique — n'utilisez les modèles coûteux que pour le raisonnement complexe.
Étape 5 : Analyse des appels d'outils¶
tool_spans = spans[spans["tool_name"].notna()]
print(f"Total tool calls: {len(tool_spans)}")
print(f"\nTools used:")
print(tool_spans["tool_name"].value_counts())
trace_tools = tool_spans.groupby("trace_id").size()
print(f"\nTrace with most tool calls: {trace_tools.idxmax()} ({trace_tools.max()} calls)")
Attendu :
Total tool calls: 8
search_products 1
get_order_status 1
get_shipping_info 1
calculate_eta 1
get_customer_history 1
search_faq 1
get_order_details 1
check_refund_policy 1
Trace with most tool calls: A002 (3 calls)
Étape 6 : Analyse des scores de qualité¶
Les agents réviseurs attribuent des scores de qualité. Analysez-les :
quality_spans = spans[spans["quality_score"].notna()]
print(f"Quality assessments: {len(quality_spans)}")
print(f"Average quality: {quality_spans['quality_score'].mean():.3f}")
print(f"Min quality: {quality_spans['quality_score'].min():.2f}")
print(f"Max quality: {quality_spans['quality_score'].max():.2f}")
# Traces that fell below the quality threshold
below_threshold = quality_spans[quality_spans["quality_score"] < 0.8]
print(f"\nTraces below 0.8 threshold: {below_threshold['trace_id'].unique().tolist()}")
Attendu :
Quality assessments: 5
Average quality: 0.790
Min quality: 0.45
Max quality: 0.95
Traces below 0.8 threshold: ['A003', 'A005']
Investigation de la revue échouée (Trace A005)¶
a005 = spans[spans["trace_id"] == "A005"].sort_values("span_id")
print(a005[["span_id", "span_name", "agent_name", "kind", "quality_score", "status"]]
.to_string(index=False))
Cela montre le pattern de relance : la première vérification du réviseur (s40) a obtenu un score de 0,45 avec le statut ERROR. Le Refund Specialist a été ré-invoqué (s42), a produit une réponse révisée, et la deuxième vérification du réviseur (s45) est passée à 0,85.
Étape 7 : Construire les métriques du tableau de bord¶
Combinez tout en un résumé de tableau de bord :
# Overall metrics
total_traces = spans["trace_id"].nunique()
total_spans = len(spans)
total_agent_spans = len(agent_spans)
total_llm_calls = len(llm_spans)
total_tools = len(tool_spans)
error_spans = spans[spans["status"] == "ERROR"]
avg_quality = quality_spans["quality_score"].mean()
dashboard = f"""
╔══════════════════════════════════════════════════╗
║ Multi-Agent Observability Dashboard ║
╠══════════════════════════════════════════════════╣
║ Traces: {total_traces:>5} ║
║ Total Spans: {total_spans:>5} ║
║ Agent Spans: {total_agent_spans:>5} (INTERNAL) ║
║ LLM Calls: {total_llm_calls:>5} (CLIENT) ║
║ Tool Calls: {total_tools:>5} (CLIENT) ║
║ Error Spans: {len(error_spans):>5} ║
║ Total Tokens: {total_tokens:>5,} ║
║ Avg Quality: {avg_quality:>5.3f} ║
║ Below Threshold: {len(below_threshold):>5} traces ║
╚══════════════════════════════════════════════════╝
"""
print(dashboard)
🐛 Exercice de correction de bugs¶
Le fichier lab-050/broken_conventions.py contient 3 bugs dans la façon dont il interprète les conventions sémantiques GenAI :
| Test | Ce qu'il vérifie | Indice |
|---|---|---|
| Test 1 | Les noms d'agents proviennent de agent_name, pas de span_name |
Quelle colonne contient l'identité de l'agent ? |
| Test 2 | Les spans d'agent doivent être de kind INTERNAL ET avoir un agent_name |
Ne comptez pas les spans LLM/outils |
| Test 3 | Total de tokens = entrée + sortie | N'oubliez pas les output_tokens |
🧠 Quiz de connaissances¶
Q1 (Choix multiple) : Dans les conventions sémantiques GenAI, quel span kind doit être utilisé pour la logique interne de routage/planification d'un agent ?
- A) CLIENT — parce que l'agent est un client du LLM
- B) SERVER — parce que l'agent sert les requêtes utilisateur
- C) INTERNAL — parce que le routage se fait localement, pas via le réseau
- D) PRODUCER — parce que l'agent produit des réponses
✅ Révéler la réponse
Correct : C) INTERNAL
La prise de décision de l'agent (routage, planification, récupération en mémoire) se fait au sein du processus — elle ne franchit pas de frontière réseau. CLIENT est utilisé pour les appels sortants vers les LLM et les outils. La convention est : logique de l'agent = INTERNAL, appels externes = CLIENT.
Q2 (Choix multiple) : Pourquoi la trace A005 a-t-elle 14 spans alors que A001 n'en a que 8 ?
- A) A005 utilise un modèle plus grand
- B) A005 a eu un échec de revue de qualité et a nécessité une boucle de relance
- C) A005 a plus de tokens d'entrée utilisateur
- D) A005 utilise un algorithme de routage différent
✅ Révéler la réponse
Correct : B) A005 a eu un échec de revue de qualité et a nécessité une boucle de relance
Le Reviewer a attribué à la première réponse d'A005 un score de 0,45 (ERROR). Le système a ré-invoqué le Refund Specialist pour réviser la réponse, puis le Reviewer a vérifié à nouveau (score : 0,85, OK). Cette relance a ajouté des spans supplémentaires : deuxième spécialiste (2 appels LLM) + deuxième réviseur (1 appel LLM) = 5 spans supplémentaires.
Q3 (Exécutez le lab) : Combien y a-t-il de spans d'agent au total (kind=INTERNAL avec un agent_name) dans les 5 traces ?
Filtrez le DataFrame des spans pour kind == "INTERNAL" et agent_name non nul.
✅ Révéler la réponse
16 spans d'agent
Sur les 5 traces : A001(3) + A002(3) + A003(3) + A004(2) + A005(5) = 16. A004 en a moins car il saute le Reviewer. A005 en a plus à cause de la relance (RefundSpec×2 + Reviewer×2).
Q4 (Exécutez le lab) : Quelle trace a le plus d'appels d'outils, et combien ?
Regroupez les spans d'outils par trace_id et trouvez le maximum.
✅ Révéler la réponse
Trace A002 — 3 appels d'outils
A002 (requête de commande complexe) a appelé : get_order_status, get_shipping_info et calculate_eta. C'est la trace la plus intensive en outils. A005 a 2 appels d'outils, et les autres en ont 1 chacune.
Q5 (Exécutez le lab) : Quel est le score de qualité moyen sur toutes les évaluations du réviseur ?
Filtrez les spans avec un quality_score non nul et calculez la moyenne.
✅ Révéler la réponse
0,790
Scores de qualité des spans du réviseur : A001 (0,95), A002 (0,92), A003 (0,78), A005-premier (0,45), A005-relance (0,85). A004 (FAQ) n'a pas de réviseur. Les données contiennent 5 entrées quality_score. Moyenne = (0,95 + 0,92 + 0,78 + 0,45 + 0,85) / 5 = 0,790. Deux traces (A003 et A005) sont passées sous le seuil de qualité de 0,8.
Résumé¶
| Sujet | Ce que vous avez appris |
|---|---|
| Conventions GenAI | Attributs standards : agent.name, request.model, usage.tokens |
| Span Kinds | INTERNAL (logique de l'agent) vs CLIENT (appels LLM/outils) |
| Hiérarchie des traces | Spans parent-enfant montrant les transferts entre agents |
| Patterns de relance | Les échecs de revue déclenchent des boucles de relance (visibles dans les traces) |
| Métriques de tableau de bord | Nombre d'agents, utilisation des tokens, appels d'outils, scores de qualité |
| Inter-frameworks | Les mêmes conventions fonctionnent entre Foundry, SK, LangChain, AutoGen |
Prochaines étapes¶
- Lab 033 — Observabilité des agents avec Application Insights (approche complémentaire Azure-native)
- Lab 034 — Orchestration multi-agents avec Semantic Kernel (construire les agents que ce lab trace)
- Lab 035 — Évaluation des agents avec le SDK Azure AI Eval (le scoring de qualité qui alimente le Reviewer)