LLM Evaluators Konzept (Legacy) für LLARS¶
Automatisierte Mailverläufe-Gegenüberstellung mit KIA-Säulen-Vergleich¶
Legacy-Konzept (Stand 2025)
Dieses Dokument beschreibt das historische LLM-as-Judge Konzept mit eigener Judge-UI und Sessions. In LLARS (Stand 2026-02-05) ist LLM-Evaluation eine Konfiguration im Scenario Manager (Wizard + Evaluation/Results Tabs). Die Inhalte unten dienen als Hintergrund und Design-Historie.
Version: 1.0 Datum: 25. November 2025 Autor: Claude Code
Inhaltsverzeichnis¶
- Zusammenfassung
- Recherche-Ergebnisse
- Architektur-Überblick
- Phasen-Plan
- Pydantic-Schema-Design
- UI/UX-Konzept
- Session-Management
- Queue-System
- Auswertungs-Dashboard
- Testplan pro Phase
1. Zusammenfassung¶
Historisches Ziel (2025)¶
Implementierung eines automatisierten LLM Evaluator Systems in LLARS, das: - Mailverläufe verschiedener KIA-Säulen paarweise vergleicht - Live-Visualisierung der LLM-Evaluation ermöglicht - Strukturierte JSON-Bewertungen mittels Pydantic-Schema generiert - Session-basiertes Arbeiten ohne aktiven Browser unterstützt - Statistische Auswertung über Säulen-Performance liefert
Aktueller Stand in LLARS¶
- LLM-Evaluation wird pro Szenario konfiguriert (Scenario Wizard).
- Auswahl von System- und eigenen Provider-Modellen.
- Optionaler Auto-Start nach Szenario-Erstellung.
- Live-Status und Ergebnisse im Scenario Manager (Evaluation/Results Tabs).
KIA-Säulen (aus Bild)¶
| Säule | Beschreibung | Verläufe |
|---|---|---|
| Säule 1 | Rollenspiele | 50 |
| Säule 2 | Aus Säule 1 → Feature (generierte Betreffs/Situationsbeschreibung) | 50 |
| Säule 3 | Vollständig anonymisiert und pseudonymisierte Daten | 83 |
| Säule 4 | Synthetisch generierte Daten | ∞ (theoretisch) |
| Säule 5 | Daten aus Testungen (Live-Testung Studierende-Beratende) | 36 |
Nutzung bestehender LLARS-Komponenten¶
- LiteLLM Client (
app/llm/litellm_client.py) - API-Zugang - ComparisonSession/Evaluation Tabellen - Datenstruktur erweitern
- Socket.IO Infrastructure - Live-Streaming
- Permission System - Zugriffskontrolle
- RAG Queue Pattern - Inspiration für Job-Queue
2. Recherche-Ergebnisse¶
2.1 Bestehende LLARS-Metriken¶
Mail History Rating Metriken (bereits implementiert)¶
# app/db/tables.py - UserMailHistoryRating
counsellor_coherence_rating # 1-5 Likert - Kohärenz Berater
client_coherence_rating # 1-5 Likert - Kohärenz Ratsuchende
quality_rating # 1-5 Likert - Beratungsqualität
overall_rating # Binary - Authentizität/Gesamtbewertung
feedback # TEXT - Freitextfeedback
Diese Metriken werden als Bewertungskriterien für den LLM Evaluator übernommen.
Bestehende Comparison-Infrastruktur¶
# Bereits vorhanden:
ComparisonSession # Session-Management
ComparisonMessage # Nachrichtenverläufe
ComparisonEvaluation # AI/User Selection + Reasoning
2.2 LLM Evaluator Best Practices (Web-Recherche)¶
Pairwise Comparison Vorteile¶
"Pairwise evaluation closely mirrors human decision-making processes by focusing on relative preferences rather than assigning absolute scores." - Evidently AI
"Studies show that pairwise comparisons lead to more stable results and smaller differences between LLM judgments and human annotations." - Cameron Wolfe
Position Bias Mitigation¶
"Position bias is identifiable when judgments consistently favor either the first or the second response. Nearly half of all verdicts were position-sensitive." - arXiv Paper
Lösung: Position-Swap-Strategie
Chain-of-Thought für bessere Evaluationen¶
"You should ask the LLM to output the rationale PRIOR to its score (as opposed to afterwards)." - Eugene Yan
Counseling-spezifische Evaluation (CounselBench)¶
"LLMs often outperform online human therapists in perceived quality, but experts frequently flag their outputs for safety concerns." - CounselBench Paper
Wichtige Dimensionen für Beratungs-Evaluation: 1. Empathie & emotionale Unterstützung 2. Problemverständnis 3. Lösungsorientierung 4. Sprachliche Angemessenheit 5. Professionelle Grenzen 6. Therapeutische Techniken
2.3 Pydantic für Structured Output¶
"Instructor extracts structured data from any LLM with type safety, validation, and automatic retries." - Instructor Docs
Beispiel-Pattern:
class Judgment(BaseModel):
thought: str = Field(description="Step-by-step reasoning")
justification: str = Field(description="Explanation for judgment")
similarity: bool = Field(description="Final verdict")
3. Architektur-Überblick¶
┌─────────────────────────────────────────────────────────────────────┐
│ FRONTEND (Vue 3) │
├─────────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────────┐ ┌─────────────────────────┐ │
│ │ JudgeConfig │ │ LiveEvaluation │ │ JudgeResultsDashboard │ │
│ │ - Upload │ │ - Streaming │ │ - Säulen-Matrix │ │
│ │ - Säulen │ │ - JSON-Preview │ │ - Win/Loss Stats │ │
│ │ - Queue │ │ - Verlauf-View │ │ - Export │ │
│ └─────────────┘ └─────────────────┘ └─────────────────────────────┘ │
│ │ │
│ Socket.IO (Bidirectional) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ BACKEND (Flask) │
├─────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ JudgeRoutes │ │ JudgeService │ │ QueueWorker │ │
│ │ /api/judge/* │──▶│ - Pydantic │◀──│ - Background │ │
│ │ │ │ - LiteLLM │ │ - Persistent │ │
│ └─────────────────┘ └──────────────────┘ └──────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ MariaDB (Persistence) │ │
│ │ judge_sessions | judge_comparisons | judge_evaluations │ │
│ │ pillar_threads | pillar_statistics │ │
│ └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ LiteLLM Proxy │
│ kiz1.in.ohmportal.de/llmproxy/v1 │
│ (Mistral / GPT-4o-mini) │
└─────────────────────────────────────────────────────────────────────┘
4. Phasen-Plan¶
Phase 1: Datenbank & Grundstruktur (Woche 1-2)¶
1.1 Neue Tabellen erstellen¶
# app/db/tables.py - Neue Tabellen
class PillarThread(db.Model):
"""Zuordnung von Threads zu KIA-Säulen"""
__tablename__ = 'pillar_threads'
id = Column(Integer, primary_key=True)
thread_id = Column(Integer, ForeignKey('email_threads.id'), nullable=False)
pillar_number = Column(Integer, nullable=False) # 1-5
pillar_name = Column(String(100)) # "Rollenspiele", etc.
metadata_json = Column(JSON) # Zusätzliche Säulen-Metadaten
created_at = Column(DateTime, default=datetime.utcnow)
class JudgeSession(db.Model):
"""Eine LLM Evaluator Evaluierungs-Session"""
__tablename__ = 'judge_sessions'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('user.id'), nullable=False)
name = Column(String(255)) # Session-Name
config_json = Column(JSON) # Konfiguration (Model, Metriken, etc.)
status = Column(Enum('created', 'queued', 'running', 'paused',
'completed', 'failed'), default='created')
total_comparisons = Column(Integer, default=0)
completed_comparisons = Column(Integer, default=0)
current_comparison_id = Column(Integer) # Aktuell laufender Vergleich
created_at = Column(DateTime, default=datetime.utcnow)
started_at = Column(DateTime)
completed_at = Column(DateTime)
class JudgeComparison(db.Model):
"""Ein einzelner paarweiser Vergleich"""
__tablename__ = 'judge_comparisons'
id = Column(Integer, primary_key=True)
session_id = Column(Integer, ForeignKey('judge_sessions.id'), nullable=False)
thread_a_id = Column(Integer, ForeignKey('email_threads.id'), nullable=False)
thread_b_id = Column(Integer, ForeignKey('email_threads.id'), nullable=False)
pillar_a = Column(Integer) # Säule von Thread A
pillar_b = Column(Integer) # Säule von Thread B
position_order = Column(Integer) # 1 = A|B, 2 = B|A (für Swap)
status = Column(Enum('pending', 'running', 'completed', 'failed'), default='pending')
queue_position = Column(Integer) # Position in Queue
created_at = Column(DateTime, default=datetime.utcnow)
started_at = Column(DateTime)
completed_at = Column(DateTime)
class JudgeEvaluation(db.Model):
"""Das Evaluationsergebnis eines Vergleichs"""
__tablename__ = 'judge_evaluations'
id = Column(Integer, primary_key=True)
comparison_id = Column(Integer, ForeignKey('judge_comparisons.id'), nullable=False)
# LLM Output (strukturiert)
raw_response = Column(Text) # Vollständige LLM-Antwort
evaluation_json = Column(JSON) # Parsed Pydantic-Objekt
# Einzelne Metriken (denormalisiert für schnelle Queries)
winner = Column(Enum('A', 'B', 'TIE'), nullable=False)
# Scores pro Metrik (1-5)
counsellor_coherence_a = Column(Float)
counsellor_coherence_b = Column(Float)
client_coherence_a = Column(Float)
client_coherence_b = Column(Float)
quality_a = Column(Float)
quality_b = Column(Float)
empathy_a = Column(Float)
empathy_b = Column(Float)
# Chain-of-Thought
reasoning = Column(Text)
confidence = Column(Float) # 0.0 - 1.0
# Position-Swap Tracking
position_variant = Column(Integer) # 1 oder 2
# Timing
llm_latency_ms = Column(Integer)
token_count = Column(Integer)
created_at = Column(DateTime, default=datetime.utcnow)
class PillarStatistics(db.Model):
"""Aggregierte Statistiken pro Säulen-Paar"""
__tablename__ = 'pillar_statistics'
id = Column(Integer, primary_key=True)
session_id = Column(Integer, ForeignKey('judge_sessions.id'))
pillar_a = Column(Integer)
pillar_b = Column(Integer)
wins_a = Column(Integer, default=0)
wins_b = Column(Integer, default=0)
ties = Column(Integer, default=0)
avg_confidence = Column(Float)
updated_at = Column(DateTime, default=datetime.utcnow)
1.2 Migration erstellen¶
1.3 Test-Kriterien Phase 1¶
- Alle Tabellen erstellt und migriert
- Foreign Keys funktionieren
- CRUD-Operationen für alle Entities
- Seed-Data für Test-Säulen
Phase 2: Pydantic Schema & LLM-Integration (Woche 2-3)¶
2.1 Pydantic Evaluation Schema¶
# app/llm/judge_schema.py
from pydantic import BaseModel, Field
from typing import Literal, Optional
from enum import Enum
class MetricScore(BaseModel):
"""Score für eine einzelne Metrik"""
score_a: float = Field(ge=1.0, le=5.0, description="Score für Verlauf A (1-5)")
score_b: float = Field(ge=1.0, le=5.0, description="Score für Verlauf B (1-5)")
reasoning: str = Field(description="Begründung für die Scores")
class EvaluationCriteria(BaseModel):
"""Bewertungskriterien basierend auf LLARS-Metriken"""
counsellor_coherence: MetricScore = Field(
description="Kohärenz der beratenden Person: Wie logisch und zusammenhängend "
"sind die Antworten des Beraters?"
)
client_coherence: MetricScore = Field(
description="Kohärenz der ratsuchenden Person: Wie realistisch und "
"nachvollziehbar verhält sich der Klient?"
)
quality: MetricScore = Field(
description="Beratungsqualität: Wie gut ist die therapeutische Qualität "
"der Beratung insgesamt?"
)
empathy: MetricScore = Field(
description="Empathie: Wie empathisch und verständnisvoll reagiert "
"der Berater auf den Klienten?"
)
authenticity: MetricScore = Field(
description="Authentizität: Wie authentisch und natürlich wirkt "
"der gesamte Gesprächsverlauf?"
)
solution_orientation: MetricScore = Field(
description="Lösungsorientierung: Wie gut werden konkrete Lösungsansätze "
"und Hilfestellungen angeboten?"
)
class ChainOfThought(BaseModel):
"""Strukturiertes Reasoning vor der Entscheidung"""
step_1_overview: str = Field(
description="Kurze Zusammenfassung beider Verläufe"
)
step_2_strengths_a: str = Field(
description="Stärken von Verlauf A"
)
step_3_strengths_b: str = Field(
description="Stärken von Verlauf B"
)
step_4_weaknesses_a: str = Field(
description="Schwächen von Verlauf A"
)
step_5_weaknesses_b: str = Field(
description="Schwächen von Verlauf B"
)
step_6_comparison: str = Field(
description="Direkter Vergleich der wichtigsten Unterschiede"
)
class JudgeEvaluationResult(BaseModel):
"""Vollständiges Evaluationsergebnis"""
chain_of_thought: ChainOfThought = Field(
description="Schrittweises Reasoning VOR der Entscheidung"
)
criteria_scores: EvaluationCriteria = Field(
description="Detaillierte Bewertung nach Kriterien"
)
winner: Literal["A", "B", "TIE"] = Field(
description="Welcher Verlauf ist insgesamt besser? "
"A, B, oder TIE bei Gleichstand"
)
confidence: float = Field(
ge=0.0, le=1.0,
description="Konfidenz der Entscheidung (0.0 = unsicher, 1.0 = sehr sicher)"
)
final_justification: str = Field(
description="Abschließende Begründung für die Gesamtentscheidung"
)
2.2 LLM Judge Service¶
# app/services/judge_service.py
import instructor
from openai import OpenAI
from app.llm.judge_schema import JudgeEvaluationResult
from app.llm.litellm_client import get_litellm_client
import json
class JudgeService:
"""Service für LLM Evaluator Evaluationen"""
SYSTEM_PROMPT = """Du bist ein Experte für die Bewertung von Beratungsgesprächen
im Kontext psychologischer Online-Beratung.
Deine Aufgabe ist es, zwei E-Mail-Verläufe zwischen Beratenden und Ratsuchenden
zu vergleichen und zu bewerten, welcher Verlauf qualitativ besser ist.
WICHTIG:
1. Führe ZUERST dein Chain-of-Thought Reasoning durch
2. Bewerte DANN jedes Kriterium einzeln für beide Verläufe
3. Triff ZULETZT deine Gesamtentscheidung
Bewertungskriterien (jeweils 1-5 Skala):
- Berater-Kohärenz: Logik und Zusammenhang der Berater-Antworten
- Klienten-Kohärenz: Realismus des Klientenverhaltens
- Beratungsqualität: Therapeutische Qualität insgesamt
- Empathie: Einfühlungsvermögen des Beraters
- Authentizität: Natürlichkeit des Gesprächs
- Lösungsorientierung: Konkrete Hilfestellungen
Ignoriere die Position der Verläufe (ob A oder B zuerst kommt) -
bewerte rein nach Inhalt und Qualität."""
def __init__(self, api_key: str, model: str = "mistralai/Mistral-Small-3.2-24B-Instruct-2506"):
self.client = instructor.from_openai(
OpenAI(
base_url="https://kiz1.in.ohmportal.de/llmproxy/v1",
api_key=api_key
),
mode=instructor.Mode.JSON
)
self.model = model
def format_thread_for_prompt(self, thread_messages: list) -> str:
"""Formatiert einen E-Mail-Verlauf für den Prompt"""
formatted = []
for msg in thread_messages:
role = "BERATER" if msg.get('is_counsellor') else "RATSUCHENDE"
formatted.append(f"[{role}]: {msg.get('content', '')}")
return "\n\n".join(formatted)
async def evaluate_pair(
self,
thread_a_messages: list,
thread_b_messages: list,
pillar_a: int,
pillar_b: int,
stream_callback=None
) -> JudgeEvaluationResult:
"""
Führt paarweisen Vergleich durch.
Args:
thread_a_messages: Nachrichten von Verlauf A
thread_b_messages: Nachrichten von Verlauf B
pillar_a: Säulennummer von A
pillar_b: Säulennummer von B
stream_callback: Optional callback für Streaming-Updates
Returns:
JudgeEvaluationResult mit strukturierter Bewertung
"""
prompt = f"""Vergleiche die folgenden zwei E-Mail-Beratungsverläufe:
=== VERLAUF A (Säule {pillar_a}) ===
{self.format_thread_for_prompt(thread_a_messages)}
=== VERLAUF B (Säule {pillar_b}) ===
{self.format_thread_for_prompt(thread_b_messages)}
Führe eine detaillierte Bewertung durch und bestimme, welcher Verlauf besser ist."""
# Mit Streaming
if stream_callback:
result = await self._evaluate_with_streaming(prompt, stream_callback)
else:
result = self.client.chat.completions.create(
model=self.model,
response_model=JudgeEvaluationResult,
messages=[
{"role": "system", "content": self.SYSTEM_PROMPT},
{"role": "user", "content": prompt}
],
temperature=0.3, # Niedrig für Konsistenz
max_tokens=4000
)
return result
async def evaluate_with_position_swap(
self,
thread_a_messages: list,
thread_b_messages: list,
pillar_a: int,
pillar_b: int
) -> tuple[JudgeEvaluationResult, JudgeEvaluationResult, str]:
"""
Führt Evaluation mit Position-Swap durch um Bias zu eliminieren.
Returns:
(result_ab, result_ba, final_winner)
"""
# Erste Evaluation: A | B
result_ab = await self.evaluate_pair(
thread_a_messages, thread_b_messages,
pillar_a, pillar_b
)
# Zweite Evaluation: B | A (getauscht)
result_ba = await self.evaluate_pair(
thread_b_messages, thread_a_messages,
pillar_b, pillar_a
)
# Ergebnis konsolidieren
# Wenn result_ba "A" sagt, meint es eigentlich B (wegen Swap)
winner_ab = result_ab.winner
winner_ba_adjusted = {
"A": "B",
"B": "A",
"TIE": "TIE"
}.get(result_ba.winner, "TIE")
# Finales Ergebnis
if winner_ab == winner_ba_adjusted:
final_winner = winner_ab
else:
# Widerspruch → TIE
final_winner = "TIE"
return result_ab, result_ba, final_winner
2.3 Test-Kriterien Phase 2¶
- Pydantic Schema validiert korrekt
- LiteLLM-Verbindung funktioniert
- JSON-Output wird korrekt geparst
- Position-Swap liefert konsistente Ergebnisse
- Streaming-Callback empfängt Daten
Phase 3: API Endpoints & Queue (Woche 3-4)¶
3.1 Judge API Routes (aktuelle Struktur)¶
Die Umsetzung ist modular (statt monolithischem judge_routes.py) und wird über routes/judge/__init__.py als /api/judge registriert.
Module (Auszug):
- session_routes.py (CRUD, Estimate, Comparison-Modes)
- session_control_routes.py (start/pause/resume/delete)
- comparison_routes.py (current, queue, comparisons, worker streams)
- statistics_routes.py + statistics_* (Ergebnisse & Analysen)
- pillar_routes.py (Säulen + Zuordnung)
- kia_sync_routes.py (GitLab Sync)
- export_routes.py (CSV/JSON)
Alle Endpoints sind via @authentik_required geschützt und nutzen @require_permission('feature:comparison:*').
from auth.decorators import authentik_required
from decorators.permission_decorator import require_permission
from routes.judge.session_routes import session_bp
@session_bp.route('/sessions', methods=['GET'])
@authentik_required
@require_permission('feature:comparison:view')
def list_sessions():
...
Wichtige Endpoints (Auszug):
- GET /api/judge/comparison-modes
- POST /api/judge/estimate
- GET/POST /api/judge/sessions
- POST /api/judge/sessions/<id>/start|pause|resume
- GET /api/judge/sessions/<id>/current|queue|comparisons
- GET /api/judge/sessions/<id>/results|verbosity-analysis|thread-performance|position-swap-analysis
- GET /api/judge/sessions/<id>/export/csv|export/json
- GET /api/judge/pillars (+ /pillars/<n>/threads, /pillars/<n>/assign)
- GET/POST /api/judge/kia/...
3.2 Socket.IO Events für Live-Updates¶
# app/socketio_handlers/events_judge.py (Auszug)
@socketio.on('judge:join_session')
def handle_join_session(data):
...
@socketio.on('judge:leave_session')
def handle_leave_session(data):
...
@socketio.on('judge:join_overview')
def handle_join_overview():
...
@socketio.on('judge:get_status')
def handle_get_status(data):
...
Server → Client (Beispiele):
- judge:comparison_start
- judge:llm_stream
- judge:comparison_complete
- judge:progress
- judge:session_complete
- judge:status
3.3 Test-Kriterien Phase 3¶
- Alle API-Endpoints erreichbar mit korrekten Permissions
- Session-Lifecycle funktioniert (create → configure → start → pause)
- Socket.IO Rooms werden korrekt verwaltet
- Live-Broadcasts erreichen verbundene Clients
Phase 4: Background Worker & Queue (Woche 4-5)¶
4.1 Judge Worker¶
Aktuell läuft die Verarbeitung über einen Worker-Pool (app/workers/judge_worker_pool.py)
mit optional mehreren parallelen Workern. Der Pool wird aus den Session-Control-Routen
gestartet (trigger_judge_worker_pool) und sendet Live-Events (judge:*) für Fortschritt,
Streaming und Abschluss.
Relevante Dateien:
- app/workers/judge_worker_pool.py (aktuell, multi-worker)
- app/workers/judge_worker.py (Legacy Single-Worker)
- app/routes/judge/session_control_routes.py
- app/routes/judge/session_health_routes.py
Pseudocode (vereinfacht):
pool.start(session_id, worker_count)
while pending comparisons:
worker.claim_next()
worker.evaluate()
emit judge:comparison_start / judge:llm_stream / judge:progress
emit judge:session_complete
4.2 Test-Kriterien Phase 4¶
- Worker startet im Background
- Queue wird sequentiell abgearbeitet
- Pause/Resume funktioniert
- Statistiken werden korrekt aggregiert
- Socket.IO Broadcasts während Verarbeitung
Phase 5: Frontend - Konfiguration & Upload (Woche 5-6)¶
5.1 JudgeConfig.vue¶
<!-- llars-frontend/src/components/Judge/JudgeConfig.vue -->
<template>
<v-container>
<v-card>
<v-card-title>
<v-icon left>mdi-scale-balance</v-icon>
LLM Evaluator Konfiguration
</v-card-title>
<v-card-text>
<!-- Session Name -->
<v-text-field
v-model="sessionName"
label="Session Name"
outlined
/>
<!-- Säulen-Auswahl -->
<v-card outlined class="mb-4">
<v-card-subtitle>KIA-Säulen auswählen</v-card-subtitle>
<v-card-text>
<v-chip-group
v-model="selectedPillars"
multiple
column
>
<v-chip
v-for="pillar in pillars"
:key="pillar.number"
:value="pillar.number"
filter
outlined
>
<v-avatar left :color="pillar.color">
{{ pillar.number }}
</v-avatar>
{{ pillar.name }}
<v-chip small class="ml-2">
{{ pillar.threadCount }} Verläufe
</v-chip>
</v-chip>
</v-chip-group>
</v-card-text>
</v-card>
<!-- Vergleichs-Modus -->
<v-radio-group v-model="comparisonMode" label="Vergleichs-Modus">
<v-radio
label="Pillar Sample (schneller Überblick)"
value="pillar_sample"
/>
<v-radio
label="Round Robin (alle Threads Säule A vs. Säule B)"
value="round_robin"
/>
<v-radio
label="Free For All (alle Threads gegeneinander)"
value="free_for_all"
/>
</v-radio-group>
<!-- Samples pro Säule -->
<v-slider
v-model="samplesPerPillar"
label="Verläufe pro Säule"
:min="1"
:max="50"
thumb-label
/>
<!-- Position Swap -->
<v-switch
v-model="positionSwap"
label="Position-Swap aktivieren (eliminiert Bias)"
hint="Jeder Vergleich wird zweimal mit getauschten Positionen durchgeführt"
persistent-hint
/>
<!-- Zusammenfassung -->
<v-alert type="info" outlined class="mt-4">
<strong>Zusammenfassung:</strong>
<ul>
<li>{{ selectedPillars.length }} Säulen ausgewählt</li>
<li>{{ estimatedComparisons }} geschätzte Vergleiche</li>
<li>{{ positionSwap ? 'Mit' : 'Ohne' }} Position-Swap</li>
</ul>
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
color="primary"
:disabled="!isValid"
@click="createSession"
>
<v-icon left>mdi-play</v-icon>
Session erstellen
</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
const router = useRouter()
// Daten
const sessionName = ref('Neue Evaluation ' + new Date().toLocaleDateString('de-DE'))
const selectedPillars = ref([1, 3])
const comparisonMode = ref('pillar_sample')
const samplesPerPillar = ref(10)
const maxThreadsPerPillar = ref(null)
const positionSwap = ref(true)
const repetitionsPerPair = ref(1)
const workerCount = ref(1)
const estimate = ref(null)
const pillars = ref([
{ number: 1, name: 'Rollenspiele', threadCount: 50, color: 'red' },
{ number: 2, name: 'Feature aus Säule 1', threadCount: 50, color: 'orange' },
{ number: 3, name: 'Anonymisierte Daten', threadCount: 83, color: 'green' },
{ number: 4, name: 'Synthetisch generiert', threadCount: 100, color: 'blue' },
{ number: 5, name: 'Live-Testungen', threadCount: 36, color: 'purple' }
])
// Computed
const estimatedComparisons = computed(() => estimate.value?.total_comparisons || 0)
const isValid = computed(() => {
return selectedPillars.value.length >= 2 && sessionName.value.length > 0
})
// Methods
async function fetchEstimate() {
if (selectedPillars.value.length < 2) return
const payload = {
pillar_ids: selectedPillars.value,
comparison_mode: comparisonMode.value,
samples_per_pillar: samplesPerPillar.value,
position_swap: positionSwap.value
}
if (maxThreadsPerPillar.value) {
payload.max_threads_per_pillar = maxThreadsPerPillar.value
}
const response = await axios.post('/api/judge/estimate', payload)
estimate.value = response.data
}
async function createSession() {
try {
const payload = {
session_name: sessionName.value,
pillar_ids: selectedPillars.value,
comparison_mode: comparisonMode.value,
samples_per_pillar: samplesPerPillar.value,
position_swap: positionSwap.value,
repetitions_per_pair: repetitionsPerPair.value,
worker_count: workerCount.value
}
if (maxThreadsPerPillar.value) {
payload.max_threads_per_pillar = maxThreadsPerPillar.value
}
// Session erstellen
const createRes = await axios.post('/api/judge/sessions', payload)
const sessionId = createRes.data.session_id || createRes.data.id
// Starten
await axios.post(`/api/judge/sessions/${sessionId}/start`)
// Zur Session navigieren
router.push(`/judge/session/${sessionId}`)
} catch (error) {
console.error('Session creation failed:', error)
}
}
</script>
5.2 Test-Kriterien Phase 5¶
- Säulen werden korrekt geladen
- Vergleichs-Berechnung stimmt
- Session wird erstellt und konfiguriert
- Navigation zur Session funktioniert
Phase 6: Frontend - Live-Evaluation View (Woche 6-7)¶
6.1 LiveEvaluation.vue¶
<!-- llars-frontend/src/components/Judge/LiveEvaluation.vue -->
<template>
<v-container fluid>
<!-- Header mit Session-Info -->
<v-row>
<v-col cols="12">
<v-card>
<v-card-title>
<v-icon left :color="statusColor">{{ statusIcon }}</v-icon>
{{ session?.name }}
<v-spacer />
<v-chip :color="statusColor" small>
{{ session?.status }}
</v-chip>
</v-card-title>
<!-- Progress -->
<v-card-text>
<v-progress-linear
:value="progress"
height="25"
:color="statusColor"
>
<template v-slot:default>
{{ session?.completed_comparisons }} / {{ session?.total_comparisons }}
({{ progress.toFixed(1) }}%)
</template>
</v-progress-linear>
<!-- Controls -->
<v-btn-toggle class="mt-4">
<v-btn
v-if="session?.status !== 'running'"
color="success"
@click="startSession"
>
<v-icon>mdi-play</v-icon>
Start
</v-btn>
<v-btn
v-if="session?.status === 'running'"
color="warning"
@click="pauseSession"
>
<v-icon>mdi-pause</v-icon>
Pause
</v-btn>
</v-btn-toggle>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Hauptbereich: Vergleich + JSON Preview -->
<v-row class="mt-4">
<!-- Linke Seite: Verlauf A -->
<v-col cols="5">
<v-card :outlined="currentWinner === 'A'" :color="currentWinner === 'A' ? 'success lighten-5' : ''">
<v-card-title>
<v-chip :color="getPillarColor(currentComparison?.pillar_a)">
Säule {{ currentComparison?.pillar_a }}
</v-chip>
Verlauf A
<v-icon v-if="currentWinner === 'A'" color="success" right>
mdi-trophy
</v-icon>
</v-card-title>
<v-card-text class="thread-preview">
<div
v-for="(msg, idx) in currentComparison?.thread_a?.messages"
:key="idx"
:class="['message', msg.is_counsellor ? 'counsellor' : 'client']"
>
<v-chip x-small :color="msg.is_counsellor ? 'blue' : 'green'" class="mb-1">
{{ msg.is_counsellor ? 'Berater' : 'Ratsuchende' }}
</v-chip>
<div class="message-content">{{ msg.content }}</div>
</div>
</v-card-text>
</v-card>
</v-col>
<!-- Mitte: LLM Output / JSON Preview -->
<v-col cols="2">
<v-card height="100%">
<v-card-title class="text-center">
<v-icon>mdi-robot</v-icon>
LLM Judge
</v-card-title>
<v-card-text>
<!-- Streaming Indicator -->
<v-progress-circular
v-if="isStreaming"
indeterminate
color="primary"
class="mb-4"
/>
<!-- Winner Anzeige -->
<div v-if="currentResult" class="text-center">
<v-chip
x-large
:color="getWinnerColor(currentResult.winner)"
>
{{ currentResult.winner === 'TIE' ? 'Unentschieden' : `Gewinner: ${currentResult.winner}` }}
</v-chip>
<div class="mt-2">
Konfidenz: {{ (currentResult.confidence * 100).toFixed(0) }}%
</div>
</div>
</v-card-text>
</v-card>
</v-col>
<!-- Rechte Seite: Verlauf B -->
<v-col cols="5">
<v-card :outlined="currentWinner === 'B'" :color="currentWinner === 'B' ? 'success lighten-5' : ''">
<v-card-title>
<v-chip :color="getPillarColor(currentComparison?.pillar_b)">
Säule {{ currentComparison?.pillar_b }}
</v-chip>
Verlauf B
<v-icon v-if="currentWinner === 'B'" color="success" right>
mdi-trophy
</v-icon>
</v-card-title>
<v-card-text class="thread-preview">
<div
v-for="(msg, idx) in currentComparison?.thread_b?.messages"
:key="idx"
:class="['message', msg.is_counsellor ? 'counsellor' : 'client']"
>
<v-chip x-small :color="msg.is_counsellor ? 'blue' : 'green'" class="mb-1">
{{ msg.is_counsellor ? 'Berater' : 'Ratsuchende' }}
</v-chip>
<div class="message-content">{{ msg.content }}</div>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- JSON Preview (expandierbar) -->
<v-row class="mt-4">
<v-col cols="12">
<v-expansion-panels>
<v-expansion-panel>
<v-expansion-panel-header>
<v-icon left>mdi-code-json</v-icon>
LLM Response (JSON)
</v-expansion-panel-header>
<v-expansion-panel-content>
<pre class="json-preview">{{ streamingJson }}</pre>
</v-expansion-panel-content>
</v-expansion-panel>
<v-expansion-panel>
<v-expansion-panel-header>
<v-icon left>mdi-brain</v-icon>
Chain-of-Thought Reasoning
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-stepper v-if="currentResult?.chain_of_thought" vertical>
<v-stepper-step step="1" complete>
Übersicht
<small>{{ currentResult.chain_of_thought.step_1_overview }}</small>
</v-stepper-step>
<v-stepper-step step="2" complete>
Stärken A
<small>{{ currentResult.chain_of_thought.step_2_strengths_a }}</small>
</v-stepper-step>
<!-- ... weitere Steps -->
</v-stepper>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</v-col>
</v-row>
<!-- Historie der abgeschlossenen Vergleiche -->
<v-row class="mt-4">
<v-col cols="12">
<v-card>
<v-card-title>
<v-icon left>mdi-history</v-icon>
Abgeschlossene Vergleiche
</v-card-title>
<v-data-table
:headers="historyHeaders"
:items="completedComparisons"
:items-per-page="10"
@click:row="showComparisonDetail"
>
<template v-slot:item.winner="{ item }">
<v-chip :color="getWinnerColor(item.winner)" small>
{{ item.winner }}
</v-chip>
</template>
<template v-slot:item.pillars="{ item }">
<v-chip :color="getPillarColor(item.pillar_a)" x-small>
{{ item.pillar_a }}
</v-chip>
vs
<v-chip :color="getPillarColor(item.pillar_b)" x-small>
{{ item.pillar_b }}
</v-chip>
</template>
</v-data-table>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { io } from 'socket.io-client'
import axios from 'axios'
const route = useRoute()
const sessionId = route.params.id
// State
const session = ref(null)
const currentComparison = ref(null)
const currentResult = ref(null)
const streamingJson = ref('')
const isStreaming = ref(false)
const completedComparisons = ref([])
const socket = ref(null)
// Computed
const progress = computed(() => {
if (!session.value) return 0
return (session.value.completed_comparisons / session.value.total_comparisons) * 100
})
const currentWinner = computed(() => currentResult.value?.winner)
const statusColor = computed(() => {
const colors = {
'created': 'grey',
'queued': 'blue',
'running': 'green',
'paused': 'orange',
'completed': 'success',
'failed': 'red'
}
return colors[session.value?.status] || 'grey'
})
// Socket.IO Setup
onMounted(async () => {
// Session laden
await loadSession()
await loadCompletedComparisons()
// Socket verbinden
socket.value = io('/judge', {
auth: { token: sessionStorage.getItem('auth_token') }
})
socket.value.emit('judge:join_session', { session_id: sessionId })
// Event Listeners
socket.value.on('judge:comparison_start', (data) => {
currentComparison.value = data
currentResult.value = null
streamingJson.value = ''
isStreaming.value = true
})
socket.value.on('judge:llm_stream', (data) => {
streamingJson.value += data.chunk
})
socket.value.on('judge:comparison_complete', (data) => {
currentResult.value = data.evaluation
isStreaming.value = false
completedComparisons.value.unshift(data)
})
socket.value.on('judge:progress', (data) => {
session.value.completed_comparisons = data.completed
})
})
onUnmounted(() => {
if (socket.value) {
socket.value.emit('judge:leave_session', { session_id: sessionId })
socket.value.disconnect()
}
})
// Methods
async function loadSession() {
const res = await axios.get(`/api/judge/sessions/${sessionId}`)
session.value = res.data
}
async function loadCompletedComparisons() {
const res = await axios.get(`/api/judge/sessions/${sessionId}/results`)
completedComparisons.value = res.data
}
async function startSession() {
await axios.post(`/api/judge/sessions/${sessionId}/start`)
session.value.status = 'running'
}
async function pauseSession() {
await axios.post(`/api/judge/sessions/${sessionId}/pause`)
session.value.status = 'paused'
}
function getPillarColor(pillar) {
const colors = {
1: 'red',
2: 'orange',
3: 'green',
4: 'blue',
5: 'purple'
}
return colors[pillar] || 'grey'
}
function getWinnerColor(winner) {
if (winner === 'TIE') return 'grey'
return 'success'
}
</script>
<style scoped>
.thread-preview {
max-height: 400px;
overflow-y: auto;
}
.message {
padding: 8px;
margin-bottom: 8px;
border-radius: 8px;
}
.message.counsellor {
background-color: rgba(33, 150, 243, 0.1);
margin-left: 20px;
}
.message.client {
background-color: rgba(76, 175, 80, 0.1);
margin-right: 20px;
}
.json-preview {
background-color: #1e1e1e;
color: #d4d4d4;
padding: 16px;
border-radius: 4px;
overflow-x: auto;
font-size: 12px;
}
</style>
6.2 Test-Kriterien Phase 6¶
- Live-View zeigt aktuelle Vergleiche
- Streaming JSON wird korrekt angezeigt
- Winner-Highlight funktioniert
- Historie wird aktualisiert
- Socket-Reconnect bei Verbindungsverlust
Phase 7: Auswertungs-Dashboard (Woche 7-8)¶
7.1 JudgeResultsDashboard.vue¶
<!-- llars-frontend/src/components/Judge/JudgeResultsDashboard.vue -->
<template>
<v-container fluid>
<v-row>
<!-- Säulen-Matrix -->
<v-col cols="8">
<v-card>
<v-card-title>
<v-icon left>mdi-table</v-icon>
Säulen-Vergleichsmatrix
</v-card-title>
<v-card-text>
<table class="matrix-table">
<thead>
<tr>
<th></th>
<th v-for="p in pillars" :key="p">
<v-chip :color="getPillarColor(p)" small>
Säule {{ p }}
</v-chip>
</th>
</tr>
</thead>
<tbody>
<tr v-for="pa in pillars" :key="pa">
<td>
<v-chip :color="getPillarColor(pa)" small>
Säule {{ pa }}
</v-chip>
</td>
<td
v-for="pb in pillars"
:key="pb"
:class="getCellClass(pa, pb)"
@click="showDetail(pa, pb)"
>
<template v-if="pa !== pb">
<div class="cell-content">
<strong>{{ getWinRate(pa, pb) }}%</strong>
<small>{{ getMatchCount(pa, pb) }} Matches</small>
</div>
</template>
<template v-else>
<span class="diagonal">-</span>
</template>
</td>
</tr>
</tbody>
</table>
<div class="legend mt-4">
<v-chip color="success" small>Gewinner (>60%)</v-chip>
<v-chip color="warning" small>Ausgeglichen (40-60%)</v-chip>
<v-chip color="error" small>Verlierer (<40%)</v-chip>
</div>
</v-card-text>
</v-card>
</v-col>
<!-- Gesamtstatistiken -->
<v-col cols="4">
<v-card>
<v-card-title>
<v-icon left>mdi-chart-bar</v-icon>
Gesamtübersicht
</v-card-title>
<v-card-text>
<!-- Säulen-Ranking -->
<v-list>
<v-subheader>Säulen-Ranking (nach Gesamtsiegen)</v-subheader>
<v-list-item
v-for="(pillar, idx) in pillarRanking"
:key="pillar.number"
>
<v-list-item-avatar>
<v-chip :color="idx === 0 ? 'gold' : idx === 1 ? 'silver' : idx === 2 ? '#cd7f32' : 'grey'">
{{ idx + 1 }}
</v-chip>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
Säule {{ pillar.number }}
</v-list-item-title>
<v-list-item-subtitle>
{{ pillar.wins }} Siege / {{ pillar.losses }} Niederlagen
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<v-progress-circular
:value="pillar.winRate"
:color="pillar.winRate > 50 ? 'success' : 'error'"
>
{{ pillar.winRate }}%
</v-progress-circular>
</v-list-item-action>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
<!-- Metriken-Durchschnitte -->
<v-card class="mt-4">
<v-card-title>
<v-icon left>mdi-chart-line</v-icon>
Durchschnittliche Metriken
</v-card-title>
<v-card-text>
<v-simple-table>
<thead>
<tr>
<th>Säule</th>
<th>Kohärenz</th>
<th>Qualität</th>
<th>Empathie</th>
</tr>
</thead>
<tbody>
<tr v-for="pillar in pillars" :key="pillar">
<td>
<v-chip :color="getPillarColor(pillar)" x-small>
{{ pillar }}
</v-chip>
</td>
<td>{{ getAvgMetric(pillar, 'coherence').toFixed(2) }}</td>
<td>{{ getAvgMetric(pillar, 'quality').toFixed(2) }}</td>
<td>{{ getAvgMetric(pillar, 'empathy').toFixed(2) }}</td>
</tr>
</tbody>
</v-simple-table>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Detail-Charts -->
<v-row class="mt-4">
<v-col cols="6">
<v-card>
<v-card-title>Siege pro Säule</v-card-title>
<v-card-text>
<canvas ref="winsChart"></canvas>
</v-card-text>
</v-card>
</v-col>
<v-col cols="6">
<v-card>
<v-card-title>Konfidenz-Verteilung</v-card-title>
<v-card-text>
<canvas ref="confidenceChart"></canvas>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Export -->
<v-row class="mt-4">
<v-col cols="12">
<v-card>
<v-card-actions>
<v-btn color="primary" @click="exportCSV">
<v-icon left>mdi-download</v-icon>
Export als CSV
</v-btn>
<v-btn color="secondary" @click="exportJSON">
<v-icon left>mdi-code-json</v-icon>
Export als JSON
</v-btn>
<v-btn color="info" @click="exportPDF">
<v-icon left>mdi-file-pdf-box</v-icon>
Report als PDF
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
// ... Implementation mit Chart.js für Visualisierungen
</script>
<style scoped>
.matrix-table {
width: 100%;
border-collapse: collapse;
}
.matrix-table th,
.matrix-table td {
padding: 12px;
text-align: center;
border: 1px solid #e0e0e0;
}
.matrix-table td.winner {
background-color: rgba(76, 175, 80, 0.2);
}
.matrix-table td.loser {
background-color: rgba(244, 67, 54, 0.2);
}
.matrix-table td.neutral {
background-color: rgba(255, 193, 7, 0.2);
}
.diagonal {
color: #9e9e9e;
}
.cell-content {
display: flex;
flex-direction: column;
}
</style>
7.2 Test-Kriterien Phase 7¶
- Matrix zeigt korrekte Win-Rates
- Ranking ist sortiert
- Charts rendern korrekt
- Export funktioniert (CSV, JSON, PDF)
Phase 8: Data Upload & Pillar Management (Woche 8-9)¶
8.1 PillarUpload.vue¶
<!-- llars-frontend/src/components/Judge/PillarUpload.vue -->
<template>
<v-container>
<v-card>
<v-card-title>
<v-icon left>mdi-upload</v-icon>
Säulen-Daten hochladen
</v-card-title>
<v-card-text>
<v-stepper v-model="step">
<v-stepper-header>
<v-stepper-step step="1">Säule auswählen</v-stepper-step>
<v-divider />
<v-stepper-step step="2">Datei hochladen</v-stepper-step>
<v-divider />
<v-stepper-step step="3">Validierung</v-stepper-step>
<v-divider />
<v-stepper-step step="4">Import</v-stepper-step>
</v-stepper-header>
<v-stepper-content step="1">
<v-select
v-model="selectedPillar"
:items="pillars"
item-text="name"
item-value="number"
label="Säule auswählen"
outlined
/>
<v-btn color="primary" @click="step = 2">
Weiter
</v-btn>
</v-stepper-content>
<v-stepper-content step="2">
<v-file-input
v-model="uploadFile"
label="JSON/CSV Datei"
accept=".json,.csv"
show-size
outlined
/>
<v-alert type="info" outlined>
<strong>Erwartetes Format:</strong>
<pre>{{ expectedFormat }}</pre>
</v-alert>
<v-btn color="primary" @click="validateFile">
Validieren
</v-btn>
</v-stepper-content>
<v-stepper-content step="3">
<v-alert :type="validationResult.valid ? 'success' : 'error'">
{{ validationResult.message }}
</v-alert>
<v-simple-table v-if="validationResult.preview">
<thead>
<tr>
<th>Thread</th>
<th>Nachrichten</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr v-for="thread in validationResult.preview" :key="thread.id">
<td>{{ thread.subject }}</td>
<td>{{ thread.messageCount }}</td>
<td>
<v-icon :color="thread.valid ? 'success' : 'error'">
{{ thread.valid ? 'mdi-check' : 'mdi-alert' }}
</v-icon>
</td>
</tr>
</tbody>
</v-simple-table>
<v-btn
color="primary"
:disabled="!validationResult.valid"
@click="importData"
>
Importieren
</v-btn>
</v-stepper-content>
<v-stepper-content step="4">
<v-progress-linear
v-if="importing"
indeterminate
color="primary"
/>
<v-alert v-else type="success">
{{ importResult.imported }} Threads erfolgreich importiert!
</v-alert>
</v-stepper-content>
</v-stepper>
</v-card-text>
</v-card>
</v-container>
</template>
8.2 Test-Kriterien Phase 8¶
- File-Upload funktioniert
- Validierung erkennt fehlerhafte Daten
- Import erstellt korrekte Datenbankeinträge
- Säulen-Zuordnung korrekt
5. Pydantic-Schema-Design (Zusammenfassung)¶
# Vollständiges Schema
from pydantic import BaseModel, Field
from typing import Literal
class MetricScore(BaseModel):
score_a: float = Field(ge=1.0, le=5.0)
score_b: float = Field(ge=1.0, le=5.0)
reasoning: str
class EvaluationCriteria(BaseModel):
counsellor_coherence: MetricScore
client_coherence: MetricScore
quality: MetricScore
empathy: MetricScore
authenticity: MetricScore
solution_orientation: MetricScore
class ChainOfThought(BaseModel):
step_1_overview: str
step_2_strengths_a: str
step_3_strengths_b: str
step_4_weaknesses_a: str
step_5_weaknesses_b: str
step_6_comparison: str
class JudgeEvaluationResult(BaseModel):
chain_of_thought: ChainOfThought
criteria_scores: EvaluationCriteria
winner: Literal["A", "B", "TIE"]
confidence: float = Field(ge=0.0, le=1.0)
final_justification: str
6. UI/UX-Konzept¶
6.1 Navigation¶
/judge
├── /config → JudgeConfig.vue (Session erstellen)
├── /sessions → SessionList.vue (Übersicht)
├── /session/:id → LiveEvaluation.vue (Live-View)
├── /session/:id/results → JudgeResultsDashboard.vue
├── /upload → PillarUpload.vue (Daten hochladen)
└── /pillar/:id → PillarDetail.vue (Säulen-Details)
6.2 Live-View Layout¶
┌─────────────────────────────────────────────────────────────────┐
│ [Progress Bar] 45/100 (45%) [Start][Pause] │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ VERLAUF A │ │ LLM JUDGE │ │ VERLAUF B │ │
│ │ Säule 1 │ │ [Robot] │ │ Säule 3 │ │
│ │ │ │ │ │ │ │
│ │ [Messages] │ │ Winner: A │ │ [Messages] │ │
│ │ │ │ Conf: 85% │ │ │ │
│ │ │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────┤
│ [JSON Preview Expandable] │
│ [Chain-of-Thought Stepper Expandable] │
├─────────────────────────────────────────────────────────────────┤
│ [Historie Table] - Klickbar für Details │
└─────────────────────────────────────────────────────────────────┘
7. Session-Management¶
7.1 Session-Lifecycle¶
┌─────────┐
│ CREATED │
└────┬────┘
│ configure()
▼
┌─────────┐
┌───────│ QUEUED │───────┐
│ └────┬────┘ │
│ │ start() │
│ ▼ │
│ ┌─────────┐ │
│ ┌───│ RUNNING │───┐ │
│ │ └────┬────┘ │ │
│ │ │ │ │
pause() │ │ │ │ │ error
│ │ ▼ │ │
│ │ ┌─────────┐ │ │
│ └──▶│ PAUSED │◀──┘ │
│ └────┬────┘ │
│ │ resume() │
│ │ │
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌────────┐
│ PAUSED │ │COMPLETED │ │ FAILED │
└─────────┘ └──────────┘ └────────┘
7.2 Persistente Sessions¶
- Sessions laufen im Backend unabhängig vom Browser
- User kann Browser schließen und später wieder verbinden
- Socket.IO Room-Join ermöglicht Live-View
- Mehrere User können dieselbe Session beobachten (mit Permission)
8. Queue-System¶
8.1 Queue-Architektur¶
# Queue-Generierung
def generate_comparisons(session_id, pillars, mode, samples, position_swap):
"""
Generiert Queue von Vergleichen.
Beispiel für pillars=[1,3,5], samples=5, position_swap=True:
Paare: 1v3, 1v5, 3v5 = 3 Paare
Pro Paar: 5 Samples = 15 Vergleiche
Mit Swap: 30 Vergleiche total
Queue:
[1v3_sample1_pos1, 1v3_sample1_pos2,
1v3_sample2_pos1, 1v3_sample2_pos2, ...]
"""
comparisons = []
queue_pos = 0
# Alle Paare generieren
for i, pa in enumerate(pillars):
for pb in pillars[i+1:]:
# Threads für beide Säulen holen
threads_a = get_pillar_threads(pa, limit=samples)
threads_b = get_pillar_threads(pb, limit=samples)
# Paare bilden
for ta, tb in zip(threads_a, threads_b):
# Position 1: A | B
comparisons.append(JudgeComparison(
session_id=session_id,
thread_a_id=ta.id,
thread_b_id=tb.id,
pillar_a=pa,
pillar_b=pb,
position_order=1,
queue_position=queue_pos
))
queue_pos += 1
# Position 2: B | A (Swap)
if position_swap:
comparisons.append(JudgeComparison(
session_id=session_id,
thread_a_id=tb.id,
thread_b_id=ta.id,
pillar_a=pb,
pillar_b=pa,
position_order=2,
queue_position=queue_pos
))
queue_pos += 1
db.session.add_all(comparisons)
db.session.commit()
return comparisons
8.2 Worker-Skalierung¶
Aktuell: Single Worker per Session
Zukünftig möglich: - Redis Queue für Multi-Worker - Celery für verteilte Verarbeitung - Rate-Limiting für API-Anfragen
9. Auswertungs-Dashboard¶
9.1 Metriken¶
| Metrik | Beschreibung | Berechnung |
|---|---|---|
| Win Rate | Gewinnrate einer Säule | wins / (wins + losses + ties) |
| Elo Rating | Relatives Ranking | Elo-Algorithmus |
| Avg Confidence | Durchschnittliche LLM-Konfidenz | mean(confidence) |
| Position Bias | Anteil Flips bei Swap | flips / total_swaps |
| Metric Scores | Durchschnitt pro Kriterium | mean(criterion_score) |
9.2 Visualisierungen¶
- Säulen-Matrix: Heatmap der Win-Rates
- Bar Chart: Gesamtsiege pro Säule
- Radar Chart: Metriken-Profil pro Säule
- Sankey Diagram: Vergleichsfluss
- Confidence Distribution: Histogramm
10. Testplan pro Phase¶
Phase 1 Tests¶
def test_pillar_thread_creation():
"""Teste Säulen-Thread Zuordnung"""
thread = PillarThread(thread_id=1, pillar_number=1, pillar_name="Rollenspiele")
assert thread.pillar_number == 1
def test_judge_session_lifecycle():
"""Teste Session-Status-Übergänge"""
session = JudgeSession(user_id=1, name="Test")
assert session.status == 'created'
Phase 2 Tests¶
def test_pydantic_schema_validation():
"""Teste Schema-Validierung"""
result = JudgeEvaluationResult(
winner="A",
confidence=0.85,
# ...
)
assert result.winner in ["A", "B", "TIE"]
def test_litellm_connection():
"""Teste API-Verbindung"""
service = JudgeService(api_key=os.getenv('LITELLM_API_KEY'))
assert service.client is not None
Phase 3 Tests¶
def test_api_permissions():
"""Teste Permission-Checks"""
# Ohne Permission
response = client.get('/api/judge/sessions')
assert response.status_code == 403
# Mit Permission
response = client.get('/api/judge/sessions', headers=auth_headers)
assert response.status_code == 200
def test_socketio_room_join():
"""Teste Socket-Room-Management"""
socketio_client.emit('judge:join_session', {'session_id': 1})
received = socketio_client.get_received()
assert any(r['name'] == 'judge:joined' for r in received)
Phase 4 Tests¶
def test_worker_queue_processing():
"""Teste Queue-Verarbeitung"""
session = create_test_session()
trigger_judge_worker(session.id)
time.sleep(5)
session.refresh()
assert session.completed_comparisons > 0
def test_position_swap_consistency():
"""Teste Bias-Elimination"""
# Führe beide Swap-Varianten durch
result_ab, result_ba, final = service.evaluate_with_position_swap(...)
# Bei konsistenter Bewertung sollte final nicht TIE sein
# es sei denn beide Evaluationen waren TIE
Phase 5-8 Tests¶
# Frontend-Tests mit Cypress/Playwright
def test_session_creation_ui():
"""E2E: Session erstellen"""
page.goto('/judge/config')
page.click('[data-testid="pillar-1"]')
page.click('[data-testid="pillar-3"]')
page.click('[data-testid="create-session"]')
expect(page).to_have_url(re.compile(r'/judge/session/\d+'))
def test_live_updates():
"""E2E: Live-Updates empfangen"""
page.goto('/judge/session/1')
page.click('[data-testid="start-session"]')
# Warte auf ersten Comparison
expect(page.locator('[data-testid="current-comparison"]')).to_be_visible()
Anhang: Quellen¶
Web-Recherche¶
- Evidently AI - LLM-as-a-Judge Guide
- Cameron Wolfe - Using LLMs for Evaluation
- Eugene Yan - LLM Evaluators
- Instructor - Pairwise LLM Judge
- arXiv - Position Bias in Pairwise Comparisons
- CounselBench - Mental Health Evaluation
- Confident AI - Chatbot Evaluation Metrics
LLARS-Codebase¶
app/db/tables.py- Bestehende Tabellen-Definitionenapp/routes/LLMComparisonRoutes.py- Comparison-Infrastrukturapp/single_message_evaluation.py- SingleEvaluator-Patternapp/llm/litellm_client.py- LiteLLM-Integrationllars-frontend/src/components/HistoryGenerator/- Rating-UI-Patterns