Aller au contenu principal
GiwiSoft
01

Construire un dashboard Grafana pour un agent IA

Giwi 10 min de lecture DevOps, Monitoring

Dans mon article sur l’observabilité d’un LLM, je montrais quoi mesurer (tokens, latence, coût, hallucinations). Mais mesurer ne suffit pas : encore faut-il visualiser tout ça d’une façon qui permette de répondre vite aux questions de production. C’est exactement le rôle d’un dashboard Grafana dédié à un agent IA. Dans cet article, je vous montre comment construire le mien, de l’exposition des métriques jusqu’à la configuration des panneaux.

Un agent IA n’est pas un service HTTP ordinaire : il fait des appels LLM, il invoque des outils (fonctions), il maintient une file de tâches, il génère plusieurs réponses par requête utilisateur. Un dashboard classique (CPU, RAM, requêtes/s) est totalement aveugle à ce qui compte vraiment : le coût, la latence de génération, le taux de succès des appels d’outils, la qualité des réponses.

Le choix de la stack : Prometheus + Grafana

J’utilise Prometheus pour le stockage des métriques et Grafana pour la visualisation. C’est le duo le plus répandu, et je le trouve parfaitement adapté. Le principe est simple : mon agent Node.js expose un endpoint HTTP /metrics au format texte Prometheus, un serveur Prometheus vient interroger cet endpoint régulièrement, et Grafana interroge Prometheus pour dessiner les graphes.

Agent Node.js → /metrics (prom-client) → Prometheus (scrape) → Grafana (queries)

Ce modèle pull a un gros avantage pour l’observabilité : l’agent n’a pas à savoir qui le surveille, il expose des métriques de façon passive, et l’infrastructure décide de quoi faire. On peut brancher plusieurs scrapers sans toucher à l’agent.

Exposer les métriques avec prom-client

La bibliothèque standard pour exposer des métriques Prometheus depuis Node.js est prom-client. On commence par l’installer et créer un registre :

npm install prom-client
import { Registry, Counter, Histogram, Gauge } from 'prom-client';

const registry = new Registry();

const tokensCounter = new Counter({
name: 'agent_llm_tokens_total',
help: 'Total tokens processed by the LLM',
labelNames: ['model', 'direction'], // direction: input | output
registers: [registry],
});

const latencyHist = new Histogram({
name: 'agent_llm_latency_seconds',
help: 'LLM latency by phase',
labelNames: ['model', 'phase'], // phase: ttft | generation | total
buckets: [0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60],
registers: [registry],
});

const costCounter = new Counter({
name: 'agent_llm_cost_usd_total',
help: 'Cumulative LLM cost in USD',
labelNames: ['model'],
registers: [registry],
});

const toolResultCounter = new Counter({
name: 'agent_tool_calls_total',
help: 'Tool calls by tool and outcome',
labelNames: ['tool', 'outcome'], // outcome: success | error
registers: [registry],
});

const errorCounter = new Counter({
name: 'agent_errors_total',
help: 'Agent errors by type',
labelNames: ['type'],
registers: [registry],
});

const hallucinationGauge = new Gauge({
name: 'agent_hallucination_risk',
help: 'Estimated hallucination risk (0-1)',
labelNames: ['model'],
registers: [registry],
});

const queueGauge = new Gauge({
name: 'agent_queue_depth',
help: 'Number of pending tasks in the queue',
labelNames: ['agent_id'],
registers: [registry],
});

Remarquez le label agent_id sur la profondeur de file : si vous déployez plusieurs instances de votre agent (par exemple un par client ou un par environnement), ce label permet de les comparer côte à côte dans le même dashboard.

Puis on expose l’endpoint HTTP /metrics. Avec Express :

import express from 'express';

const app = express();

app.get('/metrics', async (req, res) => {
res.set('Content-Type', registry.contentType);
res.end(await registry.metrics());
});

app.listen(8080);

Où injecter les mises à jour de métriques

Le piège le plus courant est de déclarer des métriques et de ne jamais les incrémenter aux bons endroits. Voici les points d’injection que j’utilise dans un agent.

À chaque appel LLM

Dès que j’obtiens l’objet usage de l’appel, je mets à jour les compteurs de tokens, de coût et de latence :

