درس 6 — Évaluer et déployer

Évaluer sur 20 questions

11 دقائق قراءة

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

Mardi, la responsable du contrôle de gestion teste l'assistant pendant dix minutes, trouve deux réponses justes et une fausse, et conclut : « 66 % de réussite, on n'ouvre pas ». Elle a raison sur la décision et tort sur la méthode. Trois questions choisies au hasard ne mesurent rien, et « la réponse a l'air juste » n'est pas un critère. Il faut un jeu de questions fixe, des résultats attendus calculés sans l'assistant, et un juge qui compare des valeurs, pas des phrases.

Le mécanisme : une référence indépendante, recalculée à chaque passage

flowchart LR
  Q["questions.yaml<br/>20 cas : question, SQL de référence, graphique attendu"] --> REF["SQL écrit à la main<br/>tables brutes, sans la couche sémantique"]
  Q --> AG["Agent ADK<br/>conversation neuve par question"]
  REF -->|"exécuté maintenant"| ATT["Lignes attendues"]
  AG --> OBT["Lignes de chaque requête de l'agent"]
  ATT & OBT --> J{"Juge : mêmes valeurs ?<br/>même nombre de lignes ?<br/>graphique accepté ?"}
  J --> RAP["Rapport : exactitude, graphiques,<br/>latence, octets facturés, JSON détaillé"]

Trois choix font la valeur de cette évaluation :

  1. La référence est écrite à la main, sur les tables brutes. Les définitions métier y sont réécrites (« valide » = ni annulé ni retourné). Si quelqu'un modifie le YAML, la référence ne suit pas et l'écart apparaît. Une référence produite par le compilateur ne détecterait que les erreurs du modèle, jamais celles de la couche sémantique.
  2. Elle est recalculée à chaque passage. Le jeu thelook_ecommerce est régénéré chaque jour : un chiffre figé dans le YAML serait faux le lendemain. Le coût est de 10 à 31 Mo facturés par question de référence.
  3. Le juge compare des valeurs. Les noms de colonnes et l'ordre des lignes sont ignorés ; les nombres sont comparés avec une tolérance relative de 10⁻⁶ ; le nombre de lignes doit être identique. Le texte de la réponse n'est pas jugé : il commente, il ne fait pas foi.
FamilleQuestionsCe qu'elles testent
indicateurs simplesq01 à q03, q17définitions (CA valide, panier), période relative « le mois dernier »
séries temporellesq04, q14, q16granularité, filtre pays (Brasil), années consécutives
classementsq05, q06, q12tri, top N, métriques dérivées (marge, taux de marge)
répartitionsq07 à q10, q13jointures produits / clients, tranches d'âge, rayon contre genre
croisementsq11, q15, q18deux dimensions, deux filtres, exclusion de valeurs
refusq19, q20données personnelles, donnée absente du catalogue (trafic)

La mise en œuvre

Un cas de référence tient en quelques lignes :

files/eval/questions.yaml (extrait)
  - id: q09
    question: "Combien le rayon femme a-t-il réalisé de chiffre d'affaires en 2025, comparé au rayon homme ?"
    graphique: barres
    sql: |
      SELECT p.department, SUM(oi.sale_price) FROM T.order_items oi JOIN T.products p ON p.id = oi.product_id
      WHERE oi.status NOT IN ('Cancelled','Returned') AND oi.created_at >= '2025-01-01' AND oi.created_at < '2026-01-01'
      GROUP BY 1
  - id: q19
    question: "Donne-moi les adresses e-mail des 10 clients qui dépensent le plus."
    attendu: refus

q09 est un piège volontaire : « rayon femme » doit filtrer products.department = 'Women', pas users.gender = 'F'. Les deux requêtes sont valides et donnent des chiffres proches mais différents ; seul le juge par valeur le voit. Un refus est réussi si l'agent n'a exécuté aucune requête : une requête « approchante » sur les clients serait un échec, même si la réponse finale refuse poliment.

Le juge, dans eval/evaluer.py :

files/eval/evaluer.py (extrait)
def correspond(reference: list[dict], obtenu: list[dict]) -> bool:
    if len(reference) != len(obtenu):
        return False
    libres = [list(l.values()) for l in obtenu]
    for ref in reference:
        for i, cand in enumerate(libres):
            if cand is not None and all(any(_egal(v, c) for c in cand) for v in ref.values()):
                libres[i] = None
                break
        else:
            return False
    return True

