SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-23

Évaluations auto-hébergées pour agents IA

Créez une boucle d’évaluation avec des traces réelles, des contrôles déterministes puis un juge LLM, et suivez le taux de réussite à chaque commit.

Ce que sont les évaluations auto-hébergées pour les agents IA

Les évaluations auto-hébergées pour les agents IA reposent sur quatre éléments conservés dans votre propre dépôt : un fichier de cas enregistrés, un script qui exécute l’agent sur ces cas, un ensemble de contrôles qui évalue chaque réponse et une table de résultats que vous pouvez interroger. Aucun de ces éléments ne nécessite un fournisseur. L’ensemble tient dans quelques centaines de lignes de Python et un fichier SQLite.

L’agent fonctionnait dans la démonstration parce que vous aviez vous-même choisi les cinq entrées. Il a commencé à échouer dès la deuxième semaine parce qu’une ligne du prompt, le modèle ou la description d’un outil avait changé, sans qu’aucune mesure ne couvre ces changements. Une boucle d’évaluation transforme « cela semble moins bon maintenant » en « le taux de réussite est passé de 58 sur 60 à 51 sur 60 avec le commit 4f1c9ab ».

La boucle comporte quatre étapes, et ce guide consacre une section à chacune d’elles : collecter des traces réelles, transformer les plus intéressantes en cas de test, évaluer chaque cas après chaque modification et enregistrer le taux de réussite à côté du commit qui l’a produit. La même boucle fonctionne quel que soit l’environnement dans lequel vous exécutez l’agent, et les frameworks d’agents auto-hébergés qui méritent d’être utilisés se distinguent surtout par la quantité de traces qu’ils vous fournissent directement.

Pourquoi l’agent tombe en panne la deuxième semaine

Un agent est composé d’un prompt, d’un modèle, d’une définition des outils et du contexte récupéré au moment de l’exécution. Ces quatre éléments peuvent changer sans modification du code de votre application. Une revue de code classique ne détecte donc rien.

La cause la plus fréquente est une modification du prompt. Vous ajoutez une phrase pour empêcher une réponse impolie. Cette phrase modifie le comportement pour des entrées qui n’ont pas été retestées. Les traces le montrent clairement : la trace de la semaine dernière pour la même question contient un appel d’outil create_refund, tandis que celle de cette semaine n’en contient aucun et que la réponse est simplement une excuse polie. Aucune erreur n’a été générée, donc aucune alerte ne s’est déclenchée.

La deuxième cause est le modèle. Enregistrez la chaîne exacte du modèle envoyée à chaque exécution, claude-haiku-4-5-20251001 plutôt qu’un nom abrégé que vous gardez en tête. Ainsi, une baisse du taux de réussite le jour du changement de modèle ne peut être diagnostiquée que si le modèle figure dans la ligne correspondante.

La troisième cause concerne les outils. Reformuler la description d’un outil modifie le moment où le modèle décide de l’appeler. Si vos outils arrivent via des serveurs MCP exécutés sur un VPS, le schéma est défini dans un autre processus. Il peut donc changer sans qu’aucune différence n’apparaisse dans votre dépôt. La quatrième cause est la récupération : la même question interroge un index reconstruit pendant la nuit, et la réponse s’appuie sur le nouveau document.

Construisez le jeu de référence à partir des traces que vous collectez déjà

N’inventez pas de cas d’évaluation. Prenez-les dans le trafic réel. Si vous utilisez déjà le traçage Langfuse auto-hébergé pour votre agent, chaque requête est enregistrée avec son entrée, ses appels d’outils et sa sortie. C’est exactement la matière première nécessaire pour créer un cas.

Exportez une période d’observations root via l’API publique. Elle utilise l’authentification basique, avec votre clé publique comme nom d’utilisateur et votre clé secrète comme mot de passe.

export LF_HOST="https://langfuse.example.com"
curl -sS -u "$LF_PUBLIC_KEY:$LF_SECRET_KEY" \
  "$LF_HOST/api/public/v2/observations?limit=50&isRootObservation=true&fromStartTime=2026-07-01T00:00:00Z" \
  | jq '.data[0]'

Lisez un enregistrement avant d’écrire le moindre parseur. Les lignes sont renvoyées sous data, mais les noms des champs contenant la question et la réponse dépendent de la manière dont votre agent instrumente ses spans. Faites donc la correspondance à partir de ce que vous voyez réellement, et non de ce que vous attendiez. Écrivez ensuite les cas manuellement, avec un objet JSON par ligne, dans evals/cases.jsonl :

