Accueil / Articles

Pourquoi les agents à exécution prolongée ont besoin d’une machine à états : les rôles de Leader, de Worker et de Verifier

Quatre manières dont un cycle d’agent unique échoue sur des tâches longues, et comment une équipe composée d’un Leader, d’un Worker et d’un Verifier, pilotée par un moteur déterministe, les corrige, ainsi que les cas où il convient de l’omettre.

Éditeur : REACTAPP.TOP11 min de lecture2547 mots

Sources: MiniMax Agent Team Best Practice: An AI Agent Is a While Loop. A Reliable One Is a State Machine.

Passer d’un chatbot à un agent nécessite une boucle while, et non une avancée majeure de la recherche. C’est vrai, et c’est aussi là que s’arrêtent la plupart des explications et que commencent les difficultés en environnement de production. La question plus utile est de savoir pourquoi cette boucle échoue silencieusement sur des tâches d’une durée supérieure à dix minutes, et quelle structure peut la réparer. MiniMax a publié deux références très utiles à ce sujet : le tutoriel open source Mini-Agent, qui présente la boucle de base, ainsi qu’un rapport technique de leur équipe d’agents, décrivant ce qui a posé problème à grande échelle et le système de temps de exécution mis en place en réponse. Ces deux documents constituent la base de notre approche, et ce modèle peut être adapté à n’importe quel stack.

Une seule boucle, du début à la fin

En simplifiant un agent, on obtient quatre composants : un modèle, une liste en temps réel des messages, un registre des outils et une boucle qui les gère. Le référentiel Mini-Agent fonctionne de la manière suivante : à chaque itération, on vérifie d’abord si l’historique approche la limite de capacité du contexte, et le cas échéant on le condense. Ensuite, on envoie les messages ainsi que les définitions des outils au modèle. Si le modèle ne demande aucun outil, la boucle se termine. Sinon, on exécute les outils demandés, on ajoute leurs résultats aux messages et on répète le processus. Voici une version complète adaptée à tout endpoint compatible OpenAI, incluant deux outils réels ainsi que la fonction de résumé :

# naive_loop.py -- a single-agent tool loop,
# Mini-Agent style: summarize if needed ->
# llm.generate(messages, tools) -> execute
# tool_calls -> append results -> repeat.
import json
import subprocess
from openai import OpenAI

client = OpenAI()
MODEL = "gpt-4o-mini"
MAX_HISTORY = 40  # crude context budget


def read_file(path: str) -> str:
    with open(path) as f:
        return f.read()[:4000]


def run_cmd(cmd: str) -> str:
    p = subprocess.run(
        cmd, shell=True, timeout=30,
        capture_output=True, text=True)
    out = p.stdout + p.stderr
    return f"exit={p.returncode}\n{out[-2000:]}"


TOOLS = {"read_file": read_file,
         "run_cmd": run_cmd}


def schema(name, desc, arg):
    return {
        "type": "function",
        "function": {
            "name": name,
            "description": desc,
            "parameters": {
                "type": "object",
                "properties": {
                    arg: {"type": "string"},
                },
                "required": [arg],
            },
        },
    }


SCHEMAS = [
    schema("read_file",
           "Read a local text file.", "path"),
    schema("run_cmd",
           "Run a shell command.", "cmd"),
]



def summarize(msgs):
    # Mini-Agent compresses old turns with an
    # LLM summary call when the context nears
    # its limit; hard truncation is the v0.
    if len(msgs) <= MAX_HISTORY:
        return msgs
    return [msgs[0]] + msgs[-MAX_HISTORY:]


def run(task: str, max_steps: int = 20):
    msgs = [{"role": "user", "content": task}]
    for _ in range(max_steps):
        msgs = summarize(msgs)
        r = client.chat.completions.create(
            model=MODEL,
            messages=msgs,
            tools=SCHEMAS,
        )
        msg = r.choices[0].message
        msgs.append(msg)
        if not msg.tool_calls:
            return msg.content  # model says done
        for call in msg.tool_calls:
            fn = TOOLS[call.function.name]
            args = json.loads(
                call.function.arguments)
            try:
                out = fn(**args)
            except Exception as e:
                out = f"tool error: {e}"
            msgs.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": out,
            })
    return "hit max_steps -- gave up"


