Votre agent a réussi la tâche avec brio. Le refera-t-il ?

Un agent qui réussit une fois peut échouer à l’exécution suivante. S’appuyant sur les travaux d’IBM Research sur la cohérence d’ALTK-Evolve, cet article examine pourquoi les benchmarks à exécution unique induisent en erreur, comment des essais répétés révèlent la variance dans l’utilisation des outils et le raisonnement, et quelles habitudes pratiques d’évaluation aident les équipes à juger si le succès d’un agent se reproduira.

Lecture audio non disponible dans ce navigateur
Votre agent a réussi la tâche avec brio. Le refera-t-il ?

Résumé rapide

Un agent qui réussit une fois peut échouer à l’exécution suivante. S’appuyant sur les travaux d’IBM Research sur la cohérence d’ALTK-Evolve, cet article examine pourquoi les benchmarks à exécution unique induisent en erreur, comment des essais répétés révèlent la variance dans l’utilisation des outils et le raisonnement, et quelles habitudes pratiques d’évaluation aident les équipes à juger si le succès d’un agent se reproduira.

Votre agent a brillamment réussi la tâche. Le refera-t-il ?

La première exécution passe. La deuxième exécution passe. La troisième rembourse discrètement la mauvaise commande, et personne ne s'en aperçoit avant un client. Cet écart — entre un agent qui peut accomplir une tâche et un agent qui l'accomplit de manière fiable — est là où se situe réellement la majeure partie du travail de production sur les agents.

Cet article porte sur la mesure de cet écart, puis sur sa réduction. C'est un guide d'ingénierie pratique : un petit harnais, une poignée de métriques et un ensemble d'expériences que vous pouvez exécuter cet après-midi pour déterminer si le succès de votre agent relevait d'une capacité ou d'une coïncidence.

Le point de départ est une question posée par IBM Research sur le blog Hugging Face : Your Agent Aced the Task. Will It Do It Again? (source). Cet article présente le problème de la cohérence des agents. Tout ce qui suit — le harnais, les commandes, les définitions de métriques — est de l'ingénierie de fiabilité standard appliquée aux agents ; ce n'est pas un résumé des méthodes de cet article, et vous devriez lire la source directement pour son propre cadrage.

Le piège de la démo

Les démos d'agents sont optimisées pour une seule trajectoire réussie. Vous choisissez une tâche, vous l'exécutez, ça marche, vous livrez. La démo est un échantillon de taille un, et un échantillon de taille un n'a pas de barres d'erreur.

Trois propriétés rendent cela pire pour les agents que pour la plupart des logiciels :

Les agents sont stochastiques par défaut. À moins d'exécuter un modèle local avec une graine fixe sur un matériel fixe, le même prompt peut produire différents appels d'outils selon les exécutions. L'échantillonnage, le traitement par lots et les changements côté fournisseur déplacent tous la distribution.

Les agents dépendent du monde. Un appel d'outil atteint une API qui renvoie des données différentes, un index de recherche qui a été réindexé, une base de données dont les lignes ont changé. Deux exécutions de la « même » tâche peuvent ne pas être la même tâche du tout.

Les agents échouent silencieusement. Un pipeline qui plante est facile à déboguer. Un agent qui renvoie une mauvaise réponse plausible semble identique à un succès au niveau de la journalisation.

Rien de tout cela ne signifie que les agents sont inutilisables. Cela signifie que l'unité de preuve n'est pas une seule exécution. Ce sont k exécutions, et la métrique qui compte n'est pas « est-ce que ça a marché » mais « à quelle fréquence cela marche-t-il, et cela marche-t-il de la même manière ».

Ce que « à nouveau » signifie réellement

Avant d'installer quoi que ce soit, soyez précis sur l'affirmation que vous voulez tester. Il y a au moins trois propriétés distinctes que les gens regroupent sous le terme « fiable » :

  • Disponibilité — l'agent se termine sans exception non gérée.
  • Exactitude — la sortie satisfait un vérificateur automatisé.
  • Cohérence — des exécutions répétées de la même tâche produisent des résultats équivalents.

Ces propriétés échouent indépendamment. Un agent peut être parfaitement cohérent et systématiquement faux. Il peut être correct en moyenne et inutilisable en pratique parce que 20 % des exécutions échouent. Il peut être disponible 100 % du temps tout en produisant des réponses différentes à chaque exécution.

