Lección 5 — Promouvoir et exploiter

Porte de promotion et registre de modèles

7 min de lectura

Este contenido aún no está traducido. Se muestra la versión original.

Les deux runs d'évaluation sont là ; reste la décision. Chez Valoria, la question posée en comité était : « qui a le droit de passer v2 en production, et sur quel critère ? ». Et l'équipe d'exploitation en posait une autre : « que déploie-t-on exactement ? ». Un prompt seul ne se déploie pas ; il faut le code qui l'appelle, le guide qu'il cite, les dépendances. La porte répond à la première question, le registre de modèles à la seconde.

Le mécanisme

La porte compare les derniers runs d'évaluation des deux versions, métrique par métrique :

  • aucune régression : si une seule métrique du candidat est inférieure à celle de la production, refus ;
  • au moins un gain : si tout est égal, refus aussi, car rien ne justifie le changement.

Si la porte passe, l'alias production du prompt passe au candidat et previous garde l'ancienne version.

Le registre de modèles stocke ensuite l'assistant empaqueté (pyfunc « models from code ») : le code de assistant.py, le guide en artefact, les dépendances épinglées, et le lien vers la version de prompt évaluée. L'alias champion désigne la version à servir.

flowchart LR
  R1["run prompt-v1<br/>0,8 / 0,8 / 1,0"] --> D{"decide()<br/>aucune régression<br/>au moins un gain"}
  R2["run prompt-v2<br/>0,9 / 1,0 / 1,0"] --> D
  D -- "gain sur cite_la_section, concision" --> A["prompt @production -> v2<br/>@previous -> v1"]
  A --> PK["package()<br/>log_model + registered_model_name"]
  PK --> M["lab-assistant-rh-modele<br/>version N, @champion"]
  M --> Q["ask() : models:/…@champion"]
  Q -. "relit le prompt par alias" .-> A

Le modèle ne fige pas le texte du prompt : AssistantRH.predict appelle answer(), qui relit @production. Changer de prompt ne demande donc pas de nouvelle version de modèle ; changer le code ou le guide, si.

La mise en œuvre

files/lab.py (extrait)
def decide(candidate: dict, production: dict) -> tuple[bool, str]:
    """Porte de promotion : aucune régression, au moins un gain."""
    keys = sorted(set(candidate) & set(production))
    if not keys:
        return False, "aucune métrique commune"
    worse = [k for k in keys if candidate[k] < production[k]]
    better = [k for k in keys if candidate[k] > production[k]]
    if worse:
        return False, "régression sur " + ", ".join(worse)
    if not better:
        return False, "aucun gain mesuré"
    return True, "gain sur " + ", ".join(better)

L'empaquetage « models from code » enregistre un fichier Python plutôt qu'un objet sérialisé par cloudpickle, ce qui évite d'exécuter un binaire arbitraire au chargement :

files/lab.py (extrait)
info = mlflow.pyfunc.log_model(
    name="assistant",
    python_model=str(HERE / "assistant_model.py"),
    code_paths=[str(HERE / "assistant.py")],
    artifacts={"guide": str(HERE / "data" / "politique.md")},
    pip_requirements=["mlflow==3.17.1", "google-genai==2.31.0"],
    prompts=[f"prompts:/{PROMPT_NAME}/{prod.version}"],
    registered_model_name=MODEL_NAME,
)
MlflowClient().set_registered_model_alias(MODEL_NAME, "champion", info.registered_model_version)
files/assistant_model.py
class AssistantRH(PythonModel):
    def load_context(self, context):
        self.guide = open(context.artifacts["guide"], encoding="utf-8").read()

    def predict(self, context, model_input: list[str], params: dict | None = None) -> list[str]:
        from assistant import answer
        uri = (params or {}).get("prompt_uri", "prompts:/lab-assistant-rh@production")
        return [answer(q, prompt_uri=uri, guide_text=self.guide) for q in model_input]

mlflow.models.set_model(AssistantRH())

Le test de la porte couvre les trois issues :

files/tests/test_lab.py (extrait)
@pytest.mark.parametrize("cand,prod,ok", [
    ({"a/mean": 1.0, "b/mean": 0.9}, {"a/mean": 0.5, "b/mean": 0.9}, True),
    ({"a/mean": 1.0, "b/mean": 0.8}, {"a/mean": 0.5, "b/mean": 0.9}, False),  # régression sur b
    ({"a/mean": 0.5}, {"a/mean": 0.5}, False),                                # aucun gain
])