async function callLLM({ model, messages }) {
const t0 = performance.now();
const stream = await openai.chat.completions.create({
model, messages, stream: true,
});

let firstTokenAt = null;
let text = '';
for await (const chunk of stream) {
if (!firstTokenAt) {
firstTokenAt = performance.now();
latencyHist.observe({ model, phase: 'ttft' },
(firstTokenAt - t0) / 1000);
}
text += chunk.choices[0]?.delta?.content || '';
}

const totalMs = performance.now() - t0;
const generationMs = totalMs - (firstTokenAt - t0);
latencyHist.observe({ model, phase: 'generation' }, generationMs / 1000);
latencyHist.observe({ model, phase: 'total' }, totalMs / 1000);

const usage = await getUsage(model, messages, text);
tokensCounter.inc({ model, direction: 'input' }, usage.prompt_tokens);
tokensCounter.inc({ model, direction: 'output' }, usage.completion_tokens);
costCounter.inc({ model }, costOf(model, usage));

return text;
}

Le plus important : utiliser les historogrammes pour la latence, pas les jauges. Un histogramme vous permet de calculer les percentiles (p50, p95, p99), ce qui est infiniment plus utile que la moyenne. La moyenne d’une latence de génération peut être trompeuse : 90 % des requêtes en 2 s et 10 % en 40 s donnent une moyenne trompeusement rassurante, alors que le p95 révèle le vrai problème.

À chaque appel d’outil

Un agent IA se distingue d’un simple chatbot par son usage d’outils (function calling). Chaque invocation est un point de défaillance potentiel : un outil qui échoue en cascade peut faire échouer toute la tâche. Je compte donc chaque appel par outil et par issue :

async function runTool(tool, args) {
try {
const result = await tools[tool](args);
toolResultCounter.inc({ tool, outcome: 'success' });
return result;
} catch (err) {
toolResultCounter.inc({ tool, outcome: 'error' });
errorCounter.inc({ type: 'tool_' + tool });
throw err;
}
}

Le taux de succès des appels d’outils est une métrique d’or pour un agent : il indique, en un coup d’oeil, si l’agent est fonctionnel ou s’il enchaîne les échecs qui le font tourner en rond.

À la détection d’hallucination

J’utilise les heuristiques décrites dans mon article précédent (citation manquante, faible confiance, écart de récupération) pour produire un risque entre 0 et 1. Je le pousse dans une jauge, et j’incrémente un compteur au-delà d’un seuil :

const risk = computeHallucinationRisk(response, contextDocs);
hallucinationGauge.set({ model }, risk);
if (risk > 0.5) highRiskCounter.inc({ model });

Une jauge est le bon type pour le risque (une valeur instantanée qui fluctue), alors qu’un compteur est le bon type pour le nombre de réponses à haut risque (une accumulation).

À chaque mise en file

Enfin, je maintiens la profondeur de la file de tâches. Contrairement aux métriques précédentes qui sont des événements ponctuels, la profondeur de file est un état continu qu’on met à jour à chaque entrée/sortie :

queueGauge.inc({ agent_id: process.env.AGENT_ID });
// ... traitement ...
queueGauge.dec({ agent_id: process.env.AGENT_ID });

La structure du dashboard : lignes et panneaux

Maintenant la partie la plus intéressante : organiser les panneaux Grafana. Je structure mon dashboard en cinq lignes, chacune répondant à une question. Tout se configure dans Grafana en choisissant une source de données Prometheus et en écrivant des requêtes PromQL.

Ligne 1 : Vue d’ensemble (overview)

C’est la ligne “tout va bien” : on la regarde en premier. Quatre panneaux de statut :

  • RPM et TPM : sum(rate(agent_llm_tokens_total[5m])) by (direction) pour voir le trafic.
  • Taux d’erreur global : 100 * sum(rate(agent_errors_total[5m])) / sum(rate(agent_llm_tokens_total[5m])) ou plus simplement le nombre d’erreurs par minute.
  • File de tâches : agent_queue_depth pour repérer un goulot d’étranglement.
  • Coût cumulé : sum(agent_llm_cost_usd_total) pour le coût total depuis le démarrage.

PromQL pour les erreurs par minute :

sum(rate(agent_errors_total[5m])) by (type)

Ligne 2 : Latence

On veut ici le détail de la latence par phase. J’affiche un graphe par phase, avec les percentiles en overlay :

histogram_quantile(0.95, sum(rate(agent_llm_latency_seconds_bucket{phase="ttft"}[5m])) by (le))
histogram_quantile(0.50, sum(rate(agent_llm_latency_seconds_bucket{phase="ttft"}[5m])) by (le))

J’affiche le p95 et le p50 côte à côte : l’écart entre les deux révèle la variance, et un p95 qui monte alors que le p50 reste stable signale des queues de latence (quelques requêtes lentes) plutôt qu’une dégradation uniforme.

J’ajoute un panneau tokens par seconde :