La métrique qui capture la question pertinente en production est ce que j'appellerai pass^k : exécuter la même tâche k fois, et compter la tâche comme réussie seulement si les k tentatives réussissent. C'est délibérément plus sévère que pass@1 (le taux de réussite moyen), car dans la plupart des déploiements d'agents, une tâche n'est pas « terminée » si elle fonctionne trois fois sur cinq. Pass^k pénalise directement l'instabilité, et il se dégrade vite : un agent avec un taux de réussite de 90 % par exécution ne passe un contrôle de 10 exécutions qu'environ 35 % du temps.

Cette arithmétique constitue tout l'argument de cet article. Un agent à 90 % sonne bien et se comporte mal.

Prérequis

Vous avez besoin d'un agent fonctionnel que vous pouvez appeler comme une fonction, et d'un vérificateur capable de décider si une sortie est correcte. Tout le reste est de l'outillage standard.

  • Python 3.10 ou plus récent.
  • pip (ou uv, si vous préférez des installations plus rapides).
  • Un ensemble de tâches : idéalement 10 à 50 tâches représentatives avec des résultats attendus connus.
  • Un vérificateur programmatique par tâche. Correspondance de chaînes, validation de schéma, tests unitaires sur la sortie, ou petit script d'assertion. Si votre seul vérificateur est un humain qui lit la sortie, commencez par là — mais automatisez d'abord les cas faciles.
  • Facultatif : Docker, si vous voulez que l'environnement d'exécution soit identique d'une machine à l'autre.
  • Git, afin que chaque exécution soit liée à un commit.

Le vérificateur est la partie difficile. Consacrez-y la majeure partie de vos efforts, pas au harnais.

Installation étape par étape

Créez un environnement isolé pour que les dépendances de votre harnais n'entrent pas en collision avec celles de votre agent.

python -m venv .venv && source .venv/bin/activate

Sous Windows, activez plutôt avec .venv\Scripts\activate. Si vous préférez uv, l'équivalent est :

uv venv && source .venv/bin/activate

Installez l'outillage de test et d'analyse. pytest exécute les vérifications, pytest-repeat réexécute un même test N fois, et pandas/numpy gèrent l'agrégation.

pip install "pytest>=8" pytest-repeat pandas numpy

Gelez l'environnement exact afin qu'une exécution future puisse être reproduite. Ce fichier est un artefact que vous devriez commiter.

pip freeze > requirements.lock

Enregistrez la version du code avec chaque ensemble de résultats. Sans cela, un changement de cohérence n'est pas attribuable.

git rev-parse HEAD > .run-commit && cat .run-commit

Si vous voulez aussi isoler l'environnement d'exécution, construisez une image une fois et exécutez toutes les tentatives à l'intérieur.

docker build -t agent-under-test:1.0 .

Enfin, fixez la graine de hachage. La randomisation des hachages de Python modifie l'ordre d'itération des dictionnaires dans certains chemins de code, ce qui est une véritable source de variation d'une exécution à l'autre dans le routage des outils.

export PYTHONHASHSEED=0

C'est toute la chaîne d'outils. Aucun framework d'agent requis.

Construire le harnais de répétabilité

Le harnais a un seul rôle : appeler l'agent k fois par tâche, tout enregistrer et ne jamais jeter un échec. Placez ceci dans consistency/harness.py.

# consistency/harness.py
from __future__ import annotations

import json
import time
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Callable


@dataclass
class RunRecord:
    task_id: str
    attempt: int
    success: bool
    latency_s: float
    output: str
    error: str | None = None


def run_attempts(
    agent: Callable[[str], str],
    task_id: str,
    task_input: str,
    checker: Callable[[str], bool],
    attempts: int = 10,
    out_path: Path = Path("runs.jsonl"),
) -> list[RunRecord]:
    """Execute one task `attempts` times and append every result to disk."""
    records: list[RunRecord] = []
    for i in range(attempts):
        t0 = time.perf_counter()
        output, error = "", None
        try:
            output = agent(task_input)
        except Exception as exc:  # record, never swallow silently
            error = f"{type(exc).__name__}: {exc}"
        latency = time.perf_counter() - t0
        records.append(
            RunRecord(
                task_id=task_id,
                attempt=i,
                success=bool(error is None and checker(output)),
                latency_s=round(latency, 3),
                output=output,
                error=error,
            )
        )

    with out_path.open("a", encoding="utf-8") as fh:
        for rec in records:
            fh.write(json.dumps(asdict(rec)) + "\n")
    return records