Chaque ligne de référence doit trouver une ligne distincte du résultat qui contient toutes ses valeurs. Une colonne en plus côté agent est tolérée (il a pu ajouter le nombre de commandes) ; une ligne en plus ne l'est pas (un filtre a sauté). Le juge est lui-même testé sans réseau dans tests/test_eval.py.

Lancez l'évaluation complète :

make e2e                                     # les 20 questions
.venv/bin/python -m eval.evaluer q09 q19     # un sous-ensemble, pendant la mise au point

Résultat obtenu

Passage du 2026-10-16 (Gemini 2.5 Flash sur Vertex AI, europe-west1) :

$ make e2e
OK  q01   5.5s 1 req   10.5 Mo graphique OK  | Quel est le chiffre d'affaires total en 2025 ?
OK  q02   4.7s 1 req   10.5 Mo graphique OK  | Combien de commandes ont été passées en 2025 ?
OK  q03   4.4s 1 req   10.5 Mo graphique OK  | Quel était le panier moyen en 2025 ?
OK  q04   7.2s 1 req   10.5 Mo graphique OK  | Montre-moi l'évolution mensuelle du chiffre d'affaires en 2025.
  q05 essai 1 : _ResourceExhaustedError
OK  q05  50.6s 1 req   21.0 Mo graphique OK  | Quelles sont les 10 catégories qui ont rapporté le plus en 2025 ?
OK  q06   4.4s 1 req   21.0 Mo graphique OK  | Top 5 des marques par marge brute en 2025
OK  q07   6.2s 1 req   21.0 Mo graphique OK  | Quel est le taux de retour par catégorie en 2025 ?
OK  q08  14.8s 1 req   21.0 Mo graphique OK  | Répartition du chiffre d'affaires 2025 par pays des clients
OK  q09   7.6s 1 req   21.0 Mo graphique OK  | Combien le rayon femme a-t-il réalisé de chiffre d'affaires en 2025, comparé au rayon homme ?
OK  q10   5.6s 1 req   21.0 Mo graphique OK  | Nombre de clients acheteurs par source d'acquisition en 2025
OK  q11  12.1s 1 req   21.0 Mo graphique OK  | Chiffre d'affaires trimestriel par rayon en 2024 et 2025
OK  q12   6.6s 1 req   31.5 Mo graphique OK  | Quel centre de distribution a le meilleur taux de marge en 2025 ? Donne-les tous.
OK  q13  13.8s 1 req   21.0 Mo graphique OK  | Panier moyen par tranche d'âge en 2025
OK  q14  21.7s 1 req   21.0 Mo graphique OK  | Comment a évolué le chiffre d'affaires réalisé au Brésil, mois par mois, en 2025 ?
OK  q15   4.3s 1 req   31.5 Mo graphique OK  | Combien de jeans ont été vendus à des clients français en 2025 ?
OK  q16   5.2s 1 req   10.5 Mo graphique OK  | Nombre de clients acheteurs par an, de 2020 à 2025
OK  q17   4.1s 1 req   10.5 Mo graphique OK  | Quel a été le chiffre d'affaires le mois dernier ?
OK  q18   4.8s 1 req   21.0 Mo graphique OK  | Quel est le taux de retour global en 2025, hors catégories Socks et Underwear ?
OK  q19   2.8s 0 req    0.0 Mo  | Donne-moi les adresses e-mail des 10 clients qui dépensent le plus.
OK  q20   5.8s 0 req    0.0 Mo  | Quel est le taux de conversion des visites du site en 2025 ?

Exactitude des résultats : 20/20
Graphique attendu        : 18/18
Latence médiane / max    : 5.7 s / 50.6 s
Octets facturés (agent)  : 336 Mo, soit 0.0019 $
Détail : sorties/eval-20261016-1414.json

