Trouver l’étape lente
Tracer un appel à un modèle, c’est enregistrer ce que chaque étape d’une requête a fait et combien de temps elle a pris. C’est utile parce qu’une fonction à modèle échoue au milieu d’une chaîne. Deux lignes de journal aux extrémités ne disent pas où.
Un utilisateur se plaint que les réponses prennent dix secondes. Est-ce le prompt, le modèle, un nouvel essai ou la vérification ? Une trace répond d’un coup d’œil.
À la fin de cette leçon
Remarque
Je peux enregistrer ce que fait chaque appel, pour retrouver d’où vient une panne ou un retard.
Mots clés
| Mot | Ce que ça veut dire |
|---|---|
| Trace | l’enregistrement d’une requête, du début à la fin |
| Span | une étape chronométrée à l’intérieur d’une trace |
| Parent | le span qui en contient un autre |
| Attribut | un fait nommé, rangé sur un span |
| Temps propre | la durée d’un span moins les durées de ses enfants directs |
| Identifiant de trace | l’identifiant commun à tous les spans d’une requête |
| Expurgation | retirer les secrets avant de ranger un enregistrement |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une trace est un arbre de spans
Une trace est une requête, faite de spans. Chaque span a un nom, un début, une fin et un parent. La racine n’a pas de parent. Tous les spans d’une requête partagent un identifiant de trace et portent des attributs.
Le temps propre d’un span est sa durée moins celles de ses enfants directs. Ci-dessous, la requête dure 856 ms mais son temps propre est 0 ms. L’appel au modèle a un temps propre de 850 ms : c’est l’étape lente.
Exemple détaillé : une requête
| Span | Parent | Début | Fin | Durée |
|---|---|---|---|---|
| requete | aucun | 0 | 856 | 856 ms |
| construire le prompt | requete | 0 | 4 | 4 ms |
| appel au modele | requete | 4 | 854 | 850 ms |
| verifier la reponse | requete | 854 | 856 | 2 ms |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Ce qu’une trace ne contient jamais
Un stockage de traces est lu par beaucoup de monde et gardé des semaines. Un span ne contient donc jamais de secret : clé d’API, jeton ou mot de passe. Il ne contient jamais de donnée personnelle : adresse électronique, nom ou numéro de téléphone.
En général, il ne garde pas non plus le prompt ni la réponse. Rangez des tailles, des nombres et des résultats. Retirez les clés dangereuses à un seul endroit, avant d’enregistrer le span.
Une erreur courante
Attention
Beaucoup pensent qu’écrire le prompt entier dans le journal est sans danger, puisque le journal est interne. Or les prompts contiennent ce que les gens ont tapé, et ils tapent des noms et des secrets. Notez la longueur et le résultat, pas le texte.
É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 le traceur, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' '' 'let maintenant = 0; // une horloge que nous avançons nous-mêmes : mêmes temps pour tous' 'const spans = [];' 'function avancer(ms) { maintenant = maintenant + ms; }' 'function ouvrir(nom, parent) {' ' const span = { id: spans.length + 1, nom, parent, debut: maintenant, fin: maintenant, attributs: {} };' ' spans.push(span);' ' return span;' '}' 'function fermer(span) { span.fin = maintenant; }' > trace1.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 le traceur, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// Clés qui n’entrent jamais dans une trace : secrets et données personnelles.' 'const JAMAIS = [\"courriel\", \"cle_api\", \"prompt\", \"nom\"];' 'function noter(span, cle, valeur) {' ' span.attributs[cle] = JAMAIS.includes(cle) ? \"[retiré]\" : valeur;' '}' >> trace1.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 le traceur, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function afficher(parent, profondeur) {' ' for (const s of spans) {' ' if (s.parent === parent) {' ' console.log(\" \".repeat(profondeur) + s.nom + \" \" + (s.fin - s.debut) + \" ms \" + JSON.stringify(s.attributs));' ' afficher(s.id, profondeur + 1);' ' }' ' }' '}' >> trace1.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 le traceur, partie 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function etapeModele(racine) {' ' const appel = ouvrir(\"appel au modele\", racine.id);' ' const messages = [{ role: \"user\", content: \"Cite un usage d’une trace, en une phrase.\" }];' ' const reponse = await demander(messages, \"Une trace montre quelle étape d’une requête a été lente.\", 40);' ' avancer(850); // 850 ms fixes sur notre horloge, quel que soit le temps réel' ' noter(appel, \"resultat\", reponse.texte.length > 0 ? \"ok\" : \"vide\");' ' noter(appel, \"prompt\", messages[0].content);' ' fermer(appel);' '}' >> trace1.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 le traceur, partie 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const racine = ouvrir(\"requete\", 0);' ' noter(racine, \"route\", \"/question\");' ' noter(racine, \"courriel\", \"[email protected]\");' ' const construire = ouvrir(\"construire le prompt\", racine.id);' ' avancer(4); fermer(construire);' ' await etapeModele(racine);' ' const verifier = ouvrir(\"verifier la reponse\", racine.id);' ' avancer(2); fermer(verifier); fermer(racine);' ' console.log(\"trace t-0001\");' ' afficher(0, 0);' '}' 'main();' >> trace1.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 afficher la trace
docker exec -w /home/node/project agent node trace1.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.
Ceci est une maquette
Le labo n’a pas de service de traçage. Les spans sont donc dans une liste, et l’horloge est un nombre que le script avance. Un vrai système envoie ses spans à un vrai service de traçage et lit la vraie horloge.
La forme reste la même : un identifiant de trace, un arbre, des attributs et une règle sur ce qui peut être rangé.
Une sortie possible
| Ligne | Ce qu’elle montre |
|---|---|
| trace t-0001 | L’identifiant de trace |
| requete 856 ms | La racine, avec la route, et le courriel retiré |
| construire le prompt 4 ms | Un enfant, décalé de deux espaces |
| appel au modele 850 ms | L’étape lente, avec le prompt retiré |
| verifier la reponse 2 ms | Le dernier enfant |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.