Agent Evals
Agent Evals to zestaw testowy ground-truth w YAML, który przepuszcza gotowe pytania przez powierzchnię AI Ask i raportuje, gdzie LLM odbiegł od oczekiwań. Każdy przypadek to plik YAML commitowany obok twojego modelu semantycznego; CI uruchamia zestaw i psuje build przy każdej regresji.
Dostarczane w saiku v4.7 jako saiku#1424.
Po co evale
Format pliku
Zestawy żyją jako YAML pod saiku-home/evals/. Jeden zestaw na plik.
Każdy zestaw celuje w jedną kostkę; każdy przypadek w zestawie działa
względem niej.
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}Pola zestawu
| Pole | Wymagane | Typ | Uwagi |
|---|---|---|---|
name | tak | string | Wyświetlane w raporcie i podsumowaniu CI |
description | nie | string | Wolny tekst pokazywany w nagłówku raportu |
cube | tak | object | connectionName / catalog / schema / cubeName |
cases | tak | array | Niepusta lista przypadków ground-truth |
Pola przypadku
| Pole | Wymagane | Typ | Uwagi |
|---|---|---|---|
name | tak | string | Unikatowe w obrębie zestawu; używane w ścieżkach niezgodności |
question | tak | string | Zapytanie w języku naturalnym podawane do /ai/ask |
history | nie | {role,content}[] | Wcześniejsze tury zasiewające ask (puste dla przypadków jednostrzałowych) |
expectedIntent | nie | string | QUERY | INSIGHT | VIEW_CHANGE | REFUSED (bez rozróżniania wielkości liter) |
expectedRefusalContains | nie | string | Podłańcuch, który musi zawierać powód odmowy |
expectedRows | nie | {key:value}[] | Statyczne oczekiwane wiersze dla QUERY. Zobacz Porównanie wierszy. |
referenceQuery | nie | object | Odporny na dryf ground truth — typowany AiQueryRequest wykonywany na żywo względem tej samej kostki; jego wiersze stają się oczekiwanym zbiorem. Przeżywa zmiany danych, które psułyby zakodowane na twardo expectedRows. Preferuj to dla przypadków QUERY. |
orderMatters | nie | boolean | Domyślnie true. Gdy false, obie strony sortują po kluczach przed diffem. |
expectedInsightContains | nie | string[] | Podłańcuchy, które musi zawierać markdown insightu |
tolerance | nie | {absolute,relative} | Tolerancja numeryczna dla porównań wierszy; obie domyślnie 0.0 (dokładne) |
Porównanie wierszy
Normalizacja kluczy
Klucze kolumn porównują się bez rozróżniania wielkości liter, z usuniętymi białymi znakami, podkreśleniami i myślnikami:
Store SalesstoreSalesstore_salesSTORE-SALES
Wszystkie normalizują się do tego samego klucza. Zapobiega to sytuacji, w której zmiana nazwy w kształcie wyjściowym projektora schemy oblewa każdy eval odwołujący się do kolumny.
Parsowanie liczb
Oczekiwane wartości, które parsują się jako liczby, są porównywane numerycznie z tolerancją. Parser jest wyrozumiały co do tego, jak autorzy evali zapisują liczby:
| Wartość YAML | Parsowana jako | Uwagi |
|---|---|---|
500 | 500 | Liczby całkowite |
500.0 | 500.0 | Liczby dziesiętne |
"$565,238.13" | 565238.13 | Prefiks waluty + separator tysięcy usunięte |
"(500)" | -500 | Liczby ujemne w nawiasach |
"12%" | 12 | Końcowy % usunięty |
Nienumeryczne oczekiwane wartości porównują się jako łańcuchy (z przyciętymi białymi znakami po obu stronach).
Tolerancja
tolerance: absolute: 0.01 # cell passes if |actual - expected| <= 0.01 relative: 0.001 # cell passes if |actual - expected| / |expected| <= 0.001Komórka zdaje, jeśli spełniona jest którakolwiek tolerancja — ustaw obie, gdy chcesz „5 centów absolutnie LUB 0,1% względnie, co luźniejsze”.
Obie domyślnie zero (dokładne dopasowanie).
Brakujące klucze są asymetryczne
- Oczekiwany klucz nieobecny w faktycznym wierszu to niezgodność — autor evala napisał oczekiwanie, którego schema nie produkuje.
- Faktyczny klucz nieobecny w oczekiwaniu nie jest niezgodnością — oczekiwania są addytywne, więc rozrost schemy o nową kolumnę nie psuje każdego evala, który go poprzedza.
Raporty
Runner emituje zagregowany raport na zestaw z jednym wynikiem na przypadek:
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"Lub jako ładnie wypisany JSON dla archiwów CI przez
EvalReportWriter.toJson().
Kody błędów parsowania
Ustrukturyzowane błędy parsowania YAML niosą stabilny kod, aby CI mogło odróżnić „autor napisał zepsuty YAML” od „powierzchnia ask wyprodukowała błędną odpowiedź”:
| Kod | Kiedy |
|---|---|
MALFORMED_YAML | Parser YAML odrzucił plik. |
MISSING_FIELD | Wymagane pole jest nieobecne (name, cube, cases, question). |
BLANK_FIELD | Wymagane pole łańcuchowe jest obecne, ale puste. |
TYPE_MISMATCH | Pole ma zły typ (np. cases to mapa, nie tablica). |
INVALID_TOLERANCE | tolerance.absolute lub tolerance.relative jest ujemne. |
Uruchamianie zestawu
Evale działają względem serwera na żywo — ćwiczą tę samą ścieżkę
ask + execute, którą uderzają twoi użytkownicy. Wskaż dołączone CLI
saiku eval na działający launcher; wysyła POST do endpointu run
admina i raportuje wskaźnik zdawalności, zwracając niezerowy kod wyjścia
przy każdej regresji, więc wpada prosto do 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>'Kody wyjścia: 0 = każdy zestaw zdał · 1 = zestaw zregresował ·
2 = błąd transportu/konfiguracji. Dodaj --no-fail-on-regression
dla przebiegu tylko-raportowego, który nigdy nie wychodzi z
niezerowym kodem.
# admin-gated; runs every suite in saiku-home/evals/ synchronouslycurl -sS -X POST -u admin:admin \ http://localhost:8080/saiku/admin/ai-evals/run | jqPanel admina czyta utrwalone wyniki — GET /saiku/admin/ai-evals
(karty per-zestaw), .../{suite}/runs i .../{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 evalMonitorowanie dokładności na żywo
Evale nie są tylko bramką CI. Każdy przebieg jest oceniany i utrwalany
(H2, w twoim saiku-home), a dashboard Admin → Agent evals kreśli
wskaźnik zdawalności w czasie per zestaw — więc możesz obserwować dryf
dokładności na wdrożeniu produkcyjnym, a nie tylko wyłapywać go w pull
requeście. Zaplanuj CLI saiku eval w cronie, aby ciągle zasilać trend.
Dołączony przykład
Działający zestaw dostarczany jest z launcherem pod saiku-launcher/src/main/resources/seed/evals/foodmart-sales.eval.yaml.
Cztery bazowe przypadki w dwóch intencjach, wszystkie odporne na dryf:
- QUERY — store-sales-by-country + unit-sales-by-product-family,
każdy z
referenceQueryjako ground truth (wykonywany na żywo, więc odświeżenie danych nie może ich zepsuć) - REFUSED — refuse-general-knowledge + refuse-coding-help ze sprawdzeniem podłańcucha powodu
Nie-cele dla v1
- Adapter nagrywający — adapter zapisujący każdą odpowiedź na żywo do pliku fikstury, aby kolejny adapter fikstury mógł odtwarzać deterministycznie bez wydawania budżetu LLM. Projekt gotowy; implementacja odroczona.
- Diff strukturalny na emitowanym
AiQueryRequest— wiele strukturalnie różnych żądań może produkować te same wiersze, więc diff wierszy to lepszy ground truth.