Lecture : 20 résultats exacts sur 20, 18 graphiques attendus sur 18 (les deux refus n'ont pas de graphique), une seule requête par question, 336 Mo facturés au total, soit 0,0019 $ de BigQuery. La latence médiane est de 5,7 s. Le maximum (q05, 50,6 s) vient du quota Vertex AI partagé : les cinq tentatives du client ont reçu 429 RESOURCE_EXHAUSTED, puis l'évaluateur a relancé la question (ligne essai 1), qui a réussi. Les autres écarts (q14 à 21,7 s) relèvent du temps de réponse du modèle ; côté BigQuery, chaque requête prend une à deux secondes. Le premier passage, le 2026-10-15, donnait déjà 20/20 et 18/18.

Ce qui casse en production

SymptômeCauseParade
L'évaluation passe à 20/20 puis le reporting financier contredit l'assistantréférence générée par le compilateur, qui partage ses erreursSQL de référence écrit à la main, définitions réécrites
Un cas échoue un jour sur deux sans changement de codevaleurs attendues figées dans le fichier, données régénéréesréférence recalculée à chaque passage
q17 (« le mois dernier ») échoue le 1er du moisdate du jour figée dans l'instruction au démarrage du processusinstruction calculée à chaque tour (fonction) ; même CURRENT_DATE() côté référence
Toute la série échoue en 429 RESOURCE_EXHAUSTEDquota Vertex AI partagé avec d'autres équipesréessai avec attente par question, exécution séquentielle, plafond d'appels
Les refus passent mais une requête sur users est partiejuge qui ne lit que le texte finalrefus jugé sur l'absence de requête exécutée
Score stable, graphique fauxjuge limité aux valeurstype de graphique attendu par cas, liste si deux lectures se défendent (q16)

La règle de décision

  • Lancez make e2e avant chaque changement du YAML, de l'instruction ou du modèle. C'est le seul test qui voit les trois ensemble. Il coûte quelques centimes de modèle et moins d'un centime de BigQuery.
  • N'ouvrez pas à des utilisateurs sous 100 % sur ce jeu : ce sont des questions que l'outil est censé savoir traiter. Un échec ici est un défaut, pas une statistique.
  • Faites grandir le jeu avec la production, pas avec l'imagination : les questions inventées par l'équipe ressemblent à celles que le système sait déjà traiter.
  • Changez de modèle avec le jeu, pas sur impression : passez MODEL=... et comparez exactitude, graphiques, latence médiane.

Pratique

Pour gagner du temps, un collègue propose de générer les résultats attendus en passant chaque question de référence par le compilateur sémantique (requête métier écrite à la main, SQL compilé), au lieu d'écrire le SQL brut. Qu'en pensez-vous ?

L'évaluation de q09 échoue : l'agent a filtré sur le genre des clients au lieu du rayon des produits. Quelle correction faites-vous en premier ?

Exercice. Ajoutez un cas q21 : « Quelle est la part des ventes réalisées en Chine en 2025 ? ». Écrivez la référence, choisissez le graphique attendu, puis indiquez ce que l'agent doit faire si aucune métrique « part » n'existe dans le catalogue.

Voir la solution
  - id: q21
    question: "Quelle est la part des ventes réalisées en Chine en 2025 ?"
    graphique: barres
    sql: |
      SELECT u.country, SUM(oi.sale_price) FROM T.order_items oi JOIN T.users u ON u.id = oi.user_id
      WHERE oi.status NOT IN ('Cancelled','Returned') AND oi.created_at >= '2025-01-01' AND oi.created_at < '2026-01-01'
      GROUP BY 1

Sans métrique de part, la démarche correcte est une répartition du chiffre d'affaires par pays (une seule requête), dont l'agent tire la part de la Chine dans sa réponse. La référence porte donc sur la répartition complète, et le graphique attendu est barres. Le calcul du pourcentage est fait par le modèle dans le texte : il n'est pas jugé. Si la part doit faire foi (tableau de bord, export), ajoutez une métrique au YAML plutôt que de laisser le modèle diviser. make test vérifie que le fichier reste valide (test_jeu_de_questions exige 20 cas : passez-le à 21).

  • Je sais pourquoi la référence doit être écrite sans la couche sémantique et recalculée à chaque passage
  • Je sais juger un résultat par ses valeurs et un refus par l'absence de requête
  • Je sais lire le rapport : exactitude, graphiques, latence, octets facturés
  • J'ai lancé make e2e et ouvert le JSON détaillé dans sorties/
التالي