Tests A/B et métriques métier
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, etc.
Mettre en place un test A/B
Principe
Un test A/B compare deux variantes en conditions réelles :
- Groupe A (contrôle) : modèle de base avec prompt optimisé
- Groupe B (test) : 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.
Implémentation
import random
import hashlib
from openai import OpenAI
client = OpenAI()
MODELE_BASE = "gpt-5.3-mini"
MODELE_FT = "ft:gpt-5.3-mini: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
}
Attribution déterministe
L’utilisation d’un hash du user_id garantit que :
- Un même utilisateur voit toujours la même variante
- La répartition est équilibrée statistiquement
- Le test est reproductible
Métriques métier à suivre
Support client
- Taux de résolution au premier contact : l’utilisateur a-t-il eu besoin de recontacter le support ?
- Score CSAT : note de satisfaction de 1 à 5 après l’interaction
- Temps de résolution : durée totale de l’échange
- Taux d’escalade : pourcentage de conversations transférées à un humain
Chatbot commercial
- Taux de conversion : pourcentage de conversations menant à un achat ou un rendez-vous
- Taux d’engagement : nombre moyen de messages par conversation
- Taux de rebond : utilisateurs qui quittent après le premier message
Classification / Extraction
- Précision : pourcentage de classifications correctes
- Rappel : pourcentage de cas positifs détectés
- F1-score : moyenne harmonique de précision et rappel
Collecter les métriques
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()
Significativité statistique
Ne tirez pas de conclusions trop tôt. Vous avez besoin d’un nombre suffisant d’interactions pour que les résultats soient significatifs.
Taille d’échantillon minimale
100
Par groupe minimum
500+
Pour des résultats fiables
p < 0.05
Seuil de significativité
Test statistique simple
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")
Déploiement progressif
Ne passez pas de 0 % à 100 % d’un coup :
- 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
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é