Challenge 6.1: Evalite Basics
Wie testest Du, ob Dein LLM-System gute Antworten gibt — manuell jedes Mal durchlesen? Was, wenn Du 50 Prompts aenderst und prüfen willst, ob die Qualität gestiegen oder gesunken ist?
OVERVIEW
Abschnitt betitelt „OVERVIEW“evalite() nimmt drei Dinge: data (was rein geht und was rauskommen soll), task (die zu testende Funktion) und scorers (wie bewertet wird). Die Ergebnisse landen in einer lokalen SQLite-Datenbank und werden im Browser-Dashboard angezeigt.
Ohne Evals: Du aenderst einen Prompt und hoffst, dass die Antworten besser werden. Du liest manuell 5 Antworten durch, die sehen gut aus, also deployest Du. Zwei Wochen später merkst Du: Bei einer bestimmten Frage halluziniert das LLM jetzt. Keine Ahnung, seit wann.
Mit Evals: Du aenderst einen Prompt, startest evalite watch, und siehst sofort: Score von 0.82 auf 0.91 gestiegen. Oder: Score von 0.82 auf 0.65 gefallen — Regression entdeckt, bevor ein User es merkt. Messen, vergleichen, iterieren.
WALKTHROUGH
Abschnitt betitelt „WALKTHROUGH“Schicht 1: Installation
Abschnitt betitelt „Schicht 1: Installation“Evalite und die Autoevals-Bibliothek als Dev-Dependencies installieren:
pnpm add -D evalite vitest autoevals ai @ai-sdk/openai zodWarum Vitest? Evalite baut auf Vitest auf — es ist eine Peer Dependency, nicht optional. Evalite nutzt Vitest’s Test Runner, Watch Mode und Reporter-Infrastruktur. Die
.eval.tsDateikonvention funktioniert analog zu.test.tsbei Vitest.
Dann ein Script in der package.json einrichten:
{ "scripts": { "eval": "evalite", "eval:dev": "evalite watch" }}evalite watch startet den Watch-Modus — wie vitest --watch. Bei jeder Änderung an .eval.ts Dateien werden die Evals automatisch neu ausgefuehrt.
Schicht 2: Die .eval.ts Dateikonvention
Abschnitt betitelt „Schicht 2: Die .eval.ts Dateikonvention“Evalite sucht nach Dateien mit der Endung .eval.ts — analog zu .test.ts bei Vitest:
src/ my-feature.ts ← Dein Code my-feature.test.ts ← Unit Tests (Vitest) my-feature.eval.ts ← Evals (Evalite)Jede .eval.ts Datei enthaelt eine oder mehrere evalite() Aufrufe.
Schicht 3: evalite() Grundstruktur
Abschnitt betitelt „Schicht 3: evalite() Grundstruktur“Die drei Bausteine — data, task, scorers:
import { evalite } from 'evalite';import { Levenshtein } from 'autoevals'; // Levenshtein misst die Edit-Distanz — wie viele Zeichenaenderungen noetig sind, um vom Output zum Expected zu kommen
evalite('My First Eval', { // 1. Test-Daten: Was geht rein, was soll rauskommen? data: [ { input: 'Hello', expected: 'Hello World!' }, ],
// 2. Die zu testende Funktion — hier noch ohne LLM task: async (input) => { return input + ' World!'; },
// 3. Bewertung: Wie nah ist der Output am Expected? scorers: [Levenshtein],});Ablauf:
dataliefert Test-Cases (Input + erwarteter Output)taskwird pro Test-Case mit deminputausgefuehrtscorersvergleichen den tatsaechlichen Output mit demexpected-Wert- Ergebnisse werden gespeichert (standardmaessig in-memory; für persistente Speicherung konfigurierst Du SQLite via
createSqliteStoragein einerevalite.config.ts) - Dashboard zeigt Scores unter
http://localhost:3006
Schicht 4: data als async Function
Abschnitt betitelt „Schicht 4: data als async Function“Statt eines statischen Arrays kann data auch eine async Function sein — nuetzlich für dynamisches Laden:
evalite('Capital Cities', { data: async () => [ { input: 'What is the capital of France?', expected: 'Paris' }, { input: 'What is the capital of Germany?', expected: 'Berlin' }, { input: 'What is the capital of Japan?', expected: 'Tokyo' }, ], task: async (input) => { // Hier kommt später Dein LLM-Call rein return input; }, scorers: [Levenshtein],});Schicht 5: traceAISDKModel — AI SDK Integration
Abschnitt betitelt „Schicht 5: traceAISDKModel — AI SDK Integration“Wenn Du einen echten LLM-Call als task nutzt, wrappst Du das Modell mit traceAISDKModel. Damit erfasst Evalite alle LLM-Aufrufe (Tokens, Latenz, Kosten) im Dashboard:
import { evalite } from 'evalite';import { traceAISDKModel } from 'evalite/ai-sdk';import { generateText } from 'ai';import { openai } from '@ai-sdk/openai';import { Levenshtein } from 'autoevals';
evalite('Capital Cities', { data: async () => [ { input: 'What is the capital of France?', expected: 'Paris' }, { input: 'What is the capital of Germany?', expected: 'Berlin' }, ], task: async (input) => { const result = await generateText({ model: traceAISDKModel(openai('gpt-4o-mini')), // ← Tracing! system: 'Answer concisely. No periods.', prompt: input, }); return result.text; }, scorers: [Levenshtein],});traceAISDKModel ist ein Wrapper, der das AI SDK Modell um Evalite-Tracing erweitert. Im Dashboard siehst Du dann nicht nur den Score, sondern auch Token-Verbrauch und Latenz pro Test-Case.
Schicht 6: Ergebnis-UI
Abschnitt betitelt „Schicht 6: Ergebnis-UI“Starte die Evals mit pnpm eval:dev und oeffne http://localhost:3006:
- Übersicht: Alle Evals mit Durchschnitts-Score
- Detail-Ansicht: Jeder Test-Case mit Input, Output, Expected und Score
- Verlauf: Scores über die Zeit — siehst Du Verbesserungen oder Regressionen
- Traces: Bei
traceAISDKModelsiehst Du die LLM-Aufrufe mit Token-Verbrauch
Aufgabe: Richte Evalite ein und schreibe eine einfache Eval mit dem Levenshtein Scorer.
Erstelle die Datei hello.eval.ts und fuehre sie mit pnpm eval:dev aus. Die Ergebnisse siehst Du im Dashboard unter http://localhost:3006.
import { evalite } from 'evalite';import { Levenshtein } from 'autoevals';
// TODO 1: Erstelle eine evalite() mit dem Namen 'Greeting Eval'
// TODO 2: Definiere data mit 3 Test-Cases:// - input: 'Hi' → expected: 'Hi! How can I help?'// - input: 'Hello' → expected: 'Hello! How can I help?'// - input: 'Hey' → expected: 'Hey! How can I help?'
// TODO 3: Implementiere eine task-Funktion die den Input nimmt// und ' How can I help?' anhaengt (mit Ausrufezeichen nach dem Input)
// TODO 4: Nutze Levenshtein als ScorerCheckliste:
-
evaliteundLevenshteinimportiert -
datamit 3 Test-Cases definiert -
taskgibt den Input mit angehaengtem Text zurück -
Levenshteinals Scorer gesetzt -
pnpm eval:devzeigt Ergebnisse im Dashboard
Lösung anzeigen
import { evalite } from 'evalite';import { Levenshtein } from 'autoevals';
evalite('Greeting Eval', { data: [ { input: 'Hi', expected: 'Hi! How can I help?' }, { input: 'Hello', expected: 'Hello! How can I help?' }, { input: 'Hey', expected: 'Hey! How can I help?' }, ], task: async (input) => { return `${input}! How can I help?`; }, scorers: [Levenshtein],});Erwarteter Output: Alle drei Test-Cases sollten Score 1.0 erreichen — die task-Funktion produziert exakt den erwarteten Output. Im Dashboard unter localhost:3006 siehst Du “Greeting Eval” mit Durchschnitts-Score 1.0.
Erklärung: Der Levenshtein Scorer misst die Aehnlichkeit zwischen dem tatsaechlichen Output und dem erwarteten Output. Score 1.0 bedeutet identisch, Score 0.0 bedeutet komplett verschieden. In diesem Beispiel sollte jeder Test-Case einen Score von 1.0 erreichen, weil die task-Funktion exakt den erwarteten Output produziert.
COMBINE
Abschnitt betitelt „COMBINE“Uebung: Nutze generateText aus Level 1.3 als task-Funktion in einer Evalite-Eval. Teste, ob ein LLM Hauptstaedte korrekt benennt.
- Erstelle eine
.eval.tsDatei mit 5 Hauptstadt-Fragen - Nutze
generateTextmittraceAISDKModelalstask - System Prompt:
'Answer with only the city name. No periods, no extra text.' - Scorer:
Levenshtein - Starte
pnpm eval:devund pruefe die Scores im Dashboard
Frage zum Nachdenken: Warum ist Levenshtein hier nicht der ideale Scorer? Was passiert, wenn das LLM “Paris, France” statt “Paris” antwortet?