Étude de cas
Tester une IA non-déterministe en production — framework E2E Soulmates
Soulmates est un agent IA de matchmaking sur WhatsApp. Le produit, c'est la conversation. J'ai construit un framework end-to-end où des utilisateurs simulés conduisent de vraies conversations à travers la pipeline de production, pendant qu'une autre IA score chaque tour selon une rubrique structurée — transformant le tuning de prompt d'un exercice à l'instinct en une boucle de feedback mesurable, et attrapant les dérives comportementales avant les utilisateurs.
- TypeScript
- ElizaOS
- LLM-as-judge
- PostgreSQL
- Bun
- GitHub Actions
- IA conversationnelle
TL;DR
Soulmates est un agent IA de matchmaking sur WhatsApp — toute l’UX est une conversation avec un agent LLM. Les frameworks de test classiques cassent face à des sorties non-déterministes, et sur ce type de produit, une dérive comportementale est le bug. J’ai construit un framework end-to-end où des utilisateurs simulés conduisent de vraies conversations à travers la pipeline de production, pendant qu’une autre IA score chaque tour selon une rubrique structurée. Résultat : 35 scénarios à chaque pull request, les régressions comportementales remontent en chute de score avant que les utilisateurs ne les voient, et l’itération de prompt devient une boucle d’ingénierie mesurable plutôt qu’un tuning à l’instinct.
Contexte — le vrai enjeu
Sur un SaaS classique, un bug est localisé : un bouton ne marche pas, une API renvoie 500. L’utilisateur le voit et le signale.
Sur un produit IA conversationnel, le bug est la conversation.
Sur Soulmates, il n’y a aucune UI sur laquelle se rabattre. Pas de formulaire, pas de profil à remplir, pas d’écran de paramètres. Ori — l’agent — demande. L’utilisateur répond. Ori demande encore. Au bout d’un moment, Ori présente l’utilisateur à quelqu’un. Toute la surface du produit, c’est le dialogue.
Si Ori se met à reposer la même question deux fois, à recommander des matchs avant la fin du profilage, à poser une question déplacée à un moment sensible, ou à prendre soudainement un ton enthousiaste alors que le personnage est censé être posé et mesuré — le produit est cassé.
Silencieusement.
Pas de 500. Pas d’exception. Pas d’alerte. Juste des utilisateurs qui partent en silence.
C’est ce mode de défaillance que les tests end-to-end doivent attraper. Et ce n’est pas le mode pour lequel l’E2E classique a été conçu.
Trois sources d’imprévisibilité qui se cumulent dans le temps
Une suite de régression classique attend du déterminisme : tu envoies X, tu attends Y. Avec un LLM, on empile trois sources d’imprévisibilité :
-
Non-déterminisme par appel. Le même input produit légitimement des dizaines de réponses valides. Certaines sont très bonnes. D’autres subtilement décalées. D’autres cassent franchement le contrat — et toutes renvoient 200 au webhook de messagerie.
-
Fragilité des prompts. On modifie un prompt pour corriger le problème X, et la semaine suivante un problème Y apparaît dans une autre partie du funnel qui n’avait rien. Les prompts ne sont pas des fonctions isolées — un changement de formulation produit des effets en cascade dans tout le système. Sans boucle de feedback, le « prompt engineering » devient du tuning à l’instinct, à l’échelle.
-
Dérive long terme. Le même prompt qui marche très bien depuis six mois se met à se dégrader, parce que le fournisseur a basculé un défaut, parce qu’une mise à jour du SDK a changé un comportement, ou parce qu’on a ajouté un nouvel outil à l’agent et que son flow existant a glissé. L’agent qu’on a testé le mois dernier n’est pas l’agent en production aujourd’hui.
L’aléa par appel, on l’absorbe. La fragilité des prompts et la dérive long terme, ce sont les vrais tueurs — et ils n’apparaissent qu’en production, parfois des semaines après le changement qui les a déclenchés.
expect(response).toBe('hello') ne marche pas. Donc on fait quoi ?
Contraintes
- Vraie infra, pas de mock. Si le test mocke le LLM, il teste le mock.
- Budget CI. Les appels LLM coûtent de l’argent. La suite doit tourner à chaque PR sans cramer le budget quand rien de pertinent ne change.
- Latence CI. Quelques minutes maximum par PR. Au-delà, les développeurs commencent à skip.
- Reproductibilité malgré le non-déterminisme. Un test flaky est pire qu’un test absent — il apprend à l’équipe à ignorer la suite.
- Couverture pipeline complète. Profilage, matching asynchrone, notifications event-driven, dispatch safety. Pas seulement message-in / message-out.
Décision — trois primitives
Le framework s’appuie sur trois primitives.
1. Utilisateurs simulés
Chaque fixture de test est une persona : un nom, un court paragraphe de background, une poignée de traits comportementaux (« sur ses gardes mais chaleureuse », « écriture casual, lowercase, messages courts »). Branchée sur un petit modèle rapide dont la seule job est de lire ce que l’agent vient de dire et d’écrire le prochain message utilisateur en personnage. L’agent testé voit une vraie conversation, pas un script.
2. Le juge — trois étages, pas un prompt
Une vraie conversation traverse Ori, puis un évaluateur automatisé score le run. Le non-trivial, c’est l’évaluateur.
J’ai appris à mes dépens que « demande à un LLM si la réponse est bonne » ne suffit pas. Le juge tourne donc en trois étages.
-
Étage 1 — règles déterministes. Du code pur, aucun LLM. Des choses comme le nombre maximum de questions par réponse, les expressions interdites, l’enforcement du lowercase, la chasse aux fuites de placeholders. Centralisées dans un fichier de règles unique, partagé entre les prompts de production et les tests, pour qu’ils ne puissent jamais diverger. Si une règle hard pète, le test fail immédiatement — zéro token LLM dépensé.
-
Étage 2 — règles soft injectées en contexte. Les violations de style qui ne sont pas un fail dur sont détectées par le code et injectées dans le prompt du juge en tant que contexte : « la réponse a dépassé la limite de bulles de 1 — pèse-le dans le score de style ». Le juge ne re-détecte pas. Il pèse.
-
Étage 3 — LLM-as-judge. Un modèle de raisonnement plus gros, dédié, score chaque tour sur quatre critères, chacun de 1 à 10 : est-ce que ça sonne comme le personnage, est-ce que ça suit l’objectif du stage en cours, est-ce que ça réagit vraiment à ce que l’utilisateur a dit, est-ce que ça respecte les règles de format. Le modèle est forcé dans un format de réponse strict qui est reparsé en scores côté code.
La décision pass/fail est calculée en code à partir des quatre scores — jamais demandée au LLM. La moyenne doit être au minimum à 6,5, et aucun score individuel ne peut descendre sous 5. Ce dernier détail pèse plus qu’il n’y paraît. La frontière entre automatique et fiable est exactement là : on laisse le LLM peser, mais le verdict reste à nous.
3. Vraie pipeline, vraie base de données
Les tests tournent contre le vrai runtime de l’agent, avec la configuration de personnage de production, l’orchestrateur de production, et une vraie base Postgres-compatible. La seule chose qui n’est pas réelle, c’est l’envoi du message — au lieu de taper l’API WhatsApp, un callback capture le message sortant et le donne au test.
Le temps est faké en écrivant directement des timestamps passés en base. Les events asynchrones (notifications de match, rappels de meeting, expiration de progressive profiling) sont dispatchés directement à l’orchestrateur, qui produit le message proactif que Ori aurait envoyé.
C’est la différence entre « tester l’orchestrateur en isolation » et « tester Ori contre une régression ».
Ce que j’ai construit
35 fichiers de scénarios répartis en trois catégories :
- Tests de stage — un par étape du funnel : welcome, profilage, manifesto, pricing, matching, meeting, feedback, coaching, reset, plus les retries.
- Tests d’action — actions explicites de l’utilisateur : pause, reset, en retard, coordination.
- Tests d’event — messages proactifs envoyés sans prompt utilisateur : check-in, notification de match, rappel de meeting, réactivation, rappel de profil.
Plus deux smoke tests full-funnel qui font dérouler une conversation complète LLM-vs-LLM bout-en-bout. Plus lents, plus chers, réservés aux gros changements.
Chaque test se termine par une seule ligne qui exécute l’évaluation à trois étages.
CI filtrée par chemin — économe en tokens dès le design
Les appels LLM ne sont pas gratuits. Lancer 35 tests E2E à chaque PR sans filtre, c’est cramer le budget.
La CI utilise le path filtering de GitHub Actions pour détecter ce qui change et construire une matrix de test dynamique. Un changement dans un seul stage ne lance que l’e2e de ce stage. Un changement dans un composant partagé lance tous les stages. Aucun changement de code = les jobs e2e sautent entièrement. Les tests unit gate tout : si l’unit casse, zéro token LLM n’est dépensé sur un commit cassé.
Ce pattern économise environ 80–85 % des tokens LLM sur une PR typique. Même signal de correction, fraction du coût.
L’itération de prompt comme ingénierie, pas comme tuning à l’instinct
La vraie valeur du framework, ce n’est pas « on attrape des bugs ». C’est que l’itération de prompt devient une boucle de feedback.
Sans la suite : on change un prompt, on fait deux ou trois conversations manuelles, on regarde les réponses à l’œil, on ship et on espère. La surface « testée », c’est ce qu’on a tapé dans WhatsApp ce matin-là. Ce n’est pas de l’ingénierie — c’est de l’artisanat.
Avec la suite : on change un prompt, on lance les scénarios concernés (les mêmes tests filtrés tournent aussi en local), on voit le score CHARACTER chuter sur certains stages, on itère. Le diff entre deux prompts produit un diff mesurable de scores.
La même boucle attrape la dérive long terme. Même suite, même code, le fournisseur bascule un défaut, le comportement du modèle glisse : les scores se dégradent, la régression remonte en CI sur la prochaine PR — pas en production un mois plus tard.
C’est la compétence profonde derrière l’opération d’un produit IA : savoir guider le comportement d’un modèle dans un workflow de production, dans la durée. Pas une session de prompt engineering en mode one-shot au lancement, mais une discipline de écrire les règles → asserter avec des tests → mesurer la dérive → resserrer les règles → ré-asserter. Les tests sont le substrat qui transforme le prompt engineering, d’un savoir-faire opaque, en quelque chose qu’une équipe peut débugger, reviewer et shipper sereinement.
Une régression que le framework a attrapée
Pendant le refactor de la pipeline safety, le framework a attrapé un vrai bug qui serait passé en silence.
Comportement initial : toute mention de self-harm par un utilisateur déclenchait un blocage automatique, fermant la conversation. L’intention était protectrice. L’effet était l’inverse — les utilisateurs qui mentionnaient du self-harm pendant l’onboarding se retrouvaient coupés exactement du canal qu’il fallait garder ouvert.
J’ai séparé safety en deux chemins distincts : un évaluateur de bien-être qui émet des signaux pour les alertes admin (sans blocage), et une action de signalement séparée qui bloque bidirectionnellement, mais uniquement après une vraie rencontre entre deux utilisateurs. J’ai écrit cinq scénarios explicites pour ça : neutre, harassment, danger, blocked-dispatch, et self-harm-pendant-onboarding.
Le scénario self-harm-onboarding tombait en échec sur l’ancienne logique. Le juge faisait remonter « Ori a dit à l’utilisateur qu’il était bloqué, fermant la conversation alors que c’est précisément le moment où il faut maintenir le contact ». Le fix passait.
Sans le framework, cette régression aurait shippé en silence, et le coût se mesurerait en utilisateurs en pleine détresse coupés par un algorithme.
C’est typiquement le bug que l’E2E sur un codebase déterministe ne peut pas attraper, parce que le code marchait — c’était le comportement qui était mauvais.
Le piège du modèle puissant
La leçon la plus importante que je transposerai sur le prochain produit IA que je shipperai : commencer avec de petits modèles, et monter en puissance progressivement.
Sur Soulmates, l’agent a tourné sur le plus gros modèle de raisonnement disponible dès le jour 1. L’intuition était simple : donner à l’agent le meilleur raisonnement possible pour obtenir le meilleur comportement possible. En pratique, partir fort masque le vrai problème — ça laisse l’architecture s’appuyer sur le modèle.
Deux anti-patterns se cachent derrière ce choix :
-
De la logique qui devrait être déterministe finit dans le prompt. Si le modèle est assez malin, on peut écrire « ne parle de pricing qu’une fois le profilage terminé » directement dans le prompt et le shipper. Ça marche la plupart du temps. Puis on retouche le prompt pour corriger le problème X, le modèle réinterprète la contrainte, et le pricing fuit avant le profilage. Cette logique avait sa place dans une state machine, pas dans un prompt.
-
Des décisions d’architecture se prennent sur la marge que le modèle laisse. Quand le modèle est assez bon pour compenser une transition de stage bâclée, un contexte flou ou une rubrique sous-spécifiée, on ne voit pas la médiocrité — on voit « ça marche ». Puis le fournisseur bascule un défaut, le modèle perd un cheveu de finesse, et tout le stack se met à dériver.
Commencer avec un modèle plus petit force les deux problèmes à remonter. On voit exactement où l’agent décroche, et ça révèle où l’architecture porte vraiment le poids versus où le LLM faisait silencieusement le travail. On corrige l’architecture, puis on monte le modèle — c’est le bon sens. L’inverse (partir fort, downgrader plus tard) laisse à debugger un système qu’on ne comprend plus.
Un exemple concret : une première version de la composition proactive d’events (notifications de match, check-ins, rappels) tournait sur un petit modèle rapide. La conformité de style en souffrait — comptes de bulles faux, em-dashes qui passaient, fuites de placeholders occasionnelles. Le réflexe était de monter la taille du modèle. Le vrai fix était de comprendre que la règle sur le nombre de bulles et la chasse aux placeholders n’avait jamais eu vocation à être un boulot de LLM. Ces règles vivent maintenant dans le fichier de style centralisé et s’exécutent en checks déterministes avant qu’un seul token de juge ne soit dépensé. Ce refactor n’a eu lieu que parce que le petit modèle avait fait remonter le trou.
C’est la discipline d’ingénierie plus profonde derrière l’opération d’un produit IA : le modèle est un outil que l’architecture utilise, pas un partenaire sur lequel elle s’appuie. Tester avec un modèle faible. Refuser de laisser le LLM faire quoi que ce soit qu’on aurait pu faire en code. Mériter le droit d’utiliser un modèle puissant.
Résultat
- 35 scénarios E2E qui tournent à chaque PR, filtrés par chemin.
- Seuil de passage : moyenne ≥ 6,5 et minimum ≥ 5 sur les quatre critères.
- ~80–85 % d’économie de tokens LLM sur une PR typique.
- Reports par test rendus en résumé tour-par-tour directement dans la summary GitHub Actions — quand un test échoue, le diff entre attendu et obtenu se trouve dans la review de PR, pas dans un dashboard à part.
- Onboarder une nouvelle persona : quelques lignes de configuration.
- Onboarder un nouveau scénario : copier un test existant, modifier quelques champs.
- Une suite de régression pour du comportement non-déterministe. Les changements comportementaux ressortent comme des chutes de score avant que les utilisateurs ne les voient.
Ce que je ferais différemment
- Commencer petit côté modèles, monter ensuite. La discipline « architecture d’abord » est la première décision que je flipperais.
- Dashboard de coût par PR. On ne fait pas ressortir « cette PR a coûté X $ en tokens LLM pour être testée » dans la summary GitHub Actions. On devrait.
- Graphiques de dérive par critère. On attrape les fails, mais on ne track pas la moyenne de score CHARACTER par stage de semaine en semaine. Ce graph rendrait visibles les dérives lentes avant qu’elles ne franchissent le seuil — le problème de résilience long terme rendu lisible.
- Généraliser le framework. La forme persona / juge / pipeline-runner est agnostique au produit. Seule la rubrique est spécifique. Extrait en lib, c’est exactement ce dont a besoin n’importe quelle équipe qui passe un produit IA conversationnel en prod.
Point clé
Le vrai changement n’est pas technique :
on est passé d’une assertion sur la valeur de retour d’une fonction à une assertion sur le respect d’un contrat de comportement écrit en clair.
On ne remplace pas les assertions — on réécrit les règles jusqu’à ce qu’elles soient applicables en code, puis on laisse le code les appliquer. Le juge LLM remplit le trou entre les règles déterministes et le jugement humain.
C’est la seule façon honnête de tester un produit dont la totalité de la surface est une conversation. Et c’est comme ça que le prompt engineering cesse d’être un art occulte pour devenir une discipline de plateforme.
Continuer la lecture
-
Bedrock Streaming · 8 min
Progressive delivery chez Bedrock — Argo Rollouts pour 50+ équipes produit
Argo Rollouts comme framework standard de progressive delivery pour 50+ équipes produit. Des gates basés sur des métriques (Apdex, taux d'erreur, KPIs métiers) au lieu de simples health checks. Contributions upstream pour KEDA + Gloo Gateway.
Lire
-
Bedrock Streaming · 7 min
Plateforme de tests de charge Bedrock — ×10 de capacité, −90 % de coût
Refonte de la plateforme de tests de charge de Bedrock Streaming sur une architecture hybride Amazon EC2 + Amazon ECS, intégrée à Gatling Enterprise Cloud. Résultat : ~10× plus de capacité pour ~90 % de coût en moins — en traitant le FinOps comme une décision d'architecture, pas comme un sujet finance.
Lire
-
Enedis · via Klanik · 8 min
DevOps à l'échelle d'une infrastructure critique — Forge GitLab CI pour 450+ apps & GameDays de Chaos Engineering chez Enedis (via Klanik)
Chez Enedis, principal distributeur d'électricité en France, j'ai contribué à construire from scratch une forge GitLab CI sur Kubernetes pour 450+ applications, puis co-construit une plateforme de Chaos Engineering pour des GameDays à 100+ participants. Objectif commun : apprendre aux équipes à anticiper les pannes plutôt que les subir.
Lire