Il y a quelques semaines, mon agent de recherche a commencé à coûter quatre fois plus que la semaine précédente. Même fonctionnalité, même trafic, même modèle. Le résultat semblait correct. Pas la facture.
J'ai passé un après-midi à ajouter des instructions print avant d'admettre l'évidence : je n'avais aucune idée de ce que l'agent faisait réellement lors d'une exécution donnée. Il effectuait un nombre variable d'appels d'outils, réessayait silencieusement sur des échecs que je n'avais jamais journalisés, et tombait parfois dans une boucle où il relisait le même document trois fois avant d'abandonner. Rien de tout cela n'apparaissait nulle part, sauf sur la facture.
Le coupable s'est avéré être un chemin de relance. Un outil instable expiration, l'agent relançait silencieusement avec le contexte complet renvoyé à chaque fois, et une exécution « réussie » effectuait neuf appels au modèle au lieu de deux. Je ne l'ai trouvé que parce que j'ai finalement consigné ce que faisait chaque étape. Cet après-midi m'a coûté plus qu'un mois de n'importe quel outil de traçage.
En résumé : un agent est une boucle que vous ne contrôlez pas entièrement, et vous ne pouvez pas améliorer une boucle que vous ne pouvez pas observer. Vous devez capturer, par exécution, ce qui s'est passé à chaque étape, combien de tokens cela a consommé, ce que ça a coûté, combien de temps ça a pris, et si ça a échoué. Vous n'avez pas besoin d'une plateforme pour commencer. Vous avez besoin d'un log.
Ce qu'il faut réellement capturer
Oubliez l'outillage un instant. La question est : quand une exécution tourne mal, que voudriez-vous savoir ? Remontez à partir de là et vous obtenez la liste.
Pour chaque exécution d'agent, je veux une trace — l'exécution complète — composée de spans, un par étape. Chaque span doit contenir :
- Le type d'étape — un appel au modèle, un appel d'outil, une récupération, l'invocation de l'agent de niveau supérieur.
- L'utilisation des tokens — tokens en entrée et tokens en sortie, séparément. Ils coûtent des montants différents et révèlent des choses différentes.
- Le coût — dérivé des tokens et du prix du modèle. Calculez-le une fois, stockez-le dans le span.
- La latence — heure de début et de fin. Les exécutions lentes et les exécutions coûteuses sont généralement des problèmes distincts.
- Les appels d'outils — quel outil, quels arguments, ce qu'il a retourné (ou comment il a échoué).
- Le résultat — l'étape a-t-elle réussi, échoué ou été relancée, et pourquoi.
- Le score Eval — si vous notez l'exécution (vous devriez), attachez le score pour pouvoir corréler qualité et coût.
Ce dernier point est celui que les gens sautent, et c'est celui qui rapporte le plus. Une trace vous dit ce qui s'est passé ; un score eval vous dit si c'était important. Les relier vous permet de poser la seule question qui compte : la version coûteuse est-elle vraiment meilleure ? Souvent, non.
Notez ce qui ne figure pas dans la liste : le texte complet du prompt et de la réponse sur chaque span par défaut. Capturer le contenu des messages est utile pour le débogage et risqué pour la vie privée et le coût de stockage. Je le capture derrière un flag que je peux activer pour une exécution spécifique, pas en mode toujours actif pour tout.
Les conventions GenAI d'OpenTelemetry, comme référence de base
Avant d'inventer vos propres noms de champs, regardez ce qui existe déjà. OpenTelemetry — le même standard neutre vis-à-vis des fournisseurs que vous utiliseriez pour tout traçage de backend — possède un ensemble de conventions sémantiques GenAI : un vocabulaire convenu pour nommer les éléments que je viens de lister.
La partie utile concerne les noms d'attributs. Un span d'appel au modèle obtient gen_ai.request.model (quel modèle), gen_ai.usage.input_tokens et gen_ai.usage.output_tokens (la répartition des tokens), gen_ai.response.finish_reasons (pourquoi il s'est arrêté — stop, tool_calls, etc.), et gen_ai.provider.name pour marquer le fournisseur. Il y a même des attributs pour la comptabilité du cache de prompts — gen_ai.usage.cache_read.input_tokens et gen_ai.usage.cache_creation.input_tokens — qui importent beaucoup une fois que le cache est en jeu, car l'entrée mise en cache peut être un ordre de grandeur moins chère. Les noms de spans suivent le format {gen_ai.operation.name} {gen_ai.request.model}, et les conventions au niveau de l'agent définissent des opérations comme invoke_agent (l'exécution complète), chat (un seul appel au modèle) et execute_tool (une invocation d'outil). (OTel GenAI spans, OTel GenAI observability blog)
Deux mises en garde honnêtes. Premièrement, à mi-2026 ces conventions sont encore marquées en statut Development — elles se stabilisent mais ne sont pas figées, donc les noms d'attributs peuvent encore changer. Elles ont récemment migré vers leur propre dépôt, ce qui indique qu'elles sont prises au sérieux et qu'elles sont encore en mouvement. Deuxièmement, vous n'avez pas à adopter la machinerie OTel pour en bénéficier. Même si vous écrivez un log JSON brut, utilisez ces noms de champs. Le jour où vous dépassez votre log maison et migrez vers un vrai backend, vos données parlent déjà la langue. C'est la compatibilité ascendante la moins chère que vous achèterez jamais.
Le log JSONL maison qui vous donne 80% du résultat
Voici la configuration que j'utilise réellement pour les petits projets. Un fichier JSONL en ajout seul, un objet par span, noms de champs de style OTel. Pas de service, pas de SDK, pas de compte.
import json, time, uuid
PRICES = { # USD per token; see your provider's pricing page
"claude-haiku-4.5": {"in": 1.00/1e6, "out": 5.00/1e6},
"claude-sonnet-4.6": {"in": 3.00/1e6, "out": 15.00/1e6},
}
def log_span(trace_id, kind, model, usage, t0, status, **extra):
p = PRICES.get(model, {"in": 0, "out": 0})
cost = usage["in"] * p["in"] + usage["out"] * p["out"]
record = {
"trace_id": trace_id,
"span_id": uuid.uuid4().hex[:12],
"gen_ai.operation.name": kind, # invoke_agent | chat | execute_tool
"gen_ai.request.model": model,
"gen_ai.usage.input_tokens": usage["in"],
"gen_ai.usage.output_tokens": usage["out"],
"cost_usd": round(cost, 6),
"latency_ms": int((time.time() - t0) * 1000),
"status": status, # ok | error | retry
**extra, # feature, tool_name, eval_score...
}
with open("traces.jsonl", "a") as f:
f.write(json.dumps(record) + "\n")
Chaque étape de votre boucle d'agent appelle log_span avec un trace_id partagé. Taguez chaque exécution avec la fonctionnalité qu'elle a servi (feature="research", feature="summarize"). Vous pouvez maintenant répondre à de vraies questions avec une commande sur le fichier :
# cost per feature, last run-set
jq -s 'group_by(.feature)[] | {feature: .[0].feature,
usd: (map(.cost_usd) | add)}' traces.jsonl
C'est tout. Coût par exécution, attribution par fonctionnalité, latence, compteurs de relances, et — si vous écrivez votre score eval dans le span — qualité versus coût, tout depuis un fichier que vous pouvez lire avec jq, grep, ou DuckDB. La boucle de relance qui a quadruplé ma facture serait apparue ici comme des lignes status: "retry" empilées sous un même trace_id, trois minutes à regarder au lieu d'un après-midi à tâtonner.
La table de prix est la seule chose que vous devez maintenir honnête. Les tokens de sortie coûtent actuellement environ 5 fois les tokens d'entrée dans les niveaux actuels de Claude, et les tarifs de référence que j'ai utilisés étaient de $1/$5 par million pour Haiku 4.5 et $3/$15 pour Sonnet 4.6 lorsque je les ai vérifiés — mais les tarifs changent et les remises de cache/lot font beaucoup varier le chiffre réel, donc traitez toute table comme un instantané et consultez la page de votre fournisseur. (Claude API pricing breakdown)
Attribution des coûts de tokens : la question qui paie vraiment
C'est là que la discipline prouve sa valeur. Une fois que chaque span porte un coût et un tag de fonctionnalité, vous pouvez arrêter de deviner votre économie unitaire.
J'ai exécuté cela sur mon propre projet et j'ai découvert qu'une fonctionnalité « gratuite » — un résumé automatique déclenché à chaque téléversement de document — représentait 60% de mes dépenses en modèle, utilisée par peut-être un dixième de mes utilisateurs. Ce n'était pas un bug. C'était une décision produit que j'avais prise à l'aveugle. Voir le coût par fonctionnalité en a fait une décision que je pouvais prendre intentionnellement : je l'ai rendue opt-in, et la facture a diminué de moitié sans plaintes.
C'est tout le sens de l'attribution. Le total des dépenses est un chiffre sur lequel vous ne pouvez pas agir. « La fonctionnalité X coûte 0,40 $ par exécution et la fonctionnalité Y coûte 0,02 $ » est un chiffre sur lequel vous pouvez. Et quand vous le combinez avec des scores eval, vous obtenez la version la plus tranchante : coût par bonne exécution. Si le chemin coûteux obtient le même score que le pas cher, le chemin coûteux n'est qu'une fuite avec une belle description.
Construire versus acheter : quand le log JSONL ne suffit plus
Le log maison est vraiment suffisant pendant longtemps. Il ne l'est plus quand vous voulez des choses qu'un fichier ne peut pas fournir facilement : une interface pour naviguer dans une trace complexe et désordonnée, une instrumentation automatique pour ne pas appeler log_span à la main partout, des exécutions eval intégrées à la CI, ou partager des traces avec quelqu'un qui ne vit pas dans un terminal.
À ce stade, les outils hébergés méritent un coup d'œil, et le résumé honnête est qu'ils se regroupent selon leur point fort, pas selon lequel est « le meilleur » :
- Langfuse capture le prompt, la réponse, l'utilisation des tokens, la latence et le coût par trace, est sous licence MIT et auto-hébergeable — l'étape naturelle suivante depuis un fichier JSONL si vous voulez une UI mais conserver vos données. (Il a été acquis par ClickHouse début 2026, ce qui vaut la peine de savoir si la stabilité du fournisseur vous importe.)
- Arize Phoenix est natif OpenTelemetry — construit sur l'instrumentation OTel et OpenInference — et fonctionne localement via Docker, avec une solide bibliothèque open-source de métriques eval. Si vous avez adopté les conventions OTel ci-dessus, c'est le backend le moins contraignant.
- LangSmith est le plus profond si vous êtes déjà sur LangChain/LangGraph, et moins intéressant sinon.
- Braintrust se concentre sur l'eval en premier, avec des portes CI qui peuvent bloquer un merge quand la qualité régresse — utile quand la discipline de livraison compte plus que les dashboards.
Je ne vais pas couronner l'un d'eux. La règle de décision que j'utilise : si je veux un dashboard, je regarde Langfuse ou Phoenix ; si je veux des portes eval dans la CI, je regarde Braintrust ; si je suis profond dans LangChain, LangSmith. Et si je ne veux encore ni l'un ni l'autre, je reste sur le fichier JSONL, parce que le fichier répond déjà aux questions que j'ai aujourd'hui. Il vaut aussi la peine de noter que les principaux agents de codage — Claude Code, Codex, Copilot — exportent maintenant eux-mêmes la télémétrie OTel, donc une partie de cela s'obtient sans écrire aucun code de liaison.
Quand c'est excessif
Laissez-moi être celui qui le dit : pour un tout nouveau prototype avec trois utilisateurs et une fonctionnalité, le traçage complet est de la procrastination déguisée en rigueur. Si vous pouvez lire chaque exécution à l'œil et que votre facture mensuelle est une erreur d'arrondi, journalisez les compteurs de tokens et le coût, sautez le reste, et allez construire le produit.
Le seuil honnête concerne deux signaux : quand une facture vous surprend, ou quand un échec vous échappe. La première fois que vous ne pouvez pas expliquer un pic de coût, ou la première fois qu'un agent échoue d'une manière que vous ne pouvez pas reproduire, vous avez franchi la ligne — et la solution est un après-midi à ajouter log_span, pas une semaine à monter une plateforme.
Commencez avec le fichier. Utilisez les noms de champs OTel pour que le hack d'aujourd'hui soit le chemin de migration de demain. Taguez chaque span avec une fonctionnalité et un coût. Achetez un outil le jour où le fichier ne répond plus à vos questions — pas avant. L'objectif n'a jamais été l'observabilité pour elle-même. C'était de pouvoir regarder une exécution qui a mal tourné et savoir, en trois minutes, exactement où l'argent est allé.
