LLM-as-Evaluator: Technisches Konzept¶
Version: 1.0 | Stand: 14. Januar 2026 Fokus: Constrained Decoding, Transparenz, anpassbare Prompts
1. Übersicht: Szenario-Typen und ihre Datenformate¶
1.1 Aktuelle Evaluation-Typen¶
| Typ | Menschliche Eingabe | LLM-Ausgabe | Transparenz-Bedarf |
|---|---|---|---|
| Ranking | Drag&Drop in 4 Buckets | {"gut": [...], "mittel": [...], ...} |
Bucket-Zuordnung + Reasoning |
| Rating | 1-5 Sterne pro Feature | {"ratings": [{"feature_id": X, "rating": Y}]} |
Score + Reasoning pro Feature |
| Authenticity | real/fake Button + Confidence | {"vote": "real", "confidence": 3} |
Vote + Confidence + Reasoning |
| Mail Rating | 1-5 Sterne für Thread | {"rating": 4, "reasoning": "..."} |
Gesamtscore + Reasoning |
| Comparison | A/B/Tie Auswahl | {"winner": "A", "confidence": 4} |
Winner + Reasoning |
| Text Classification | Label-Auswahl | {"label": "X", "confidence": 3} |
Label + Reasoning |
1.2 Was fehlt für vollständige Transparenz¶
| Typ | Aktuell | Benötigt |
|---|---|---|
| Ranking | Nur Bucket-Zuordnung | + Reasoning pro Bucket, + Gesamtbegründung |
| Rating | Nur Scores | + Reasoning pro Feature |
| Authenticity | Vote + Confidence | + Detailliertes Reasoning |
| Mail Rating | Rating + Reasoning | Vollständig |
| Comparison | Winner + Reasoning | + Kriterien-basierte Scores |
2. Erweiterte Pydantic-Schemas für Constrained Decoding¶
2.1 Base-Schema für alle Evaluation-Typen¶
# app/schemas/evaluation_schemas.py
from pydantic import BaseModel, Field
from typing import Literal, List, Optional, Dict, Any
from enum import Enum
class EvaluatorMeta(BaseModel):
"""Metadaten für jede LLM-Evaluation."""
processing_time_ms: Optional[int] = Field(None, description="Verarbeitungszeit")
model_version: Optional[str] = Field(None, description="Modellversion")
prompt_version: Optional[str] = Field(None, description="Prompt-Version für Reproduzierbarkeit")
class BaseEvaluationResult(BaseModel):
"""Basis-Schema für alle Evaluations-Ergebnisse."""
reasoning: str = Field(
min_length=20,
max_length=2000,
description="Ausführliche Begründung der Entscheidung"
)
confidence: float = Field(
ge=0.0, le=1.0,
description="Konfidenz der Bewertung (0=unsicher, 1=sehr sicher)"
)
meta: Optional[EvaluatorMeta] = None
2.2 Ranking-Schema (Erweitert)¶
class BucketReasoning(BaseModel):
"""Begründung für die Zuordnung zu einem Bucket."""
feature_ids: List[int] = Field(description="Feature-IDs in diesem Bucket")
reasoning: str = Field(
min_length=10,
max_length=500,
description="Warum wurden diese Features diesem Bucket zugeordnet?"
)
class RankingEvaluationResult(BaseEvaluationResult):
"""
Strukturiertes Ranking-Ergebnis für LLM-Evaluatoren.
Features werden in 4 Qualitäts-Buckets eingeteilt:
- gut: Hochwertige Features
- mittel: Akzeptable Features
- schlecht: Minderwertige Features
- neutral: Nicht eindeutig zuordenbar
"""
buckets: Dict[Literal["gut", "mittel", "schlecht", "neutral"], BucketReasoning] = Field(
description="Bucket-Zuordnungen mit Begründungen"
)
overall_assessment: str = Field(
min_length=50,
max_length=500,
description="Gesamteinschätzung der Feature-Qualität für diesen Thread"
)
class Config:
json_schema_extra = {
"example": {
"buckets": {
"gut": {
"feature_ids": [1, 3],
"reasoning": "Diese Features zeigen präzise Analyse und sind gut formuliert."
},
"mittel": {
"feature_ids": [2, 5],
"reasoning": "Brauchbar, aber mit Verbesserungspotential."
},
"schlecht": {
"feature_ids": [4],
"reasoning": "Oberflächliche Analyse ohne Mehrwert."
},
"neutral": {
"feature_ids": [],
"reasoning": "Keine Features in dieser Kategorie."
}
},
"overall_assessment": "Die Mehrzahl der Features ist von guter Qualität...",
"reasoning": "Die Bewertung basiert auf Präzision, Relevanz und...",
"confidence": 0.85
}
}
2.3 Rating-Schema (Erweitert)¶
class FeatureRating(BaseModel):
"""Bewertung eines einzelnen Features."""
feature_id: int
rating: int = Field(ge=1, le=5, description="Bewertung 1-5")
reasoning: str = Field(
min_length=10,
max_length=300,
description="Begründung für diese Bewertung"
)
strengths: Optional[List[str]] = Field(None, description="Stärken des Features")
weaknesses: Optional[List[str]] = Field(None, description="Schwächen des Features")
class RatingEvaluationResult(BaseEvaluationResult):
"""
Strukturiertes Rating-Ergebnis für LLM-Evaluatoren.
Jedes Feature wird auf einer Skala von 1-5 bewertet.
"""
ratings: List[FeatureRating] = Field(
description="Bewertungen pro Feature"
)
average_rating: float = Field(
ge=1.0, le=5.0,
description="Durchschnittliche Bewertung"
)
thread_summary: str = Field(
min_length=30,
max_length=500,
description="Zusammenfassung der Konversation als Kontext"
)
class Config:
json_schema_extra = {
"example": {
"ratings": [
{
"feature_id": 1,
"rating": 4,
"reasoning": "Präzise Analyse der Kundenbedürfnisse.",
"strengths": ["Klar formuliert", "Relevant"],
"weaknesses": ["Könnte detaillierter sein"]
}
],
"average_rating": 3.8,
"thread_summary": "Beratungsgespräch über Kreditantrag...",
"reasoning": "Die Features zeigen insgesamt gute Qualität...",
"confidence": 0.8
}
}
2.4 Authenticity-Schema (Erweitert)¶
class AuthenticityIndicator(BaseModel):
"""Ein einzelner Indikator für Echtheit/Künstlichkeit."""
indicator: str = Field(description="Beschreibung des Indikators")
supports: Literal["real", "fake"] = Field(description="Unterstützt welche Hypothese?")
weight: float = Field(ge=0.0, le=1.0, description="Gewichtung dieses Indikators")
class AuthenticityEvaluationResult(BaseEvaluationResult):
"""
Strukturiertes Authenticity-Ergebnis für LLM-Evaluatoren.
Bewertet ob eine Konversation echt (menschlich) oder
künstlich (KI-generiert) ist.
"""
vote: Literal["real", "fake"] = Field(
description="Finale Entscheidung: real oder fake"
)
confidence_score: int = Field(
ge=1, le=5,
description="Konfidenz 1-5 (für Kompatibilität mit UI)"
)
indicators: List[AuthenticityIndicator] = Field(
min_items=2,
max_items=10,
description="Indikatoren die zur Entscheidung geführt haben"
)
linguistic_analysis: str = Field(
min_length=30,
max_length=500,
description="Sprachliche Analyse der Konversation"
)
behavioral_analysis: str = Field(
min_length=30,
max_length=500,
description="Verhaltensanalyse der Gesprächspartner"
)
class Config:
json_schema_extra = {
"example": {
"vote": "fake",
"confidence_score": 4,
"indicators": [
{
"indicator": "Unnatürlich konsistenter Sprachstil",
"supports": "fake",
"weight": 0.8
},
{
"indicator": "Fehlende Tippfehler oder Korrekturen",
"supports": "fake",
"weight": 0.6
}
],
"linguistic_analysis": "Der Text zeigt ungewöhnlich perfekte Grammatik...",
"behavioral_analysis": "Das Verhalten des Klienten wirkt zu linear...",
"reasoning": "Mehrere Indikatoren deuten auf KI-Generierung hin...",
"confidence": 0.75
}
}
2.5 Mail-Rating-Schema (Erweitert)¶
class QualityCriterion(BaseModel):
"""Bewertung eines Qualitätskriteriums."""
name: str
score: int = Field(ge=1, le=5)
reasoning: str = Field(min_length=10, max_length=200)
class MailRatingEvaluationResult(BaseEvaluationResult):
"""
Strukturiertes Mail-Rating-Ergebnis für LLM-Evaluatoren.
Bewertet die Gesamtqualität einer E-Mail-Beratungskonversation.
"""
overall_rating: int = Field(
ge=1, le=5,
description="Gesamtbewertung 1-5"
)
criteria: List[QualityCriterion] = Field(
min_items=3,
max_items=6,
description="Bewertung nach einzelnen Kriterien"
)
strengths: List[str] = Field(
min_items=1,
max_items=5,
description="Stärken der Beratung"
)
areas_for_improvement: List[str] = Field(
min_items=0,
max_items=5,
description="Verbesserungspotential"
)
summary: str = Field(
min_length=50,
max_length=500,
description="Zusammenfassende Bewertung"
)
class Config:
json_schema_extra = {
"example": {
"overall_rating": 4,
"criteria": [
{"name": "Empathie", "score": 5, "reasoning": "Sehr einfühlsam..."},
{"name": "Fachlichkeit", "score": 4, "reasoning": "Kompetent..."},
{"name": "Verständlichkeit", "score": 4, "reasoning": "Klar..."},
{"name": "Hilfsbereitschaft", "score": 4, "reasoning": "Engagiert..."},
{"name": "Lösungsorientierung", "score": 3, "reasoning": "Könnte konkreter..."}
],
"strengths": ["Empathische Kommunikation", "Fachliche Kompetenz"],
"areas_for_improvement": ["Konkretere Handlungsempfehlungen"],
"summary": "Eine insgesamt gute Beratung mit Stärken in...",
"reasoning": "Die Bewertung basiert auf der Analyse von...",
"confidence": 0.85
}
}
2.6 Text-Classification-Schema (Generisch)¶
class ClassificationEvaluationResult(BaseEvaluationResult):
"""
Strukturiertes Classification-Ergebnis für LLM-Evaluatoren.
Klassifiziert Text in vordefinierte Kategorien.
"""
label: str = Field(description="Gewähltes Label/Kategorie")
confidence_score: int = Field(
ge=1, le=5,
description="Konfidenz 1-5"
)
alternative_labels: List[Dict[str, Any]] = Field(
default=[],
description="Alternative Labels mit Wahrscheinlichkeiten"
)
key_phrases: List[str] = Field(
min_items=1,
max_items=5,
description="Schlüsselphrasen die zur Klassifikation führten"
)
class Config:
json_schema_extra = {
"example": {
"label": "complaint",
"confidence_score": 4,
"alternative_labels": [
{"label": "inquiry", "probability": 0.2}
],
"key_phrases": ["unzufrieden", "Beschwerde", "inakzeptabel"],
"reasoning": "Der Text enthält eindeutige Beschwerdemerkmale...",
"confidence": 0.8
}
}
3. Constrained Decoding Implementation¶
3.1 Schema-basierte LLM-Anfrage¶
# app/services/llm/constrained_llm_service.py
from pydantic import BaseModel
from typing import Type, TypeVar, Optional
import json
T = TypeVar('T', bound=BaseModel)
class ConstrainedLLMService:
"""
Service für strukturierte LLM-Ausgaben mit Constrained Decoding.
Unterstützt:
- OpenAI JSON Mode + Schema-Validierung
- Anthropic Tool Use für strukturierte Ausgaben
- vLLM/Outlines für echtes Constrained Decoding (lokal)
"""
@staticmethod
def request_structured(
client,
model_id: str,
prompt: str,
response_schema: Type[T],
system_prompt: Optional[str] = None,
max_retries: int = 2
) -> T:
"""
Fordert strukturierte Ausgabe vom LLM an.
Args:
client: LLM Client (OpenAI, Anthropic, etc.)
model_id: Modell-ID
prompt: User-Prompt
response_schema: Pydantic-Schema für die Antwort
system_prompt: Optional System-Prompt
max_retries: Anzahl Wiederholungsversuche
Returns:
Validiertes Pydantic-Objekt
"""
# JSON Schema aus Pydantic extrahieren
json_schema = response_schema.model_json_schema()
# Schema in Prompt einbetten
schema_instruction = f"""
Du MUSST deine Antwort EXAKT im folgenden JSON-Schema formatieren:
```json
{json.dumps(json_schema, indent=2, ensure_ascii=False)}
Antworte NUR mit validem JSON. Keine Erklärungen außerhalb des JSON. """
full_system = (system_prompt or "") + "\n\n" + schema_instruction
for attempt in range(max_retries + 1):
try:
response = client.chat.completions.create(
model=model_id,
messages=[
{"role": "system", "content": full_system},
{"role": "user", "content": prompt}
],
temperature=0.2,
max_tokens=2000,
extra_body={"response_format": {"type": "json_object"}}
)
content = response.choices[0].message.content
parsed = json.loads(content)
# Pydantic-Validierung
return response_schema.model_validate(parsed)
except Exception as e:
if attempt == max_retries:
raise
# Retry mit Fehler-Feedback
prompt += f"\n\nFehler bei vorherigem Versuch: {str(e)}. Bitte korrigieren."
raise ValueError("Konnte keine valide Antwort erhalten")
@staticmethod
def get_schema_for_task_type(task_type: str) -> Type[BaseModel]:
"""Gibt das passende Schema für einen Task-Typ zurück."""
schema_map = {
"ranking": RankingEvaluationResult,
"rating": RatingEvaluationResult,
"labeling": ClassificationEvaluationResult, # includes authenticity
"comparison": ComparisonEvaluationResult,
}
return schema_map.get(task_type, BaseEvaluationResult)
```
3.2 Integration in LLMAITaskRunner¶
```python
Erweiterung von llm_ai_task_runner.py¶
@staticmethod def _run_ranking_structured( model_id: str, thread_ids: Iterable[int], scenario_id: int, prompt_template: Optional[str] = None ) -> None: """Ranking mit strukturierter Ausgabe und Constrained Decoding.""" client = LLMClientFactory.get_client_for_model(model_id) for thread_id in thread_ids: # ... existing checks ... features = Feature.query.filter_by(thread_id=thread_id).all() # Prompt aus Template oder Default prompt = prompt_template or DEFAULT_RANKING_PROMPT prompt = prompt.format( features=format_features(features), buckets="gut, mittel, schlecht, neutral" ) # Strukturierte Anfrage result = ConstrainedLLMService.request_structured( client=client, model_id=model_id, prompt=prompt, response_schema=RankingEvaluationResult, system_prompt=RANKING_SYSTEM_PROMPT ) # Speichern mit vollem Reasoning LLMTaskResult.create_or_update( scenario_id=scenario_id, thread_id=thread_id, model_id=model_id, task_type="ranking", payload_json=result.model_dump(), raw_response=None, # Nicht mehr nötig - alles im payload error=None ) ```
4. Anpassbare Prompt-Templates¶
4.1 Prompt-Template-Modell¶
# app/db/models/prompt_template.py
class PromptTemplate(db.Model):
"""
Speichert anpassbare Prompt-Templates für LLM-Evaluatoren.
"""
__tablename__ = 'prompt_templates'
id = db.Column(db.Integer, primary_key=True)
# Identifikation
name = db.Column(db.String(100), nullable=False)
task_type = db.Column(db.String(50), nullable=False) # ranking, rating, etc.
version = db.Column(db.String(20), default="1.0")
# Prompts
system_prompt = db.Column(db.Text, nullable=False)
user_prompt_template = db.Column(db.Text, nullable=False)
# Konfiguration
variables = db.Column(db.JSON, default=list) # ["features", "thread_content", ...]
output_schema_version = db.Column(db.String(20), default="1.0")
# Metadaten
is_default = db.Column(db.Boolean, default=False)
is_active = db.Column(db.Boolean, default=True)
created_by = db.Column(db.String(100))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow)
# Optional: Scenario-spezifisch
scenario_id = db.Column(db.Integer, db.ForeignKey('rating_scenarios.id'), nullable=True)
4.2 Default-Prompt-Templates¶
# app/services/llm/default_prompts.py
DEFAULT_PROMPTS = {
"ranking": {
"system": """Du bist ein erfahrener Evaluator für die Qualitätsbewertung von LLM-generierten Features.
Deine Aufgabe ist es, Features einer E-Mail-Beratungskonversation in Qualitätskategorien einzuordnen.
Bewertungskriterien:
- Präzision: Wie genau beschreibt das Feature einen relevanten Aspekt?
- Relevanz: Wie wichtig ist das Feature für die Beratungsqualität?
- Formulierung: Wie klar und verständlich ist das Feature formuliert?
- Mehrwert: Bietet das Feature neue Erkenntnisse?
Du MUSST deine Antwort im vorgegebenen JSON-Schema formatieren.""",
"user": """Ordne die folgenden Features den Qualitäts-Buckets zu.
**Buckets:**
- gut: Hochwertige Features (präzise, relevant, gut formuliert)
- mittel: Akzeptable Features (brauchbar, aber verbesserungsfähig)
- schlecht: Minderwertige Features (unpräzise, irrelevant, schlecht formuliert)
- neutral: Nicht eindeutig zuordenbar
**Konversationskontext:**
{thread_content}
**Features zur Bewertung:**
{features}
Begründe jede Bucket-Zuordnung und gib eine Gesamteinschätzung."""
},
"rating": {
"system": """Du bist ein Experte für die Bewertung von Feature-Qualität.
Bewerte jedes Feature auf einer Skala von 1 bis 5:
- 5: Exzellent - Präzise, relevant, wertvoll
- 4: Gut - Überdurchschnittlich, minor Verbesserungspotential
- 3: Durchschnittlich - Akzeptabel, aber mit Schwächen
- 2: Unterdurchschnittlich - Mehrere Probleme
- 1: Mangelhaft - Kaum brauchbar
Begründe jede Bewertung konkret.""",
"user": """Bewerte die Qualität der folgenden Features.
**Konversation:**
{thread_content}
**Features:**
{features}
Gib für jedes Feature eine Bewertung (1-5) mit Begründung."""
},
"authenticity": {
"system": """Du bist ein Experte für die Erkennung von KI-generierten Texten.
Analysiere Konversationen auf Hinweise für menschliche oder künstliche Erstellung.
Achte auf:
- Sprachliche Natürlichkeit (Tippfehler, Korrekturen, umgangssprachliche Elemente)
- Emotionale Authentizität (Variabilität, Konsistenz mit Situation)
- Verhaltensrealismus (Nachfragen, Themenwechsel, Ablenkungen)
- Konversationsdynamik (Timing, Antwortlängen, Turn-Taking)""",
"user": """Analysiere die folgende Konversation.
Entscheide:
- "real": Wahrscheinlich von Menschen geschrieben
- "fake": Wahrscheinlich KI-generiert
**Konversation:**
{thread_content}
Begründe deine Entscheidung mit konkreten Indikatoren."""
},
"mail_rating": {
"system": """Du bist ein Experte für die Bewertung von E-Mail-Beratungsqualität.
Bewertungskriterien:
1. Empathie: Einfühlungsvermögen und emotionale Unterstützung
2. Fachlichkeit: Kompetenz und korrektes Wissen
3. Verständlichkeit: Klarheit und Zugänglichkeit der Kommunikation
4. Hilfsbereitschaft: Engagement und Serviceorientierung
5. Lösungsorientierung: Konkrete Handlungsempfehlungen
Gesamtbewertung 1-5:
- 5: Exzellente Beratung
- 4: Gute Beratung
- 3: Akzeptable Beratung
- 2: Verbesserungswürdige Beratung
- 1: Mangelhafte Beratung""",
"user": """Bewerte die Qualität dieser E-Mail-Beratung.
**Betreff:** {subject}
**Konversation:**
{thread_content}
Gib Scores für jedes Kriterium und eine Gesamtbewertung."""
}
}
4.3 Scenario-Level Prompt-Override¶
# In RatingScenarios.config_json
{
"function_type_id": 1,
"llm_evaluators": ["gpt-4o", "claude-3-5-sonnet"],
"distribution_mode": "round_robin",
# NEU: Custom Prompts für dieses Szenario
"prompt_config": {
"template_id": 42, # Referenz auf PromptTemplate
# ODER inline:
"system_prompt_override": "Du bist ein Berater-Evaluator für Finanzberatungen...",
"user_prompt_override": null, # Verwendet Default
"additional_instructions": "Achte besonders auf regulatorische Compliance."
}
}
5. Transparenz-Explorer: Darstellung pro Szenario-Typ¶
5.1 ResponseCard-Komponente (Universal)¶
<!-- components/Evaluation/ResponseCard.vue -->
<template>
<v-card class="response-card" :class="{ 'response-card--llm': isLlm }">
<!-- Header -->
<div class="response-header">
<div class="evaluator-info">
<v-avatar :color="isLlm ? 'accent' : 'primary'" size="36">
<v-icon v-if="isLlm">mdi-robot</v-icon>
<span v-else>{{ evaluatorInitial }}</span>
</v-avatar>
<div class="evaluator-details">
<span class="evaluator-name">{{ evaluatorName }}</span>
<span class="evaluator-type">
<LTag :variant="isLlm ? 'info' : 'primary'" size="sm">
{{ isLlm ? 'LLM' : 'Mensch' }}
</LTag>
</span>
</div>
</div>
<span class="response-time">{{ formattedTime }}</span>
</div>
<!-- Response Content - Dynamic based on type -->
<div class="response-content">
<!-- RANKING -->
<template v-if="taskType === 'ranking'">
<RankingResponseDisplay :data="responseData" />
</template>
<!-- RATING -->
<template v-else-if="taskType === 'rating'">
<RatingResponseDisplay :data="responseData" :features="features" />
</template>
<!-- AUTHENTICITY -->
<template v-else-if="taskType === 'authenticity'">
<AuthenticityResponseDisplay :data="responseData" />
</template>
<!-- MAIL RATING -->
<template v-else-if="taskType === 'mail_rating'">
<MailRatingResponseDisplay :data="responseData" />
</template>
<!-- COMPARISON -->
<template v-else-if="taskType === 'comparison'">
<ComparisonResponseDisplay :data="responseData" />
</template>
<!-- CLASSIFICATION -->
<template v-else-if="taskType === 'text_classification'">
<ClassificationResponseDisplay :data="responseData" />
</template>
</div>
<!-- LLM Reasoning Section (expandable) -->
<v-expansion-panels v-if="isLlm && hasReasoning" variant="accordion">
<v-expansion-panel>
<v-expansion-panel-title>
<v-icon class="mr-2">mdi-brain</v-icon>
LLM Reasoning
</v-expansion-panel-title>
<v-expansion-panel-text>
<div class="reasoning-content">
<p>{{ responseData.reasoning }}</p>
</div>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<!-- Confidence Indicator -->
<div v-if="hasConfidence" class="confidence-bar">
<span class="confidence-label">Konfidenz:</span>
<v-progress-linear
:model-value="confidencePercent"
:color="confidenceColor"
height="8"
rounded
/>
<span class="confidence-value">{{ confidencePercent }}%</span>
</div>
</v-card>
</template>
5.2 Ranking-Response-Display¶
<!-- components/Evaluation/displays/RankingResponseDisplay.vue -->
<template>
<div class="ranking-display">
<!-- Bucket-Visualization -->
<div class="buckets-grid">
<div
v-for="(bucket, key) in orderedBuckets"
:key="key"
class="bucket"
:class="`bucket--${key}`"
>
<div class="bucket-header">
<span class="bucket-icon">{{ bucketIcons[key] }}</span>
<span class="bucket-label">{{ bucketLabels[key] }}</span>
<span class="bucket-count">{{ bucket.feature_ids?.length || 0 }}</span>
</div>
<div class="bucket-features">
<div
v-for="featureId in bucket.feature_ids"
:key="featureId"
class="feature-chip"
>
#{{ featureId }}
</div>
</div>
<!-- Reasoning for this bucket -->
<div v-if="bucket.reasoning" class="bucket-reasoning">
<v-icon size="14" class="mr-1">mdi-comment-text</v-icon>
{{ bucket.reasoning }}
</div>
</div>
</div>
<!-- Overall Assessment -->
<div v-if="data.overall_assessment" class="overall-assessment">
<h4>Gesamteinschätzung</h4>
<p>{{ data.overall_assessment }}</p>
</div>
</div>
</template>
<script setup>
const bucketIcons = {
gut: '✅',
mittel: '➖',
schlecht: '❌',
neutral: '⚪'
}
const bucketLabels = {
gut: 'Gut',
mittel: 'Mittel',
schlecht: 'Schlecht',
neutral: 'Neutral'
}
</script>
<style scoped>
.buckets-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.bucket--gut { border-left: 3px solid var(--llars-success); }
.bucket--mittel { border-left: 3px solid var(--llars-warning); }
.bucket--schlecht { border-left: 3px solid var(--llars-danger); }
.bucket--neutral { border-left: 3px solid var(--llars-secondary); }
</style>
5.3 Authenticity-Response-Display¶
<!-- components/Evaluation/displays/AuthenticityResponseDisplay.vue -->
<template>
<div class="authenticity-display">
<!-- Vote Display -->
<div class="vote-display" :class="`vote--${data.vote}`">
<v-icon size="48">
{{ data.vote === 'real' ? 'mdi-account-check' : 'mdi-robot-angry' }}
</v-icon>
<span class="vote-label">
{{ data.vote === 'real' ? 'Echt (Mensch)' : 'Fake (KI)' }}
</span>
</div>
<!-- Confidence -->
<div class="confidence-stars">
<v-rating
:model-value="data.confidence_score || data.confidence * 5"
readonly
density="compact"
color="warning"
/>
<span class="confidence-text">Konfidenz</span>
</div>
<!-- Indicators (for LLM) -->
<div v-if="data.indicators?.length" class="indicators-section">
<h4>Erkennungsmerkmale</h4>
<div class="indicators-list">
<div
v-for="(indicator, idx) in data.indicators"
:key="idx"
class="indicator"
:class="`indicator--${indicator.supports}`"
>
<span class="indicator-text">{{ indicator.indicator }}</span>
<LTag :variant="indicator.supports === 'real' ? 'success' : 'danger'" size="sm">
{{ indicator.supports === 'real' ? 'Echt' : 'Fake' }}
</LTag>
<span class="indicator-weight">{{ Math.round(indicator.weight * 100) }}%</span>
</div>
</div>
</div>
<!-- Analysis Sections -->
<div v-if="data.linguistic_analysis" class="analysis-section">
<h4>Sprachliche Analyse</h4>
<p>{{ data.linguistic_analysis }}</p>
</div>
<div v-if="data.behavioral_analysis" class="analysis-section">
<h4>Verhaltensanalyse</h4>
<p>{{ data.behavioral_analysis }}</p>
</div>
</div>
</template>
<style scoped>
.vote--real {
background: rgba(var(--v-theme-success), 0.1);
color: rgb(var(--v-theme-success));
}
.vote--fake {
background: rgba(var(--v-theme-error), 0.1);
color: rgb(var(--v-theme-error));
}
</style>
5.4 Agreement-Comparison-View¶
<!-- components/Evaluation/AgreementComparisonView.vue -->
<template>
<div class="agreement-view">
<h3>Vergleich: Mensch vs. LLM</h3>
<!-- Agreement Matrix -->
<div class="agreement-matrix">
<table>
<thead>
<tr>
<th></th>
<th v-for="evaluator in evaluators" :key="evaluator.id">
<span :class="{ 'llm-header': evaluator.is_llm }">
{{ evaluator.name }}
<v-icon v-if="evaluator.is_llm" size="14">mdi-robot</v-icon>
</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="evaluator in evaluators" :key="evaluator.id">
<td class="row-header">{{ evaluator.name }}</td>
<td
v-for="other in evaluators"
:key="other.id"
:class="getAgreementClass(evaluator.id, other.id)"
>
{{ getAgreementScore(evaluator.id, other.id) }}%
</td>
</tr>
</tbody>
</table>
</div>
<!-- Summary Stats -->
<div class="summary-stats">
<div class="stat">
<span class="stat-label">Mensch-Mensch Agreement</span>
<span class="stat-value">{{ humanHumanAgreement }}%</span>
</div>
<div class="stat">
<span class="stat-label">LLM-Mensch Agreement</span>
<span class="stat-value">{{ llmHumanAgreement }}%</span>
</div>
<div class="stat">
<span class="stat-label">LLM-LLM Agreement</span>
<span class="stat-value">{{ llmLlmAgreement }}%</span>
</div>
</div>
<!-- Disagreement Highlights -->
<div v-if="disagreements.length" class="disagreements">
<h4>Auffällige Unterschiede</h4>
<div v-for="d in disagreements" :key="d.id" class="disagreement-item">
<span class="disagreement-aspect">{{ d.aspect }}</span>
<span class="disagreement-detail">{{ d.detail }}</span>
</div>
</div>
</div>
</template>
6. Live-Monitoring für LLM-Evaluatoren¶
6.1 Socket.IO Events (Erweiterung)¶
# app/socketio_handlers/events_llm_evaluation.py
@socketio.on('scenario:subscribe')
def handle_scenario_subscribe(data):
"""Subscribe to scenario updates."""
scenario_id = data.get('scenario_id')
if scenario_id:
join_room(f'scenario_{scenario_id}')
emit('scenario:subscribed', {'scenario_id': scenario_id})
def emit_llm_evaluation_started(scenario_id: int, model_id: str, thread_id: int):
"""Emit when LLM starts evaluating a thread."""
socketio.emit(
'scenario:llm_started',
{
'scenario_id': scenario_id,
'model_id': model_id,
'thread_id': thread_id,
'timestamp': datetime.utcnow().isoformat()
},
room=f'scenario_{scenario_id}'
)
def emit_llm_evaluation_completed(
scenario_id: int,
model_id: str,
thread_id: int,
result_summary: dict
):
"""Emit when LLM completes evaluating a thread."""
socketio.emit(
'scenario:llm_completed',
{
'scenario_id': scenario_id,
'model_id': model_id,
'thread_id': thread_id,
'result_summary': result_summary, # Kurze Zusammenfassung
'timestamp': datetime.utcnow().isoformat()
},
room=f'scenario_{scenario_id}'
)
def emit_llm_progress(scenario_id: int, model_id: str, progress: dict):
"""Emit LLM progress update."""
socketio.emit(
'scenario:llm_progress',
{
'scenario_id': scenario_id,
'model_id': model_id,
'completed': progress['completed'],
'total': progress['total'],
'current_thread': progress.get('current_thread'),
'avg_time_ms': progress.get('avg_time_ms'),
'timestamp': datetime.utcnow().isoformat()
},
room=f'scenario_{scenario_id}'
)
6.2 Integration in LLMAITaskRunner¶
# Erweiterung der _run_* Methoden
@staticmethod
def _run_ranking_with_events(model_id: str, thread_ids: List[int], scenario_id: int):
"""Ranking mit Live-Events."""
from socketio_handlers.events_llm_evaluation import (
emit_llm_evaluation_started,
emit_llm_evaluation_completed,
emit_llm_progress
)
total = len(thread_ids)
completed = 0
start_times = []
for thread_id in thread_ids:
# Event: Started
emit_llm_evaluation_started(scenario_id, model_id, thread_id)
thread_start = time.time()
try:
# ... existing evaluation logic ...
result = perform_ranking_evaluation(...)
# Event: Completed
emit_llm_evaluation_completed(
scenario_id, model_id, thread_id,
result_summary={
'buckets': {k: len(v) for k, v in result['buckets'].items()},
'confidence': result.get('confidence', 0)
}
)
except Exception as e:
# ... error handling ...
pass
# Progress Update
completed += 1
elapsed = (time.time() - thread_start) * 1000
start_times.append(elapsed)
emit_llm_progress(
scenario_id, model_id,
{
'completed': completed,
'total': total,
'current_thread': thread_id,
'avg_time_ms': sum(start_times) / len(start_times)
}
)
7. Datei-Struktur (Neue/Geänderte Dateien)¶
app/
├── schemas/
│ └── evaluation_schemas.py # NEU: Alle Pydantic-Schemas
├── services/
│ └── llm/
│ ├── llm_ai_task_runner.py # ERWEITERT: Constrained Decoding
│ ├── constrained_llm_service.py # NEU: Schema-basierte Anfragen
│ └── default_prompts.py # NEU: Default Prompt-Templates
├── db/
│ └── models/
│ └── prompt_template.py # NEU: PromptTemplate Model
├── socketio_handlers/
│ └── events_llm_evaluation.py # NEU: LLM-spezifische Events
llars-frontend/src/
├── components/
│ └── Evaluation/
│ ├── ResponseCard.vue # NEU: Universal Response Card
│ ├── AgreementComparisonView.vue # NEU: Agreement-Visualisierung
│ └── displays/
│ ├── RankingResponseDisplay.vue
│ ├── RatingResponseDisplay.vue
│ ├── AuthenticityResponseDisplay.vue
│ ├── MailRatingResponseDisplay.vue
│ ├── ComparisonResponseDisplay.vue
│ └── ClassificationResponseDisplay.vue
└── composables/
└── useLlmEvaluationStream.js # NEU: Live-Updates Composable
8. Zusammenfassung¶
Was jetzt möglich ist:¶
| Feature | Vorher | Nachher |
|---|---|---|
| LLM-Ausgabeformat | Einfaches JSON | Pydantic-validiert + Reasoning |
| Prompts | Hartcodiert | Pro Szenario anpassbar |
| Live-Monitoring | Nur Fortschritt | Thread-Level Events |
| Transparenz | Nur Ergebnis | Vollständiges Reasoning |
| Constrained Decoding | json_object Mode | Schema-Validierung |
IJCAI Demo Highlights:¶
- LLM-as-Evaluator arbeitet parallel zu Menschen
- Live-Feed zeigt LLM-Aktivität in Echtzeit
- Transparenz: Jedes LLM-Reasoning sichtbar
- Agreement-Analyse: Mensch vs. LLM Vergleich
- Anpassbare Prompts: Für verschiedene Domänen
Stand: 14. Januar 2026