درس 2 — Concevoir

Préparer le jeu et évaluer un prompt

7 دقائق قراءة

هذا المحتوى غير مترجم بعد. تُعرض النسخة الأصلية.

Le jeu de référence d'Assurances Ventoux compte 20 tickets étiquetés par l'équipe support. Le ticket T-1019 est le T-1003 recopié avec une minuscule et un double espace : compté deux fois, il donne à ce cas un poids double dans le score. Un autre jour, quelqu'un étiquette un ticket « Achats » au lieu de « achats » et l'évaluation compte une erreur que le modèle n'a pas commise. L'évaluation ne vaut que ce que vaut la préparation du jeu.

Le mécanisme

Deux composants font le travail de mesure :

  • prepare_dataset normalise les espaces, rejette les étiquettes inconnues et les doublons (comparaison insensible à la casse), refuse un jeu de moins de 5 exemples, et calcule l'empreinte (SHA-256 tronqué) du jeu obtenu.
  • evaluate_prompt envoie chaque ticket à Gemini avec le prompt de la version évaluée, et produit trois artefacts : les prédictions ligne à ligne, les métriques, la matrice de confusion.
sequenceDiagram
  participant P as prepare_dataset
  participant E as evaluate_prompt
  participant G as Gemini (Vertex AI)
  participant M as ML Metadata
  P->>P: 20 lignes lues, 1 doublon rejeté
  P->>M: Dataset (19 exemples, empreinte)
  P->>E: dataset (artefact)
  loop 19 tickets
    E->>G: prompt + ticket, sortie = une des 5 files
    G-->>E: "achats" (ou 429 : nouvel essai)
  end
  E->>M: Metrics (accuracy, latence p50, tokens, échecs)
  E->>M: ClassificationMetrics (matrice 5 x 5)

Deux choix rendent la mesure fiable. La sortie est contrainte à une énumération : response_mime_type="text/x.enum" et un schéma qui liste les cinq files, si bien que Gemini ne peut répondre ni « Achats. » ni « Je pense que c'est achats ». Et la température est à 0, ce qui réduit la variance sans la supprimer (la leçon 3 montre un écart réel d'une exécution à l'autre).

La mise en œuvre

Un composant léger s'exécute dans un conteneur python:3.11-slim où seul le corps de la fonction est copié. D'où les imports à l'intérieur, et packages_to_install pour google-genai :

files/components.py (extrait)
@dsl.component(base_image=BASE_IMAGE, packages_to_install=[GENAI])
def evaluate_prompt(dataset: Input[Dataset], prompt_template: str, model: str, version: str,
                    labels: list, project: str, location: str, predictions: Output[Dataset],
                    metrics: Output[Metrics], confusion: Output[ClassificationMetrics]):
    from google import genai
    from google.genai import errors, types
    # Délai maximal par appel : sans lui, une connexion bloquée fige la tâche (et la facture).
    http = types.HttpOptions(timeout=60_000)
    client = (genai.Client(vertexai=True, project=project, location=location, http_options=http)
              if project else genai.Client(http_options=http))
    config = types.GenerateContentConfig(
        temperature=0, response_mime_type="text/x.enum",
        response_schema={"type": "STRING", "enum": labels})

Une erreur sur un ticket n'annule pas l'évaluation : elle est comptée dans failures, et c'est la porte de promotion qui décide (leçon 3). Les métriques s'écrivent avec l'API des artefacts KFP, que Vertex affiche sans code supplémentaire :

files/components.py (extrait)
metrics.log_metric("accuracy", round(accuracy, 4))
metrics.log_metric("latency_p50_ms", round(statistics.median(latencies), 1))
metrics.log_metric("total_tokens", tokens)
metrics.log_metric("failures", failures)
confusion.log_confusion_matrix(labels, matrix)

Les tests appellent la fonction Python derrière le composant (python_func) avec de faux artefacts et un faux client Gemini qui se trompe volontairement sur un ticket :

