L'IA qui s'améliore elle-même : boucle d'itération autonome sur un prompt

RateMyFace est un side project en cours — un site de roast IA par photo : l'utilisateur uploade une photo, Claude l'analyse et génère un texte satirique accompagné d'une note et d'un "tier label" (genre "WiFi Signal With Legs"). Le tout est rendu sous forme de carte à collectionner. Stack : Go monolithe, SQLite, Claude CLI appelé en sous-processus.

Le prompt demandait à Claude de produire 5 styles de roast (standard, rap, Shakespeare, mère passive-agressive, Gordon Ramsay) + un score + un label, le tout en JSON. Deux problèmes : les roasts prenaient ~50 secondes et leur qualité était opaque. On savait qu'on générait quelque chose, pas si c'était bon.

Le prompt avait été écrit à l'instinct et jamais sérieusement évalué. La question : comment savoir si le prompt est bon, et comment l'améliorer sans passer la journée à lire des roasts manuellement ?

La réponse : faire faire le travail d'évaluation par l'IA elle-même, dans une boucle automatisée. Écrire un outil qui envoie 30 photos à Claude, mesure des métriques de qualité, produit un rapport. Modifier le prompt, relancer, comparer. Cinq itérations plus tard, voilà ce qu'on a appris.

L'idée : mesurer avant d'optimiser

Le réflexe habituel en prompt engineering c'est d'itérer manuellement — modifier, tester sur 2-3 exemples, estimer si c'est mieux. Le problème : on optimise sur les exemples qu'on choisit, pas sur la distribution réelle. Et "c'est mieux" n'est pas une métrique.

Autre approche : définir ce que "bon" veut dire de façon mesurable, générer suffisamment d'exemples pour avoir des statistiques stables, et automatiser l'évaluation. Les métriques choisies :

  • Longueur moyenne — cible < 150 chars. Un roast viral est court.
  • Variance des scores — cible > 2.0. Si tout le monde a 5-6, le score ne sert à rien.
  • Taux de fallbacks — combien de fois Claude échoue et on retourne le texte par défaut.
  • Distribution des scores — histogramme 1-10, pour visualiser les biais.

L'outil : un harnais d'évaluation en Go

Un binaire autonome dans cmd/prompttest/main.go. Pas de serveur HTTP, appel direct à Claude CLI. 30 cas de test fixes (photos randomuser.me — hommes et femmes FR/EN), exécutés séquentiellement avec mesure de durée.

func callClaude(ctx context.Context, photoURL, lang string) (*RoastResult, string, time.Duration, error) {
    prompt := buildPrompt(lang)
    fullPrompt := fmt.Sprintf("First, read the image file at %s and look at it carefully. Then:\n\n%s", photoURL, prompt)

    args := []string{
        "--print",
        "--model", "sonnet",
        "--effort", "low",       // réduit le temps de ~50s à ~27s
        "--allowedTools", "Read",
        "--dangerously-skip-permissions",
        "-p", fullPrompt,
    }

    start := time.Now()
    out, err := exec.CommandContext(ctx, "claude", args...).Output()
    dur := time.Since(start)
    // ...
}

Le flag --effort low est la première optimisation de vitesse : il réduit le temps de réponse de ~50s à ~27s. Ce n'est pas documenté officiellement mais le comportement est stable.

Le rapport de fin de run :

╔══════════════════════════════════════════════╗
║   PROMPTTEST v4 — 30 tests
╠══════════════════════════════════════════════╣
║ Avg chars standard : 128 (cible < 200)
║ Score variance     : 1.09 (cible > 2.0)
║ Fallbacks          : 0/30 (cible < 5%)
║ Avg score          : 5.8
║ Avg duration       : 33.859s
╠══════════════════════════════════════════════╣
║ Score distribution:
║    4: █████ (5)
║    5: ████ (4)
║    6: ███████ (7)
║    7: ██████████████ (14)
╠══════════════════════════════════════════════╣
║ Sample roasts (first 5 valid):
║  [homme FR 1] score=4.0 tier="PDG de Rien du Tout"
║  → Le costume + les joues de bébé + le regard vide — t'es le seul
║    mec à avoir l'air d'un enfant ET d'un PDG raté en même temps.
╚══════════════════════════════════════════════╝

Cinq versions, cinq leçons

Version Chars moy. Variance score Fallbacks Problème principal
v1 216 0.44 0 "T'as [X] de quelqu'un qui" — 17/30 roasts identiques en structure
v2 110 0.58 0 "[item] dit/says [X]" — nouveau cliché dominant
v3 95 0.67 0 "C'est la photo LinkedIn de..." — troisième cliché
v4 128 1.09 0 Meilleure version — roasts spécifiques et variés
v5 146 0.90 1 3 scores de 8 apparus, mais variance globale en baisse

Leçon 1 — Chaque exemple positif crée un cliché

Dans v1, le prompt donnait des exemples de bons roasts. Claude a immédiatement repris la structure de ces exemples sur 17 des 30 cas. On a banni ce pattern, donné de nouveaux exemples — et Claude a utilisé les nouveaux exemples comme nouveau cliché. Trois fois de suite.

La solution (v4) : abandonner les exemples positifs de structure. À la place, décrire la cible émotionnelle ("un roast qu'un étranger screenshoterait et enverrait à un groupe chat") et accumuler uniquement des exemples négatifs (patterns explicitement bannis).

