Leçon 1 — Concevoir

Un cycle LLM qu'on peut rejouer

6 min de lecture

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.

TermeCe que c'est ici
Composantune fonction Python décorée par @dsl.component, exécutée dans son propre conteneur
Tâcheune instance de composant dans le DAG (evaluate-prompt et evaluate-prompt-2 sont deux tâches du même composant)
Paramètreune valeur simple (texte, nombre, liste) passée par valeur : le prompt, les seuils
Artefactun fichier produit par une tâche et suivi par Vertex ML Metadata : le jeu préparé, les prédictions, les métriques
Racine du pipelinele préfixe Cloud Storage où chaque tâche écrit ses artefacts (gs://<bucket>/root)
ML Metadatala 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.yaml

Le 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 :

files/pipeline.py (extrait)
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 :

files/tests/test_pipeline.py (extrait)
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 False

Résultat obtenu (2026-10-16)

$ make test
............                                                             [100%]
12 passed in 0.74s
$ make compile
.../files/sorties/prompt_gate.yaml

Ce 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 dans production.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 ?

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 ?

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 False

Une 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.
Suivant