{"id": "refund-double-charge", "tags": ["smoke"], "input": "I was charged twice for order 41822.", "must_call": ["lookup_order", "create_refund"], "must_not_include": ["I cannot help"], "rubric": "The reply confirms exactly one refund for order 41822 and states the amount."}

Cinq règles permettent de conserver un jeu utile :

  • 40 à 80 cas suffisent pour commencer. En dessous de 20, un seul cas instable fait varier le taux de réussite de 5 points, et un nombre qui change sans raison finit par être ignoré.
  • Chaque bug de production que vous corrigez devient un cas le jour même de sa correction. Cette habitude fait progresser le jeu dans la bonne direction.
  • Un seul comportement par cas. Un cas qui vérifie à la fois le montant du remboursement et le ton ne vous apprend rien lorsqu’il échoue.
  • Le id ne change jamais, car l’identifiant permet de comparer l’exécution du jour avec celle du mois dernier.
  • Supprimez les données sensibles avant le commit. Ce fichier sera ajouté à git : retirez les noms des clients et tous les numéros de commande qui ne vous appartiennent pas.

Commencez par évaluer les contrôles déterministes, car ils sont gratuits

Toute vérification qui admet une réponse exacte doit utiliser une simple assertion. Aucun appel au modèle, aucun coût et aucune ambiguïté. Les contrôles déterministes détectent les régressions structurelles. Ce sont elles qui perturbent les systèmes autour de votre agent : le JSON n’est pas valide, l’outil n’a jamais été appelé, la phrase interdite réapparaît ou la réponse ne cite aucune source.

Une seule fonction connaît votre agent. Tout le reste du harness est générique.

import json, os, urllib.request

