Aller au contenu principal

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 :

  1. Phase 1 (1 semaine) : 10 % de trafic sur le modèle fine-tuné
  2. Phase 2 (1 semaine) : 30 % si les métriques sont positives
  3. Phase 3 (1 semaine) : 50 % pour le test A/B principal
  4. 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é