Aller au contenu

Évaluations d'agent

Les évaluations d’agent (Agent Evals) constituent un harnais de vérité terrain YAML qui fait passer des questions préparées à travers la surface AI Ask et signale les endroits où le LLM a divergé des attentes. Chaque cas est un fichier YAML commité aux côtés de votre modèle sémantique ; la CI exécute la suite et fait échouer le build à la moindre régression.

Livré dans saiku v4.7 comme saiku#1424.

Pourquoi des évaluations

Format de fichier

Les suites vivent sous forme de YAML dans saiku-home/evals/. Une suite par fichier. Chaque suite cible un cube ; chaque cas de la suite s’exécute contre celui-ci.

saiku-home/evals/foodmart-sales.eval.yaml
name: foodmart-sales-evals
description: >
Ground-truth cases for the FoodMart Sales cube. Baseline for AI ask
regressions when the LLM provider, model, or prompt changes.
cube:
connectionName: unknown_foodmart
catalog: FoodMart
schema: FoodMart
cubeName: Sales
cases:
# QUERY case — expected rows compared with tolerance.
- name: sales-by-country
question: show me store sales by country
expectedIntent: QUERY
expectedRows:
- {country: "USA", storeSales: 565238.13}
- {country: "Canada", storeSales: 79063.11}
- {country: "Mexico", storeSales: 51298.13}
tolerance:
relative: 0.001 # 0.1% relative — masks sub-cent warehouse drift
orderMatters: true
# INSIGHT case — insight markdown must contain each string.
- name: trend-analysis
question: are sales trending up week-on-week?
expectedIntent: INSIGHT
expectedInsightContains:
- Store Sales
- week-on-week
# REFUSED case — model must decline off-topic with a matching reason.
- name: refuse-off-topic
question: what's the weather in Paris?
expectedIntent: REFUSED
expectedRefusalContains: cube
# Multi-turn case — history seeds the ask.
- name: follow-up-turn
question: now break it down by state
history:
- {role: user, content: show store sales by country}
- {role: assistant, content: 3 rows returned.}
expectedIntent: QUERY
expectedRows:
- {state: "CA", storeSales: 214893.10}

Champs de suite

ChampRequisTypeNotes
nameouistringAffiché dans le rapport et le résumé de CI
descriptionnonstringTexte libre affiché dans l’en-tête du rapport
cubeouiobjectconnectionName / catalog / schema / cubeName
casesouiarrayListe non vide de cas de vérité terrain

Champs de cas

ChampRequisTypeNotes
nameouistringUnique au sein de la suite ; utilisé dans les chemins de non-correspondance
questionouistringQuestion en langage naturel envoyée à /ai/ask
historynon{role,content}[]Tours précédents pour amorcer la question (vide pour les cas à un seul coup)
expectedIntentnonstringQUERY | INSIGHT | VIEW_CHANGE | REFUSED (insensible à la casse)
expectedRefusalContainsnonstringSous-chaîne que la raison du refus doit contenir
expectedRowsnon{key:value}[]Lignes attendues statiques pour QUERY. Voir Comparaison de lignes.
referenceQuerynonobjectVérité terrain à l’épreuve de la dérive — une AiQueryRequest typée exécutée en direct contre le même cube ; ses lignes deviennent le jeu attendu. Survit aux changements de données qui casseraient des expectedRows codés en dur. À préférer pour les cas QUERY.
orderMattersnonbooleantrue par défaut. Quand false, les deux côtés sont triés par clés avant le diff.
expectedInsightContainsnonstring[]Sous-chaînes que le markdown de l’insight doit contenir
tolerancenon{absolute,relative}Tolérance numérique pour les comparaisons de lignes ; les deux valent 0.0 par défaut (exact)

Comparaison de lignes

Normalisation des clés

Les clés de colonnes se comparent de manière insensible à la casse, avec les espaces, tirets bas et traits d’union supprimés :

  • Store Sales
  • storeSales
  • store_sales
  • STORE-SALES

Tout se normalise vers la même clé. Cela évite qu’un renommage dans la forme de sortie du schema-projector fasse échouer chaque évaluation qui référence la colonne.

Analyse numérique

Les valeurs attendues qui s’analysent comme des nombres sont comparées numériquement avec tolérance. Le parseur est indulgent quant à la façon dont les auteurs d’évaluations écrivent les nombres :

Valeur YAMLAnalysée commeNotes
500500Entiers
500.0500.0Décimaux
"$565,238.13"565238.13Préfixe de devise + séparateur de milliers supprimés
"(500)"-500Négatifs entre parenthèses
"12%"12% en fin de chaîne supprimé

Les valeurs attendues non numériques se comparent comme des chaînes (espaces rognés des deux côtés).

Tolérance

