Challenge 9.3: Comparing Outputs
Wie findest Du heraus, welches Modell die beste Antwort gibt — ohne jede Antwort selbst zu lesen? Und was, wenn die “beste” Antwort von der Aufgabe abhaengt?
OVERVIEW
Abschnitt betitelt „OVERVIEW“Derselbe Prompt geht parallel an mehrere Modelle. Alle Ergebnisse werden gesammelt und von einem Judge-LLM bewertet. Das beste Ergebnis wird zurueckgegeben.
Ohne systematischen Vergleich: Du wahlst ein Modell nach Bauchgefuehl oder Marketing. “Claude ist besser” oder “GPT-4o ist schneller” — ohne Daten. Du weisst nicht, ob ein billigeres Modell für Deinen Use Case reichen würde.
Mit systematischem Vergleich: Du testest objektiv, welches Modell für Deine spezifische Aufgabe die beste Antwort liefert. Du triffst datengetriebene Modell-Entscheidungen. Du findest heraus, wo ein Flash-Modell ausreicht und wo Du ein Pro-Modell brauchst.
WALKTHROUGH
Abschnitt betitelt „WALKTHROUGH“Schicht 1: Parallele Aufrufe mit Promise.all
Abschnitt betitelt „Schicht 1: Parallele Aufrufe mit Promise.all“Der erste Schritt — denselben Prompt an mehrere Modelle senden und alle Ergebnisse sammeln:
import { generateText } from 'ai';import { anthropic } from '@ai-sdk/anthropic';import { openai } from '@ai-sdk/openai';import { google } from '@ai-sdk/google';
const prompt = 'Erklaere den Unterschied zwischen REST und GraphQL in 3 Saetzen.';
const models = [ { name: 'Claude Sonnet', model: anthropic('claude-sonnet-4-5-20250514') }, { name: 'GPT-4o', model: openai('gpt-4o') }, { name: 'Gemini Flash', model: google('gemini-2.5-flash') },];
// Alle Modelle parallel aufrufenconst results = await Promise.all( models.map(async ({ name, model }) => { const start = Date.now(); const result = await generateText({ model, prompt }); return { name, text: result.text, tokens: result.usage.totalTokens, durationMs: Date.now() - start, }; }),);
// Ergebnisse ausgebenfor (const r of results) { console.log(`\n--- ${r.name} (${r.tokens} Tokens, ${r.durationMs}ms) ---`); console.log(r.text);}Promise.all sendet alle Requests gleichzeitig. Die Gesamtdauer ist die des langsamsten Modells, nicht die Summe aller. Du bekommst für jeden Call Text, Token-Verbrauch und Dauer.
Schicht 2: Einfacher Vergleich — Metriken
Abschnitt betitelt „Schicht 2: Einfacher Vergleich — Metriken“Bevor Du ein LLM als Judge einsetzt, kannst Du einfache Metriken automatisch vergleichen:
function compareBasicMetrics( results: Array<{ name: string; text: string; tokens: number; durationMs: number }>) { console.log('\n=== Vergleich ===\n'); console.log('| Modell | Zeichen | Tokens | Dauer | Tokens/Sec |'); console.log('|--------|---------|--------|-------|------------|');
for (const r of results) { const tokensPerSec = Math.round(r.tokens / (r.durationMs / 1000)); console.log( `| ${r.name} | ${r.text.length} | ${r.tokens} | ${r.durationMs}ms | ${tokensPerSec} |`, ); }
// Kuerzeste Antwort (oft praeziser) const shortest = results.reduce((a, b) => (a.text.length < b.text.length ? a : b)); console.log(`\nKuerzeste Antwort: ${shortest.name}`);
// Schnellste Antwort const fastest = results.reduce((a, b) => (a.durationMs < b.durationMs ? a : b)); console.log(`Schnellste Antwort: ${fastest.name}`);
// Guenstigste Antwort const cheapest = results.reduce((a, b) => (a.tokens < b.tokens ? a : b)); console.log(`Guenstigste Antwort: ${cheapest.name}`);}
compareBasicMetrics(results);Einfache Metriken — Laenge, Geschwindigkeit, Token-Verbrauch — geben einen ersten Überblick. Aber sie sagen nichts über die Qualität der Antwort.
Schicht 3: LLM-as-a-Judge
Abschnitt betitelt „Schicht 3: LLM-as-a-Judge“Für Qualitaetsbewertung: Ein starkes LLM bewertet die Antworten der anderen. Das Muster kennst Du aus Level 6.3:
import { generateText, Output } from 'ai';import { anthropic } from '@ai-sdk/anthropic';import { z } from 'zod';
const JudgmentSchema = z.object({ rankings: z.array(z.object({ model: z.string(), score: z.number().min(1).max(10), reasoning: z.string(), })), winner: z.string(), summary: z.string(),});
async function judgeOutputs( prompt: string, results: Array<{ name: string; text: string }>,) { const formattedOutputs = results .map((r, i) => `<output model="${r.name}">\n${r.text}\n</output>`) .join('\n\n');
const judgment = await generateText({ model: anthropic('claude-sonnet-4-5-20250514'), // ← Starkes Modell als Judge system: `Du bist ein objektiver Qualitaets-Reviewer.Bewerte die folgenden LLM-Outputs nach:1. Korrektheit — Sind die Fakten richtig?2. Praezision — Ist die Antwort auf den Punkt?3. Verstaendlichkeit — Ist die Erklärung klar?Vergib Scores von 1-10 und begruende Deine Bewertung.`, prompt: `<task>${prompt}</task>\n\n${formattedOutputs}`, output: Output.object({ schema: JudgmentSchema }), });
return judgment.output;}
// Nutzungconst judgment = await judgeOutputs(prompt, results);console.log('\n=== LLM-as-a-Judge ===\n');console.log(`Gewinner: ${judgment.winner}`);console.log(`Zusammenfassung: ${judgment.summary}`);for (const r of judgment.rankings) { console.log(`\n${r.model}: ${r.score}/10`); console.log(` ${r.reasoning}`);}Der Judge bekommt die Originalaufgabe und alle Outputs. Durch das Zod Schema ist die Bewertung strukturiert — Score, Begruendung und Gewinner. Wichtig: Der Judge sollte ein starkes Modell sein, idealerweise nicht eines der bewerteten Modelle.
Hinweis: In diesem Beispiel nutzen wir Sonnet als Judge und als eines der bewerteten Modelle — in Production wuerdest Du ein staerkeres Modell (z.B. Opus) als Judge einsetzen, das nicht selbst bewertet wird.
Schicht 4: Systematischer Vergleich mit Evals
Abschnitt betitelt „Schicht 4: Systematischer Vergleich mit Evals“Für wiederholbare Vergleiche nutzt Du ein Eval-Framework wie Evalite aus Level 6:
import { evalite } from 'evalite';import { generateText } from 'ai';import { anthropic } from '@ai-sdk/anthropic';import { openai } from '@ai-sdk/openai';import { google } from '@ai-sdk/google';
// Eval für jedes Modell separat ausführenconst modelsToCompare = [ { name: 'claude-sonnet', model: anthropic('claude-sonnet-4-5-20250514') }, { name: 'gpt-4o', model: openai('gpt-4o') }, { name: 'gemini-flash', model: google('gemini-2.5-flash') },];
for (const { name, model } of modelsToCompare) { evalite(`compare-${name}`, { data: async () => [ { input: 'Erklaere Promises in 2 Saetzen.', expected: 'Promise' }, { input: 'Was ist der Unterschied zwischen let und const?', expected: 'const' }, { input: 'Erklaere async/await.', expected: 'await' }, ], task: async (input) => { const result = await generateText({ model, prompt: input }); return result.text; }, scorers: [ // Pruefe ob der erwartete Begriff vorkommt (output, expected) => { const contains = output.toLowerCase().includes(expected.toLowerCase()); return { score: contains ? 1 : 0, name: 'contains-keyword' }; }, // Pruefe Praezision (kuerzere Antworten = hoeher) (output) => { const score = Math.max(0, 1 - output.length / 2000); return { score, name: 'conciseness' }; }, ], });}Mit Evalite bekommst Du reproduzierbare Ergebnisse über mehrere Testfaelle hinweg. Du kannst Modelle systematisch auf Deinen spezifischen Use Case benchmarken — nicht nach allgemeinen Benchmarks, sondern nach Deinen Anforderungen.
Aufgabe: Rufe 3 Modelle parallel auf, vergleiche die Ergebnisse nach Laenge und einem einfachen Keyword-Scorer.
Erstelle compare-outputs.ts und fuehre aus mit npx tsx compare-outputs.ts.
import { generateText } from 'ai';import { anthropic } from '@ai-sdk/anthropic';import { openai } from '@ai-sdk/openai';import { google } from '@ai-sdk/google';
const prompt = 'Erklaere den Unterschied zwischen var, let und const in JavaScript in maximal 3 Saetzen.';const expectedKeywords = ['var', 'let', 'const', 'scope', 'block'];
// TODO 1: Definiere ein Array mit 3 Modellen (name + model)
// TODO 2: Rufe alle 3 parallel auf mit Promise.all
// TODO 3: Vergleiche die Ergebnisse:// - Wie viele expectedKeywords kommen in jeder Antwort vor?// - Wie lang ist jede Antwort (Zeichen)?// - Wie viele Tokens hat jede Antwort verbraucht?
// TODO 4: Bestimme den "Gewinner" basierend auf:// - Meiste Keywords = hoechste Qualität// - Bei Gleichstand: kuerzere Antwort gewinntCheckliste:
- 3 Modelle parallel aufgerufen mit
Promise.all - Keyword-Count für jede Antwort berechnet
- Laenge und Token-Verbrauch verglichen
- Gewinner bestimmt und begruendet
- Ergebnisse uebersichtlich formatiert ausgegeben
Lösung anzeigen
import { generateText } from 'ai';import { anthropic } from '@ai-sdk/anthropic';import { openai } from '@ai-sdk/openai';import { google } from '@ai-sdk/google';
const prompt = 'Erklaere den Unterschied zwischen var, let und const in JavaScript in maximal 3 Saetzen.';const expectedKeywords = ['var', 'let', 'const', 'scope', 'block'];
const models = [ { name: 'Claude Sonnet', model: anthropic('claude-sonnet-4-5-20250514') }, { name: 'GPT-4o', model: openai('gpt-4o') }, { name: 'Gemini Flash', model: google('gemini-2.5-flash') },];
// Parallel aufrufenconst results = await Promise.all( models.map(async ({ name, model }) => { const start = Date.now(); const result = await generateText({ model, prompt }); const text = result.text;
// Keyword-Scoring const keywordCount = expectedKeywords.filter(kw => text.toLowerCase().includes(kw.toLowerCase()), ).length;
return { name, text, tokens: result.usage.totalTokens, durationMs: Date.now() - start, keywordCount, charCount: text.length, }; }),);
// Ergebnisse ausgebenconsole.log('=== Vergleich ===\n');console.log('| Modell | Keywords | Zeichen | Tokens | Dauer |');console.log('|--------|----------|---------|--------|-------|');
for (const r of results) { console.log( `| ${r.name} | ${r.keywordCount}/${expectedKeywords.length} | ${r.charCount} | ${r.tokens} | ${r.durationMs}ms |`, );}
// Gewinner bestimmenconst sorted = [...results].sort((a, b) => { if (b.keywordCount !== a.keywordCount) return b.keywordCount - a.keywordCount; return a.charCount - b.charCount; // Bei Gleichstand: kuerzere Antwort gewinnt});
console.log(`\nGewinner: ${sorted[0].name} (${sorted[0].keywordCount} Keywords, ${sorted[0].charCount} Zeichen)`);
// Antworten anzeigenfor (const r of results) { console.log(`\n--- ${r.name} ---`); console.log(r.text);}Erklärung: Promise.all sendet alle Requests parallel — die Gesamtdauer ist die des langsamsten Modells. Der Keyword-Scorer zaehlt, wie viele erwartete Begriffe in der Antwort vorkommen. Bei Gleichstand gewinnt die kuerzere Antwort, weil Praezision in der Aufgabe gefordert war (“maximal 3 Saetze”).
Erwarteter Output (ungefaehr):=== Vergleich ===
| Modell | Keywords | Zeichen | Tokens | Dauer ||--------|----------|---------|--------|-------|| Claude Sonnet | 5/5 | 312 | 198 | 1.2s || GPT-4o | 4/5 | 287 | 176 | 0.9s || Gemini Flash | 5/5 | 256 | 143 | 0.6s |
Gewinner: Gemini Flash (5 Keywords, 256 Zeichen)COMBINE
Abschnitt betitelt „COMBINE“Uebung: Kombiniere Comparing Outputs mit dem Eval-Framework aus Level 6. Statt eines einmaligen Vergleichs:
- Definiere 5 Test-Prompts für Deinen Use Case (z.B. TypeScript-Erklärungen)
- Definiere 2 Scorer — einen Keyword-Scorer und einen Laengen-Scorer
- Laufe alle 3 Modelle über alle 5 Prompts mit Evalite
- Vergleiche die Ergebnisse — welches Modell gewinnt im Durchschnitt?
Das Ergebnis ist ein datengetriebener Report, der Dir sagt: “Für TypeScript-Erklärungen ist Modell X am besten.” Das ist die Grundlage für den Model Router aus Challenge 9.2.
Optional Stretch Goal: Implementiere den LLM-as-a-Judge als Evalite-Scorer. Nutze ein starkes Modell das die Outputs auf einer Skala von 1-10 bewertet und eine Begruendung liefert.