Aller au contenu principal

Versioning et rollback

Mis à jour le 28 juillet 2026

Pourquoi versionner vos modèles

En production, vous allez itérer sur votre modèle fine-tuné : nouvelles données, hyperparamètres ajustés, nouveau modèle de base. Sans un système de versioning rigoureux, vous perdrez la traçabilité et la capacité de revenir en arrière en cas de régression. Le scénario est toujours le même : un lundi matin, les retours utilisateurs se dégradent, personne ne sait quel modèle tourne réellement ni quelles données l’ont produit, et l’équipe passe la journée à reconstituer une information qu’un fichier de quelques lignes aurait conservée.

Nommer les versions

Le paramètre suffix du job de fine-tuning est votre premier outil de traçabilité : la chaîne que vous y placez se retrouve dans l’identifiant final du modèle et reste lisible partout — dans vos journaux, dans votre configuration, dans la console. Adoptez une convention et tenez-vous-y.

# Convention : {nom}-v{version_majeure}.{version_mineure}
job = client.fine_tuning.jobs.create(
    training_file=training_file.id,
    model="gpt-5.6-luna",
    suffix="support-client-v2.1"
)

Le numéro lui-même doit porter une information, et non compter les tentatives. Une version majeure change quand la nature du modèle change ; une version mineure, quand vous améliorez sans rompre. Concrètement, un correctif de données appliqué aux mêmes hyperparamètres reste sur la même majeure, alors qu’un changement de modèle de base impose de repartir sur une nouvelle.

VersionCe qu’elle signale
v1.0Premier modèle validé en production
v1.1Correction de données (mêmes hyperparamètres)
v2.0Changement de modèle de base ou de stratégie
v2.1Ajustement d’hyperparamètres

Tenir un registre de modèles

Le nom seul ne suffit pas : il ne dit ni ce qu’il y avait dans le jeu de données, ni les résultats obtenus, ni pourquoi cette version a été promue. Un registre centralisé conserve ces métadonnées à côté de l’identifiant technique et devient la référence unique de votre équipe. La classe ci-dessous fait tenir l’essentiel dans un fichier JSON : elle enregistre un modèle candidat, en promeut un en production tout en marquant le précédent comme retiré, restitue l’identifiant actuellement actif et affiche l’historique. Prenez le temps de remplir le champ notes : une seule phrase sur ce qui a changé depuis la version précédente vous épargnera plus tard une demi-journée d’archéologie.

import json
import datetime

class ModelRegistry:
    """Registre des modèles fine-tunés."""

    def __init__(self, chemin: str = "model_registry.json"):
        self.chemin = chemin
        try:
            with open(chemin, "r") as f:
                self.registre = json.load(f)
        except FileNotFoundError:
            self.registre = {"modeles": [], "production": None}

    def enregistrer(
        self,
        version: str,
        model_id: str,
        job_id: str,
        dataset_info: dict,
        hyperparams: dict,
        metriques: dict,
        notes: str = ""
    ):
        """Enregistre un nouveau modèle dans le registre."""
        entree = {
            "version": version,
            "model_id": model_id,
            "job_id": job_id,
            "date": datetime.datetime.now().isoformat(),
            "dataset": dataset_info,
            "hyperparametres": hyperparams,
            "metriques": metriques,
            "notes": notes,
            "statut": "candidat"
        }
        self.registre["modeles"].append(entree)
        self._sauvegarder()
        print(f"Modèle {version} enregistré : {model_id}")

    def promouvoir(self, version: str):
        """Promeut un modèle en production."""
        ancien = self.registre["production"]
        for m in self.registre["modeles"]:
            if m["version"] == version:
                m["statut"] = "production"
                self.registre["production"] = version
                break

        if ancien:
            for m in self.registre["modeles"]:
                if m["version"] == ancien:
                    m["statut"] = "retiré"

        self._sauvegarder()
        print(f"Production : {ancien} -> {version}")

    def rollback(self, version: str):
        """Rollback vers une version précédente."""
        self.promouvoir(version)
        print(f"Rollback effectué vers {version}")

    def modele_production(self) -> str:
        """Retourne l'ID du modèle en production."""
        version = self.registre["production"]
        for m in self.registre["modeles"]:
            if m["version"] == version:
                return m["model_id"]
        return None

    def historique(self):
        """Affiche l'historique des modèles."""
        print(f"{'Version':<12} {'Statut':<12} {'Date':<12} {'Loss val':<10}")
        print("-" * 50)
        for m in self.registre["modeles"]:
            date = m["date"][:10]
            loss = m["metriques"].get("validation_loss", "N/A")
            print(f"{m['version']:<12} {m['statut']:<12} {date:<12} {loss}")

    def _sauvegarder(self):
        with open(self.chemin, "w") as f:
            json.dump(self.registre, f, indent=2, ensure_ascii=False)