Trois choix de conception comptent ici. Les sorties sont stockées intégralement, car vous ne pouvez pas déboguer une instabilité que vous n'avez pas capturée. Les échecs sont enregistrés plutôt que levés, car un plantage à la tentative 3 ne doit pas masquer les données des tentatives 1 et 2. Les résultats sont ajoutés à un fichier JSONL, de sorte qu'un processus planté ne perd que la tâche en cours.

Intégrez-le dans un test qui vérifie la distribution, et non une seule exécution.

# tests/test_consistency.py
from consistency.harness import run_attempts


def test_refund_agent_is_stable(agent, refund_checker):
    records = run_attempts(
        agent,
        task_id="refund-order-42",
        task_input="Refund order 42 in full.",
        checker=refund_checker,
        attempts=10,
    )
    failures = [r for r in records if not r.success]
    assert not failures, (
        f"{len(failures)}/10 attempts failed. "
        f"First error: {failures[0].error or failures[0].output[:200]}"
    )

Ce test ne passe que lorsque l'agent réussit 10 fois sur 10. Sur un agent réel, il échouera la première fois que vous l'exécuterez, et cet échec est le résultat utile.

Exemples d'utilisation

Exemple 1 : Conditionner un changement de prompt

Vous avez réécrit le prompt système. Est-ce que cela aide ? Calculez pass^k avant et après, sur le même ensemble de tâches.

import json
import pandas as pd

rows = [json.loads(line) for line in open("runs.jsonl", encoding="utf-8")]
df = pd.DataFrame(rows)

summary = (
    df.groupby("task_id")["success"]
      .agg(attempts="size", passes="sum")
      .assign(pass_at_1=lambda d: d["passes"] / d["attempts"])
      .assign(stable=lambda d: d["passes"] == d["attempts"])
)
print(summary)
print("pass^k (all attempts passed):", summary["stable"].mean().round(3))

Exécutez le bloc une fois sur le runs.jsonl de l'ancien prompt et une fois sur celui du nouveau. Si pass@1 s'améliore de 0,82 à 0,85 mais que pass^k chute de 0,60 à 0,45, le nouveau prompt achète une performance moyenne avec de la variance — généralement un mauvais compromis.

Exemple 2 : Déployer en canari une dépendance ou une mise à niveau de modèle

Exécutez le harnais sur le candidat et sur la version en place dans la même session, puis comparez tâche par tâche plutôt qu'en agrégé. Les nombres agrégés masquent les changements qui se compensent : cinq tâches corrigées, cinq tâches cassées, une moyenne indiscernable.

pivot = (
    df.pivot_table(index="task_id", columns="variant",
                   values="success", aggfunc="mean")
)
pivot["delta"] = pivot["candidate"] - pivot["baseline"]
print(pivot.sort_values("delta").head(10))  # regressions first

Le tri croissant fait apparaître d'abord les régressions, ce que vous voulez examiner.

Exemple 3 : Reproduire un échec instable

Quand une tâche échoue 3 fois sur 10, la sortie d'échec se trouve dans runs.jsonl, mais la cause est généralement dans la trace. Enveloppez vos appels d'outils dans une couche d'enregistrement/relecture afin qu'une tentative échouée puisse être réexécutée sans toucher au monde réel.

import hashlib
import json
from pathlib import Path

CASSETTE = Path("cassettes/tools.json")


def tool_key(name: str, args: dict) -> str:
    payload = json.dumps({"name": name, "args": args}, sort_keys=True)
    return hashlib.sha256(payload.encode()).hexdigest()[:16]


def load_cassette() -> dict:
    return json.loads(CASSETTE.read_text()) if CASSETTE.exists() else {}

En mode relecture, recherchez tool_key(name, args) dans la cassette et renvoyez la réponse enregistrée. En mode enregistrement, appelez l'outil réel et réécrivez la réponse. Cela transforme un symptôme non reproductible en test unitaire déterministe, et c'est l'étape au plus fort levier de tout le flux de travail.

D'où vient la variance

