Pourquoi cette réponse a-t-elle pris cinq secondes ?
Tracer une fonction d’IA, c’est chronométrer chaque appel de modèle et d’outil d’une requête. Un assistant de support planifie, appelle deux outils, interroge de nouveau un modèle et contrôle sa réponse. L’utilisateur attend cinq secondes, et la facture dépasse le prévu. Quelle étape était lente, et laquelle a coûté l’argent ?
Le module 69 traçait un seul appel. Cette leçon va plus loin : une exécution d’agent entière, avec les jetons, le coût et les échecs de chaque étape.
À la fin de cette leçon
Remarque
Je peux tracer une requête à travers une fonction d’IA et lire la trace pour trouver ce qui était lent, cher ou en échec.
Mots clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| Identifiant de span | l’identifiant d’un seul span | span_id |
| Identifiant du parent | l’identifiant du span qui contient celui-ci | parent_span_id |
| Statut | si un span s’est bien terminé ou en erreur | erreur |
| Span d’outil | un span pour un outil appelé par un agent | |
| Nombre de jetons | combien de jetons sont entrés ou sortis | |
| Standard ouvert | une conception commune que chacun peut employer |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Un span porte identifiants, durées et faits
Chaque span a un identifiant de span et l’identifiant de trace de sa requête. Il nomme son parent par un identifiant du parent, vide pour la racine.
Il a un nom, un début, une fin et un statut : ok ou erreur. Les faits vont dans ses attributs. OpenTelemetry compte le temps en nanosecondes depuis 1970. Ce labo compte en millisecondes depuis le début, pour garder des nombres courts.
Exemple travaillé : le span d’un appel de modèle
| Champ | Valeur |
|---|---|
| trace_id | 7a3f0c1d00000000000000000000aa01 |
| span_id | 0000000000000005 |
| parent_span_id | 0000000000000001 |
| nom | modèle : réponse |
| debut et fin | 3100 et 4900 |
| statut | ok |
| attributs | modele m-small-2026-08, jetons_entree 2100, jetons_sortie 300, repli true |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Ce que consigne un span de modèle
Consignez le modèle et sa version, car une nouvelle version peut changer la vitesse et le coût. Consignez les jetons d’entrée et de sortie, et la durée. Notez si l’appel a été repris ou a basculé vers un autre modèle.
Ne mettez jamais de donnée personnelle en clair sur un span. Le module 86, dans la leçon « Journaliser sobrement une application d’IA », montre comment l’écarter.
Un agent ajoute un span pour chaque outil
Un agent fait plus qu’appeler un modèle. Il planifie, appelle des outils, puis interroge de nouveau le modèle. Donnez à chaque appel d’outil son propre span d’outil, enfant du span de la requête.
Un span d’outil consigne le nom de l’outil, sa durée et son statut. Un outil en échec apparaît comme une erreur dans une requête qui peut quand même bien finir.
Écrire l’assistant du modèle, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Les jetons d’une réponse, ou null si elle n’en donne pas.' 'function compter(donnees, nom) {' ' return donnees.usage && typeof donnees.usage[nom] === \"number\" ? donnees.usage[nom] : null;' '}' > model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire l’assistant du modèle, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function demander(messages, echantillon, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"statut \" + res.status);' ' const d = await res.json();' ' return { texte: d.choices[0].message.content, entree: compter(d, \"prompt_tokens\"), sortie: compter(d, \"completion_tokens\"), enregistre: false };' ' } catch (erreur) {' ' console.log(\"Échantillon enregistré : aucun modèle n’a répondu (\" + erreur.message + \").\");' ' return { texte: echantillon, entree: null, sortie: null, enregistre: true };' ' }' '}' 'module.exports = { demander };' >> model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la trace de l’agent, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' 'const TRACE = \"7a3f0c1d00000000000000000000aa01\";' 'const PRIX = { \"m-large-2026-09\": [3, 15], \"m-small-2026-08\": [1, 4] };' 'const spans = [];' 'function sid(n) { return String(n).padStart(16, \"0\"); }' 'function span(n, parent, nom, debut, fin, statut, attributs) {' ' spans.push({ trace_id: TRACE, span_id: sid(n), parent_span_id: parent ? sid(parent) : \"\",' ' nom, debut, fin, statut, attributs });' '}' > trace2.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la trace de l’agent, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'span(1, 0, \"requête de l’agent\", 0, 5000, \"ok\", {});' 'span(2, 1, \"modèle : plan\", 0, 800, \"ok\", { modele: \"m-large-2026-09\", jetons_entree: 900, jetons_sortie: 60 });' 'span(3, 1, \"outil : trouver la commande\", 800, 1100, \"ok\", { outil: \"trouver_commande\" });' 'span(4, 1, \"outil : date de livraison\", 1100, 3100, \"erreur\", { outil: \"date_livraison\" });' 'span(5, 1, \"modèle : réponse\", 3100, 4900, \"ok\", { modele: \"m-small-2026-08\", jetons_entree: 2100, jetons_sortie: 300, repli: true });' 'span(6, 1, \"contrôle de la réponse\", 4900, 5000, \"ok\", {});' 'function enfants(id) { return spans.filter(function (s) { return s.parent_span_id === id; }); }' 'function duree(s) { return s.fin - s.debut; }' >> trace2.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la trace de l’agent, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function cout(a) {' ' const p = PRIX[a.modele];' ' return p ? a.jetons_entree * p[0] + a.jetons_sortie * p[1] : 0;' '}' 'function afficher(id, niveau) {' ' for (const s of enfants(id)) {' ' const marque = s.attributs.repli ? \", repli\" : \"\";' ' console.log(\" \".repeat(niveau) + s.nom + \" \" + duree(s) + \" ms \" + s.statut + marque);' ' afficher(s.span_id, niveau + 1);' ' }' '}' >> trace2.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la trace de l’agent, partie 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' console.log(\"trace \" + TRACE);' ' afficher(\"\", 0);' ' const feuilles = spans.filter(function (s) { return enfants(s.span_id).length === 0; });' ' feuilles.sort(function (a, b) { return duree(b) - duree(a); });' ' console.log(\"étape la plus lente : \" + feuilles[0].nom + \", \" + duree(feuilles[0]) + \" ms\");' ' const payants = spans.filter(function (s) { return cout(s.attributs) > 0; });' ' payants.sort(function (a, b) { return cout(b.attributs) - cout(a.attributs); });' ' console.log(\"étape la plus chère : \" + payants[0].nom + \", \" + cout(payants[0].attributs) + \" unités\");' ' for (const s of spans.filter(function (un) { return un.statut === \"erreur\"; })) {' ' const haut = spans.find(function (un) { return un.span_id === s.parent_span_id; });' ' console.log(\"en échec : \" + s.nom + \", dans \" + (haut ? haut.nom : \"rien\"));' ' }' >> trace2.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la trace de l’agent, partie 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' const direct = { trace_id: \"7a3f0c1d00000000000000000000aa02\", span_id: sid(7), parent_span_id: \"\",' ' nom: \"modèle : contrôle en direct\", debut: 0, fin: 600, statut: \"ok\", attributs: {} };' ' const messages = [{ role: \"user\", content: \"En une phrase courte, que consigne un span ?\" }];' ' const r = await demander(messages, \"Un span consigne une étape chronométrée d’une requête.\", 40);' ' direct.attributs = { modele: \"lab\", jetons_entree: r.entree, jetons_sortie: r.sortie, enregistre: r.enregistre };' ' console.log(\"span en direct : \" + JSON.stringify(direct));' ' console.log(\"le modèle dit : \" + r.texte);' '}' 'main();' >> trace2.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Lancer et lire la trace
docker exec -w /home/node/project agent node trace2.jsAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Ce que le Run affiche
| Ligne | Ce qu’elle montre |
|---|---|
| trace 7a3f0c1d00000000000000000000aa01 | L’identifiant de trace que partagent tous les spans |
| outil : date de livraison 2000 ms erreur | Un span d’outil en échec |
| modèle : réponse 1800 ms ok, repli | Un span de modèle qui a basculé vers un autre modèle |
| étape la plus lente : outil : date de livraison, 2000 ms | L’étape sans enfant la plus longue |
| étape la plus chère : modèle : plan, 3600 unités | Les jetons fois le prix de son modèle |
| span en direct : {...} | Un appel réel en span. Ses jetons valent null quand un échantillon enregistré répond |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Lire une trace de trois façons
Pour la vitesse, cherchez l’étape sans enfant qui a duré le plus. Pour le coût, multipliez les jetons de chaque span de modèle par les prix de son modèle.
Pour les échecs, listez chaque span au statut erreur et le span qui le contient. Un enfant en échec dans un span qui finit bien veut dire que l’agent s’est rattrapé.
Une erreur courante
Attention
Beaucoup pensent que l’étape la plus lente est aussi la plus chère. Ici, l’outil de date de livraison est le plus lent, mais l’appel de plan coûte le plus. L’attente fait le temps, les jetons font le coût : vérifiez les deux avant de changer quoi que ce soit.
OpenTelemetry, un standard ouvert pour les traces
OpenTelemetry est un standard ouvert pour les traces, les métriques et les journaux. Beaucoup d’outils lisent sa forme de span : un même enregistrement sert à plusieurs stockages.
Il propose aussi des noms provisoires pour les attributs des appels d’IA. Ces noms changent encore : prenez ceux du tableau comme un exemple. Vérifiez la documentation actuelle avant d’en employer un.
Noms provisoires, à vérifier dans la documentation
| Notre clé | Un nom provisoire | Ce qu’il contient |
|---|---|---|
| modele | gen_ai.request.model | Le modèle demandé |
| jetons_entree | gen_ai.usage.input_tokens | Les jetons envoyés |
| jetons_sortie | gen_ai.usage.output_tokens | Les jetons reçus |
| outil | gen_ai.tool.name | L’outil appelé |
| aucune ici | gen_ai.operation.name | Le genre d’appel, par exemple chat |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.