À l’usage, le cycle de vie d’une version tient en trois appels : on enregistre le modèle sorti de l’entraînement avec ses chiffres, on le promeut quand l’évaluation le valide, et on revient à la version précédente si la production dément l’évaluation. Cette dernière ligne doit être aussi banale à écrire que les deux premières.

registry = ModelRegistry()

# Enregistrer un nouveau modèle
registry.enregistrer(
    version="v2.1",
    model_id="ft:gpt-5.6-luna:org::support-v2.1:abc123",
    job_id="ftjob-xyz789",
    dataset_info={"train": 350, "validation": 45},
    hyperparams={"n_epochs": 3, "lr": "auto"},
    metriques={"validation_loss": 0.38, "precision_test": 0.93},
    notes="Ajout de 50 exemples correctifs pour les remboursements"
)

# Promouvoir en production
registry.promouvoir("v2.1")

# En cas de problème, rollback
registry.rollback("v2.0")

Revenir en arrière en quelques secondes

Un rollback n’a de valeur que s’il est rapide et sans réflexion. Puisque votre application lit le nom du modèle dans une variable d’environnement, changer de version revient à changer cette variable et à mettre le registre à jour — aucun redéploiement de code, aucune recompilation. La fonction rollback_rapide va plus loin en se passant même du choix humain : elle reprend la dernière version stable disponible et, s’il n’y en a aucune, retombe sur le modèle de base. Cette dernière branche est votre filet de sécurité absolu ; déclenchez-la volontairement une fois en environnement de test, pendant que l’enjeu est nul.

import os

def deployer_modele(version: str, registry: ModelRegistry):
    """Déploie un modèle en mettant à jour la variable d'environnement."""
    model_id = None
    for m in registry.registre["modeles"]:
        if m["version"] == version:
            model_id = m["model_id"]
            break

    if not model_id:
        raise ValueError(f"Version {version} non trouvée")

    # Mettre à jour la configuration
    os.environ["OPENAI_FT_MODEL"] = model_id
    registry.promouvoir(version)

    print(f"Modèle déployé : {version} ({model_id})")
    return model_id

def rollback_rapide(registry: ModelRegistry):
    """Rollback vers la dernière version stable."""
    production = registry.registre["production"]
    versions = [
        m for m in registry.registre["modeles"]
        if m["version"] != production and m["statut"] != "retiré"
    ]

    if not versions:
        # Fallback sur le modèle de base
        os.environ["OPENAI_FT_MODEL"] = "gpt-5.6-luna"
        print("Rollback vers le modèle de base")
        return

    derniere_stable = versions[-1]
    deployer_modele(derniere_stable["version"], registry)

Archiver les données, pas seulement les modèles

Versionner un modèle sans versionner ce qui l’a produit ne vous mène qu’à mi-chemin. Le jour où vous voudrez comprendre pourquoi la v2.1 tenait mieux le ton que la v2.2, il vous faudra les trois fichiers exacts qui ont servi à l’entraîner, y compris le jeu de test — sans lui, aucune comparaison ultérieure n’est équitable. Archivez-les dans un dossier portant le numéro de version, au moment même où vous enregistrez le modèle.

import shutil

def archiver_dataset(version: str, fichiers: list[str], dossier_archive: str):
    """Archive les données d'entraînement pour une version donnée."""
    dossier = f"{dossier_archive}/{version}"
    os.makedirs(dossier, exist_ok=True)

    for fichier in fichiers:
        shutil.copy2(fichier, dossier)

    print(f"Données archivées dans {dossier}")

archiver_dataset(
    "v2.1",
    ["train.jsonl", "validation.jsonl", "test.jsonl"],
    "archives/datasets"
)

Automatiser la chaîne complète

Ces gestes, exécutés à la main, finissent par sauter — surtout l’archivage, qui vient en dernier et ne bloque rien. Rassemblez-les dans un script unique qui enchaîne l’upload des fichiers, le lancement du job, l’attente de sa terminaison, l’enregistrement au registre puis l’archivage des données. La boucle d’attente interroge le job toutes les trente secondes et s’arrête sur un statut terminal ; en cas d’échec, la fonction rend la main immédiatement plutôt que de poursuivre sur un modèle inexistant.

