Prompty
Des prompts livrés comme du code : versionnés, évalués, testés en A/B
Prompty traite un prompt comme un artefact versionné, et non comme une chaîne de caractères que quelqu’un modifie en production avant de guetter les plaintes. Chaque version est notée par un LLM juge sur un jeu de référence avant de pouvoir être livrée, puis déployée sur une part du trafic réel et jugée par les statistiques. Déployer et revenir en arrière sont une seule et même opération : déplacer un label.
- Type
- Projet personnel, 2026, conçu et réalisé de bout en bout
- Réalisé
- Service FastAPI, SDK Python, dashboard React
- Statistiques
- Intervalles de Wilson, test z, test t de Welch, test t apparié, analyse de puissance, contrôle de SRM
- Modèles
- Tout endpoint compatible OpenAI, ou un modèle simulé déterministe
- Tests
- 151 fonctions de test (pytest), statistiques vérifiées par simulation
Un prototype local fonctionnel : mono-utilisateur, sans authentification. Ses données de démonstration proviennent d’un modèle simulé déterministe : elles montrent donc la mécanique, pas la qualité d’un modèle.
Une complétion, de bout en bout
Les applications appellent Prompty à la place du modèle. Un seul appel décide de la version servie à cet utilisateur, en fait le rendu, appelle le modèle et enregistre tout ce dont une expérience aura besoin plus tard.
- Entrée ou sortie
- Appel LLM
- Code
- Stockage
- Appel SDK client.complete("support-reply", variables={"message": "Ma commande n’est jamais arrivée"}, unit_id="user-42").
- Choix de la cible Une expérience est en cours sur support-reply : c’est donc elle qui décide de la version. Sans expérience, ce serait le label « production ».
- Affectation Bucket = sha256(experiment:unit) mod 10 000, confronté aux poids des variantes exprimés en points de base. L’affectation est enregistrée : user-42 garde donc la même variante même si les poids changent.
- Rendu Simple substitution de {{name}}, sans logique de template. Une variable manquante donne un 422, jamais un prompt à moitié rempli.
- Appel du modèle Le modèle et les réglages propres à la variante. Les limites de débit et les erreurs de connexion sont enregistrées avec le statut « error » au lieu de lever une exception ; un filtre de contenu devient « refusal ».
- Journal d’appels Une ligne dans l’unique table completions, tarifée au million de tokens et marquée source = live. Les appels d’évaluation et de juge partagent la table, chacun sous sa propre source.
- Réponse Texte, variante, tokens, coût et latence. Plus tard, client.track() rattache le résultat, un pouce levé ou une valeur numérique, à la même variante.
Cette requête ne passe pas par ici. Les autres chemins sont listés plus bas.
Voilà toute la requête. Envie de voir tourner quelque chose de ce genre sur vos données ?
Sélectionnez une case du schéma, ou suivez la requête étape par étape.
Les autres chemins du graphe
- Aucune expérience en cours : le label de déploiement choisit la version, et rien n’est enregistré.
- Pas d’identifiant d’unité : un tirage aléatoire, non persisté.
- Erreur du fournisseur : la complétion journalisée revient dans un 502, et l’appelant voit donc ce qui a été enregistré.
D’une nouvelle version à la production
Une version doit passer deux épreuves différentes : hors ligne, face à des réponses déjà jugées bonnes ; en ligne, face au trafic réel. Chacune peut l’arrêter.
- Entrée ou sortie
- Appel LLM
- Code
- Contrôle
- Nouvelle version La v3 de support-reply. Une version ne change plus une fois créée ; les labels sont des pointeurs vers les versions.
- Éval hors ligne Chaque cas du jeu de référence passe par la v3, cinq à la fois, puis un modèle juge note la réponse de 1 à 5 selon une grille absolue, et non en comparaison côte à côte, ce qui évite le biais de position.
- Verrou avant prod Comparaison cas par cas avec la version en production : un test t apparié sur au moins 5 cas communs. Seul un recul significatif bloque ; l’absence de preuve, jamais.
- Expérience A/B Le trafic est réparti selon des poids en points de base, de façon stable pour chaque utilisateur, avec une taille d’échantillon fixée à l’avance par une analyse de puissance.
- Résultats Intervalles de Wilson par variante, test z poolé pour les taux, test t de Welch pour les métriques continues. Un déséquilibre d’échantillonnage (SRM) à p < 0,001 supprime tous les indicateurs « significatif » ; des garde-fous sur le coût, la latence et le taux d’erreur surveillent les régressions.
- Production Clôturer l’expérience déplace le label de production vers la variante gagnante. Une régression sur un garde-fou se solde par un refus en 409, sauf passage en force : c’est la seule dérogation humaine.
Cette requête ne passe pas par ici. Les autres chemins sont listés plus bas.
Voilà toute la requête. Envie de voir tourner quelque chose de ce genre sur vos données ?
Sélectionnez une case du schéma, ou suivez la requête étape par étape.
Les autres chemins du graphe
- Le verrou de déploiement échoue : le déploiement est refusé et le label reste où il était.
- Moins de 5 événements dans une cellule : le test z n’est plus fiable, et le garde-fou sur le taux d’erreur bascule sur le test exact de Fisher.
Des statistiques choisies à dessein
Une plateforme A/B n’est honnête que dans la mesure où ses tests le sont. Chaque choix répond ici à une façon précise dont les expériences induisent en erreur.
-
Poolée pour le test, non poolée pour l’intervalle
Le test z utilise l’erreur type poolée, comme l’implique l’hypothèse nulle ; l’intervalle sur la différence utilise l’erreur type non poolée. Le taux propre à chaque variante reçoit un intervalle de Wilson, qui reste pertinent près de 0 et de 1.
-
Apparié quand les cas sont communs
Les exécutions hors ligne notent les mêmes cas de référence : les comparaisons sont donc appariées. Dans la suite de tests, 40 cas communs donnent p < 0,001 en apparié et p > 0,05 en non apparié, sur les mêmes scores.
-
Le SRM (sample ratio mismatch) d’abord
Si une répartition 50/50 produit du 45/55, l’affectation est cassée et tous les chiffres en aval sont suspects. Le contrôle s’exécute à partir de 100 observations, à p < 0,001, et fait taire tous les indicateurs de significativité quand il se déclenche.
-
Des garde-fous à côté de l’objectif
Une variante qui gagne en pouces levés mais coûte plus cher, répond plus lentement ou échoue plus souvent est signalée. Pour le coût et la latence, il faut à la fois une régression relative de 10 % et la significativité.
-
Des agrégats, pas des lignes
Le module de statistiques n’a aucun accès à la base. Un seul GROUP BY l’alimente : l’endpoint de résultats coûte donc autant quel que soit le trafic.
-
Vérifié par simulation
Le calcul de puissance est testé par simulation : 20 000 exécutions aboutissent à une puissance de 0,80 ± 0,02, et sans effet réel le taux de faux positifs reste à 0,05 ± 0,01.
Choix d’ingénierie
-
Une seule table pour tous les appels
Les appels réels, d’évaluation et de juge arrivent dans une même table completions, dotée d’une colonne source. L’agrégation doit filtrer sur le trafic réel, et un test échoue si elle cesse de le faire.
-
Déploiement et rollback sont une seule opération
Les versions sont immuables et les labels pointent dessus. Faire un rollback, c’est ramener le label en arrière.
-
Stable, et enregistrée
L’affectation est un hash de l’expérience et de l’utilisateur, persisté dès la première requête : elle survit donc à un changement de poids. Deux premières requêtes concurrentes pour un même utilisateur aboutissent à la ligne enregistrée.
-
Les erreurs sont des données
Les échecs du fournisseur sont journalisés comme n’importe quelle complétion : le garde-fou sur le taux d’erreur peut ainsi repérer une variante qui échoue plus souvent.
-
Des juges immuables
Un juge ne peut plus être modifié après sa création : les scores obtenus à plusieurs semaines d’écart restent donc comparables.
-
Échouer au démarrage, pas sur la facture
Un nom de fournisseur inconnu arrête le service dès le démarrage : une faute de frappe ne peut donc pas retomber sur un endpoint payant.
Technologies
- Backend
- Python, FastAPI, SQLAlchemy asynchrone, SQLite (WAL), Pydantic settings
- Statistiques
- SciPy, NumPy
- LLM
- SDK compatible OpenAI, LLM juge avec sortie JSON, fournisseur simulé déterministe
- SDK
- client Python basé sur httpx
- Frontend
- React 19, TanStack Query, React Router, Radix, Tailwind CSS 4, Vite
- Qualité
- pytest, pytest-asyncio, tests des statistiques par simulation
Vous construisez quelque chose de similaire ?
Je détaille volontiers la conception, les compromis, ou ce que je changerais ensuite.