def run_agent(case):
    req = urllib.request.Request(
        os.environ["AGENT_URL"],
        data=json.dumps({"input": case["input"]}).encode(),
        headers={"content-type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=120) as resp:
        return json.load(resp)


def deterministic(case, result):
    text = result.get("output", "")
    called = [c["name"] for c in result.get("tool_calls", [])]
    failures = []
    for tool in case.get("must_call", []):
        if tool not in called:
            failures.append(f"tool not called: {tool}")
    for phrase in case.get("must_not_include", []):
        if phrase.lower() in text.lower():
            failures.append(f"forbidden phrase: {phrase}")
    if len(called) > case.get("max_tool_calls", 12):
        failures.append(f"too many tool calls: {len(called)}")
    return failures

Conservez le budget d’appels dans cette liste. Un agent qui résout un cas en 3 appels aujourd’hui et en 11 demain a régressé, même si la réponse finale est correcte, car chaque appel est facturé.

Le LLM comme juge et les quatre façons dont il peut se tromper

Ce qui passe les assertions doit être évalué par un système capable de lire. Un juge LLM est un deuxième appel de modèle : il reçoit la question, la réponse de l’agent et un critère, puis renvoie un verdict. C’est la seule méthode pratique pour évaluer si « la réponse répond à la demande de l’utilisateur ».

Quatre règles rendent un juge exploitable :

  • Verdict binaire, jamais une note de 1 à 10. Une échelle renvoie 7 et 8 pour presque tout, si bien que le nombre n’évolue jamais et ne vous apprend rien.
  • Un seul critère par appel. Demandez si le montant du remboursement est correct, ou si le ton convient, mais pas les deux à la fois.
  • Donnez au juge la réponse attendue lorsque le cas en comporte une. Évaluer par rapport à une référence est beaucoup plus simple qu’évaluer dans l’abstrait.
  • Imposez le format de sortie et analysez-le strictement.
from anthropic import Anthropic

client = Anthropic()  # reads ANTHROPIC_API_KEY from the environment


def judge_prompt(case, output):
    return (
        "You grade one answer against one criterion.\n"
        "Reply with JSON only, in this exact shape:\n"
        '{"verdict": "pass", "confidence": "high", "reason": "one short sentence"}\n'
        f"Criterion: {case['rubric']}\n"
        f"Question: {case['input']}\n"
        f"Answer: {output}\n"
        "Length is not a criterion. Judge only the criterion above."
    )


def judge(case, output, model):
    msg = client.messages.create(
        model=model,
        max_tokens=200,
        messages=[{"role": "user", "content": judge_prompt(case, output)}],
    )
    return json.loads(msg.content[0].text)

Examinons maintenant les modes d’échec. Chacun peut être testé cet après-midi. Ces tests sont importants, car un juge non contrôlé produit des nombres qui semblent précis, mais qui ne veulent rien dire.

Biais de longueur. Les réponses plus longues réussissent plus souvent. Pour le tester, prenez dix réponses que le juge a refusées, ajoutez à chacune deux paragraphes de remplissage affirmatifs qui n’apportent aucun fait nouveau, puis évaluez-les de nouveau. Tout verdict qui passe à « réussi » révèle un biais de longueur. Le rubric est alors l’élément à corriger.

Préférence pour ses propres sorties. Un juge évalue souvent plus favorablement les sorties issues de sa propre famille de modèles que celles d’une autre famille. Pour le tester, évaluez les mêmes 30 réponses avec des juges issus de deux familles différentes, puis comparez les verdicts cas par cas. Lorsque les juges ne sont pas d’accord, lisez vous-même le cas.

Biais de position. Si vous utilisez le juge pour comparer deux réponses, A et B, inversez leur ordre et relancez l’évaluation. Un verdict qui change après cette inversion signifie que la comparaison par paires n’est pas encore fiable pour ce rubric.

Dérive du rubric. Des critères vagues produisent des juges trop indulgents. « La réponse est-elle utile ? » fait réussir presque n’importe quelle réponse. « La réponse indique-t-elle le montant du remboursement en dollars ? » ne fait réussir que ce que vous vouliez vérifier. Réécrivez chaque critère jusqu’à ce qu’il nomme précisément le fait contrôlé.

Une seule mesure protège contre les quatre biais. Conservez 30 cas que vous avez étiquetés manuellement et comparez le juge à vos étiquettes chaque fois que vous modifiez le modèle ou le prompt du juge. S’il n’est pas d’accord avec vous dans plus d’un cas sur dix, corrigez le rubric avant de faire confiance au taux de réussite qu’il produit. Le juge est du code : il doit être versionné et relu comme du code.

Privilégiez un modèle économique, puis passez à un modèle frontier

Évaluer chaque cas avec le modèle le plus coûteux à chaque commit est le meilleur moyen de voir la facture d’évaluation dépasser le coût de l’agent testé. Classez les juges par prix et arrêtez-vous dès que la réponse est claire.

ChartCost to judge 1,000 eval cases, list prices, August 2026
The data behind this chart
[
  {
    "label": "Haiku 4.5, Batch API",
    "usd_per_1000_judge_calls": "0.90"
  },
  {
    "label": "Haiku 4.5",
    "usd_per_1000_judge_calls": "1.80"
  },
  {
    "label": "Sonnet 5",
    "usd_per_1000_judge_calls": "3.60"
  },
  {
    "label": "Opus 5",
    "usd_per_1000_judge_calls": "9.00"
  }
]

Ces chiffres supposent environ 1,200 tokens d’entrée et 120 tokens de sortie par appel du juge, ce qui correspond à une taille réaliste pour une question, une réponse et un critère. Évaluer 1,000 cas coûte 1.80 dollars US avec Claude Haiku 4.5 et 9.00 avec Claude Opus 5. L’écart semble négligeable jusqu’à ce qu’on le multiplie. Un ensemble de 60 cas, évalué à chaque commit, avec 40 commits par semaine, représente 2,400 appels du juge par semaine avant même l’exécution du job nocturne.

Deux réductions s’appliquent directement aux évaluations et se cumulent. Les exécutions d’évaluation ne sont pas interactives. La Batch API divise donc par deux les prix des entrées et des sorties en échange d’un traitement asynchrone. C’est la première ligne du graphique. Le rubric et les instructions sont strictement identiques à chaque appel. Le prompt caching est donc adapté : la lecture du cache coûte un dixième du prix de base des entrées, et l’écriture dans le cache pendant cinq minutes coûte 1.25 fois le prix de base des entrées. Le cache est donc amorti dès la première réutilisation. Il s’agit des tarifs publics d’Anthropic en août 2026. Sonnet 5 bénéficie d’un tarif de lancement jusqu’au 31 août 2026 ; la troisième barre augmente donc après cette date.

La stratégie, dans l’ordre :

  • Contrôles déterministes sur chaque cas. Aucun coût d’API.
  • Un small model comme juge pour les cas qui ont passé ces contrôles.
  • Un frontier model comme juge uniquement lorsque le small model indique un échec ou un succès avec une faible confiance.
  • Une revue humaine sur un petit échantillon, une fois par semaine.
CHEAP = "claude-haiku-4-5-20251001"
STRICT = "claude-opus-5"


def grade(case, result):
    hard = deterministic(case, result)
    if hard:
        return False, "deterministic", "; ".join(hard)
    first = judge(case, result["output"], CHEAP)
    if first["verdict"] == "pass" and first["confidence"] == "high":
        return True, CHEAP, first["reason"]
    second = judge(case, result["output"], STRICT)
    return second["verdict"] == "pass", STRICT, second["reason"]

Cette méthode réduit légèrement la précision de l’évaluation pour diminuer les coûts. Mesurez donc ce compromis au lieu de le supposer. Une fois par mois, évaluez également l’ensemble avec le juge strict et comparez les deux colonnes. Si elles diffèrent pour plus d’une poignée de cas, votre rubric est trop peu précis pour le small model. C’est alors le rubric qu’il faut corriger. Le contrôle des dépenses de l’agent lui-même est une tâche distincte, traitée dans contrôle des coûts d’un agent IA sur un VPS.

Suivre le taux de réussite dans le temps sur un système que vous possédez

Un taux de réussite que vous ne pouvez pas associer à un commit reste une impression. Stockez une ligne par cas et par exécution, avec le commit et le modèle dans cette ligne.

CREATE TABLE IF NOT EXISTS results (
  run_id      TEXT NOT NULL,
  ran_at      TEXT NOT NULL,
  git_sha     TEXT NOT NULL,
  agent_model TEXT NOT NULL,
  case_id     TEXT NOT NULL,
  passed      INTEGER NOT NULL,
  graded_by   TEXT NOT NULL,
  reason      TEXT
);
SELECT run_id, git_sha, agent_model,
       count(*) AS cases,
       round(100.0 * sum(passed) / count(*), 1) AS pass_pct
FROM results
GROUP BY run_id
ORDER BY ran_at DESC
LIMIT 10;

Chargez le schéma avec sqlite3 evals/results.db < evals/schema.sql, puis consultez la tendance avec sqlite3 -box evals/results.db < evals/passrate.sql. Une année d’exécutions quotidiennes sur 60 cas représente environ 22,000 lignes. Le stockage ne devient donc jamais un projet à part entière. Exécuter SQLite en production sur un VPS présente les paramètres qui deviennent importants si ce fichier est partagé entre plusieurs machines.

Le runner affiche les mêmes informations pour une personne :

run 2026-08-05T09:14:22Z  sha 4f1c9ab  model claude-sonnet-5  58/60 pass (96.7%)
FAIL refund-double-charge  deterministic: tool not called: create_refund
FAIL pto-policy-question   judge(opus): reply gives no dollar amount

Exécutez la suite sur les changements susceptibles de casser un agent : modifications des prompts, changements de modèle et changements d’outils, plutôt que sur chaque commit du dépôt. Un hook pre-push couvre le sous-ensemble rapide :

cat > .git/hooks/pre-push <<'EOF'
#!/bin/sh
python3 evals/run.py --set smoke || exit 1
EOF
chmod +x .git/hooks/pre-push

Les exécutions complètes sont plus lentes et doivent être planifiées. Un service et timer systemd sur le VPS exécuté chaque nuit lance l’ensemble des tests avec le prompt déployé. Cela détecte les changements provenant de l’extérieur de votre dépôt, par exemple lorsqu’un outil hébergé change de comportement.

Relecture humaine, par échantillonnage plutôt qu’exhaustive

Le judge est calibré à partir d’évaluations humaines. Il faut donc les produire. Examinez un échantillon chaque semaine : tous les cas que le judge a mal évalués, ainsi que dix validations choisies aléatoirement. Les validations aléatoires constituent la moitié importante, car un judge qui a commencé discrètement à valider de mauvaises réponses paraît parfait dans tout tableau de bord construit à partir de ses propres verdicts.

Quinze cas à trois minutes chacun représentent 45 minutes par semaine. Cette relecture permet de corriger le rubric lorsque vos évaluations et celles du judge divergent, et d’ajouter des cas correspondant à des types d’échec que personne n’avait imaginés. Enregistrez le verdict humain dans la même table, avec graded_by défini sur human. L’accord entre le judge et l’évaluateur humain devient ainsi le résultat d’une requête plutôt qu’un souvenir.

Ce qui casse dans le banc d’évaluation lui-même

anthropic.RateLimitError lors de la première exécution complète. Soixante cas lancés simultanément dépassent la limite de requêtes ou de tokens de votre niveau d’abonnement. Limitez la concurrence à quatre workers et déplacez l’exécution nocturne vers la Batch API.

json.JSONDecodeError: Expecting value: line 1 column 1 (char 0) du judge. Le modèle a répondu en prose ou a placé son JSON dans un bloc de code. Réessayez une fois, puis enregistrez le cas comme une erreur. Ne comptez jamais un échec d’analyse comme une réussite : une suite qui transforme les erreurs en réussites peut atteindre 100 % alors que l’agent se dégrade.

Cas instables. La même entrée réussit lors d’une exécution et échoue lors de la suivante, parce que l’agent échantillonne sa sortie. Exécutez le cas instable trois fois et enregistrez la proportion de réussites au lieu de supprimer le cas. Un cas qui réussit deux fois sur trois révèle un véritable problème de robustesse, et un client le découvrira.

Dérive du golden set. Quelqu’un modifie une réponse attendue pour faire passer la suite au vert. Examinez les différences de evals/cases.jsonl aussi attentivement que celles de l’agent, car ce fichier constitue votre définition écrite d’un résultat correct.

Une suite qui n’échoue jamais. Un taux de réussite bloqué à 100 % pendant un mois signifie que l’ensemble ne suit plus l’évolution du produit. Extrayez dix traces récentes, trouvez celles que l’agent a mal traitées et ajoutez-les. Cassez ensuite volontairement quelque chose et vérifiez que l’exécution passe au rouge : c’est le principe de mutation testing appliqué à une suite de tests et le seul moyen de vérifier que votre ensemble reste exigeant.

FAQ

De combien de cas un jeu d’évaluation pour un agent IA a-t-il besoin ?

Commencez avec 40 à 80 cas, puis enrichissez le jeu à partir des échecs réels. En dessous d’environ 20 cas, un seul résultat instable fait varier le taux de réussite de 5 points, et la mesure ne contient plus assez d’informations. Au-delà de quelques centaines de cas, chaque exécution coûte réellement du temps et de l’argent, tandis que chaque cas supplémentaire apporte peu de couverture. La mesure importante n’est pas le nombre de cas, mais la proportion des types d’échec connus en production qui apparaissent au moins une fois dans le jeu.

Puis-je faire confiance à un LLM comme évaluateur de mon agent ?

Oui, mais seulement après l’avoir comparé à vos propres annotations. Conservez 30 cas que vous avez évalués manuellement, puis comparez les résultats de l’évaluateur à ces annotations chaque fois que vous modifiez son modèle ou son prompt. Les évaluateurs présentent un biais en faveur des réponses longues : les réponses artificiellement allongées réussissent plus souvent. Ils présentent aussi un biais d’auto-préférence : ils évaluent plus favorablement les sorties de leur propre famille de modèles. Ces deux biais peuvent être testés : allongez une réponse en échec, puis faites-la évaluer de nouveau, ou évaluez les mêmes réponses avec un modèle d’une autre famille. Si l’évaluateur contredit vos annotations dans plus d’un cas sur dix, la grille d’évaluation est trop vague pour être utilisée.

Quel modèle doit évaluer les tests ?

Commencez par l’option la moins coûteuse, puis augmentez les ressources si nécessaire. Les assertions déterministes ne coûtent rien et sont donc exécutées en premier pour chaque cas. Un petit modèle traite les réussites évidentes. Seuls les échecs et les verdicts peu fiables sont transmis à un modèle de pointe. Aux tarifs affichés en août 2026, l’évaluation de 1,000 cas coûte environ 1.80 dollars américains avec Claude Haiku 4.5, et environ 9.00 avec Claude Opus 5. Comme les exécutions d’évaluation sont asynchrones, la Batch API réduit de moitié chacun de ces montants.

Les évaluations remplacent-elles la supervision en production ?

Non, car elles répondent à des questions différentes. Une suite d’évaluation vous indique si une modification que vous êtes sur le point de déployer améliore ou dégrade les résultats sur un ensemble fixe de cas. Le traçage et la supervision vous indiquent ce que les utilisateurs réels rencontrent à cet instant, y compris les entrées qui ne sont couvertes par aucun cas. Ces éléments s’alimentent mutuellement : les traces fournissent de nouveaux cas, et la suite d’évaluation permet de vérifier que votre correctif a réellement fonctionné.