Agent Evals
Agent Evals es un arnés de verdad-de-referencia en YAML que ejecuta preguntas enlatadas a través de la superficie AI Ask e informa dónde el LLM divergió de las expectativas. Cada caso es un archivo YAML comprometido junto a su modelo semántico; CI ejecuta la suite y hace fallar el build ante cualquier regresión.
Se entrega en saiku v4.7 como saiku#1424.
Por qué evals
Formato de archivo
Las suites viven como YAML bajo saiku-home/evals/. Una suite por
archivo. Cada suite apunta a un cubo; cada caso de la suite se ejecuta
contra él.
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}Campos de la suite
| Campo | Requerido | Tipo | Notas |
|---|---|---|---|
name | sí | string | Mostrado en el informe y el resumen de CI |
description | no | string | Texto libre mostrado en la cabecera del informe |
cube | sí | object | connectionName / catalog / schema / cubeName |
cases | sí | array | Lista no vacía de casos de verdad-de-referencia |
Campos del caso
| Campo | Requerido | Tipo | Notas |
|---|---|---|---|
name | sí | string | Único dentro de la suite; usado en las rutas de discrepancia |
question | sí | string | Pregunta en lenguaje natural alimentada a /ai/ask |
history | no | {role,content}[] | Turnos previos para sembrar la ask (vacío para casos de un solo disparo) |
expectedIntent | no | string | QUERY | INSIGHT | VIEW_CHANGE | REFUSED (insensible a mayúsculas) |
expectedRefusalContains | no | string | Subcadena que la razón del rechazo debe contener |
expectedRows | no | {key:value}[] | Filas estáticas esperadas para QUERY. Vea Comparación de filas. |
referenceQuery | no | object | Verdad-de-referencia a prueba de deriva — un AiQueryRequest tipado ejecutado en vivo contra el mismo cubo; sus filas se convierten en el conjunto esperado. Sobrevive a cambios de datos que romperían los expectedRows codificados a mano. Prefiéralo para los casos QUERY. |
orderMatters | no | boolean | Por defecto true. Cuando es false, ambos lados ordenan por claves antes del diff. |
expectedInsightContains | no | string[] | Subcadenas que el markdown de insight debe contener |
tolerance | no | {absolute,relative} | Tolerancia numérica para comparaciones de filas; ambas por defecto 0.0 (exacto) |
Comparación de filas
Normalización de claves
Las claves de columna se comparan de forma insensible a mayúsculas con espacios en blanco, guiones bajos y guiones eliminados:
Store SalesstoreSalesstore_salesSTORE-SALES
Todas normalizan a la misma clave. Evita que un renombrado en la forma de salida del schema-projector haga fallar cada eval que referencia la columna.
Parseo numérico
Los valores esperados que parsean como números se comparan numéricamente con tolerancia. El parser es indulgente sobre cómo los autores de evals escriben números:
| Valor YAML | Parseado como | Notas |
|---|---|---|
500 | 500 | Enteros |
500.0 | 500.0 | Decimales |
"$565,238.13" | 565238.13 | Prefijo de moneda + separador de miles eliminado |
"(500)" | -500 | Negativos entre paréntesis |
"12%" | 12 | % final eliminado |
Los valores esperados no numéricos se comparan como cadenas (recortadas de espacios en blanco en ambos lados).
Tolerancia
tolerance: absolute: 0.01 # cell passes if |actual - expected| <= 0.01 relative: 0.001 # cell passes if |actual - expected| / |expected| <= 0.001Una celda pasa si cualquiera de las tolerancias se satisface — defina ambas cuando quiera “5 céntimos absolutos O 0,1% relativo, lo que sea más laxo”.
Ambas por defecto son cero (coincidencia exacta).
Las claves faltantes son asimétricas
- Una clave esperada que no está en la fila real es una discrepancia — el autor del eval escribió una expectativa que el schema no produce.
- Una clave real que no está en la expectativa no es una discrepancia — las expectativas son aditivas, así que hacer crecer el schema con una nueva columna no rompe cada eval que la precede.
Informes
El runner emite un informe agregado por suite con un resultado por caso:
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"O como JSON con formato bonito para archivos de CI mediante
EvalReportWriter.toJson().
Códigos de error de parseo
Los fallos estructurados de parseo YAML llevan un código estable para que CI pueda distinguir “el autor escribió un YAML roto” de “la superficie de ask produjo una respuesta incorrecta”:
| Código | Cuándo |
|---|---|
MALFORMED_YAML | El parser YAML rechazó el archivo. |
MISSING_FIELD | Un campo requerido está ausente (name, cube, cases, question). |
BLANK_FIELD | Un campo string requerido está presente pero vacío. |
TYPE_MISMATCH | Un campo tiene el tipo incorrecto (p. ej. cases es un mapa, no un array). |
INVALID_TOLERANCE | tolerance.absolute o tolerance.relative es negativa. |
Ejecutar una suite
Los evals se ejecutan contra un servidor en vivo — ejercitan la
misma ruta de ask + ejecución que tocan sus usuarios. Apunte el CLI
incluido saiku eval a un launcher en ejecución; hace POST al endpoint
de ejecución de admin e informa la tasa de aprobados, devolviendo un
código de salida distinto de cero ante cualquier regresión para que caiga
directamente en 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>'Códigos de salida: 0 = cada suite pasó · 1 = una suite regresó ·
2 = error de transporte/config. Añada --no-fail-on-regression
para una ejecución de solo informe que nunca sale con código distinto
de cero.
# admin-gated; runs every suite in saiku-home/evals/ synchronouslycurl -sS -X POST -u admin:admin \ http://localhost:8080/saiku/admin/ai-evals/run | jqEl panel de admin lee los resultados persistidos —
GET /saiku/admin/ai-evals (tarjetas por suite),
.../{suite}/runs, y .../{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 evalMonitorización de precisión en vivo
Los evals no son solo una puerta de CI. Cada ejecución se puntúa y
persiste (H2, en su saiku-home), y el dashboard Admin → Agent evals
grafica la tasa de aprobados a lo largo del tiempo por suite — así puede
vigilar la deriva de precisión en un despliegue de producción, no solo
detectarla en un pull request. Programe el CLI saiku eval en un cron
para alimentar la tendencia continuamente.
Ejemplo incluido
Una suite funcional se entrega con el launcher en saiku-launcher/src/main/resources/seed/evals/foodmart-sales.eval.yaml.
Cuatro casos de base a través de dos intents, todos a prueba de deriva:
- QUERY — store-sales-by-country + unit-sales-by-product-family, cada uno con un
referenceQuerycomo verdad-de-referencia (ejecutado en vivo, así que un refresco de datos no puede romperlos) - REFUSED — refuse-general-knowledge + refuse-coding-help con una comprobación de subcadena de razón
No-objetivos para v1
- Adaptador de grabación — un adaptador que escribe cada respuesta en vivo a un archivo de fixture para que un adaptador de fixture posterior pueda reproducir de forma determinista sin gastar presupuesto de LLM. Diseño listo; implementación diferida.
- Diff estructural del
AiQueryRequestemitido — múltiples peticiones estructuralmente diferentes pueden producir las mismas filas, así que el diff de filas es una mejor verdad-de-referencia.