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.
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 :
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é :
| Commande | Supprime | Garde |
|---|---|---|
make down | conteneurs, réseau, volumes pgdata et artifacts (runs, prompts, traces, modèles) | image, .venv, .env |
make clean | down + caches Python, mlruns/, mlartifacts/, *.db locaux | image, .venv, .env |
make reset | clean + .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 totalPuis 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
0Ce 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
productiondoit 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_dumpplanifié, 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 updu matin tourne encore le soir, port 5050 occupé, 400 Mo de volume. Parade :make downen fin de session ;docker ps --filter label=com.docker.compose.project=lab-mlflowdoit ê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 ?
La porte ne voit que le jeu. Un défaut constaté hors du jeu se traite en l'y ajoutant (avec l'attendu), ce qui le transforme en régression détectable pour toutes les versions futures. Accuser la porte ou le juge sans cette étape ne garantit rien pour la prochaine version.
Vous devez libérer le port 5050 et repartir de zéro demain, sans réinstaller les dépendances Python. Quelle commande ?
make reset supprimerait aussi .venv et l'image, donc une réinstallation et une reconstruction. docker compose down sans -p lab-mlflow ni -v ne vise pas forcément le bon projet et garde les volumes : demain, vous repartiriez avec les données d'aujourd'hui.
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 cleanetmake reset, et je l'ai vérifié. - Je sais ce qu'il faut sauvegarder avant d'utiliser MLflow en équipe.