Faire en sorte qu'un agent de code aille vraiment au bout
J'ai passé des mois à regarder des agents de code démarrer une tâche avec enthousiasme, puis s'arrêter net à mi-chemin : une fonction écrite mais jamais testée, un import oublié, un « voilà, ça devrait marcher » suivi de rien. Sur Klody Code AI, mon agent 100 % local, j'ai fini par comprendre que « aller au bout » n'est pas une question de modèle plus gros, mais d'architecture autour du modèle.
Pourquoi un agent naïf abandonne
Un agent naïf, c'est une seule requête au modèle : « voici la tâche, débrouille-toi ». Le problème est structurel. Le modèle génère une réponse plausible et s'arrête dès que le texte ressemble à une solution terminée. Il n'a aucun moyen de savoir si son code compile, si les tests passent, si le fichier existe vraiment. Il optimise la vraisemblance, pas le résultat.
Concrètement, ce qui casse un agent local, je l'ai vu des dizaines de fois :
- Il « imagine » une API qui n'existe pas (
foo.bar()hallucinée) et ne le vérifie jamais. - Il modifie un fichier, oublie un
import, et déclare la tâche finie. - Face à une tâche complexe, il produit une réponse trop courte parce qu'il n'a pas « prévu » assez d'étapes.
- Il tourne en rond sur la même erreur sans jamais changer d'approche.
Le point commun : aucune boucle de rétroaction. Le modèle ne se confronte jamais au réel. Les briques qui suivent servent toutes à réintroduire ce réel dans la boucle.
Routage adaptatif : ne pas tirer au canon sur une mouche
La première erreur serait de traiter chaque tâche avec le même effort. Renommer une variable et refactoriser un module d'authentification ne demandent pas la même profondeur. Sur un modèle local, ce n'est pas anecdotique : chaque itération coûte des secondes bien réelles sur ma machine.
Klody classe donc la tâche en amont — easy, medium, hard — et adapte le budget en conséquence : nombre d'itérations autorisées, nombre de candidats générés, activation ou non de la vérification lourde.
tache = classifier(prompt)
if tache == "easy":
budget = Budget(iterations=3, candidats=1)
elif tache == "medium":
budget = Budget(iterations=8, candidats=3)
else: # hard
budget = Budget(iterations=20, candidats=5, tests=True)
L'intérêt est double : on ne gaspille pas trois minutes sur un one-liner, et surtout on autorise l'agent à insister sur les tâches difficiles. Un agent qui s'arrête trop tôt, c'est souvent un agent à qui on n'a jamais donné le droit d'itérer vingt fois.
ReAct : raisonner, agir, observer — puis recommencer
Le cœur de la persistance, c'est la boucle ReAct (Reasoning + Acting). Au lieu d'une réponse d'un bloc, l'agent alterne trois temps : il raisonne sur l'étape suivante, il agit (lance un outil, écrit un fichier, exécute une commande), puis il observe le résultat réel. Et il recommence tant que la tâche n'est pas résolue.
while not resolu and iterations < budget.max:
pensee = modele.reflechir(contexte)
action = modele.choisir_outil(pensee)
obs = executer(action) # le RÉEL entre ici
contexte = contexte + obs
resolu = verifier(contexte)
Ce obs change tout. Quand l'agent hallucine une méthode, l'exécution renvoie une AttributeError, et cette erreur revient dans le contexte. Le modèle voit son propre échec et corrige. L'observation est ce qui empêche de déclarer victoire dans le vide. C'est aussi ce qui transforme un modèle 7B local, individuellement médiocre, en quelque chose qui va au bout : il ne devine pas juste du premier coup, il converge par corrections successives.
Best-of-N : plusieurs tentatives, on garde la meilleure
Un modèle local a de la variance. La même invite donne parfois une bonne solution, parfois une bancale. Plutôt que de parier sur un seul tirage, le Best-of-N génère plusieurs candidats en parallèle et sélectionne le meilleur selon un critère objectif : est-ce que les tests passent ? le code compile-t-il ? le linter est-il content ?
candidats = [generer(prompt) for _ in range(N)]
notes = [scorer(c) for c in candidats] # tests, lint, compilation
meilleur = candidats[argmax(notes)]
La nuance importante : le scoring doit être mesuré, pas jugé par le modèle lui-même. Un modèle qui note ses propres réponses se congratule volontiers. Un test qui échoue, lui, ne ment pas. C'est pour ça que Best-of-N et vérification vont de pair — l'un sans l'autre ne sert à rien.
La vérification : le seul juge qui ne triche pas
Tout repose au fond sur une conviction : une tâche n'est finie que si quelque chose de vérifiable le confirme. Sur Klody, ce « quelque chose » est une combinaison de garde-fous : exécution des tests, vérification de compilation, linting, contrôle que les fichiers annoncés existent bien. Le projet lui-même tourne sur environ 699 tests — non pas comme trophée, mais parce que ce sont eux qui servent de filet à l'agent quand il travaille sur son propre code.
La vérification agit comme condition de sortie de la boucle ReAct et comme fonction de score du Best-of-N. C'est la même idée déclinée : remplacer « le texte a l'air fini » par « une machine confirme que c'est fini ».
Le prix à payer, honnêtement
Rien de tout cela n'est gratuit. Best-of-N à cinq candidats, c'est cinq fois plus de calcul. Une boucle ReAct à vingt itérations peut prendre plusieurs minutes en local. La vérification par tests suppose que ces tests existent et soient rapides. Sur une petite tâche, tout ce dispositif serait un gâchis pur — d'où le routage adaptatif, qui n'est pas un luxe mais le régulateur qui rend l'ensemble soutenable.
Ce que j'ai fini par accepter, c'est qu'un agent local fiable n'est pas un modèle qui a toujours raison. C'est un système médiocre en un coup mais têtu : il se trompe, il l'observe, il recommence, et il ne s'arrête que quand une preuve — pas une impression — lui dit que c'est bon.