Dans la première version de l'assistant de Valoria, le prompt est une chaîne de caractères dans assistant.py. Le changer demande une revue de code, un déploiement, et rien ne relie la réponse d'un mardi au texte qui l'a produite. Pire : pour comparer deux prompts, il faut deux branches. Le prompt est une configuration qui évolue plus vite que le code ; il lui faut son propre cycle de version.
Le mécanisme
Le registre de prompts de MLflow stocke des versions immuables d'un prompt nommé, et des alias mobiles qui pointent vers une version. Le code ne référence jamais un numéro : il demande prompts:/lab-assistant-rh@production. Promouvoir, c'est déplacer l'alias ; revenir en arrière, c'est le remettre sur la version précédente, sans déploiement.
flowchart LR F1["prompts/v1.txt"] -- "register_prompt" --> V1["version 1<br/>(immuable)"] F2["prompts/v2.txt"] -- "register_prompt" --> V2["version 2<br/>(immuable)"] AP(["@production"]) -.-> V1 AP -. "après la porte" .-> V2 APR(["@previous"]) -.-> V1 APP["assistant.answer()"] -- "load_prompt(@production), cache 60 s" --> AP
| Référence | Désigne | Usage |
|---|---|---|
prompts:/lab-assistant-rh/2 | la version 2, pour toujours | évaluation d'une version précise |
prompts:/lab-assistant-rh@production | ce que l'alias désigne au moment du chargement | application |
prompts:/lab-assistant-rh@previous | la version remplacée lors de la dernière promotion | retour arrière |
Les variables s'écrivent entre doubles accolades ({{guide}}, {{question}}) ; prompt.format(...) les remplit, et prompt.variables les liste.
La mise en œuvre
Les fichiers prompts/v1.txt et prompts/v2.txt restent la source, revue en demande de fusion. register() les pousse dans le registre de façon idempotente : une nouvelle version n'est créée que si le texte a changé.
def register() -> dict[int, int]:
"""Idempotent : ne crée une version que si le texte du fichier a changé."""
versions = {}
for n in (1, 2):
template = (HERE / "prompts" / f"v{n}.txt").read_text(encoding="utf-8")
existing = [v for v in search_versions() if v.template == template]
pv = existing[0] if existing else mlflow.genai.register_prompt(
name=PROMPT_NAME, template=template, commit_message=f"prompts/v{n}.txt",
tags={"source": f"prompts/v{n}.txt"},
)
versions[n] = pv.version
try:
MlflowClient().get_prompt_version_by_alias(PROMPT_NAME, "production")
except Exception:
mlflow.genai.set_prompt_alias(PROMPT_NAME, alias="production", version=versions[1])
return versionsL'alias production n'est posé que s'il n'existe pas : relancer register() après une promotion ne remet pas v1 en production.
Côté application, le chargement par alias passe par un cache de 60 secondes, pour ne pas interroger le serveur à chaque question :
@mlflow.trace(name="assistant_rh", span_type="CHAIN")
def answer(question: str, prompt_uri: str = f"prompts:/{PROMPT_NAME}@production",
guide_text: str | None = None, llm=None) -> str:
prompt = mlflow.genai.load_prompt(prompt_uri, cache_ttl_seconds=60)
if mlflow.get_current_active_span(): # absent quand le traçage est désactivé
mlflow.update_current_trace(tags={"prompt_uri": prompt_uri, "prompt_version": str(prompt.version)})
text = prompt.format(guide=guide_text or guide(), question=question)
return generate(text, llm=llm)Le test vérifie l'idempotence et que toutes les variables sont remplies :
first = lab.register()
again = lab.register()
assert first == again == {1: 1, 2: 2}
...
assert "{{" not in llm.prompts[0] # toutes les variables du template sont rempliesRésultat obtenu (2026-10-16)
Premier make e2e sur une pile neuve, puis second sur la même pile :
INFO lab: prompt v1 -> version 1
INFO lab: prompt v2 -> version 2
INFO lab: alias production -> version 1
...
INFO lab: prompt v1 -> version 1 (déjà là)
INFO lab: prompt v2 -> version 2 (déjà là)Après la promotion (leçon 5), l'interface montre les deux versions et leurs alias :
Et l'onglet de comparaison met les deux textes côte à côte :
Ce qui casse en production
- Prompt et modèle de même nom. Prompts et modèles partagent un espace de noms.
create_registered_model("lab-assistant-rh")est refusé (« name is already taken by a prompt »), maislog_model(registered_model_name="lab-assistant-rh")crée sans erreur une « version 3 » du prompt qui contient un modèle (vérifié avec MLflow 3.17.1). Parade : le modèle s'appellelab-assistant-rh-modele. - Alias déplacé, application inchangée pendant une minute. Le cache de 60 s fait coexister deux versions pendant ce délai sur plusieurs instances. Parade : l'accepter et le dire (la trace porte
prompt_version), ou un délai plus court si une promotion doit être instantanée ; ne pas désactiver le cache, qui ajoute un appel au serveur par question. - Variable oubliée. Un prompt qui contient
{{politique}}au lieu de{{guide}}part chez Gemini avec les accolades littérales. Parade : le test « pas de{{dans le texte envoyé », et la comparaison deprompt.variablesà la liste attendue avant l'enregistrement. - Serveur MLflow indisponible.
load_promptéchoue et l'assistant ne répond plus. Parade : en production, le prompt est aussi embarqué au déploiement (l'artefact du modèle, leçon 5) et le chargement par alias sert à changer de version sans redéployer ; décidez explicitement lequel l'emporte en cas de panne.
La règle de décision
Versionnez le prompt dans le dépôt (revue) et dans le registre (exécution). Le code ne référence qu'un alias ; les évaluations référencent un numéro de version. Un alias ne se déplace que par la porte de promotion, jamais à la main dans l'interface, sauf retour arrière documenté.
Pratique
Une réponse fausse est signalée à 10 h 02. La promotion de v3 a eu lieu à 10 h 01 et l'application a quatre instances. Quelle version a produit la réponse ?
Le cache de 60 secondes fait coexister les deux versions pendant une minute après le déplacement de l'alias. C'est précisément pour cela que chaque trace est étiquetée avec la version effectivement chargée. Il n'y a pas de déploiement en jeu : l'alias est lu à l'exécution.
Un collègue corrige une faute de frappe directement dans le texte de la version 2 du prompt depuis l'interface. Que se passe-t-il ?
Les versions sont immuables : toute modification crée une nouvelle version. Aucun alias ne bouge seul. La correction doit donc repasser par l'évaluation et la porte, et le fichier prompts/v2.txt du dépôt doit suivre, sinon register() recréera une version à partir de l'ancien texte.
Exercice. Écrivez rollback() : remet l'alias production sur la version désignée par previous, et échoue proprement si previous n'existe pas.
Voir la solution
def rollback() -> int:
client = MlflowClient()
try:
prev = client.get_prompt_version_by_alias(PROMPT_NAME, "previous")
except Exception:
sys.exit("Aucun alias previous : rien à restaurer.")
cur = client.get_prompt_version_by_alias(PROMPT_NAME, "production")
mlflow.genai.set_prompt_alias(PROMPT_NAME, alias="production", version=prev.version)
mlflow.genai.set_prompt_alias(PROMPT_NAME, alias="previous", version=cur.version)
print(f"Alias production : v{cur.version} -> v{prev.version}")
return int(prev.version)Échanger les deux alias permet d'annuler le retour arrière lui-même. En moins de 60 secondes, toutes les instances servent la version restaurée, sans déploiement.
- J'enregistre les prompts depuis les fichiers du dépôt, de façon idempotente.
- Mon application ne charge qu'un alias, avec un cache court.
- Chaque trace porte la version du prompt réellement utilisée.
- Je sais revenir en arrière en déplaçant un alias.