Le prompt de routage d'Assurances Ventoux vit dans un fichier, il est modifié à la main, testé sur « quelques tickets » dans un notebook, puis copié dans la configuration du service. Quand l'exactitude a chuté en septembre, trois questions sont restées sans réponse : quel texte exact tournait, sur quels tickets il avait été testé, et quel score il avait obtenu face à l'ancien. Un notebook ne répond à aucune des trois : il n'a ni entrées figées, ni historique, ni décision écrite.
Le mécanisme
Un pipeline transforme ce geste manuel en une suite d'étapes dont les entrées et les sorties sont déclarées. Kubeflow Pipelines (KFP) v2 fournit le vocabulaire ; Vertex AI Pipelines l'exécute et garde la trace de tout.
| Terme | Ce que c'est ici |
|---|---|
| Composant | une fonction Python décorée par @dsl.component, exécutée dans son propre conteneur |
| Tâche | une instance de composant dans le DAG (evaluate-prompt et evaluate-prompt-2 sont deux tâches du même composant) |
| Paramètre | une valeur simple (texte, nombre, liste) passée par valeur : le prompt, les seuils |
| Artefact | un fichier produit par une tâche et suivi par Vertex ML Metadata : le jeu préparé, les prédictions, les métriques |
| Racine du pipeline | le préfixe Cloud Storage où chaque tâche écrit ses artefacts (gs://<bucket>/root) |
| ML Metadata | la base de lignage de Vertex AI : quelle exécution a produit quel artefact à partir de quoi |
flowchart TB
subgraph Avant["Avant : à la main"]
A1["prompt modifié"] --> A2["notebook,<br/>quelques tickets"] --> A3["copie en production"]
end
subgraph Apres["Après : pipeline"]
B1["prompt candidat<br/>(paramètre)"] --> B3["évaluation candidat"]
B2["jeu de référence<br/>(artefact + empreinte)"] --> B3
B2 --> B4["évaluation production"]
B3 --> B5{"porte écrite"}
B4 --> B5
B5 -- "promote" --> B6["registre + historique"]
end
Le point qui change tout est la comparaison dans la même exécution : la version en production est réévaluée sur le même jeu, par le même modèle, le même jour. Un score historique ne sert pas de référence, parce que le modèle sous-jacent et le jeu ont pu bouger entre-temps (la leçon 3 le mesure).
La mise en œuvre
Le dépôt tient en trois fichiers Python : components.py (les étapes), pipeline.py (le DAG), run.py (compilation, exécution locale, soumission, nettoyage).
cd files
make setup # .venv, kfp 2.17, google-cloud-pipeline-components 2.22, google-genai 2.31
make test # 12 tests, sans clé ni réseau
make compile # écrit sorties/prompt_gate.yamlLe DAG se lit dans pipeline.py. Chaque tâche reçoit des limites de ressources basses, et la lecture du registre est exclue du cache :
def _small(task: dsl.PipelineTask) -> dsl.PipelineTask:
# Vertex choisit la machine prise en charge la plus proche de ces limites.
return task.set_cpu_limit("1").set_memory_limit("2G")
prep = _small(prepare_dataset(source_path=source_path, source_sha=source_sha, labels=LABELS))
# Le registre change entre deux exécutions : jamais de cache sur cette lecture.
prod = _small(load_production(registry_dir=registry_dir)).set_caching_options(False)Le YAML compilé (813 lignes) est l'artefact que l'on versionne et que l'on soumet : il fige les images, les commandes et les signatures. Un test vérifie la forme du DAG plutôt que de faire confiance à la lecture du code :
assert {"prepare-dataset", "load-production", "evaluate-prompt", "evaluate-prompt-2",
"compare-versions", "condition-1"} <= set(tasks)
assert "compare-versions" in tasks["condition-1"]["triggerPolicy"]["condition"]
assert tasks["load-production"]["cachingOptions"].get("enableCache", False) is FalseRésultat obtenu (2026-10-16)
$ make test
............ [100%]
12 passed in 0.74s
$ make compile
.../files/sorties/prompt_gate.yamlCe qui casse en production
- Comparer à un score historique. Le score « 0,92 » noté dans un ticket il y a trois mois a été obtenu avec un autre jeu et une autre révision du modèle. Symptôme : un candidat promu qui fait moins bien que l'ancien une fois en place. Parade : réévaluer la production dans chaque exécution, comme ici.
- Jeu de référence modifié en douce. Quelqu'un ajoute dix tickets faciles et le score monte. Parade : l'empreinte du jeu (
fingerprint) est écrite dans les métadonnées et dansproduction.json; deux scores ne se comparent que si l'empreinte est la même. - YAML compilé non versionné. On ne sait plus quel DAG a produit une promotion. Parade : la CI compile et archive
prompt_gate.yamlà chaque fusion (ci/github-actions.yml). - Composant qui importe le module voisin. Il fonctionne en local et échoue sur Vertex, où seul le corps de la fonction est copié dans le conteneur. Parade : imports à l'intérieur de la fonction, aucune référence au reste du fichier (leçon 2).
La règle de décision
Un pipeline se justifie dès qu'une modification de prompt ou de modèle peut atteindre des utilisateurs et que quelqu'un vous demandera un jour « pourquoi cette version ? ». Pour une expérience d'un après-midi, un notebook suffit. Surveillez deux choses : que la production soit réévaluée à chaque exécution, et que l'empreinte du jeu accompagne chaque score.
Pratique
Le candidat obtient 0,95 aujourd'hui. Le ticket de mise en production de la version actuelle indique 0,97, mesuré en juin. Que faites-vous ?
Le 0,97 de juin a été obtenu dans d'autres conditions (jeu, révision du modèle, éventuellement région). Seule une mesure appariée dit si le candidat est meilleur. Rejeter sur la foi d'un vieux score est la réaction prudente mais aveugle ; promouvoir au seul seuil absolu ignore une possible régression.
Un collègue propose d'appeler le module utils.py du dépôt depuis le composant evaluate_prompt pour partager la fonction de reprise sur erreur. Le pipeline tourne en local. Que se passe-t-il sur Vertex AI ?
Un composant léger (lightweight Python component) est sérialisé à partir du corps de la fonction ; les modules voisins ne suivent pas. Pour partager du code, on construit une image (composant conteneurisé) ou on publie un paquet listé dans packages_to_install.
Exercice. Ajoutez au test test_dag_shape une vérification qui échoue si quelqu'un réactive le cache de la tâche promote.
Voir la solution
La tâche promote vit dans le sous-DAG de la condition :
inner = spec["root"]["dag"]["tasks"]["condition-1"]["componentRef"]["name"]
promote = spec["components"][inner]["dag"]["tasks"]["promote"]
assert promote["cachingOptions"].get("enableCache", False) is FalseUne promotion mise en cache renverrait « v2 » sans rien écrire dans le registre : le pipeline serait vert et la production inchangée.
- Je sais distinguer composant, tâche, paramètre et artefact.
- Je compile le pipeline et je vérifie sa forme par un test.
- Je compare toujours le candidat à une production réévaluée dans la même exécution.
- Je garde l'empreinte du jeu de référence à côté de chaque score.