É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.
name: foodmart-sales-evalsdescription: > 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
| Champ | Requis | Type | Notes |
|---|---|---|---|
name | oui | string | Affiché dans le rapport et le résumé de CI |
description | non | string | Texte libre affiché dans l’en-tête du rapport |
cube | oui | object | connectionName / catalog / schema / cubeName |
cases | oui | array | Liste non vide de cas de vérité terrain |
Champs de cas
| Champ | Requis | Type | Notes |
|---|---|---|---|
name | oui | string | Unique au sein de la suite ; utilisé dans les chemins de non-correspondance |
question | oui | string | Question en langage naturel envoyée à /ai/ask |
history | non | {role,content}[] | Tours précédents pour amorcer la question (vide pour les cas à un seul coup) |
expectedIntent | non | string | QUERY | INSIGHT | VIEW_CHANGE | REFUSED (insensible à la casse) |
expectedRefusalContains | non | string | Sous-chaîne que la raison du refus doit contenir |
expectedRows | non | {key:value}[] | Lignes attendues statiques pour QUERY. Voir Comparaison de lignes. |
referenceQuery | non | object | Vé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. |
orderMatters | non | boolean | true par défaut. Quand false, les deux côtés sont triés par clés avant le diff. |
expectedInsightContains | non | string[] | Sous-chaînes que le markdown de l’insight doit contenir |
tolerance | non | {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 SalesstoreSalesstore_salesSTORE-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 YAML | Analysée comme | Notes |
|---|---|---|
500 | 500 | Entiers |
500.0 | 500.0 | Décimaux |
"$565,238.13" | 565238.13 | Préfixe de devise + séparateur de milliers supprimés |
"(500)" | -500 | Né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.001Une 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-evalsDescription: Ground-truth cases for the FoodMart Sales cube9/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 » :
| Code | Quand |
|---|---|
MALFORMED_YAML | Le parseur YAML a rejeté le fichier. |
MISSING_FIELD | Un champ requis est absent (name, cube, cases, question). |
BLANK_FIELD | Un champ chaîne requis est présent mais vide. |
TYPE_MISMATCH | Un champ a le mauvais type (par ex. cases est un mapping, pas un array). |
INVALID_TOLERANCE | tolerance.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.
# against a locally-running server (default admin/admin)java -jar saiku-<version>.jar eval
# against a remote serverjava -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.
# admin-gated; runs every suite in saiku-home/evals/ synchronouslycurl -sS -X POST -u admin:admin \ http://localhost:8080/saiku/admin/ai-evals/run | jqLe panneau admin lit les résultats persistés — GET /saiku/admin/ai-evals
(cartes par suite), .../{suite}/runs et .../{suite}/trend.
name: agent-evalson: [pull_request]
jobs: evals: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' - name: Run eval suite env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} run: | # Boot the launcher, wait for health, run the suites. java -jar saiku-*.jar serve --port 8080 & npx wait-on http://localhost:8080/saiku/api/info java -jar saiku-*.jar evalSurveillance 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
referenceQuerycomme 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.