Tests A/B et métriques métier
Mis à jour le 28 juillet 2026
Au-delà de la loss : mesurer l’impact métier
La loss d’entraînement et les scores de qualité ne suffisent pas. Ce qui compte vraiment, c’est l’impact de votre modèle fine-tuné sur vos indicateurs métier : satisfaction client, temps de résolution, taux de conversion. Un modèle peut très bien produire des réponses que vos relecteurs jugent élégantes et faire, dans le même temps, remonter le taux d’escalade vers vos conseillers humains. C’est ce second chiffre qui décide de l’avenir du projet, pas le premier.
Mettre en place un test A/B
Un test A/B compare deux variantes en conditions réelles, sur du trafic authentique. Le groupe A, dit de contrôle, reçoit les réponses du modèle de base avec votre prompt optimisé ; le groupe B, dit de test, celles du modèle fine-tuné. Les utilisateurs sont répartis aléatoirement entre les deux groupes et vous mesurez les résultats sur les mêmes métriques. Toute la validité de l’exercice tient à cette répartition : si le groupe B reçoit par hasard les demandes les plus simples, sa supériorité apparente ne mesurera rien d’autre que ce hasard.
La fonction choisir_variante résout ce problème avec un hachage de l’identifiant utilisateur. Le procédé garantit qu’un même utilisateur voit toujours la même variante — indispensable, car alterner d’une session à l’autre exposerait la personne à deux styles de réponse et fausserait sa note de satisfaction. Il produit une répartition équilibrée statistiquement dès que le volume est suffisant, et il reste reproductible : rejouer le calcul plus tard donne exactement les mêmes groupes, ce qui vous permet de recalculer une métrique oubliée sans relancer le test.
import random
import hashlib
from openai import OpenAI
client = OpenAI()
MODELE_BASE = "gpt-5.6-luna"
MODELE_FT = "ft:gpt-5.6-luna:org::suffix:xxxxxxxx"
PROMPT_BASE = "Vous êtes un assistant support client professionnel..."
def choisir_variante(user_id: str, pourcentage_test: int = 50) -> str:
"""Attribue un utilisateur au groupe A ou B de manière déterministe."""
h = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
return "B" if (h % 100) < pourcentage_test else "A"
def repondre(user_id: str, question: str) -> dict:
"""Répond avec le bon modèle selon le groupe A/B."""
variante = choisir_variante(user_id)
if variante == "A":
response = client.responses.create(
model=MODELE_BASE,
instructions=PROMPT_BASE,
input=question
)
else:
response = client.responses.create(
model=MODELE_FT,
input=question
)
return {
"variante": variante,
"modele": MODELE_BASE if variante == "A" else MODELE_FT,
"reponse": response.output_text,
"user_id": user_id
}
Choisir les bonnes métriques métier
Les indicateurs à suivre dépendent entièrement de ce que votre assistant est censé produire, et chaque famille d’usage a les siens.
Sur un service de support client, le taux de résolution au premier contact répond à la question la plus concrète : l’utilisateur a-t-il eu besoin de recontacter le support ? Le score CSAT, une note de satisfaction de 1 à 5 recueillie après l’interaction, capte le ressenti que les métriques automatiques manquent. Le temps de résolution mesure la durée totale de l’échange, et le taux d’escalade — la part des conversations transférées à un humain — se lit comme un aveu d’échec du modèle sur les cas concernés. Un fine-tuning réussi fait typiquement baisser les deux derniers en même temps.
Sur un chatbot commercial, le raisonnement se déplace vers l’aval. Le taux de conversion compte les conversations menant à un achat ou un rendez-vous ; le taux d’engagement, exprimé en nombre moyen de messages par conversation, s’interprète avec prudence puisqu’un échange qui s’allonge peut aussi bien signaler l’intérêt qu’une incompréhension ; le taux de rebond isole les utilisateurs qui quittent après le premier message, souvent parce que la première réponse a manqué sa cible.
Pour une tâche de classification ou d’extraction, on retrouve le trio classique. La précision donne le pourcentage de classifications correctes, le rappel le pourcentage de cas positifs effectivement détectés, et le F1-score, moyenne harmonique des deux, empêche de se satisfaire d’un modèle qui n’annonce jamais la classe rare pour ne jamais se tromper.
Collecter les mesures
Ces indicateurs ne servent à rien s’ils vivent dans trois tableaux séparés. Un collecteur unique, alimenté à chaque interaction, garde la variante à côté de la mesure et permet de produire le rapport comparatif d’une seule commande. La classe ABTracker ci-dessous fait ce travail minimal : elle journalise, puis regroupe par variante et calcule moyennes et taux.
import datetime
class ABTracker:
"""Collecteur de métriques pour test A/B."""
def __init__(self):
self.events = []
def log_interaction(
self,
user_id: str,
variante: str,
question: str,
reponse: str,
metriques: dict
):
"""Enregistre une interaction avec ses métriques."""
self.events.append({
"timestamp": datetime.datetime.now().isoformat(),
"user_id": user_id,
"variante": variante,
"question": question,
"reponse": reponse,
**metriques
})
def rapport(self) -> dict:
"""Génère un rapport comparatif A vs B."""
groupes = {"A": [], "B": []}
for e in self.events:
groupes[e["variante"]].append(e)
rapport = {}
for variante, events in groupes.items():
if not events:
continue
rapport[variante] = {
"nb_interactions": len(events),
"csat_moyen": self._moyenne(events, "csat"),
"resolution_premier_contact": self._taux(events, "resolu"),
"temps_moyen_sec": self._moyenne(events, "temps_resolution"),
}
return rapport
def _moyenne(self, events, cle):
vals = [e[cle] for e in events if cle in e and e[cle] is not None]
return sum(vals) / len(vals) if vals else None
def _taux(self, events, cle):
vals = [e[cle] for e in events if cle in e]
return sum(vals) / len(vals) * 100 if vals else None
tracker = ABTracker()
Attendre la significativité statistique
Ne tirez pas de conclusions trop tôt. Après vingt interactions par groupe, un écart de CSAT de 0,4 point n’a aucune valeur : trois clients de bonne humeur suffisent à le produire. Il vous faut un nombre suffisant d’interactions pour que les résultats soient significatifs, et les repères ci-dessous vous donnent l’ordre de grandeur à viser avant même de regarder les chiffres.
Le volume atteint, un test statistique tranche à votre place. Le test de Mann-Whitney convient bien aux scores de satisfaction, qui sont des notes ordinales rarement distribuées normalement. Sa p-value répond à une question précise : quelle est la probabilité d’observer un écart aussi grand si les deux modèles se valaient réellement ? En dessous de 0,05, l’hypothèse d’un simple hasard devient difficile à défendre.
from scipy import stats
def test_significativite(scores_a: list[float], scores_b: list[float]):
"""Teste si la différence entre A et B est significative."""
stat, p_value = stats.mannwhitneyu(scores_a, scores_b, alternative="two-sided")
moy_a = sum(scores_a) / len(scores_a)
moy_b = sum(scores_b) / len(scores_b)
print(f"Moyenne A : {moy_a:.3f}")
print(f"Moyenne B : {moy_b:.3f}")
print(f"p-value : {p_value:.4f}")
if p_value < 0.05:
gagnant = "B (fine-tuné)" if moy_b > moy_a else "A (base)"
print(f"Différence significative — {gagnant} est meilleur")
else:
print("Pas de différence significative")
Monter en charge progressivement
Même un test concluant ne justifie pas de basculer tout le trafic du jour au lendemain : un défaut qui n’apparaît qu’une fois sur mille reste invisible sur un échantillon et devient très visible sur un volume complet. Procédez par paliers d’une semaine, en vérifiant vos métriques à chaque étape avant d’ouvrir la suivante.
- Phase 1 (1 semaine) : 10 % de trafic sur le modèle fine-tuné
- Phase 2 (1 semaine) : 30 % si les métriques sont positives
- Phase 3 (1 semaine) : 50 % pour le test A/B principal
- Phase 4 : 100 % si le test est concluant
Si un palier dégrade vos métriques, revenez au précédent au lieu d’ouvrir le suivant en espérant que le chiffre se redresse de lui-même. Une semaine passée à comprendre pourquoi coûte toujours moins cher qu’un mois de trafic servi par un modèle qui déçoit.
Points clés à retenir
- Les métriques métier comptent plus que la loss d’entraînement
- Utilisez un hash déterministe pour l’attribution A/B
- Attendez la significativité statistique avant de conclure (p < 0.05)
- Déployez progressivement : 10 %, 30 %, 50 %, 100 %
- Mesurez coûts et latence en plus de la qualité