Gérer des prompts versionnés
Chez Arvea, le prompt de l'assistant vivait dans le code. La responsable qualité du service client voulait que chaque réponse cite la fiche de procédure utilisée ; il a fallu un ticket, une revue de code, un déploiement, puis un second déploiement pour revenir en arrière quand une formulation a dégradé les réponses. Pendant ce temps, personne ne pouvait dire quelle version du prompt avait produit une réponse donnée. Un registre de prompts sépare le texte du code, garde chaque version et relie chaque génération à la version qui l'a produite.
Le mécanisme : versions immuables, étiquettes mobiles
Dans Langfuse, un prompt a un nom (conseiller-reponse) et une suite de versions numérotées. Une version ne change jamais : texte, type (text ou chat) et config (ici la température et max_output_tokens) sont figés. Les étiquettes (labels) sont des pointeurs déplaçables : production désigne la version servie, staging celle en cours d'évaluation, latest la plus récente. Déployer un prompt, c'est déplacer une étiquette ; revenir en arrière, c'est la remettre sur l'ancienne version.
flowchart LR
subgraph PG["PostgreSQL de Langfuse"]
V1["v1 : réponse courte<br/>config temperature 0.2"]
V2["v2 : cite la référence<br/>config temperature 0.2"]
end
P(["production"]) -->|"avant l'expérience"| V1
S(["staging"]) --> V2
P -.->|"après promotion"| V2
APP["Application<br/>get_prompt(label=production)"] --> C["Cache du SDK<br/>60 s, rafraîchi en arrière-plan"]
C --> PG
C -->|"Langfuse injoignable au 1er appel"| F["PROMPT_SECOURS<br/>dans le code"]
La mise en œuvre
1. Publier les versions
catalogue.py crée v1 avec l'étiquette production et v2 avec staging, une seule fois :
config = {"temperature": 0.2, "max_output_tokens": 1024}
langfuse.create_prompt(name=NOM_PROMPT, type="chat", prompt=PROMPT_SECOURS, config=config,
labels=["production"], commit_message="v1 : réponse courte fondée sur les fiches")
langfuse.create_prompt(name=NOM_PROMPT, type="chat", prompt=PROMPT_V2, config=config,
labels=["staging"], commit_message="v2 : cite la référence de la fiche")Le prompt est de type chat : un message système et un message utilisateur avec deux variables, {{fiches}} et {{question}}. Les paramètres du modèle sont dans config, donc versionnés avec le texte : une baisse de température est un changement de prompt comme un autre, avec sa trace.
2. Le lire, avec un filet
def charger_prompt(langfuse, label):
return langfuse.get_prompt(NOM_PROMPT, type="chat", label=label,
fallback=PROMPT_SECOURS, cache_ttl_seconds=60)Trois comportements sont à connaître :
- Cache. Le SDK garde le prompt 60 secondes en mémoire. À expiration, il continue de servir l'ancienne version et la rafraîchit en arrière-plan : la latence d'un appel ne dépend pas de Langfuse.
- Repli. Si le tout premier appel échoue (Langfuse arrêté au démarrage de l'application),
fallbackfournit le texte du code.prompt.is_fallbackvaut alorsTrue: l'application le journalise et la génération n'est pas reliée à une version. - Lien. Le prompt passé à
start_as_current_observation(..., prompt=prompt)relie la génération à sa version : l'onglet Metrics du prompt agrège latence, coût et scores par version.
3. Promouvoir par l'API
Après l'expérience de la leçon 5, la promotion est une ligne :
version = lf.get_prompt(NOM_PROMPT, type="chat", label="staging", cache_ttl_seconds=0).version
lf.api.prompt_version.update(name=NOM_PROMPT, version=version, new_labels=["production"])cache_ttl_seconds=0 force une lecture fraîche : on promeut ce qui est réellement en staging maintenant, pas ce que le cache a vu il y a une minute.
Résultat obtenu
make seed puis lecture directe, le 2026-10-15 :
INFO assistant.catalogue prompt conseiller-reponse : v1 (production) et v2 (staging) créés
INFO assistant.catalogue jeu de données questions-conseillers : 8 éléments
# après make eval (promotion de v2) :
production -> 2 ['production', 'staging', 'latest'] {'temperature': 0.2, 'max_output_tokens': 1024}
v1 labels []
2e appel (cache) 0.0 ms
sans cache 8.5 msCapture réelle après promotion : v1 est conservée sans étiquette, v2 est en production. Revenir en arrière = remettre production sur v1.
Ce qui casse en production
| Symptôme | Cause | Parade |
|---|---|---|
| Le modèle répond à « {{question}} » | variable mal nommée ou oubliée dans compile() : le SDK laisse le marqueur tel quel, sans erreur (vérifié : compile(fiches="F") renvoie Question du conseiller : {{question}}) | test unitaire qui compile chaque prompt et cherche {{ dans le résultat |
| Un retour arrière met une minute à prendre effet | cache de 60 s dans chaque processus | TTL plus court pour les prompts critiques, ou redémarrage ; le savoir avant l'incident |
| Après une panne de Langfuse, des générations ne sont plus reliées à un prompt | le repli local a servi au démarrage | journaliser is_fallback, alerte si le taux dépasse zéro |
| Le repli local diverge de la production | PROMPT_SECOURS jamais mis à jour | le repli est la dernière version validée, mise à jour à chaque promotion (revue de code) |
Quelqu'un déplace production dans l'interface sans évaluation | droits trop larges sur le projet | rôles Langfuse (lecture seule pour la plupart des membres) ; la promotion passe par le script d'évaluation |
La règle de décision
Mettez dans le registre ce qui change plus souvent que le code et que des non-développeurs doivent pouvoir proposer : consignes, ton, exemples, paramètres du modèle. Gardez dans le code ce qui est structurel : schéma de sortie, liste des outils, logique de recherche. Une version n'atteint production qu'après une expérience mesurée. Surveillez la part de générations reliées à chaque version et le taux de repli.
Pratique
Lundi 9 h, une nouvelle version du prompt (v7) passée en production vendredi fait chuter la satisfaction. Quelle est l'action la plus rapide et la plus sûre ?
Déplacer l'étiquette remet exactement v6 en service, en une action, et garde v7 intacte pour l'analyse. Recopier v6 dans une v8 marche mais brouille l'historique (deux versions identiques). Modifier v7 est impossible : une version est immuable, et ce serait de toute façon changer la production sans évaluation.
Pendant une maintenance de Langfuse, l'équipe redémarre l'application. Que se passe-t-il pour les conseillers ?
Au démarrage, le cache est vide : get_prompt échoue et renvoie le repli fourni. Le SDK ne persiste pas les prompts sur disque. Sans repli, l'appel lèverait une exception et l'assistant tomberait : c'est pour cela que le paramètre fallback existe.
Exercice. Écrivez un test unitaire, sans réseau, qui garantit qu'aucun prompt du catalogue (PROMPT_SECOURS, PROMPT_V2) ne laisse de variable non remplie quand on fournit fiches et question.
Voir la solution
La substitution des variables suit une règle simple ({{nom}} remplacé par la valeur) ; le test la reproduit sur les messages :
import re
from assistant.appli_gemini import PROMPT_SECOURS
from assistant.catalogue import PROMPT_V2
def compiler(messages, **variables):
return [re.sub(r"\{\{(\w+)\}\}", lambda x: variables.get(x.group(1), x.group(0)), m["content"])
for m in messages]
def test_aucune_variable_orpheline():
for prompt in (PROMPT_SECOURS, PROMPT_V2):
textes = compiler(prompt, fiches="F", question="Q")
assert not any("{{" in t for t in textes), textesCe test est dans tests/test_assistant.py. Ajoutez une variable {{date}} dans PROMPT_V2 : le test échoue, ce qui est le but.
- Je sais expliquer la différence entre une version et une étiquette.
- Je sais lire un prompt avec cache et repli, et savoir lequel a servi.
- Je sais promouvoir et revenir en arrière sans déploiement de code.
- Je sais ce qui doit rester dans le code et ce qui va dans le registre.