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.
| Version | Ce qu’elle signale |
|---|---|
| v1.0 | Premier modèle validé en production |
| v1.1 | Correction de données (mêmes hyperparamètres) |
| v2.0 | Changement de modèle de base ou de stratégie |
| v2.1 | Ajustement 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.