Przejdź do głównej zawartości

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.

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}

Pola zestawu

PoleWymaganeTypUwagi
nametakstringWyświetlane w raporcie i podsumowaniu CI
descriptionniestringWolny tekst pokazywany w nagłówku raportu
cubetakobjectconnectionName / catalog / schema / cubeName
casestakarrayNiepusta lista przypadków ground-truth

Pola przypadku

PoleWymaganeTypUwagi
nametakstringUnikatowe w obrębie zestawu; używane w ścieżkach niezgodności
questiontakstringZapytanie w języku naturalnym podawane do /ai/ask
historynie{role,content}[]Wcześniejsze tury zasiewające ask (puste dla przypadków jednostrzałowych)
expectedIntentniestringQUERY | INSIGHT | VIEW_CHANGE | REFUSED (bez rozróżniania wielkości liter)
expectedRefusalContainsniestringPodłańcuch, który musi zawierać powód odmowy
expectedRowsnie{key:value}[]Statyczne oczekiwane wiersze dla QUERY. Zobacz Porównanie wierszy.
referenceQuerynieobjectOdporny 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.
orderMattersniebooleanDomyślnie true. Gdy false, obie strony sortują po kluczach przed diffem.
expectedInsightContainsniestring[]Podłańcuchy, które musi zawierać markdown insightu
tolerancenie{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 Sales
  • storeSales
  • store_sales
  • STORE-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ść YAMLParsowana jakoUwagi
500500Liczby całkowite
500.0500.0Liczby dziesiętne
"$565,238.13"565238.13Prefiks waluty + separator tysięcy usunięte
"(500)"-500Liczby ujemne w nawiasach
"12%"12Koń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.001

Komó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-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"

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ź”:

KodKiedy
MALFORMED_YAMLParser YAML odrzucił plik.
MISSING_FIELDWymagane pole jest nieobecne (name, cube, cases, question).
BLANK_FIELDWymagane pole łańcuchowe jest obecne, ale puste.
TYPE_MISMATCHPole ma zły typ (np. cases to mapa, nie tablica).
INVALID_TOLERANCEtolerance.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.

Okno terminala
# 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>'

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.

Monitorowanie 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 referenceQuery jako 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.

Dokąd dalej