Résultat obtenu (2026-10-16)

Candidat v2 {'cite_la_section/mean': 0.9, 'concision/mean': 1.0, 'correctness/mean': 1.0}
Production v1 {'cite_la_section/mean': 0.8, 'concision/mean': 0.8, 'correctness/mean': 1.0}
-> gain sur cite_la_section/mean, concision/mean
Alias production : v1 -> v2 (previous -> v1)
Successfully registered model 'lab-assistant-rh-modele'.
Created version '1' of model 'lab-assistant-rh-modele'.
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 est autorisée pour un trajet de plus de 3 heures. Votre trajet de 2 heures
    ne remplit pas cette condition. Vous devez donc réserver en seconde classe. [§2]

Un second make e2e sur une pile qui contenait déjà cette promotion (exécution précédente du même jour) : La version 2 est déjà en production., puis Created version '2' of model 'lab-assistant-rh-modele' et l'alias champion déplacé sur la version 2.

Registre de modèles MLflow après le premier make e2e sur une pile neuve (2026-10-16) : lab-assistant-rh-modele, version 1, alias champion

Ce qui casse en production

  • Comparer des runs d'âges différents. latest_runs() prend le dernier run de chaque version ; si la production a été évaluée il y a un mois, sur un autre jeu ou un autre modèle, la comparaison est faussée. Parade : réévaluer les deux versions dans la même exécution, comme le fait demo(), et comparer les paramètres model et judge des deux runs avant de décider.
  • « Aucune régression » sur du bruit. Avec dix questions, correctness a varié de 1,0 à 0,9 entre deux exécutions de la même version : la porte peut refuser un bon candidat sur une ligne. Parade : un jeu plus grand, ou une tolérance par métrique (candidate[k] < production[k] - 0.05) décidée et écrite.
  • Modèle qui embarque un vieux prompt. Si predict figeait le texte au moment de log_model, déplacer l'alias du prompt n'aurait aucun effet sur le modèle servi. Parade : relire par alias, comme ici, ou au contraire figer volontairement et créer une version de modèle à chaque promotion ; choisissez, puis écrivez-le.
  • Modèle chargé sans ses dépendances. pip_requirements épinglé évite qu'un serveur de modèles installe une version de google-genai incompatible. Parade : versions exactes, et un test de chargement (ask()) dans make e2e.

La règle de décision

Une porte simple et stricte (aucune régression, au moins un gain) convient tant que le jeu est petit et que les métriques sont des taux. Dès que le bruit d'une exécution à l'autre dépasse l'écart que vous voulez détecter, agrandissez le jeu avant d'ajouter des tolérances. Enregistrez dans le registre de modèles ce qui se déploie (code, données, dépendances) ; laissez au registre de prompts ce qui change souvent.

Pratique

Le candidat v3 obtient cite_la_section 1,0 (production 0,9), concision 1,0 (1,0), correctness 0,9 (1,0). La porte refuse. Un collègue propose de retirer correctness de la porte « parce que le juge est bruité ». Que répondez-vous ?

Vous déplacez l'alias production du prompt de v2 vers v1 (retour arrière). Que faut-il faire du modèle lab-assistant-rh-modele@champion ?

Exercice. Ajoutez une tolérance de 0,05 pour correctness/mean seulement (le juge est la métrique la plus bruitée), avec les tests correspondants.

Voir la solution
TOLERANCE = {"correctness/mean": 0.05}

worse = [k for k in keys if candidate[k] < production[k] - TOLERANCE.get(k, 0.0)]

Tests :

({"correctness/mean": 0.96, "x/mean": 1.0}, {"correctness/mean": 1.0, "x/mean": 0.9}, True),   # dans la tolérance
({"correctness/mean": 0.90, "x/mean": 1.0}, {"correctness/mean": 1.0, "x/mean": 0.9}, False),  # au-delà

Avec 10 lignes, une erreur du juge vaut 0,1 : une tolérance de 0,05 ne couvre donc pas une ligne. Elle n'a de sens qu'avec un jeu d'au moins une vingtaine de questions. Le seuil se choisit à partir de la variance mesurée, pas au jugé.

  • J'écris la porte de promotion comme une fonction pure, testée.
  • Je déplace les alias production et previous ensemble.
  • J'empaquète l'assistant en « models from code » avec ses dépendances épinglées.
  • Je sais ce que change un déplacement d'alias de prompt pour le modèle servi.
Siguiente