Leçon 6 — Promouvoir et exploiter

Comparer, exploiter, nettoyer

7 min de lecture

Le comité RH de Valoria ne lira ni un tableau de moyennes ni un journal. Il veut voir, question par question, ce que répondait l'ancienne version et ce que répond la nouvelle. L'équipe d'exploitation, elle, veut savoir que la démo se relance de zéro, que le test de bout en bout prouve quelque chose, et que rien ne reste sur le poste après. Cette leçon couvre les deux publics.

Le mécanisme

make e2e enchaîne la démo complète puis quatre vérifications contre le serveur réel :

flowchart LR
  R["register<br/>v1, v2, @production -> v1"] --> E1["evaluate v1"] --> E2["evaluate v2"] --> P["promote<br/>porte"] --> K["package<br/>@champion"] --> Q["ask<br/>models:/…@champion"]
  Q --> C{"4 vérifications"}
  C --> C1["alias production posé"]
  C --> C2["alias champion posé"]
  C --> C3["traces enregistrées"]
  C --> C4["deux versions évaluées"]

Dans l'interface, la page Evaluation runs compare deux runs : elle apparie les traces par requête et affiche côte à côte les réponses et les évaluations. L'appariement repose sur l'entrée de la racine de la trace ; c'est pourquoi evaluate() trace une fonction predict(question) qui ne reçoit que la question (leçon 4).

La mise en œuvre

Pour comparer : ouvrez http://localhost:5050, expérience lab-mlflow-assistant-rh, Evaluation runs, cochez prompt-v2 et prompt-v1, puis Actions > Compare.