if __name__ == "__main__":
    print(run("Count the .py files here, then "
              "summarize naive_loop.py."))

Le boucle dispose de deux sorties, toutes deux importantes. Le succès est déterminé par le jugement du modèle lui-même, exprimé par l’absence de nouvelles appels d’outils. La sécurité repose sur un compteur simple, la limite de pas. Cette asymétrie, où seul le modèle décide quand le travail est terminé, est précisément ce qui cède en premier.

Lorsqu’une seule boucle échoue sur des tâches longues

Exécutez la même boucle sur une tâche de deux heures plutôt que sur une de deux minutes, et les échecs commencent. L’équipe MiniMax a listé ceux qu’ils rencontraient constamment. Aucun n’est un bug. Ce sont des propriétés liées à un contexte unique, de sorte qu’une fenêtre de temps plus large ou un prompt amélioré ne résout aucun d’eux.

L’anxiété liée au contexte apparaît en premier : l’agent s’arrête sans qu’on le lui demande. Après avoir effectué sept sous-tâches, il en termine trois et indique avoir accompli les tâches 1, 2 et 3, avant de demander s’il doit continuer. Les modèles ont une évaluation floue quant au moment où une tâche longue peut se terminer, et entraîner directement cette évaluation est extrêmement coûteux en termes financiers et d’efforts algorithmiques. Une boucle simple laisse la décision d’arrêt à sa partie la moins fiable.

Ensuite vient la dégradation : l’agent devient de plus en plus stupide à mesure que le contexte s’agrandit. À la cinquième minute, il est performant. À la soixante-cinquième minute, il se comporte comme un stagiaire distrait. Un écart mineur, comme une tâche de recherche qui se transforme en texte publicitaire, contamine tout ce qui suit, car chaque génération ultérieure s’appuie sur cette histoire altérée.

Troisièmement, rien ne vérifie le travail, puisque le juge est également le concurrent. Un agent unique qui produit le résultat final se note lui-même dans exactement le même contexte où il a commis des erreurs. Ce n’est pas de la malhonnêteté : il s’agit simplement d’inspecter une scène que le modèle a lui-même construite.

Quatrièmement, il y a le silence. Les interfaces de type chat font en sorte que les utilisateurs s’attendent à une réponse en quelques secondes, même pour une tâche d’une heure, mais une boucle intégrée à une appel de modèle ne peut pas indiquer qu’elle a reçu la demande ni promettre de donner des nouvelles. Selon le compte MiniMax, la question « pourquoi mon agent ne répond-il pas ? » a suscité plus de retours des utilisateurs que tout autre sujet.

La solution : trois rôles et un moteur déterministe