def pipeline_deploiement(
    training_file: str,
    validation_file: str,
    version: str,
    modele_base: str = "gpt-5.6-luna",
    hyperparams: dict = None
):
    """Pipeline complet : upload, train, évaluer, enregistrer."""
    client = OpenAI()
    registry = ModelRegistry()

    # 1. Upload
    print("Upload des données...")
    train = client.files.create(file=open(training_file, "rb"), purpose="fine-tune")
    val = client.files.create(file=open(validation_file, "rb"), purpose="fine-tune")

    # 2. Entraînement
    print("Lancement du fine-tuning...")
    params = {"training_file": train.id, "validation_file": val.id,
              "model": modele_base, "suffix": version}
    if hyperparams:
        params["hyperparameters"] = hyperparams

    job = client.fine_tuning.jobs.create(**params)

    # 3. Attendre la fin
    import time
    while True:
        job = client.fine_tuning.jobs.retrieve(job.id)
        if job.status in ("succeeded", "failed"):
            break
        time.sleep(30)

    if job.status == "failed":
        print(f"Échec : {job.error}")
        return None

    # 4. Enregistrer
    registry.enregistrer(
        version=version,
        model_id=job.fine_tuned_model,
        job_id=job.id,
        dataset_info={"train": training_file, "validation": validation_file},
        hyperparams=hyperparams or {},
        metriques={"trained_tokens": job.trained_tokens}
    )

    # 5. Archiver les données
    archiver_dataset(version, [training_file, validation_file], "archives/datasets")

    print(f"Pipeline terminé : {job.fine_tuned_model}")
    return job.fine_tuned_model

Faire le ménage

Les modèles fine-tunés sont stockés chez OpenAI et peuvent être supprimés quand vous n’en avez plus besoin. Au bout d’une dizaine d’itérations, la liste devient illisible et le tri manuel dangereux. Le nettoyage automatique ci-dessous conserve les trois versions les plus récentes et ne touche jamais à celle qui tourne en production. Gardez-vous d’être trop agressif : c’est précisément l’avant-dernière version, souvent jugée inutile, qui vous sauvera lors d’un rollback.

def nettoyer_anciens_modeles(registry: ModelRegistry, garder: int = 3):
    """Supprime les anciens modèles en gardant les N derniers."""
    modeles = registry.registre["modeles"]
    production = registry.registre["production"]

    candidats = [
        m for m in modeles
        if m["version"] != production
    ]

    a_supprimer = candidats[:-garder] if len(candidats) > garder else []

    for m in a_supprimer:
        try:
            client.models.delete(m["model_id"])
            print(f"Supprimé : {m['version']} ({m['model_id']})")
        except Exception as e:
            print(f"Erreur suppression {m['version']} : {e}")

Points clés à retenir

  • Utilisez des suffixes versionnés pour identifier chaque modèle
  • Maintenez un registre centralisé avec métriques et métadonnées
  • Implémentez un mécanisme de rollback rapide
  • Archivez les données d’entraînement avec chaque version
  • Automatisez le pipeline et nettoyez régulièrement les anciens modèles : moins d’erreurs humaines, moins de coûts inutiles

Testez vos connaissances

Avant de versionner vos modèles, validez le cycle complet du fine-tuning.

1. Quand le fine-tuning est-il le bon outil — et quand ne l'est-il pas ?

Réponse : Bon pour ancrer un style, un format ou un comportement récurrent que le prompting n’obtient pas de façon fiable ; mauvais pour injecter des connaissances fraîches — c’est le rôle du RAG.

2. Qu'est-ce qui fait la qualité d'un jeu d'entraînement ?

Réponse : Des exemples propres, cohérents et représentatifs au format JSONL conversationnel : quelques centaines d’exemples excellents battent des milliers d’exemples bruités.

3. Qu'apporte le DPO par rapport au fine-tuning supervisé ?

Réponse : L’apprentissage par préférences : on fournit des paires réponse préférée/rejetée, et le modèle apprend le « mieux » plutôt que la simple imitation — utile pour le ton et les arbitrages fins.

4. Comment valide-t-on un modèle fine-tuné ?

Réponse : En le comparant au modèle de base sur un jeu d’évaluation tenu à l’écart de l’entraînement, puis par tests A/B sur métriques métier — jamais sur la seule impression.

5. Pourquoi versionner et prévoir le rollback ?

Réponse : Chaque fine-tuning produit un modèle daté : en cas de régression en production, on rebascule sur la version précédente immédiatement — le modèle se gère comme du code.

Données propres, évaluation honnête, versions maîtrisées : le fine-tuning est une discipline d’ingénierie — ce cours vous en a donné le cycle complet.