Évaluations auto-hébergées pour agents IA
Construisez une boucle d’évaluation maîtrisée : cas issus de traces réelles, contrôles déterministes, juge LLM, puis taux de réussite suivi à 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 regroupent quatre éléments que vous conservez 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 notent chaque réponse et une table de résultats que vous pouvez interroger. Aucun de ces éléments ne nécessite un fournisseur. La boucle complète tient dans quelques centaines de lignes de Python et un fichier SQLite.
L’agent a fonctionné dans la démonstration parce que vous aviez choisi vous-même les cinq entrées. Il a cessé de fonctionner dès la deuxième semaine parce qu’une ligne du prompt, un modèle ou la description d’un outil avait changé, sans qu’aucune mesure ne couvre ces changements. Une boucle d’évaluation transforme « le résultat 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 à chaque changement 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 d’exécution de 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 dès la deuxième semaine
Un agent se compose d’un prompt, d’un modèle, d’un ensemble de définitions d’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 aucun problème.
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. Aucune alerte ne s’est donc déclenchée.
La deuxième cause est le modèle. Enregistrez la chaîne exacte du modèle envoyée avec chaque exécution, claude-haiku-4-5-20251001, plutôt qu’un raccourci mémorisé, car une baisse du taux de réussite le jour où vous avez changé 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 depuis des serveurs MCP exécutés sur un VPS, le schéma se trouve 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 les cas d’évaluation. Prenez-les dans le trafic réel. Si vous utilisez déjà le tracing 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 basic, 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 parsing. Les lignes sont renvoyées sous data, mais les noms des champs qui contiennent 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 peut faire varier le taux de réussite de 5 points, et un chiffre qui change sans raison finit par être ignoré.
- Chaque bug de production corrigé devient un cas le jour de sa correction. Cette habitude fait progresser le jeu dans la bonne direction.
- Un 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
idne change jamais, car l’identifiant permet de comparer l’exécution d’aujourd’hui à celle du mois dernier. - Supprimez les données sensibles avant le commit. Ce fichier sera ajouté à git : retirez donc 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 ne coûtent rien
Tout ce qui admet une réponse exacte doit faire l’objet d’une simple assertion. Aucun appel de modèle, aucun coût, aucune ambiguïté. Les contrôles déterministes détectent les régressions structurelles. Ce sont celles qui perturbent les systèmes autour de votre agent : le JSON n’est pas analysable, l’outil n’a jamais été appelé, la phrase interdite est réapparue 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 failuresConservez 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é.
Un LLM comme évaluateur : les quatre façons dont il peut se tromper
Tout ce qui passe les assertions a besoin d’un évaluateur capable de lire. Un évaluateur LLM correspond à un second 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 évaluateur exploitable :
- Un verdict binaire, jamais une note de 1 à 10. Une échelle renvoie 7 et 8 pour presque tout, si bien que le nombre ne varie 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 à l’évaluateur 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)Voici maintenant les modes d’échec. Chacun dispose d’un test que vous pouvez exécuter cet après-midi. Ces tests sont importants, car un évaluateur non vérifié produit des nombres qui semblent précis, mais qui ne veulent rien dire.
Biais en faveur des réponses longues. Les réponses longues réussissent plus souvent. Pour le tester, prenez dix réponses que l’évaluateur a refusées, ajoutez à chacune deux paragraphes de remplissage affirmatif qui n’apportent aucun fait nouveau, puis évaluez-les à nouveau. Si un verdict passe alors à « accepté », il s’agit d’un biais en faveur de la longueur. C’est le rubric qu’il faut corriger.
Préférence pour ses propres sorties. Un évaluateur note souvent plus favorablement les sorties 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 évaluateurs issus de deux familles différentes et comparez les verdicts cas par cas. Lorsque leurs verdicts diffèrent, lisez vous-même le cas.
Biais de position. Si vous utilisez l’évaluateur pour comparer deux réponses, A et B, inversez leur ordre et relancez l’évaluation. Si le verdict change après cette inversion, la comparaison par paires n’est pas encore fiable pour ce rubric.
Dérive du rubric. Les critères vagues produisent des évaluateurs trop conciliants. « La réponse est-elle utile ? » accepte presque tout. « La réponse indique-t-elle le montant du remboursement en dollars ? » n’accepte que ce que vous vouliez vérifier. Réécrivez chaque critère jusqu’à ce qu’il désigne précisément le fait contrôlé.
Une seule mesure de protection couvre les quatre cas. Conservez 30 cas que vous avez étiquetés manuellement et comparez les résultats de l’évaluateur à vos étiquettes chaque fois que vous modifiez le modèle ou le prompt de l’évaluateur. 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. L’évaluateur est du code : versionnez-le et relisez-le comme du code.
Privilégiez un modèle peu coûteux, puis passez à un modèle frontier
Évaluer chaque cas avec le modèle le plus coûteux à chaque commit suffit à faire dépasser la facture d’évaluation au coût de l’agent testé. Classez les évaluateurs par prix et arrêtez-vous dès que la réponse est claire.
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 en entrée et 120 tokens en sortie par appel d’évaluation, 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 que vous le multipliiez. Un jeu de 60 cas, évalué à chaque commit, avec 40 commits par semaine, représente 2,400 appels d’évaluation par semaine avant même l’exécution du job nocturne.
Deux remises s’appliquent directement au travail d’évaluation et se cumulent. Les eval runs ne sont pas interactifs. La Batch API divise donc par deux les prix des entrées et des sorties en échange d’une livraison asynchrone. C’est la première ligne du graphique. Le rubric et les instructions sont identiques octet par octet à chaque appel, ce qui permet d’utiliser le prompt caching : une lecture du cache coûte un dixième du prix de base des entrées, et une écriture dans le cache valable cinq minutes coûte 1.25 fois le prix de base des entrées. Le cache est donc rentabilisé dès la première réutilisation. Il s’agit des tarifs publics d’Anthropic en vigueur 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 séquence, dans l’ordre :
- Contrôles déterministes sur chaque cas. Aucun coût d’API.
- Évaluation par un petit modèle pour les cas qui ont passé ces contrôles.
- Évaluation par un modèle frontier uniquement lorsque le petit modèle indique un échec ou un succès avec une faible confiance.
- Revue humaine d’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 quelque peu la précision de l’évaluation. Mesurez donc ce compromis au lieu de le supposer. Une fois par mois, évaluez également l’ensemble du jeu avec l’évaluateur strict et comparez les deux colonnes. Si elles divergent pour plus de quelques cas, votre rubric est trop vague pour le petit modèle. 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 le contrôle des coûts d’un agent IA sur un VPS.
Suivez le taux de réussite au fil du temps dans un système que vous contrôlez
Un taux de réussite que vous ne pouvez pas relier à un commit reste une impression. Stockez une ligne par cas et par exécution, avec le commit et le modèle dans la 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 commencent à compter 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 amountExécutez la suite sur les modifications 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-pushLes exécutions complètes sont plus lentes et doivent être planifiées. Un service et un timer systemd sur le VPS exécutés chaque nuit lancent l’ensemble des tests avec le prompt déployé. Cela détecte les modifications provenant de l’extérieur de votre dépôt, par exemple lorsqu’un outil hébergé change de comportement.
Relecture humaine, sur un échantillon plutôt que de façon exhaustive
Le judge est calibré à partir d’évaluations humaines. Quelqu’un doit donc les produire. Examinez un échantillon chaque semaine : tous les cas sur lesquels le judge s’est trompé, ainsi que dix validations sélectionnées aléatoirement. Les validations aléatoires sont essentielles, car un judge qui a commencé discrètement à valider de mauvaises réponses semble 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 auxquels personne n’avait pensé. Écrivez 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 une requête plutôt qu’un souvenir.
Ce qui casse dans le harness d’évaluation lui-même
anthropic.RateLimitError lors du premier run complet. 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, car une suite qui transforme les erreurs en réussites peut atteindre 100 % alors que l’agent devient moins performant.
Cas instables. La même entrée réussit lors d’une exécution et échoue lors de la suivante, car 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 trouvera.
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 de ce qui est correct.
Une suite qui n’échoue jamais. Un taux de réussite bloqué à 100 % pendant un mois signifie que le jeu de tests ne suit plus le produit. Prenez dix traces récentes, trouvez celles que l’agent a mal traitées et ajoutez-les.
FAQ
Combien de cas faut-il dans un jeu d’évaluation pour un agent IA ?
Commencez avec 40 à 80 cas, puis enrichissez le jeu à partir des échecs réels. En dessous d’environ 20 cas, un résultat instable fait varier le taux de réussite de 5 points. Le résultat perd donc sa valeur informative. Au-delà de quelques centaines de cas, chaque exécution coûte réellement du temps et de l’argent, alors que chaque cas supplémentaire apporte peu de couverture. La mesure importante n’est pas le nombre de cas. C’est la proportion des types d’échec connus en production qui apparaissent au moins une fois dans le jeu.
Puis-je faire confiance à un LLM judge pour évaluer mon agent ?
Seulement après l’avoir comparé à vos propres annotations. Conservez 30 cas que vous avez évalués manuellement, puis comparez-les au judge chaque fois que vous modifiez son modèle ou son prompt. Les judges présentent un biais de longueur : les réponses inutilement longues réussissent plus souvent. Ils présentent aussi un biais de préférence pour leur propre modèle : les sorties de leur propre famille de modèles sont évaluées plus favorablement. Ces deux biais peuvent être testés. Ajoutez du texte à une réponse ayant échoué, puis faites-la évaluer de nouveau. Vous pouvez aussi faire évaluer les mêmes réponses par un judge d’une autre famille de modèles. Si le judge n’est pas d’accord avec vos annotations dans plus d’un cas sur dix, le rubric est trop vague pour être utilisable.
Quel modèle doit évaluer les evals ?
Commencez par le moins cher, puis augmentez le niveau si nécessaire. Les assertions déterministes ne coûtent rien. Elles sont donc exécutées en premier sur chaque cas. Un petit modèle traite les réussites évidentes. Seuls les échecs et les verdicts peu fiables sont transmis à un modèle frontier. Aux tarifs catalogue d’août 2026, l’évaluation de 1,000 cas coûte environ 1.80 dollars US avec Claude Haiku 4.5 et environ 9.00 avec Claude Opus 5. Comme les exécutions d’eval sont asynchrones, la Batch API réduit de moitié chacun de ces montants.
Les evals remplacent-elles le monitoring en production ?
Non, car elles répondent à des questions différentes. Une suite d’evals indique si une modification que vous êtes sur le point de déployer améliore ou dégrade un jeu fixe de cas. Le tracing et le monitoring indiquent ce que les utilisateurs réels rencontrent actuellement, y compris les entrées couvertes par aucun cas. Ils s’alimentent mutuellement : les traces fournissent de nouveaux cas, et la suite d’evals permet de vérifier que votre correctif a réellement fonctionné.