Le levier sur le prompt & l'équation Ingénieurs + Code + Agents
Ce qu'une usine logicielle vous apporte — des résultats d'agents reproductibles, mesurables et moins chers — et la première pièce que vous posez : le plan.
Vendredi, vous confiez une fonctionnalité à un agent de code. Vingt minutes plus tard, l’implémentation est propre et les tests passent. Lundi, la même demande produit une autre architecture, deux tests rouges et un transcript de quarante pages à relire.
Ce livre transforme le vendredi en régime normal. L’outil s’appelle une usine logicielle :
un petit système en Python et en YAML qui entoure vos agents et vous donne quatre choses
qu’aucun prompt ne produit seul. Des résultats reproductibles. Un « fini » vérifiable
par une commande. Une trace lisible de chaque exécution. Un coût connu, réductible phase
par phase. Vous la construisez pièce par pièce, un chapitre par jour, dans un repo compagnon
nommé plume-factory. Aujourd’hui vous posez la première pièce : le plan.
Le levier sur le prompt
L’idée en une phrase
Une usine logicielle donne du levier sur votre prompt : la même demande, formulée pareil,
produit un résultat meilleur, reproductible et mesurable, parce que l’installation autour du
prompt fait le travail que vous refaisiez à la main à chaque session. La première pièce de cette
installation, PLAN.md, vit du côté déterministe de l’usine. C’est un document que vous
possédez, pas une sortie d’agent.
Points clés
- Tout le monde obtient un bon résultat une fois. Presque personne ne l’obtient deux fois. C’est le constat qui fonde ce livre. Le problème n’est pas la qualité du modèle, c’est que personne ne possède la boucle.
- Le remède : sortir le pilotage du prompt et le mettre dans du Python. Le séquencement, les reprises et l’acceptation appartiennent au code. L’agent travaille à l’intérieur d’une phase nommée, avec une entrée et une sortie bornées.
- Le levier se mesure : nombre de phases nommées, taux de runs acceptés, coût et durée par run. « Ça marche mieux » n’est pas une mesure.
- Le levier est cumulatif. Une pièce posée sert tous les runs suivants. Un prompt mieux écrit sert un run, puis disparaît avec sa session.
Exemple concret
Prenez une demande que vous ferez réellement plus loin dans ce livre : ajouter une fonctionnalité à Plume, la petite application d’écriture qui naîtra au chapitre 2 et servira de banc d’essai à l’usine.
- Sans usine : une session, un agent, aucune coupure. Le contexte est renvoyé à chaque tour et gonfle sans limite. Sur un travail de cette taille, comptez de l’ordre du million de tokens cumulés en entrée. « Fini » veut dire « l’agent s’est tu ». Trois exécutions donnent trois implémentations différentes, et la seule trace est un transcript à relire en entier.
- Avec l’usine : quatre phases nommées, plan → build → test → review, chacune avec son entrée et sa sortie bornées. Le volume de tokens baisse d’environ un tiers à une moitié, la durée tombe à quelques minutes, et le verdict devient binaire. La suite de tests passe, ou l’échec repart vers l’agent pour correction.
- Le coût dépend surtout du modèle choisi. Le même run coûte quelques dizaines de centimes sur un modèle intermédiaire et plusieurs dollars sur un modèle frontier, soit un facteur 10 environ. Choisir le modèle phase par phase est le sujet du module 4. Retenez ici que l’usine rend ce choix possible, parce qu’elle sait de quelle phase on parle.
Les étages du levier
| Étage | Ce que vous investissez | Ce que vous obtenez | Reproductible ? |
|---|---|---|---|
| Prompt nu | deux minutes d’écriture | un résultat, parfois bon | non |
| Deux agents chaînés | un script shell | un enchaînement, aucun verdict | non |
| Phases + enveloppes | ~200 lignes de Python | une trace lisible, un « fini » nommé | en partie |
| Phases + gates + roster | l’usine de ce livre | un verdict binaire, un coût connu | oui |
Commande — le même travail, deux étages du levier
Les deux harnais au programme de ce livre, pi et Claude Code, acceptent un tour non interactif.
C’est l’étage 0 : déjà utile, mais sans mémoire d’un run à l’autre.
# Étage 0 — le prompt nu, lancé à la main. Le résultat dépend entièrement
# de cette session-là ; demain, il faudra tout redemander.
pi -p "ajoute l'export Markdown a Plume et fais passer les tests"
claude -p "ajoute l'export Markdown a Plume et fais passer les tests"
# Étage 3 — le même travail confié à l'usine. Le harnais n'est plus invoqué
# à la main : un script Python le lance phase par phase, puis juge le résultat.
uv run adws/adw_plan_build.py "ajoute l'export Markdown a Plume"
Sous le capot, l’usine construit un appel bien plus riche : sortie machine, session reprenable, outils autorisés. Vous écrirez cet appel vous-même au chapitre 7, pour les deux harnais.
Piège courant : « il suffit d’un meilleur prompt » est inexact. Un prompt mieux écrit améliore un run et meurt avec lui. Il ne crée aucune phase nommée, aucun critère d’acceptation, aucune trace interrogeable. La bonne question n’est pas « comment mieux demander ? » mais « qu’est-ce qui, dans ce run, mérite d’être écrit une fois pour tous les runs suivants ? ».
L’équation Ingénieurs + Code + Agents
L’idée en une phrase
Trois acteurs fabriquent le logiciel — vous, le code et les agents — et l’usine les tient séparés à dessein. Chacun devient un couloir de la trace, avec son coût et sa compétence propres. Le levier vient de solliciter le bon acteur au bon moment, pas du nombre d’agents. La frontière entre code et agent passe exactement là où le jugement commence.
Points clés
- Trois sortes de phases, trois couloirs :
engineer(vous),agent(un appel borné, prompt en entrée et réponse structurée en sortie) etcode(un pas déterministe qui tient seul). - Le code ne coûte rien. Il s’exécute instantanément, se modifie en une seconde, et vous le possédez, ce qui n’est vrai d’aucun modèle loué au token.
- La règle de partage : si l’invocation est déjà connue, elle s’écrit en code.
bun testn’est pas un jugement,ruff checknon plus. Ce qui demande de lire et de décider reste à l’agent. - Un agent qui redécouvre votre lanceur de tests brûle une fenêtre de contexte pour apprendre ce qu’un subprocess sait déjà, et vous facture ce privilège à chaque run.
- Une phase
coden’est jamais enfouie dans une phase agent. La trace doit montrer exactement quand le code a tourné et quand un agent travaillait.
Exemple concret
Regardez la phase de test aux deux extrêmes.
- En phase agent : l’agent ouvre
package.json, cherche le lanceur, l’exécute, lit des centaines de lignes de sortie et rédige un verdict en prose. Comptez quelques dizaines de milliers de tokens et une bonne demi-minute par run, pour une opinion qu’il vous faudra encore interpréter. - En phase code : un
subprocess.runsurbun test. Zéro token, zéro dollar, quelques secondes, et un code retour qui ne se discute pas. - Sur mille runs, l’écart se chiffre en dizaines de dollars et en heures. Le vrai gain est ailleurs : vous avez remplacé une opinion par un verdict binaire, donc reproductible.
Qui répond à quelle question
| Question posée | Acteur | Pourquoi lui | Coût marginal |
|---|---|---|---|
| Que fabrique-t-on ? | vous | vous seul portez l’intention | votre temps |
| Ce plan est-il implémentable ? | agent | il faut lire et décider | des tokens |
| La suite passe-t-elle ? | code | l’invocation est connue | zéro |
| Ce code répond-il à la demande ? | agent | c’est un jugement | des tokens |
| Faut-il rejouer la phase ? | code | règle écrite, pas arbitrage | zéro |
| Le commit part-il ? | code | l’agent propose le message, le code écrit | zéro |
Script — la frontière, en cinq lignes
Un extrait du workflow vers lequel tout le module 3 converge. La phase test_1 est du code. La
phase fix_1 est un agent qui reçoit l’échec par la même porte qu’un rapport d’agent. Vous
écrirez ce mécanisme vous-même aux chapitres 8 et 9.
# La suite est une commande connue : la lancer ne demande aucun jugement,
# donc c'est une phase code — zéro token, un code retour.
with run.phase(PhaseParams(name="test_1", kind="code", owner="quality",
description="Run the suite")) as ph:
test = quality.run_tests(run)
# L'échec repart vers le builder sous forme structurée : la session reste
# vivante, rien ne redémarre à froid, la boucle de correction est bornée.
with run.phase(PhaseParams(name="fix_1", kind="agent", owner="builder", retries=1,
description="Repair what the suite reported")) as ph:
previous = ph.call(AgentCall(output_type=BuildOutput, prompt=prompt,
previous=quality.as_envelope(test, "tests")))
Un détail contre-intuitif mérite d’être noté dès maintenant. Une suite rouge ne fait pas échouer sa phase : le runner a fait son travail, c’est le code du projet qui a échoué. La suite rouge fait échouer le run, et seulement à la fin, une fois la boucle de correction épuisée.
Piège courant : croire que « plus d’agents = plus de levier ». Chaîner cinq agents là où trois phases code suffisaient multiplie le coût, la latence et la variance sans rien ajouter au verdict. Le bon réflexe est l’inverse. Chaque fois que vous envisagez une phase agent, cherchez d’abord la commande qui répondrait à la même question. Si elle existe, elle gagne : elle est gratuite, instantanée, et rend toujours le même verdict.
Fil rouge — la pièce posée aujourd’hui
La pièce du jour est le plan lui-même : PLAN.md et son garde-fou .gitignore, déposés dans un
dépôt plume-factory fraîchement initialisé. Aucune machine n’existe encore. Ce qui existe,
c’est le contrat que les vingt-six pièces suivantes vont honorer : l’arbre cible, les zones de
l’usine, les jalons. La couture passe ici au plus net. PLAN.md est un document que vous
possédez et qu’aucun agent n’écrira à votre place, parce que l’intention ne se délègue pas. La
gate du jour est la première de la série et la seule à ne rien consommer : zéro token, zéro
dollar, moins d’une seconde. Ce que cette pièce vous fait économiser ne se mesure pas en
dollars mais en dérive. Sans plan écrit, chaque chapitre réinventerait un chemin, et vingt-sept
pièces qui ne s’emboîtent pas ne font pas une usine.
Travaux pratiques — la pièce du jour
Une pièce complète à poser dans le repo compagnon plume-factory, qui devient, chapitre après
chapitre, votre usine logicielle agentique.
Mise en place
# à côté de vos autres dépôts — une seule fois
mkdir plume-factory && cd plume-factory
git init -b main
Pièce — PLAN.md
L’état cible de l’usine, en un seul fichier que vous possédez. C’est la référence que vous rouvrirez à chaque chapitre pour savoir quelle pièce arrive et sur quoi elle s’appuie. Toute déviation ultérieure, outil disparu ou chemin devenu invalide, se règle en modifiant ce fichier, jamais en laissant deux chapitres diverger en silence.
# plume-factory — le plan de l'usine
Ce dépôt contient une **usine logicielle agentique** et le payload qu'elle travaille.
Loi fondamentale : **l'agent propose, le code dispose.** Le code déterministe possède le
séquencement, les reprises et l'acceptation. Un agent n'est qu'un nœud borné à l'intérieur d'une
phase nommée. Le contexte ne traverse une frontière que dans une enveloppe JSON typée. Les gates
définissent « fini » ; un échec de gate revient à l'agent par la même porte qu'un rapport d'agent.
## Arbre cible
plume-factory/
├── PLAN.md # ce fichier — ch. 1
├── justfile # recettes racine — ch. 5
├── just/
│ ├── adws.just # lancer les workflows
│ ├── obs.just # observer les runs
│ └── sandbox/ # lifecycle, keys, orch — module 6
├── apps/plume/ # le payload : app d'écriture Bun + TS — ch. 2
├── specs/ # les specs : la vôtre (ch. 2), puis celles d'adw_plan (ch. 11+)
├── adws/
│ ├── hello_factory.py # l'embryon du runner — ch. 3
│ ├── adw_prompt.py # une phase agent, une enveloppe — ch. 8
│ ├── adw_scout.py / adw_plan.py # reconnaissance et plan — ch. 11
│ ├── adw_build.py # implémentation + gates — ch. 12
│ ├── adw_sdlc.py # la chaîne complète — ch. 13
│ ├── adw_bench.py # mini-benchs par roster — ch. 17
│ ├── adw_modules/
│ │ ├── harness.py # LE port harnais + adaptateurs pi / claude_code — ch. 7
│ │ ├── runner.py # phases, séquencement, reprises — ch. 8
│ │ ├── envelopes.py # enveloppes JSON typées — ch. 9
│ │ ├── roster.py # chargement de factory.config.yaml — ch. 10
│ │ ├── permissions.py # tools, writes, fichiers protégés — ch. 10
│ │ ├── gates.py # lint / tests / typecheck — ch. 12-13
│ │ └── tracer.py # événements de phase vers SQLite — ch. 18-20
│ ├── adw_config/ # factory, frontier, open-weights, eco — ch. 10, 15-17
│ ├── prompts/ # system.md + user.md par agent
│ └── adw_data/ # runtime : sessions, enveloppes, factory.db — jamais commité
├── app_docs/ # sorties du documenter — ch. 13
├── .claude/skills/factory/ # l'usine empaquetée en skill — ch. 26
├── .env.sample # OPENROUTER_API_KEY ; provisioning côté hôte seulement
└── .gitignore # posé ch. 1
## Zones de l'usine
| Module | Zone | Pièces principales |
|---|---|---|
| 1 — Fondations | le plan et la baseline | PLAN.md, apps/plume, hello_factory.py |
| 2 — Poste de pilotage | l'outillage | uv, justfile, herdr, harness.py |
| 3 — Le squelette ADW | le coeur | runner, envelopes, roster, gates, adw_sdlc |
| 4 — Model stack | les moteurs | 4 rosters, adw_bench |
| 5 — Observabilité | la salle de contrôle | tracer.py, obs.just, export OTel |
| 6 — Sandboxes & scale | le hors-site | just/sandbox, clés provisionnées, best-of-N |
| 7 — Distribution | l'emballage | skill factory, /install, capstone |
## Jalons
- [x] ch. 1 — dépôt initialisé, PLAN.md et .gitignore posés
- [ ] ch. 2 — Plume générée en un shot, `bun test` vert : la baseline « sans usine »
- [ ] ch. 3 — hello_factory.py : un subprocess, une sortie JSON validée
- [ ] ch. 7 — plus aucun appel direct au harnais hors adw_modules/harness.py
- [ ] ch. 13 — premier adw_sdlc.py vert de bout en bout sur Plume
- [ ] ch. 17 — quatre rosters interchangeables, adw_bench les compare
- [ ] ch. 20 — chaque run laisse une trace SQLite exploitable
- [ ] ch. 25 — un best-of-N de 3 à 5 rosters lancé, moissonné, comparé
- [ ] ch. 27 — l'usine s'installe dans un repo vierge via son skill
## Règles de continuité
1. Une pièce posée ne se casse pas. Toute évolution redonne le fichier entier et l'annonce.
2. `adws/adw_data/` et `.env` ne sont jamais commités.
3. Chaque pièce a sa gate : la commande qui prouve qu'elle fonctionne, coût et durée à l'appui.
4. Le payload Plume reste petit (5 à 8 fichiers) : les runs restent lisibles et bon marché.
5. Les ADW nomment des agents, jamais des modèles. Le roster décide du modèle.
Pièce — .gitignore
Le garde-fou, posé avant la première ligne de code de l’usine. Le runtime et les secrets ne
doivent jamais pouvoir partir dans un commit, y compris par accident lors d’un git add -A
déclenché par un agent. C’est un pas déterministe qui protège toutes les pièces à venir.
# secrets — jamais commités
.env
.env.*
!.env.sample
# runtime de l'usine : sessions, enveloppes, traces, base SQLite
adws/adw_data/
# dépendances et artefacts du payload
node_modules/
dist/
build/
# Python
__pycache__/
*.pyc
.venv/
# OS et éditeurs
.DS_Store
Thumbs.db
.idea/
.vscode/
La gate du TP
Le bloc ci-dessous est du shell : collez-le tel quel dans votre terminal, à la racine de
plume-factory. Après le commit, il enchaîne trois vérifications. La boucle for interroge git
(git check-ignore) sur trois chemins sensibles pour prouver que le .gitignore les couvre,
avant même qu’ils existent. test -s vérifie que PLAN.md existe et n’est pas vide. Enfin
git status --porcelain liste une ligne par fichier non commité, wc -l les compte, et zéro
signifie que l’arbre est propre.
git add PLAN.md .gitignore
git commit -m "chore: le plan de l'usine et ses garde-fous"
# 1. rien de sensible ni de runtime ne doit pouvoir partir dans un commit
for p in .env adws/adw_data/ node_modules/; do
git check-ignore -q "$p" && echo "ignore : $p" || echo "EXPOSE : $p"
done
# 2. le plan existe et n'est pas vide
test -s PLAN.md && echo "plan : PLAN.md pose, $(wc -l < PLAN.md) lignes"
# 3. l'arbre est propre : tout ce qui devait être commité l'est
echo "sale : $(git status --porcelain | wc -l) fichier(s)"
Attendu : trois lignes ignore, une ligne EXPOSE signalerait un chemin que le .gitignore ne
couvre pas, puis le décompte de lignes de PLAN.md, et sale : 0. Coût : zéro token, 0,00 $,
moins d’une seconde. C’est la seule gate du livre qui ne consomme rien, et exactement le genre
de vérification qu’on ne confie jamais à un agent.