Comparaison de prompt-v2 et prompt-v1 dans MLflow (2026-10-16) : pour chaque question, la réponse de v2 (refus exact, réponse courte) face à celle de v1 (« Bonjour, en tant qu'assistant RH… »)

Sur la question des lunettes, v2 répond par le refus exact attendu (« Le guide ne couvre pas ce point, contactez… ») ; v1 commence par « Bonjour, En tant qu'assistant RH de Valoria Mutuelle » et n'atteint pas la formule. Les lignes dont un côté vaut null sont des traces sans vis-à-vis : les deux tentatives de v2 en erreur sur 429, rejouées par le harnais (leçon 3).

Les vérifications de make e2e portent sur des effets, pas sur des codes de retour :

files/lab.py (extrait)
checks = {
    "alias production posé": client.get_prompt_version_by_alias(PROMPT_NAME, "production") is not None,
    "alias champion posé": client.get_model_version_by_alias(MODEL_NAME, "champion") is not None,
    "traces enregistrées": len(mlflow.search_traces(max_results=50, return_type="list", flush=True)) > 0,
    "deux versions évaluées": len(latest_runs()) >= 2,
}

Le nettoyage est hiérarchisé, et chaque niveau est vérifié :

CommandeSupprimeGarde
make downconteneurs, réseau, volumes pgdata et artifacts (runs, prompts, traces, modèles)image, .venv, .env
make cleandown + caches Python, mlruns/, mlartifacts/, *.db locauximage, .venv, .env
make resetclean + .venv + image lab-mlflow-server:3.17.1.env

Résultat obtenu (2026-10-16)

Dernier make e2e sur une pile neuve (make up : 15,8 s), Gemini 2.5 Flash sur Vertex AI, 31 reprises sur 429 pendant l'exécution :

INFO lab: prompt v1 -> version 1
INFO lab: prompt v2 -> version 2
INFO lab: alias production -> version 1
INFO lab: v1 run=b232ad53 {'concision/mean': 0.8, 'cite_la_section/mean': 0.6, 'correctness/mean': 1.0}
INFO lab: v2 run=8bcb08c0 {'cite_la_section/mean': 1.0, 'concision/mean': 1.0, 'correctness/mean': 1.0}
Candidat v2 {'cite_la_section/mean': 1.0, 'correctness/mean': 1.0, 'concision/mean': 1.0}
Production v1 {'cite_la_section/mean': 0.6, 'correctness/mean': 1.0, 'concision/mean': 0.8}
-> gain sur cite_la_section/mean, concision/mean
Alias production : v1 -> v2 (previous -> v1)
Modèle lab-assistant-rh-modele version 1 -> alias champion
Q : Je pars à Lyon en train, 2h de trajet : première classe possible ?
R : La première classe n'est pas autorisée pour un trajet de 2 heures. Le train doit être réservé en
    seconde classe. La première classe est possible uniquement pour les trajets de plus de 3 heures. [§2]
  [OK] alias production posé
  [OK] alias champion posé
  [OK] traces enregistrées
  [OK] deux versions évaluées
timeout 900 make e2e  19,40s user 1,71s system 12% cpu 2:45,84 total

Puis le nettoyage complet, vérifié par Docker lui-même :

$ make clean
Vérifié : aucun conteneur ni volume lab-mlflow.
Artefacts supprimés.
$ make reset
Artefacts supprimés.
Environnement supprimé (.env conservé).
$ docker ps -aq --filter label=com.docker.compose.project=lab-mlflow | wc -l
0
$ docker volume ls -q --filter label=com.docker.compose.project=lab-mlflow | wc -l
0
$ docker images -q lab-mlflow-server | wc -l
0

Ce qui casse en production

  • Un e2e qui ne vérifie que le code de sortie. Une démo peut finir en 0 sans avoir promu (porte refusée) ni tracé (export perdu). Parade : des vérifications d'effets, comme les quatre ci-dessus ; la porte refusée n'est pas une erreur, mais l'alias production doit exister dans tous les cas.
  • Pas de sauvegarde. Le volume PostgreSQL est la seule copie des prompts, des évaluations et des alias. Parade en équipe : pg_dump planifié, ou une base managée (Cloud SQL) avec sauvegardes automatiques ; et les prompts restent aussi dans le dépôt (prompts/*.txt), d'où register() les recrée.
  • Interface ouverte à tous. Les traces contiennent les questions des collaborateurs. Parade : authentification devant le serveur, comme en leçon 1, et rôles distincts (lecture pour le comité, écriture pour la CI).
  • Pile oubliée. Un make up du matin tourne encore le soir, port 5050 occupé, 400 Mo de volume. Parade : make down en fin de session ; docker ps --filter label=com.docker.compose.project=lab-mlflow doit être vide.

La règle de décision

Montrez la comparaison question par question à ceux qui décident, et gardez les moyennes pour la porte automatique. Un test de bout en bout vérifie des effets observables dans le serveur. En équipe, sortez la base de Docker (base managée, sauvegardée) avant d'y stocker quoi que ce soit dont vous aurez besoin dans six mois.

Pratique

make e2e affiche les quatre [OK], mais le comité RH constate dans l'interface que v2 refuse de répondre à une question couverte par le guide. Que concluez-vous sur la chaîne ?

Vous devez libérer le port 5050 et repartir de zéro demain, sans réinstaller les dépendances Python. Quelle commande ?

Exercice. Ajoutez une cinquième vérification à e2e() : la trace la plus récente porte un tag prompt_version égal à la version désignée par l'alias production.

Voir la solution
prod = client.get_prompt_version_by_alias(PROMPT_NAME, "production").version
last = mlflow.search_traces(max_results=1, order_by=["timestamp_ms DESC"], return_type="list", flush=True)
checks["dernière trace sur la version en production"] = (
    bool(last) and last[0].info.tags.get("prompt_version") == str(prod))

La dernière trace est celle de ask(), appelée après la promotion : elle doit porter la nouvelle version. Vérifié sur le serveur après le make e2e ci-dessus : alias production sur 2, dernière trace (« Je pars à Lyon… ») étiquetée prompt_version=2. Si un jour elle porte l'ancienne version, c'est qu'un texte périmé a été servi (cache de load_prompt, ou alias lu avant la promotion), exactement ce que la vérification doit attraper.

  • Je compare deux versions question par question dans l'interface.
  • Mon test de bout en bout vérifie des effets dans le serveur.
  • Je connais ce que suppriment make down, make clean et make reset, et je l'ai vérifié.
  • Je sais ce qu'il faut sauvegarder avant d'utiliser MLflow en équipe.