BANNED STARTERS (these patterns are overused trash):
- "[item] dit/crie/says [X]" → BANNED
- "T'as [X] de quelqu'un qui..." → BANNED
- "C'est la photo de profil LinkedIn de..." → BANNED
- Any sentence starting with "C'est la photo" → BANNED

Leçon 2 — La variance de score a un plafond naturel

Peu importe comment on formule l'instruction de scoring, la variance plafonne autour de 1.1 avec des photos randomuser.me. Ces photos sont intentionnellement "moyennes" — elles servent de photos de profil génériques. On ne peut pas extraire une variance de 2.0 d'une distribution naturellement resserrée entre 4 et 7.

Ce n'est pas un problème de prompt. C'est une contrainte physique des données d'entrée. Avec de vraies photos d'utilisateurs (qui incluent des gens vraiment moches ou vraiment beaux), la variance sera naturellement plus élevée. Le prompt v4 est optimal pour ce qu'on peut obtenir avec ce jeu de test.

Leçon 3 — Claude est conservateur sur les notes basses

Même en demandant explicitement des notes de 2-3 pour les gens "objectivement difficiles à regarder", Claude résiste. Les mécanismes de safety d'Anthropic le poussent à éviter de dire qu'une vraie personne est laide. On obtient rarement moins de 4.0 malgré des instructions répétées.

Pour un use case comme le nôtre (roast humoristique consenti), c'est légèrement frustrant mais compréhensible. La vraie question : est-ce que l'utilisateur qui uploade une photo s'attend à avoir un 2/10 ? Probablement pas, même si c'est "plus honnête".

Leçon 4 — La qualité du texte s'améliore radicalement

C'est le vrai gain de l'itération. Entre v1 et v4, la qualité des roasts n'est pas comparable :

v1 : "T'as la tête de quelqu'un qui a mis 'passionné par les synergies' dans son bio LinkedIn — le bâtiment derrière toi est plus intéressant que toi."

v4 : "Le front avance plus vite que ta carrière et le regard est resté coincé à la page de chargement."

Même sujet, même personne. La v4 est deux fois plus courte, deux fois plus spécifique, trois fois plus drôle. L'itération sur métriques mesurables (longueur, fallbacks) a forcé des changements de prompt qui ont eu un effet indirect sur la qualité subjective.

Les limites de l'approche

La boucle d'itération autonome a des limites importantes à garder en tête.

Pas de ground truth. Les métriques (longueur, variance) mesurent des propriétés du texte, pas sa qualité. Un roast de 90 chars n'est pas nécessairement plus drôle qu'un de 180. On optimise des proxies, pas la vraie cible.

Le jeu de test ne représente pas les utilisateurs réels. randomuser.me = photos de profil génériques, neutres, bien éclairées. Les utilisateurs réels uploadent des photos de soirée, des selfies flous, des gens grimés. La distribution réelle est différente.

Chaque run prend ~15 minutes. 30 appels × ~30s = 15 min d'attente par itération. On a fait 5 itérations = 75 minutes de run + temps d'analyse. Ce n'est pas de l'optimisation en temps réel.

La variance de score plafonne, et c'est acceptable. On a essayé pendant 3 itérations d'améliorer la variance sans succès majeur. Reconnaître le plateau et s'arrêter est une compétence à part entière.

Ce que la boucle permet vraiment

La valeur principale n'est pas d'atteindre le "prompt parfait". C'est de rendre visible ce qui est invisible. Sans l'outil, on n'aurait pas su que 17/30 roasts avaient exactement la même structure. On aurait continué à trouver les roasts "plutôt bien" sur les 3 exemples qu'on testait à la main.

La boucle force à définir "bon" avant d'optimiser. C'est le vrai travail : pas écrire le prompt, mais décider quelles métriques proxy sont pertinentes. Une fois les métriques définies, l'IA fait le reste — générer les tests, mesurer, révéler les patterns.

Cette approche est reproductible pour n'importe quelle génération de texte avec des propriétés mesurables : longueur, présence de certains patterns, taux d'échec de parsing JSON, distribution d'une valeur numérique produite. Si tu peux l'écrire dans un rapport de 5 lignes, tu peux l'automatiser.

Conclusion

On a commencé avec un prompt écrit à l'instinct, des roasts de 216 caractères en moyenne, et une cliché dominant sur 56% des cas. On a terminé avec un prompt à 128 caractères, 0 fallback, des roasts spécifiques et variés — et surtout, une compréhension claire de pourquoi chaque version était meilleure ou pire que la précédente.

Ce qui m'a surpris : l'itération la plus efficace (v4) n'est pas celle qui donnait le plus d'instructions à l'IA. C'est celle qui en donnait le moins — décrire la cible émotionnelle, bannir les patterns ratés, et faire confiance au modèle pour trouver autre chose. Moins de contraintes positives, plus de liberté créative dans les contraintes négatives.

Le plateau de qualité existe. À un moment, les itérations n'améliorent plus rien de mesurable. C'est le signal pour s'arrêter — pas parce que le prompt est parfait, mais parce que les gains marginaux ne valent plus le temps investi. Savoir quand arrêter est aussi important que savoir comment itérer.

Commentaires (0)