L'ingénierie de boucle : arrête de prompter, construis la boucle
Passer de prompter un agent de code à la main à une boucle bornée qui le relance jusqu'à l'atteinte d'un objectif, avec contrôles, mémoire, budgets et un script bash à essayer.
par Patrick Kamtchueng Kom · Publié
Observe-toi travailler avec un agent de code pendant une heure. Tu tapes une demande, tu attends, tu lis le résultat, tu lances les tests, tu recolles l’échec, tu écris « réessaie, mais touche pas à la migration », tu attends encore. La majorité de ce temps-là, ce n’est pas du jugement. C’est toi qui joues le rôle d’une boucle while très lente.
L’ingénierie de boucle, c’est l’habitude de remarquer ça et de déplacer la partie répétitive dans un script. Tu écris l’objectif, les contrôles et les limites une seule fois. La boucle s’occupe de prompter. Tu reviens à une branche au vert, ou à une note claire qui explique pourquoi elle s’est arrêtée.
- DécouvrirLire l'objectif, le repo, le fichier mémoire et le dernier échec
- PlanifierChoisir un seul petit pas vers l'objectif
- ExécuterL'agent modifie le code, en mode headless
- VérifierLa boucle lance les contrôles, pas l'agent
↺ Pas encore fini? On note ce qui s'est passé et on refait un tour, jusqu'à l'objectif atteint ou le budget épuisé
Prompter vs boucler
Prompter, c’est une conversation. Boucler, c’est un système. Les deux sont utiles, mais pas pour le même travail.
| Prompter à la main | Construire une boucle | |
|---|---|---|
| Qui décide de « réessayer »? | Toi, après avoir lu le résultat | Un script, selon le résultat des contrôles |
| C’est quoi, « fini »? | « Ça m’a l’air correct » | Une commande qui sort avec le code 0 |
| Où vit le contexte? | Dans ta tête et l’historique du chat | Dans des fichiers que la boucle relit à chaque tour |
| Où passe ton temps | À surveiller chaque tour | À écrire des objectifs, des contrôles et des limites |
| Idéal pour | Explorer, problèmes flous, design | Du travail bien défini avec une ligne d’arrivée vérifiable |
Boucle ouverte vs boucle fermée
« Laisse l’agent continuer tout seul », c’est aussi une boucle. Mais une boucle ouverte : rien à l’extérieur de l’agent ne juge si le travail est bon, et rien ne décide quand arrêter. C’est comme ça qu’on se retrouve lundi matin avec un diff de 90 fichiers et une facture surprise.
Une boucle fermée a un chemin de rétroaction et une limite. Le résultat de chaque tour est mesuré contre un objectif, et la boucle se termine volontairement.
✕ Boucle ouverte
- –Roule jusqu'à ce que quelqu'un remarque qu'elle roule encore
- –« Fini » veut dire que l'agent a dit que c'était fini
- –Aucun plafond d'itérations, de jetons ou de temps
- –L'agent corrige sa propre copie
- –Chaque tour repart de zéro
✓ Boucle fermée (bornée)
- +Part d'un objectif écrit avec des critères de fin vérifiables
- +La vérification roule en dehors de l'agent, dans le script de boucle
- +Plafonds fermes sur les itérations, la dépense et la durée
- +Sort sur succès, sur budget épuisé ou quand elle est bloquée
- +Note ce qu'elle a appris pour l'itération suivante
L’anatomie d’une boucle fermée
Toutes les boucles fermées que j’ai construites ont les mêmes cinq morceaux. S’il en manque un, la boucle roule quand même; on ne peut juste pas s’y fier.
🃏 Les cinq morceaux, une ligne chacun
Touchez une carte pour la retourner
Écrire un objectif que la boucle peut vérifier
L’objectif, c’est la partie qu’on bâcle, et celle qui décide de tout le reste. « Améliorer la page de recherche » ne donne rien à mesurer à la boucle. « Le fichier search.spec.ts passe et le script bench:search rapporte un p95 sous 200 ms », oui.
Un bon test : est-ce qu’un script shell pourrait décider, sans LLM, si c’est fini? Si oui, tu as des critères de fin. Sinon, tu as un souhait, et il te faut un point de contrôle humain quelque part.
🎮 La boucle peut-elle vérifier ça toute seule?
1 / 9 · Pointage: 0
Classe chaque objectif : un script peut-il le vérifier, ou faut-il un point de contrôle humain?
Les contrôles : les moins chers d’abord
Les contrôles, ce sont les yeux de la boucle. Lance-les du moins cher au plus cher, pour que les rapides échouent en premier et que tu ne paies pas une suite de 10 minutes pour découvrir une erreur de syntaxe.
- 1
à chaque tour
Formatage et lint
Quelques secondes. Élimine le bruit pour que les contrôles suivants voient les vrais problèmes.
- 2
à chaque tour
Types
Attrape les API inventées et les mauvaises structures, l'erreur d'agent la plus fréquente.
- 3
à chaque tour
Tests ciblés
Les tests les plus proches du changement. Une rétroaction rapide sur l'objectif lui-même.
- 4
si le reste passe
Suite complète et critère d'acceptation
La commande qui définit « fini ». Seulement quand les contrôles rapides sont au vert.
- 5
fonctionnalités LLM
Evals
Pour des prompts, des agents ou du code de classement : un jeu de données fixe, noté de la même façon à chaque fois.
- 6
à la frontière
Point de contrôle humain
Avant le merge, avant tout ce qui est irréversible, et chaque fois que la boucle sort pour une autre raison que le succès.
La mémoire entre les itérations
Un agent headless commence chaque tour avec une page blanche. Sans mémoire, l’itération 5 refait l’erreur de l’itération 2. La solution est plate, mais elle marche : de simples fichiers que la boucle relit avant chaque tour et que l’agent complète après chaque tour.
J’en garde deux. Un fichier de progression pour la tâche en cours, et un fichier de leçons qui lui survit.
# progress.md (cette tâche seulement; la boucle réinjecte ~60 dernières lignes)
## iter 3
- Ajout du paramètre cursor dans OrderRepository.list; typecheck au vert.
- orders.spec.ts : 2 échecs, les deux « expected nextCursor to be null on last page ».
- Prochaine étape : gérer la dernière page dans le service, pas le contrôleur.
## iter 4
- Cas de la dernière page corrigé dans OrderService. Tests orders au vert.
- Le lint échoue : import inutilisé dans orders.controller.ts. Prochaine étape : l'enlever.
# lessons.md (survit d'une tâche à l'autre, révisé par un humain chaque semaine)
- Les tests d'intégration ont besoin de `docker compose up db` avant; pas les tests unitaires.
- Ne jamais mocker OrderRepository dans les tests de service, utiliser le fake en mémoire.
- Les dates de l'API sont des chaînes ISO en UTC. Ne pas convertir en heure locale.
Git, c’est aussi de la mémoire. Faire un commit à chaque étape au vert donne à la boucle des points de retour, et à toi un historique lisible de comment elle s’est rendue là.
Une boucle minimale à essayer
Voici une petite esquisse bash d’une boucle fermée. Ce n’est pas un framework, et c’est voulu : une cinquantaine de lignes que tu peux lire, t’approprier et modifier. Elle utilise Claude Code en mode headless (claude -p); le commentaire montre où mettre codex exec si tu utilises Codex.
#!/usr/bin/env bash
# loop.sh : relance un agent de code jusqu'à ce que les contrôles passent,
# que le budget soit épuisé ou qu'il soit bloqué.
# À lancer sur une nouvelle branche, à partir d'un arbre de travail propre.
set -uo pipefail
MAX_ITER="${MAX_ITER:-8}" # plafond de tours
GATES="${GATES:-npm run lint && npm run typecheck && npm test}"
PROTECTED="tests/" # fichiers qui définissent « fini »
TASK=task.md; PROGRESS=progress.md; LOGS=.loop
mkdir -p "$LOGS"; touch "$PROGRESS"
git diff --quiet && git diff --cached --quiet || { echo "Pars d'un arbre propre."; exit 1; }
last_sig=""; same=0
for i in $(seq 1 "$MAX_ITER"); do
echo "== itération $i/$MAX_ITER"
prompt="$(cat "$TASK")
## Progression jusqu'ici
$(tail -n 60 "$PROGRESS")
## Sortie de la dernière passe de contrôles
$(tail -n 80 "$LOGS/gates.log" 2>/dev/null)
Fais UN seul petit pas vers l'objectif. Ne modifie rien sous $PROTECTED.
Ensuite, ajoute une courte entrée '## iter $i' dans $PROGRESS : ce que tu as
changé, ce que tu as appris, quelle devrait être la prochaine étape."
# Remplace cette ligne par : codex exec --sandbox workspace-write "$prompt"
claude -p "$prompt" --permission-mode acceptEdits --max-budget-usd 1 \
> "$LOGS/agent-$i.log" 2>&1
# Garde-fou : annuler toute modification des fichiers qui définissent « fini ».
if git status --porcelain -- "$PROTECTED" | grep -q .; then
git checkout -- "$PROTECTED"; git clean -fdq -- "$PROTECTED"
echo "- iter $i : l'agent a modifié $PROTECTED, changements annulés." >> "$PROGRESS"
fi
# Vérifier : c'est la boucle qui lance les contrôles, pas l'agent.
if bash -c "$GATES" > "$LOGS/gates.log" 2>&1; then
git add -A && git commit -qm "loop: objectif atteint à l'itération $i"
echo "Terminé en $i itérations. Révise la branche."; exit 0
fi
# Détection de blocage : même signature d'échec trois fois de suite.
sig="$(grep -iE 'error|fail' "$LOGS/gates.log" | sort | cksum)"
if [ "$sig" = "$last_sig" ]; then same=$((same + 1)); else same=1; fi
last_sig="$sig"
if [ "$same" -ge 3 ]; then
echo "Bloqué : même échec 3 fois. On passe la main à un humain."; exit 3
fi
done
echo "Budget épuisé après $MAX_ITER itérations. Voir $PROGRESS."; exit 2
Quelques choix qui valent la peine d’être copiés :
- C’est la boucle qui lance les contrôles. L’agent n’a jamais le droit d’annoncer « tout est vert ». Ce sont les codes de sortie qui parlent.
- Trois sorties distinctes. 0 pour le succès, 2 pour le budget, 3 pour le blocage. Ce qui enveloppe le script (toi, un cron, la CI) peut réagir différemment à chacune.
- Un pas par tour. Des petits tours donnent des petits diffs, et un échec pointe vers un seul changement au lieu de dix.
- Deux budgets.
MAX_ITERplafonne le nombre de tours, et le plafond de dépense par exécution (vérifie l’option équivalente dans la doc de ton agent) plafonne chaque tour.
De ton portable à la CI
Une fois qu’une boucle marche en local, le même script roule partout où un agent headless peut rouler : un cron, un conteneur, un runner de CI. Le modèle qui fonctionne bien : « la boucle sur une branche, l’humain sur la pull request ».
# Esquisse d'une job de CI planifiée. Adapte les noms et les secrets à ta plateforme.
on:
schedule: [{ cron: "0 3 * * 1-5" }]
jobs:
dependency-loop:
runs-on: ubuntu-latest
timeout-minutes: 45 # budget de temps réel
steps:
- uses: actions/checkout@v4
- run: npm ci && npm i -g @anthropic-ai/claude-code
- run: git switch -c loop/deps-$(date +%F)
- run: MAX_ITER=6 ./loop.sh # sort avec 0, 2 ou 3
- run: gh pr create --fill --draft # un humain révise chaque PR de boucle
if: success()
Garde les permissions du runner serrées : un jeton limité qui peut pousser des branches et ouvrir des PR en brouillon, pas merger dans main. Le timeout de la CI, c’est un budget de plus, et un qui coûte rien à configurer.
Une boucle ou une flotte?
Une fois qu’une boucle marche, c’est tentant d’en rouler dix. Les boucles en parallèle sont excellentes quand les tâches sont vraiment indépendantes : une boucle par package qui échoue dans un monorepo, une par règle de lint à adopter, une par module à migrer. Donne à chacune sa branche et son répertoire de travail (les git worktrees sont parfaits pour ça) pour qu’elles ne se marchent jamais sur les pieds.
| Une seule boucle | Flotte de boucles en parallèle | |
|---|---|---|
| Idéal pour | Un objectif avec une ligne d’arrivée claire | Plusieurs objectifs semblables et indépendants |
| Isolation | Une branche | Une branche et un worktree par boucle |
| Risque principal | Rester bloquée en silence | Conflits de merge, dépense totale qui s’emballe |
| Vrai goulot | Tes contrôles | Ta capacité de revue |
Comment les boucles échouent
Les boucles échouent de façons prévisibles. Retourne chaque carte pour le symptôme et le garde-fou qui le prévient.
🃏 Modes d'échec des boucles
Touchez une carte pour la retourner
D’opérateur à ingénieur
Quand tu promptes à la main, ta valeur est dans chaque tour : lire, pousser, corriger. Quand tu construis des boucles, ta valeur monte d’un niveau. Tu écris des objectifs assez précis pour être vérifiés, des contrôles assez stricts pour qu’on s’y fie et des limites assez serrées pour dormir tranquille. Ensuite, tu améliores la boucle elle-même, comme tu améliorerais un pipeline de build.
Ça change vraiment ta journée. Moins de « réessaie ». Plus de « pourquoi la boucle a eu besoin de trois essais pour ça, et qu’est-ce qui ferait que ça en prenne un seul? » La réponse, c’est souvent un meilleur test, un objectif plus clair ou une nouvelle ligne dans le fichier d’instructions. Et tout ça aide les humains aussi.
Ton repo est-il prêt pour une boucle?
Sois honnête. Une boucle amplifie ce que tu as déjà, y compris des tests faibles.
📊 Prêt à boucler?
1. Les tests, le typecheck et le lint roulent chacun avec une seule commande à partir d'un checkout propre.
Pas du toutComplètement2. Les contrôles rapides prennent moins de deux ou trois minutes.
Pas du toutComplètement3. Nos tests échouent quand le comportement brise, pas seulement quand le code ne compile pas.
Pas du toutComplètement4. On peut écrire l'objectif d'une tâche typique comme une commande qui sort avec 0 quand c'est fini.
Pas du toutComplètement5. Notre fichier d'instructions d'agent liste les commandes, les conventions et les zones interdites.
Pas du toutComplètement6. On a un endroit pour rouler des agents headless avec des accès limités (conteneur, runner, sandbox).
Pas du toutComplètement7. Quelqu'un est responsable de réviser ce que sortent les boucles en un jour ou deux.
Pas du toutComplètement
Teste-toi
🧠 Ingénierie de boucle : vérification rapide
Pointage: 0 / 5
1. Qu'est-ce qui rend une boucle « fermée »?
2. Qui devrait décider que les tests ont passé?
3. Le même test échoue avec la même erreur depuis trois itérations. Meilleur réflexe?
4. Pourquoi garder un fichier de progression entre les itérations?
5. Ton équipe roule 12 boucles en parallèle et les PR attendent une semaine avant d'être révisées. La solution?
Construis ta première boucle cette semaine
✅ Ta première boucle fermée
0 / 9 complété
✅ Avant de laisser une boucle rouler toute seule
0 / 6 complété