Une fois les échecs reproductibles, attribuez-les. En pratique, l'incohérence des agents se regroupe en cinq endroits.

Échantillonnage. Régler la température à zéro réduit la variance d'échantillonnage mais ne garantit pas des sorties identiques d'une exécution à l'autre ; le traitement par lots côté fournisseur, le matériel et les changements de version peuvent encore déplacer les résultats.

Temps et environnement. Les prompts qui intègrent la date actuelle, la locale de l'utilisateur ou un identifiant de session différeront d'une exécution à l'autre par construction. Gelez-les explicitement.

Non-déterminisme des outils. Les API en direct renvoient des données différentes. L'enregistrement/relecture élimine ce facteur comme variable de confusion — cela ne le corrige pas en production, mais cela vous dit si votre instabilité vient de vous ou du monde.

Dérive de la récupération. Si l'agent interroge un index qui est reconstruit entre les exécutions, le contexte récupéré change. Faites un instantané de l'index pour une exécution de benchmark.

Flux de contrôle. Les agents multi-étapes ont de nombreux points de décision. De petits taux d'erreur par étape se composent : dix étapes à 98 % de fiabilité par étape donnent environ 82 % de bout en bout. Mesurez par étape, pas seulement de bout en bout, sinon vous ne saurez pas où investir l'effort.

Lire les chiffres honnêtement

Un petit k produit des estimations bruitées, et il est facile de les surinterpréter. Utilisez un intervalle.

import math


def wilson(passes: int, n: int, z: float = 1.96) -> tuple[float, float]:
    """95% Wilson score interval for a binomial proportion."""
    if n == 0:
        return (0.0, 1.0)
    p = passes / n
    denom = 1 + z**2 / n
    centre = (p + z**2 / (2 * n)) / denom
    half = (z * math.sqrt(p * (1 - p) / n + z**2 / (4 * n**2))) / denom
    return (round(max(0.0, centre - half), 3), round(min(1.0, centre + half), 3))


print(wilson(8, 10))  # e.g. (0.49, 0.94)

Un « taux de réussite de 80 % » mesuré sur 10 exécutions est compatible avec un taux réel situé approximativement entre 50 % et 95 %. Ce n'est pas une preuve suffisante pour conditionner une release. Dix tentatives par tâche sur 20 tâches — 200 exécutions — donne une image bien plus précise, à un coût 20 fois supérieur. Choisissez la taille d'échantillon délibérément, et indiquez-la chaque fois que vous rapportez un taux.

Deux autres habitudes de reporting :

Séparez les faits vérifiés de l'interprétation. « L'agent a échoué 4 fois sur 50 exécutions sur la tâche X, avec l'erreur Y » est un fait. « Le récupérateur est le goulot d'étranglement » est une interprétation jusqu'à ce que vous la testiez.

Rapportez la pire tâche, pas seulement la moyenne. Un pass@1 de 0,95 avec une tâche à 0,30 est un système différent d'un 0,95 uniforme. La moyenne masque la queue que les utilisateurs trouveront.

Une checklist pratique

  • Chaque tâche a un vérificateur automatisé, même rudimentaire.
  • Chaque exécution est journalisée intégralement, y compris la sortie d'échec.
  • Chaque ensemble de résultats est étiqueté avec un hash de commit et un fichier de dépendances verrouillé.
  • Pass^k est rapporté aux côtés de pass@1, jamais à sa place.
  • Les appels d'outils sont enregistrés et rejouables.
  • Le temps, la locale et les identifiants de session sont gelés ou injectés.
  • Les régressions sont examinées tâche par tâche, triées par delta, avant tout agrégat.

Conclusion

« Ça a marché » est une hypothèse, pas un résultat. La question que soulève l'article d'IBM Research — le refera-t-il ? — est la bonne à poser à tout agent avant qu'il ne touche à la production, et y répondre ne nécessite pas un nouveau framework. Cela nécessite une boucle, un vérificateur, un journal et la volonté de rapporter le nombre pass^k plutôt que la meilleure exécution.

Exécutez votre meilleure tâche dix fois cette semaine. Si elle réussit les dix, exécutez-la vingt fois. Si ce n'est pas le cas, vous avez trouvé le travail — et vous l'avez trouvé dans une suite de tests plutôt que dans le compte d'un client.

Sources