Zum Inhalt springen

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.

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}

Suite-Felder

FeldErforderlichTypAnmerkungen
namejastringIm Report und der CI-Zusammenfassung angezeigt
descriptionneinstringFreitext, in der Report-Kopfzeile gezeigt
cubejaobjectconnectionName / catalog / schema / cubeName
casesjaarrayNicht-leere Liste von Ground-Truth-Fällen

Fall-Felder

FeldErforderlichTypAnmerkungen
namejastringEindeutig innerhalb der Suite; in Mismatch-Pfaden verwendet
questionjastringNatürlichsprachliche Anfrage, die /ai/ask gefüttert wird
historynein{role,content}[]Vorherige Turns zum Seeden der Anfrage (leer für Single-Shot-Fälle)
expectedIntentneinstringQUERY | INSIGHT | VIEW_CHANGE | REFUSED (Groß-/Kleinschreibung egal)
expectedRefusalContainsneinstringSubstring, den der Ablehnungsgrund enthalten muss
expectedRowsnein{key:value}[]Statische erwartete Zeilen für QUERY. Siehe Zeilenvergleich.
referenceQueryneinobjectDrift-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.
orderMattersneinbooleanStandard true. Bei false sortieren beide Seiten vor dem Diff nach Schlüsseln.
expectedInsightContainsneinstring[]Substrings, die das Insight-Markdown enthalten muss
tolerancenein{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 Sales
  • storeSales
  • store_sales
  • STORE-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-WertGeparst alsAnmerkungen
500500Ganzzahlen
500.0500.0Dezimalzahlen
"$565,238.13"565238.13Währungspräfix + Tausendertrennzeichen entfernt
"(500)"-500In Klammern gesetzte Negative
"12%"12Nachfolgendes % 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.001

Eine 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-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"

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:

CodeWann
MALFORMED_YAMLDer YAML-Parser hat die Datei abgelehnt.
MISSING_FIELDEin erforderliches Feld fehlt (name, cube, cases, question).
BLANK_FIELDEin erforderliches String-Feld ist vorhanden, aber leer.
TYPE_MISMATCHEin Feld hat den falschen Typ (z. B. cases ist ein Mapping, kein Array).
INVALID_TOLERANCEtolerance.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.

Terminal-Fenster
# 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>'

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.

Live-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 referenceQuery als 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.

Wohin als Nächstes