tolerance:
absolute: 0.01 # cell passes if |actual - expected| <= 0.01
relative: 0.001 # cell passes if |actual - expected| / |expected| <= 0.001

Une cellule passe si l’une ou l’autre tolérance est satisfaite — définissez les deux quand vous voulez « 5 centimes en absolu OU 0,1 % en relatif, selon ce qui est le plus large ».

Les deux valent zéro par défaut (correspondance exacte).

Les clés manquantes sont asymétriques

  • Une clé attendue absente de la ligne réelle est une non-correspondance — l’auteur de l’évaluation a écrit une attente que le schéma ne produit pas.
  • Une clé réelle absente de l’attente n’est pas une non-correspondance — les attentes sont additives, donc étendre le schéma avec une nouvelle colonne ne casse pas toutes les évaluations qui la précèdent.

Rapports

Le runner émet un rapport agrégé par suite avec un résultat par cas :

Suite: foodmart-sales-evals
Description: Ground-truth cases for the FoodMart Sales cube
9/10 passed, 1 failed, 0 degraded, 0 skipped (elapsed 12483ms)
PASS: sales-by-country (854ms, intent=QUERY, model=claude-sonnet-4-6)
PASS: trend-analysis (742ms, intent=INSIGHT, model=claude-sonnet-4-6)
PASS: refuse-off-topic (312ms, intent=REFUSED, model=claude-sonnet-4-6)
FAIL: top-3-product-families (1024ms, intent=QUERY, model=claude-sonnet-4-6)
rows[0].productFamily: expected "Food", got "Drink"
rows[1].productFamily: expected "Non-Consumable", got "Food"
rows[2].productFamily: expected "Drink", got "Non-Consumable"

Ou sous forme de JSON joliment formaté pour les archives de CI via EvalReportWriter.toJson().

Codes d’erreur d’analyse

Les échecs structurés d’analyse YAML portent un code stable pour que la CI puisse distinguer « l’auteur a écrit un YAML cassé » de « la surface de question a produit une mauvaise réponse » :

CodeQuand
MALFORMED_YAMLLe parseur YAML a rejeté le fichier.
MISSING_FIELDUn champ requis est absent (name, cube, cases, question).
BLANK_FIELDUn champ chaîne requis est présent mais vide.
TYPE_MISMATCHUn champ a le mauvais type (par ex. cases est un mapping, pas un array).
INVALID_TOLERANCEtolerance.absolute ou tolerance.relative est négatif.

Exécuter une suite

Les évaluations s’exécutent contre un serveur en direct — elles sollicitent le même chemin ask + execute que celui que vos utilisateurs empruntent. Pointez le CLI saiku eval fourni vers un launcher en cours d’exécution ; il envoie un POST à l’endpoint d’exécution admin et rapporte le taux de réussite, retournant un code de sortie non nul à la moindre régression pour qu’il s’intègre directement dans la CI.

Fenêtre de terminal
# against a locally-running server (default admin/admin)
java -jar saiku-<version>.jar eval
# against a remote server
java -jar saiku-<version>.jar eval \
--server https://analytics.example.com \
--username admin --password '<secret>'

Codes de sortie : 0 = toutes les suites ont réussi · 1 = une suite a régressé · 2 = erreur de transport/config. Ajoutez --no-fail-on-regression pour une exécution en mode rapport seul qui ne sort jamais avec un code non nul.

Surveillance de la précision en direct

Les évaluations ne sont pas seulement une barrière de CI. Chaque exécution est notée et persistée (H2, dans votre saiku-home), et le tableau de bord Admin → Agent evals trace le taux de réussite dans le temps par suite — pour que vous puissiez surveiller une dérive de précision sur un déploiement de production, pas seulement la détecter dans une pull request. Programmez le CLI saiku eval sur un cron pour alimenter la tendance en continu.

Exemple fourni

Une suite fonctionnelle est livrée avec le launcher à saiku-launcher/src/main/resources/seed/evals/foodmart-sales.eval.yaml.

Quatre cas de référence répartis sur deux intentions, tous à l’épreuve de la dérive :

  • QUERY — store-sales-by-country + unit-sales-by-product-family, chacun avec une referenceQuery comme vérité terrain (exécutée en direct, donc un rafraîchissement des données ne peut pas les casser)
  • REFUSED — refuse-general-knowledge + refuse-coding-help avec une vérification de sous-chaîne de raison

Non-objectifs pour la v1

  • Adaptateur d’enregistrement — un adaptateur qui écrit chaque réponse en direct dans un fichier de fixture pour qu’un adaptateur de fixture ultérieur puisse rejouer de manière déterministe sans dépenser de budget LLM. Conception prête ; implémentation reportée.
  • Diff structurel sur l’AiQueryRequest émise — plusieurs requêtes structurellement différentes peuvent produire les mêmes lignes, donc le diff de lignes est une meilleure vérité terrain.

Où aller ensuite