sum(rate(agent_llm_tokens_total{direction="output"}[5m])) / sum(rate(agent_llm_latency_seconds_bucket{phase="generation"}[5m])) 

Plus simplement, je calcule le débit de génération en divisant le nombre de tokens de sortie par le temps de génération cumulé.

Ligne 3 : Tokens et coût

Deux graphes complémentaires :

  • Tokens par minute, décomposés par direction : sum(rate(agent_llm_tokens_total[5m])) by (direction). La distinction entrée/sortie est visible en un coup d’œil : si l’entrée domine, c’est le contexte qui explose.
  • Coût par heure en dollars : sum(rate(agent_llm_cost_usd_total[1h])) * 3600. C’est une façon lisible d’estimer le coût par heure.

J’affiche aussi un tableau de coût par modèle pour comparer, en bar chart :

sum(increase(agent_llm_cost_usd_total[24h])) by (model)

Ce panneau répond immédiatement à “quel modèle me coûte le plus cher ?”, ce qui guide les décisions de changement de modèle.

Ligne 4 : Appels d’outils

Le taux réel de succès des outils, décomposé par outil :

sum(increase(agent_tool_calls_total{outcome="error"}[1h])) by (tool)

et le taux de succès global :

sum(rate(agent_tool_calls_total{outcome="success"}[5m])) /
sum(rate(agent_tool_calls_total[5m]))

J’affiche un gauge (jauge radial) pour le taux de succès global, et un bar chart par outil pour repérer immédiatement quel outil échoue en masse. Un outil qui passe de 99 % à 60 % de succès, c’est souvent une API en aval qui a changé.

Ligne 5 : Qualité et hallucinations

Ici on surveille la santé qualitative de l’agent :

  • Risque d’hallucination moyen : avg(agent_hallucination_risk) par modèle.
  • Réponses à haut risque : sum(rate(agent_high_risk_responses_total[5m])).
  • Note de citations : la proportion de réponses qui citent correctement leurs sources.

C’est la ligne la plus spécifique aux agents IA : elle n’a pas d’équivalent dans un dashboard web classique, et pourtant c’est elle qui révèle une dérive de qualité bien avant que les utilisateurs se plaignent.

Configurer un alerting basique

Un dashboard ne sert à rien si personne ne le regarde. J’ajoute des alertes Grafana sur les signaux les plus critiques :

  • Taux d’erreur > seuil : si agent_errors_total augmente brusquement, on veut le savoir.
  • File de tâches > N : un backlog qui grossit signale un agent saturé.
  • Coût par heure > budget : pour déclencher une alerte avant la fin de mois sur budget.
  • Risque d’hallucination > 0.6 en moyenne : une dégradation de la qualité qui passe l’alerte rouge.

Une règle d’alerte typique dans Grafana, via l’interface ou en provisionning YAML :

groups:
- name: ai-agent
rules:
- alert: AgentQueueDepthHigh
expr: agent_queue_depth > 20
for: 5m
labels:
severity: warning
annotations:
summary: "Queue depth high: {{ $value }} tasks pending"

L’expr est une condition PromQL, le for exige que la condition persiste 5 minutes (pour éviter les faux positifs sur un pic transitoire), et les annotations donnent un contexte lisible à l’alerte.

Réflexion finale

Le dashboard Grafana a transformé ma façon d’opérer l’agent : au lieu de fouiller les logs au hasard quand quelque chose cloche, je consulte les graphes et je vois immédiatement où se situe l’anomalie (latence, coût, outils, qualité). C’est le même principe que pour n’importe quel service, mais adapté aux spécificités des agents IA : le coût et la qualité y sont des métriques de premier plan, pas des détails.

Mon conseil : commencez petit. Le dashboard minimal (tokens, latence par phase, taux d’erreur, coût par heure) couvre déjà 80 % des questions de production. Ajoutez la ligne “outils” et la ligne “hallucination” une fois que l’agent embauche de vraies fonctionnalités. L’outillage se fait au rythme du besoin, pas en avance.

Pour aller plus loin

  • OpenTelemetry pour les traces : les métriques donnent la vue globale, mais les traces donnent le détail d’une requête. Voir mon article sur l’observabilité avec OpenTelemetry.
  • Les logs des requêtes LLM : le dashboard agrège, mais gardez les logs individuels pour le diagnostic fin (article dédié).
  • Provisionning as code : déclarez vos dashboards en YAML (via Grafana provisioning) pour les versionner dans Git plutôt que de les cliquer à la main.
  • Alerting multi-tenant : si vous déployez des agents par client, déclinez les règles d’alerte par agent_id.