MiniMax répond de manière structurée. Trois types de rôles sont coordonnés par du code déterministe ordinaire, et non par une instruction astucieuse ; ce code est appelé le Team Engine. Un cycle de vie d’une tâche correspond à une Session, et le moteur fait passer chaque Session par trois états : production, vérification et finition, réactivant le producteur chaque fois que la vérification échoue.

  • Le Leader divise la demande de l’utilisateur en un plan structuré, choisit quels éléments s’exécutent en parallèle, répond à l’utilisateur en quelques secondes tandis que le reste se déroule en arrière-plan, puis fusionne les résultats.
  • Les Workers s’occupent de sous-tâches, chacune dans un contexte distinct doté de ses propres outils, mémoire et format de sortie requis. Cette séparation est importante car un Worker ne voit jamais qu’une seule sous-tâche, et non deux heures de données accumulées.
  • Les vérificateurs transforment « c’est terminé » en « cela peut être livré ». La relation entre eux est délibérément antagoniste : un travailleur qui termine une tâche lance le vérificateur, tandis qu’un vérificateur qui rejette la tâche redémarre le travailleur, à l’image de la relation entre la recherche et le développement et le contrôle qualité, où la qualité provient des itérations et non d’une surveillance minutieuse par un humain.
  • Le noyau du moteur est suffisamment petit pour être entièrement codé. Cette version n’a pas besoin de clé API, car des rôles fictifs remplacent les appels aux LLM et peuvent être remplacés sans affecter la machine à états :

    # team_engine.py -- the loop promoted to a
    # state machine. One task lifecycle is one
    # Session; deterministic code owns every
    # transition: producing -> verifying -> done.
    from dataclasses import dataclass, field
    
    PRODUCING = "producing"
    VERIFYING = "verifying"
    DONE = "done"
    FAILED = "failed"
    
    
    @dataclass
    class Session:
        goal: str
        state: str = PRODUCING
        attempts: int = 0
        artifact: str = ""
        reviews: list = field(default_factory=list)
    
    
    def run_session(s, worker, verifier,
                    max_retries: int = 3):
        while s.state not in (DONE, FAILED):
            if s.state == PRODUCING:
                s.attempts += 1
                s.artifact = worker(
                    s.goal, s.reviews)
                s.state = VERIFYING
            elif s.state == VERIFYING:
                ok, report = verifier(
                    s.goal, s.artifact)
                s.reviews.append(report)
                if ok:
                    s.state = DONE
                elif s.attempts >= max_retries:
                    s.state = FAILED
                else:
                    # Reject: wake the worker with
                    # the review log in context.
                    s.state = PRODUCING
        return s
    
    
    def run_team(goals, worker, verifier):
        # Leader's job: split the goal, run the
        # sessions (a thread pool in real use),
        # then merge N artifacts into 1 result.
        done, failed = [], []
        for g in goals:
            s = run_session(Session(g),
                            worker, verifier)
            if s.state == DONE:
                done.append(s.artifact)
            else:
                failed.append(s.goal)
        return done, failed
    
    
    if __name__ == "__main__":
        # Stub roles so the engine runs anywhere;
        # swap in LLM calls for the real thing.
        def worker(goal, reviews):
            v = len(reviews) + 1
            return f"draft v{v}: {goal}"
    
        def verifier(goal, artifact):
            ok = "v2" in artifact
            return ok, f"review {artifact!r}: {ok}"
    
        done, failed = run_team(
            ["intro", "benchmarks", "faq"],
            worker, verifier)
        print("delivered:", done)
        print("failed:", failed)
    

    Lorsque l’on l’exécute, la première version de chaque session est rejetée, le Worker se réveille avec le journal des révisions dans son contexte, et la deuxième version est acceptée. En le comparant à la boucle naïve, quelques différences sautent aux yeux.

    • Arrêt : la boucle unique se termine lorsque le modèle cesse d’émettre des appels d’outil (méthode floue) ou atteint la limite de pas, tandis que le moteur utilise des états explicites « done » et « failed » avec un budget de tentatives défini dans le code.
    • Vérification : dans la boucle, le même contexte qui a généré le travail le vérifie, alors que l’équipe utilise un vérificateur adversarial dans un contexte séparé, basé sur des commandes.
    • Parallélisme : la boucle fonctionne dans un seul contexte séquentiel, tandis que l’équipe exécute des Workers dans des sessions parallèles, avec le Leader qui fusionne plusieurs en un seul.
    • Persistence : la boucle conserve une liste de messages jusqu’à la synthèse, tandis que l’équipe conserve l’état de la session, les enregistrements de révision, les artefacts ainsi que la mémoire par agent.

    Le Leader n’est pas non plus limité à des appels ponctuels qui génèrent une sous-tâche et renvoient du texte. Dans la conception de MiniMax, il lit l’état en temps réel du moteur et peut envoyer des instructions supplémentaires dans une Session en cours de traitement ou de vérification, ce qui permet à la fois des actions proactives et des requêtes sur demande. Cela résout également le problème du silence : le Leader confirme en quelques secondes, et l’utilisateur reçoit ensuite des mises à jour aux points clés tels que démarré, bloqué, décision requise et terminé.

    Ancre du Vérificateur

    Le défaut d’auto-évaluation disparaît uniquement lorsque le verdict provient en dehors de tout modèle. Pour le code, MiniMax entoure son exécution d’un cadre de contrôle. Le travail s’effectue sur des branches, s’exécute dans des environnements isolés, les modifications sont représentées sous forme de différences, les tests peuvent être répétés, les revues laissent des traces et les échecs peuvent être reproduits. La condition la plus importante : une condition d’arrêt doit être liée à quelque chose de déterministe et observable depuis l’extérieur, comme les tests, les outils de vérification et le processus de compilation, et jamais au sentiment du modèle selon lequel le travail est terminé.

    # grounded_verifier.py -- evidence over vibes.
    # The verdict comes from exit codes and logs,
    # never from the model's self-assessment.
    import subprocess
    import sys
    
    PY = sys.executable
    
    # pytest exit codes: 0 = all passed,
    # 1 = failures, 5 = no tests collected
    # (acceptable for a docs-only change).
    PYTEST_OK = (0, 5)
    
    CHECKS = [
        ("diff", ["git", "diff", "--stat"], (0,)),
        ("build", [PY, "-m", "compileall",
                   "-q", "."], (0,)),
        ("tests", [PY, "-m", "pytest", "-q",
                   "--maxfail", "1"], PYTEST_OK),
    ]
    
    
    def run_check(name, cmd, ok_codes, cwd):
        p = subprocess.run(
            cmd, cwd=cwd,
            capture_output=True, text=True)
        tail = (p.stdout + p.stderr)[-2000:]
        return {
            "check": name,
            "cmd": " ".join(cmd),
            "exit_code": p.returncode,
            "ok": p.returncode in ok_codes,
            "output_tail": tail,
        }
    
    
    def verify(repo: str):
        evidence = []
        for name, cmd, ok_codes in CHECKS:
            e = run_check(name, cmd, ok_codes, repo)
            evidence.append(e)
            if not e["ok"]:
                # Reject with logs attached: the
                # worker retries against real
                # errors, not "please improve".
                return False, evidence
        return True, evidence
    
    
    
    if __name__ == "__main__":
        ok, ev = verify(".")
        for e in ev:
            print(f"{e['check']:>6} exit "
                  f"{e['exit_code']} ok={e['ok']}")
        print("verdict:", "pass" if ok else "fail")
    

    La liste des preuves sert également de registre d’examen, faisant partie d’un suivi reproductible et traçable qui permet de distinguer ce qui est véritablement livrable de ce qui n’est que plausible. MiniMax divise encore davantage le rôle de l’examinateur. Un testeur génère des preuves externes en exécutant les tests, en compressant les journaux d’erreurs et en ajoutant une reproduction minimale. Un examinateur pose une question différente : non pas savoir si le test a réussi, mais plutôt si le code doit être modifié de cette manière, en examinant les limites d’abstraction, les nouvelles dépendances, les permissions élargies ainsi que les secrets qui s’échappent dans les journaux. Les examinateurs peuvent travailler en parallèle selon des spécialités telles que générale, sécurité et domaine.

    Le coût d’une équipe, et quand l’éviter

    Une équipe n’est pas un parallélisme gratuit. Trois coûts apparaissent dès que les agents collaborent, et un élargissement de la fenêtre de contexte n’en élimine aucun.

    Coût de transfert : les mêmes informations doivent être réorganisées entre les agents, de sorte que les pages de recherche deviennent un bref destiné à l’auteur, qui sert ensuite de données d’entrée pour une vérification de format. La solution consiste à transmettre des artefacts, c’est-à-dire des fichiers lisible transmis via un chemin accompagné de brèves synthèses, plutôt que des données brutes issues du contexte.

    Coût de partage : chaque jeton partagé à l’échelle mondiale est facturé par chaque travailleur à chaque tour. Un schéma fonctionnel permet de conserver la mémoire privée à chaque agent, de leur permettre de s’interagir via des interruptions CLI, et offre une table blanche partagée que les agents consultent uniquement lorsqu’ils en ont besoin.

    Le troisième coût est celui de l’agrégation. Collecter dix versions partielles est simple. Les fusionner en un seul artefact contenant des faits, des citations et un ton cohérent constitue la tâche la plus difficile pour le chef de projet, et cela nécessite une étape de fusion plutôt que davantage de personnel.

    Le consensus a également un prix. Un débat libre entre les agents peut consommer plusieurs fois plus de tokens que la correction autonome isolée sans pour autant améliorer la précision. La valeur réside dans la structure : sessions délimitées, artefacts explicites et un vérificateur autorisé à rejeter.

    Omettez l’équipe lorsque la tâche est petite, locale et peu coûteuse à vérifier, car un seul cycle coûte moins cher. Utilisez-la lorsque le travail se divise clairement, que les erreurs sont coûteuses et qu’une vérification indépendante peut fournir des preuves que le modèle ne peut pas se convaincre lui-même. Un bon critère : n’ajoutez un rôle que s’il couvre un mode de défaillance que aucun autre rôle ne couvre, sinon la surcharge liée à la collaboration ne rapporte rien.

    Quoi construire en premier

    Commencez avec la plus petite machine à états en laquelle vous pouvez avoir confiance. Chaque session dispose d’un objectif, d’un artefact, d’un journal de révision et d’un budget de tentatives, et chaque transition est clairement définie. Placez le vérificateur derrière un système dont les réponses proviennent de tests, d’outils de vérification de syntaxe, de processus de compilation ou d’autres contrôles mécaniques similaires. Les travailleurs en parallèle viendront plus tard : le parallélisme est une optimisation, tandis que les limites de correction relèvent de l’architecture. Laissez visible l’étape de fusion effectuée par le leader, conservez les artefacts de transfert ouverts à l’inspection et notez pourquoi chaque session s’est terminée.

    Ce qui en ressort n’est pas une intelligence autonome. Il s’agit d’un flux de travail contrôlé : les modèles proposent des idées, les outils effectuent les tâches, et du code ordinaire détermine si le travail est terminé. Construisez manuellement la boucle de base une fois avant d’adopter un framework. Prévoyez que des tâches longues puissent endommager sa structure en raison de l’anxiété, du dérive, de l’auto-évaluation et du silence, ce dont aucune incitation ne peut venir à bout. Considérez les systèmes multi-agents comme un environnement de exécution doté d’un plan de contrôle comprenant des états, des événements, des permissions, une capacité d’observation et des artefacts réparables, et non comme une simple simulation dans une instruction. Prévoyez également des mécanismes de transfert, de partage et d’agrégation avant de former une équipe, et ne formez jamais d’équipe à cause d’une faute de frappe.

    Lectures complémentaires

  • Contrats de délégation pour les équipes d’agents IA : portée, validation et gestion des échecs — Pourquoi les titres de poste des agents n’empêchent pas des transferts défaillants, et comment tester un contrat de délégation avec des politiques contraignantes, des règles de validation et d’échec indépendantes.
  • Exécuter des agents de codage locaux sur un Mac avec oMLX : explication du cache KV basé sur le disque — Pourquoi les agents locaux ralentissent lors de sessions longues, comment oMLX cache les préfixes sur SSD, et comment l’installer ainsi que le connecter à Claude Code, Codex, Cursor et OpenCode sur Apple Silicon.
  • Permettre aux IA de interroger Postgres en toute sécurité : serveurs MCP, configuration et bonnes pratiques — Comment un serveur PostgreSQL MCP permet à Claude Code ou Cursor de lire des schémas en temps réel, ainsi qu’une comparaison entre CrystalDBA, Supabase et le serveur de référence archivé, et des habitudes de sécurité à adopter.
  • Un agent, c’est une boucle while : LangChain create_agent et LangGraph Fit — Un agent qui appelle des outils en vingt lignes, les quatre problèmes de production rencontrés par les boucles, et quand passer de LangChain create_agent aux graphes d’état de LangGraph.
  • Notes pratiques : Agent Loop Engineering : Un blog complet pour passer de l’IA en développement à sa mise en production — Guide pas à pas des Notes pratiques : Agent Loop Engineering : Un blog complet pour passer de l’IA en développement à sa mise en production : contrats, vérifications et emplacements de code prêts à l’emploi pour les équipes qui implémentent ce modèle.