files/tests/test_components.py (extrait)
monkeypatch.setattr(genai, "Client", lambda **kw: SimpleNamespace(models=FakeModels()))
c.evaluate_prompt.python_func(dataset=Art(ds), prompt_template="Ticket : {text}", model="fake", ...)
assert metrics.metadata["accuracy"] == round(5 / 6, 4)
assert sum(map(sum, conf.metadata["confusion"]["matrix"])) == 6

Résultat obtenu (2026-10-16)

Extrait de make run (Gemini 2.5 Flash sur Vertex AI) :

[KFP Executor 2026-10-16 16:07:47,391 WARNING]: ligne 19 rejetée : doublon de "l'écran de mon poste clignote puis s'éte"
[KFP Executor 2026-10-16 16:09:32,932 INFO]: v2 : accuracy=1.000 sur 19 exemples
[KFP Executor 2026-10-16 16:10:57,938 INFO]: v1 : accuracy=0.947 sur 19 exemples

La seule erreur de v1, lue dans l'artefact predictions :

{"id": "T-1013", "label": "achats", "text": "La licence de l'outil de BI de mon équipe expire vendredi, il faut la renouveler.", "prediction": "logiciel", "ok": false}

C'est exactement l'incident de septembre : sans règle de départage, un renouvellement de licence ressemble à un sujet logiciel. Le prompt v2 l'écrit explicitement.

Ce qui casse en production

  • Quota partagé saturé. Notre première exécution du 2026-10-16 a reçu un 429 sur T-1002 au bout de cinq essais (attentes de 1, 2, 4 et 8 s) ; le ticket a été compté en échec et la porte a rejeté le candidat (reason: '1.0 appels en échec (max 0)'). Parade appliquée : six essais espacés de 2 à 32 s, environ une minute de patience par ticket. Rejeter sur un échec technique reste le bon comportement : mieux vaut relancer que promouvoir sur une mesure incomplète.
  • Appel sans délai maximal. Une connexion bloquée garde la tâche vivante, et la machine facturée, jusqu'au délai global du job. Parade : HttpOptions(timeout=60_000).
  • Sortie libre au lieu d'une énumération. « Achats. » ne vaut pas « achats » : le score baisse sans que le modèle se soit trompé. Parade : sortie contrainte, et étiquettes inconnues rejetées dès la préparation.
  • Jeu trop petit. Avec 19 tickets, une erreur vaut 5 points d'exactitude. Parade : le seuil min_gain à 0 ici ; en production, visez plusieurs centaines de tickets et exigez un gain supérieur à l'écart observé entre deux exécutions identiques.

La règle de décision

Contraignez la sortie dès que la tâche a un ensemble fini de réponses. Comptez les erreurs techniques à part des erreurs de classement, et faites-les bloquer la promotion. Un jeu de référence se prépare dans le pipeline, pas à la main : la validation et l'empreinte font partie de la mesure.

Pratique

Après l'ajout de 30 tickets au jeu, l'exactitude de la production passe de 0,95 à 0,88 sans que rien d'autre ait changé. Que concluez-vous d'abord ?

Une évaluation compte 3 échecs techniques (429) sur 19 tickets et une exactitude de 1,00 sur les 16 restants. La production est à 0,95. Promouvez-vous ?

Exercice. Ajoutez à evaluate_prompt une métrique macro_recall (moyenne, sur les cinq files, de la part de tickets de chaque file correctement classés) et le test correspondant.

Voir la solution

À partir de matrix (ligne = vraie file, colonne = prédiction) :

recalls = [row[i] / sum(row) for i, row in enumerate(matrix) if sum(row)]
metrics.log_metric("macro_recall", round(sum(recalls) / len(recalls), 4))

Dans le test, avec le faux Gemini qui classe T0 (« acces ») en « achats » : la file acces a 2 tickets dont 1 juste, les autres files sont parfaites, donc macro_recall == round((0.5 + 1 + 1 + 1 + 1) / 5, 4) == 0.9. Cette métrique révèle une file sacrifiée que l'exactitude globale masque quand les files sont déséquilibrées.

  • Je valide, dédoublonne et prends l'empreinte du jeu dans le pipeline.
  • Je contrains la sortie de Gemini à une énumération.
  • Je donne un délai maximal et une reprise mesurée à chaque appel.
  • Je teste un composant par python_func avec un faux client.
التالي