Agent Evals
Agent Evals ist ein YAML-Ground-Truth-Harness, der vorgefertigte Fragen durch die AI-Ask-Oberfläche laufen lässt und meldet, wo das LLM von den Erwartungen abgewichen ist. Jeder Fall ist eine YAML-Datei, die neben Ihrem Semantikmodell committet wird; CI führt die Suite aus und lässt den Build bei jeder Regression fehlschlagen.
Ausgeliefert in saiku v4.7 als saiku#1424.
Warum Evals
Dateiformat
Suiten leben als YAML unter saiku-home/evals/. Eine Suite pro Datei.
Jede Suite zielt auf einen Cube; jeder Fall in der Suite läuft gegen ihn.
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}Suite-Felder
| Feld | Erforderlich | Typ | Anmerkungen |
|---|---|---|---|
name | ja | string | Im Report und der CI-Zusammenfassung angezeigt |
description | nein | string | Freitext, in der Report-Kopfzeile gezeigt |
cube | ja | object | connectionName / catalog / schema / cubeName |
cases | ja | array | Nicht-leere Liste von Ground-Truth-Fällen |
Fall-Felder
| Feld | Erforderlich | Typ | Anmerkungen |
|---|---|---|---|
name | ja | string | Eindeutig innerhalb der Suite; in Mismatch-Pfaden verwendet |
question | ja | string | Natürlichsprachliche Anfrage, die /ai/ask gefüttert wird |
history | nein | {role,content}[] | Vorherige Turns zum Seeden der Anfrage (leer für Single-Shot-Fälle) |
expectedIntent | nein | string | QUERY | INSIGHT | VIEW_CHANGE | REFUSED (Groß-/Kleinschreibung egal) |
expectedRefusalContains | nein | string | Substring, den der Ablehnungsgrund enthalten muss |
expectedRows | nein | {key:value}[] | Statische erwartete Zeilen für QUERY. Siehe Zeilenvergleich. |
referenceQuery | nein | object | Drift-sichere Ground Truth — ein typisierter AiQueryRequest, live gegen denselben Cube ausgeführt; seine Zeilen werden zur erwarteten Menge. Übersteht Datenänderungen, die hartcodierte expectedRows brechen würden. Bevorzugen Sie dies für QUERY-Fälle. |
orderMatters | nein | boolean | Standard true. Bei false sortieren beide Seiten vor dem Diff nach Schlüsseln. |
expectedInsightContains | nein | string[] | Substrings, die das Insight-Markdown enthalten muss |
tolerance | nein | {absolute,relative} | Numerische Toleranz für Zeilenvergleiche; beide standardmäßig 0.0 (exakt) |
Zeilenvergleich
Schlüsselnormalisierung
Spaltenschlüssel vergleichen ohne Beachtung von Groß-/Kleinschreibung, mit entfernten Leerzeichen, Unterstrichen und Bindestrichen:
Store SalesstoreSalesstore_salesSTORE-SALES
Alle normalisieren zum selben Schlüssel. Verhindert, dass ein Rename in der Ausgabeform des Schema-Projektors jedes Eval fehlschlagen lässt, das die Spalte referenziert.
Numerisches Parsing
Erwartete Werte, die als Zahlen parsen, werden numerisch mit Toleranz verglichen. Der Parser ist nachsichtig damit, wie Eval-Autoren Zahlen schreiben:
| YAML-Wert | Geparst als | Anmerkungen |
|---|---|---|
500 | 500 | Ganzzahlen |
500.0 | 500.0 | Dezimalzahlen |
"$565,238.13" | 565238.13 | Währungspräfix + Tausendertrennzeichen entfernt |
"(500)" | -500 | In Klammern gesetzte Negative |
"12%" | 12 | Nachfolgendes % entfernt |
Nicht-numerische erwartete Werte vergleichen als Strings (auf beiden Seiten von Leerzeichen befreit).
Toleranz
tolerance: absolute: 0.01 # cell passes if |actual - expected| <= 0.01 relative: 0.001 # cell passes if |actual - expected| / |expected| <= 0.001Eine Zelle besteht, wenn eine der beiden Toleranzen erfüllt ist — setzen Sie beide, wenn Sie „5 Cent absolut ODER 0,1 % relativ, was auch immer lockerer ist” möchten.
Beide standardmäßig null (exakte Übereinstimmung).
Fehlende Schlüssel sind asymmetrisch
- Ein erwarteter Schlüssel, der nicht in der tatsächlichen Zeile ist, ist ein Mismatch — der Eval-Autor hat eine Erwartung geschrieben, die das Schema nicht produziert.
- Ein tatsächlicher Schlüssel, der nicht in der Erwartung ist, ist kein Mismatch — Erwartungen sind additiv, sodass das Erweitern des Schemas um eine neue Spalte nicht jedes Eval bricht, das ihm vorausging.
Reports
Der Runner gibt einen aggregierten Report pro Suite mit einem Ergebnis pro Fall aus:
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"Oder als hübsch formatiertes JSON für CI-Archive via
EvalReportWriter.toJson().
Parse-Fehler-Codes
Strukturierte YAML-Parse-Fehler tragen einen stabilen Code, sodass CI „Autor hat kaputtes YAML geschrieben” von „die Ask-Oberfläche hat eine falsche Antwort produziert” unterscheiden kann:
| Code | Wann |
|---|---|
MALFORMED_YAML | Der YAML-Parser hat die Datei abgelehnt. |
MISSING_FIELD | Ein erforderliches Feld fehlt (name, cube, cases, question). |
BLANK_FIELD | Ein erforderliches String-Feld ist vorhanden, aber leer. |
TYPE_MISMATCH | Ein Feld hat den falschen Typ (z. B. cases ist ein Mapping, kein Array). |
INVALID_TOLERANCE | tolerance.absolute oder tolerance.relative ist negativ. |
Eine Suite ausführen
Evals laufen gegen einen Live-Server — sie üben denselben Ask- +
Execute-Pfad aus, den Ihre Nutzer treffen. Zeigen Sie die mitgelieferte
saiku eval-CLI auf einen laufenden Launcher; sie POSTet an den
Admin-Run-Endpunkt und meldet die Pass-Rate, wobei sie bei jeder
Regression mit einem Nicht-Null-Exit zurückkehrt, sodass sie direkt in CI
fällt.
# 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>'Exit-Codes: 0 = jede Suite bestanden · 1 = eine Suite ist regrediert ·
2 = Transport-/Config-Fehler. Fügen Sie --no-fail-on-regression
für einen reinen Report-Lauf hinzu, der nie mit Nicht-Null endet.
# admin-gated; runs every suite in saiku-home/evals/ synchronouslycurl -sS -X POST -u admin:admin \ http://localhost:8080/saiku/admin/ai-evals/run | jqDas Admin-Panel liest die persistierten Ergebnisse —
GET /saiku/admin/ai-evals (Karten pro Suite), .../{suite}/runs
und .../{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 evalLive-Genauigkeitsüberwachung
Evals sind nicht nur ein CI-Gate. Jeder Lauf wird bewertet und
persistiert (H2, in Ihrem saiku-home), und das Admin → Agent
evals-Dashboard plottet die Pass-Rate über die Zeit pro Suite — sodass
Sie auf einem Produktions-Deployment auf Genauigkeitsdrift achten
können, nicht nur in einem Pull Request. Planen Sie die saiku eval-CLI per Cron, um den Trend kontinuierlich zu füttern.
Mitgeliefertes Beispiel
Eine funktionierende Suite wird mit dem Launcher unter
saiku-launcher/src/main/resources/seed/evals/foodmart-sales.eval.yaml
ausgeliefert.
Vier Baseline-Fälle über zwei Intents, alle drift-sicher:
- QUERY — store-sales-by-country + unit-sales-by-product-family, jeder mit einer
referenceQueryals Ground Truth (live ausgeführt, sodass ein Daten-Refresh sie nicht brechen kann) - REFUSED — refuse-general-knowledge + refuse-coding-help mit einer Grund-Substring-Prüfung
Nicht-Ziele für v1
- Recording-Adapter — ein Adapter, der jede Live-Antwort in eine Fixture-Datei schreibt, sodass ein nachfolgender Fixture-Adapter deterministisch abspielen kann, ohne LLM-Budget auszugeben. Design fertig; Implementierung zurückgestellt.
- Struktureller Diff auf dem ausgegebenen
AiQueryRequest— mehrere strukturell unterschiedliche Requests können dieselben Zeilen produzieren, sodass Row-Diff eine bessere Ground Truth ist.