Use-Case: Sentiment-Drift

Das Problem: Unsere herkömmlichen Keyword-Tracker und ML-Dashboards für Kundenrezensionen leuchteten sattgrün und zeigten 92 % positive Sentiment-Scores, obgleich die tatsächlichen Churn-Raten um 18 % stiegen. Eine manuelle Stichprobe von 200 Support-Tickets enthüllte die Diskrepanz: B2B-Kunden hatten aufgehört, offenen Ärger zu äußern, und wechselten zu hochgradigem Sarkasmus. Klassiker: „Der Service war heute mal wieder absolut top, herzlichen Dank für nur schlappe 4 Stunden Downtime." – von unserem System als 91 % positiv klassifiziert.

Das strukturelle Versagen klassischer ML-Sentiment-Modelle

Unser bisheriges Modell – ein fine-getuntes DistilBERT auf dem Stanford Sentiment Treebank (SST-2) Datensatz – arbeitet auf Token-Ebene mit einem Bag-of-Words-ähnlichen Aufmerksamkeitsmechanismus. Es bewertet Adjektive und deren direkte Nachbarn, versteht aber keine übergreifende pragmatische Ironie. Die Confusion Matrix unseres Backtests auf 500 manuell gelabelten sarkastischen Tickets zeigte eine False-Negative-Rate von 83 % für Sarkasmus: Das Modell klassifizierte 415 von 500 sarkastischen Tickets als positiv, weil positive Trigger-Wörter wie „top", „super", „perfekt" dominieren, während die negativen Kontextinformationen (Downtime, Ausfall, Wartezeit) niedrigere Attention-Scores erhielten. Das Modell hatte kein Weltmodell – es kannte nicht den Zustand unserer Systeme zum Zeitpunkt der Review.

Sentiment Dashboard

Kontextuelle LLM-Sentiment-Analyse mit dynamischem Incident-Kontext

Die neue Pipeline hat drei Stufen. Stufe 1 (Filter): Das alte DistilBERT-Modell läuft weiter, aber nur als Vorfilter. Tickets mit Confidence-Score > 0.95 für Positiv werden direkt akzeptiert – kein LLM-Aufruf, keine Kosten. Das sind typischerweise eindeutige Felder wie „Danke, alles funktioniert!". Stufe 2 (LLM mit Kontext): Alle anderen Tickets werden an gpt-4o-mini mit einem dynamischen System-Prompt gesendet. Der entscheidende Unterschied: Der Prompt enthält immer einen automatisch generierten Incident-Context-Block. Jedes Mal, wenn unser Monitoring einen P1- oder P2-Incident öffnet, schreibt ein Webhook-Handler eine strukturierte Zusammenfassung in Redis (SETEX incidents:active 7200 ...). Der Sentiment-Classifier liest diesen Block bei jeder Anfrage und injiziert ihn in den Prompt. Stufe 3 (Multi-Label): Das LLM klassifiziert nicht binär, sondern gibt eine von vier Labels zurück: POSITIVE, NEGATIVE_EXPLICIT, NEGATIVE_SARCASM oder AMBIGUOUS, jeweils mit einem Confidence-Float. NEGATIVE_SARCASM triggert automatisch ein Slack-Alert in #customer-churn-watch.

from openai import OpenAI
import redis, json
from enum import Enum
from dataclasses import dataclass

client = OpenAI()
r = redis.Redis(host='localhost', port=6379, db=2)

class SentimentLabel(str, Enum):
    POSITIVE = 'POSITIVE'
    NEGATIVE_EXPLICIT = 'NEGATIVE_EXPLICIT'
    NEGATIVE_SARCASM = 'NEGATIVE_SARCASM'
    AMBIGUOUS = 'AMBIGUOUS'

@dataclass
class SentimentResult:
    label: SentimentLabel
    confidence: float
    reasoning: str

def get_active_incidents() -> list:
    raw = r.get('incidents:active')
    return json.loads(raw) if raw else []

def analyze_sentiment(ticket_text: str, customer_tier: str = 'standard') -> SentimentResult:
    incidents = get_active_incidents()
    incident_block = '\n'.join(f'- {i}' for i in incidents) if incidents else 'No active incidents.'

    system_prompt = (
        'You are a precision sentiment classifier for B2B customer support tickets.\n\n'
        f'ACTIVE SYSTEM INCIDENTS RIGHT NOW:\n{incident_block}\n\n'
        'TASK: Classify the true emotional intent of the customer message.\n'
        'Labels: POSITIVE | NEGATIVE_EXPLICIT | NEGATIVE_SARCASM | AMBIGUOUS\n'
        f'Customer tier: {customer_tier} (enterprise customers use more formal sarcasm)\n\n'
        'Return JSON: {"label": "...", "confidence": 0.0-1.0, "reasoning": "one sentence"}'
    )

    response = client.chat.completions.create(
        model='gpt-4o-mini',
        temperature=0.05,
        response_format={'type': 'json_object'},
        messages=[
            {'role': 'system', 'content': system_prompt},
            {'role': 'user', 'content': ticket_text}
        ]
    )
    result = json.loads(response.choices[0].message.content)
    return SentimentResult(**result)

# Example: During a known 4-hour database outage
result = analyze_sentiment(
    'Klasse, mal wieder eine Stunde auf meine Daten warten. Exzellenter Service.',
    customer_tier='enterprise'
)
# -> SentimentResult(label=NEGATIVE_SARCASM, confidence=0.97,
#    reasoning='Positive words combined with wieder/warten directly reference active DB outage.')

Ergebnis: Confusion Matrix Vorher/Nachher

Nach 30-tägigem Betrieb evaluierten wir erneut auf denselben 500 manuell gelabelten Tickets. Die False-Negative-Rate für Sarkasmus sank von 83 % auf 4.2 %. Precision für NEGATIVE_SARCASM: 0.94. Recall: 0.96. Der Churn-Risk-Score korreliert jetzt mit einer Vorhersagegenauigkeit von 78 % mit tatsächlichen Kündigungen innerhalb der nächsten 14 Tage – ein Frühwarn-System, das zuvor vollständig fehlte. Kosten pro Ticket: ca. 0.002 € (gpt-4o-mini), bei einem Volumen von 800 Tickets täglich also unter 50 € im Monat.