Vom Incident zum Regressionstest: Wie Produktionsfehler in dein Eval-Set wandern

Eine Eval-Suite, die nicht mit den Incidents wächst, schrumpft relativ zur Realität. Wie der Postmortem-zu-Eval-Loop konkret aussieht - von der Trace-ID bis zum Pflichtfeld im Postmortem-Template.

· 34 Min. Lesezeit
Podcast --:--
0:00 / --:--

Eine Eval-Suite, die nicht mit den Incidents wächst, schrumpft relativ zur Realität.

Im Rollout-Post stand am Schluss eine Fußnote: “Produktions-Incidents als Test-Dataset ernstnehmen. Von Post-Mortem zur Eval. Aber das ist ein anderer Post.” Das hier ist dieser Post.

Stell dir die Situation vor: Alle Gates waren grün. Pre-Merge-Eval grün. Shadow grün. Canary grün. Zwei Wochen nach 100%-Rollout meldet ein User: “Mein Account ist weg, ich wollte doch nur die Marketing-Mails abbestellen.” Im Trace steht es klar: User schreibt “Bitte deaktiviert die Marketing-Mails an meine Adresse”, der Agent ruft direkt delete_account(user_id=...) auf - statt unsubscribe_marketing(email=...). Der Tool-Call läuft sauber durch, success: true. Der Agent antwortet: “Ich habe deinen Wunsch umgesetzt.” Drei Tage später will der User auf seine Rechnungshistorie zugreifen - die ist mit dem Account weg. Routing-Failure auf einem destruktiven Tool, ausgelöst durch eine harmlose User-Formulierung - genau der Fall, gegen den keine bestehende Eval testet, weil niemand ihn vorher formuliert hat.

Was passiert jetzt typischerweise? Hotfix in den Prompt, Ticket geschlossen, weiter im Tagesgeschäft. Was passieren sollte: erst der Test, dann der Fix - genau wie bei klassischen Bugs. Sonst wiederholt sich derselbe Failure beim nächsten Modell-Update, beim nächsten Prompt-Refactor, beim nächsten Tool-Schema-Wechsel. Eine Suite, die solche Fälle nicht aufnimmt, vergisst sie - und vergessene Failures kommen wieder.

Release-Disziplin für AI-Features: Canary, Shadow Runs und automatischer Rollback
Eval grün im PR, zwei Wochen später rote Zahlen in Produktion - ohne neuen Deploy. Warum AI-Features zwischen Merge und 100% Traffic eigene Gates brauchen.
rubeen.dev/blog/ai-rollout-canary-shadow
i
Hinweis

In den anderen Posts dieser Serie zieht sich ein Refund-Agent als Beispiel durch - hier wechsle ich bewusst auf einen Self-Service-Agent für Account-Einstellungen. Der Loop sieht man am klarsten, wenn die Failure-Klasse eine andere ist als die, mit der wir Evals normalerweise illustrieren. Routing auf einem destruktiven Tool ist genau so eine Klasse: irreversibel, leicht zu reproduzieren, schwer durch ein vorab gebautes Eval-Set abzufangen.

Warum AI-Systeme ohne Incident-Loop stagnieren

Eval-Sets sind kuratierte Welten. Production ist eine Long-Tail-Verteilung.

Das ist der Riss. Wenn ich eine Eval-Suite baue, baue ich sie aus dem, was ich mir vorstellen kann: Happy Paths, ein paar absichtliche Edge Cases, vielleicht eine Handvoll adversariale Prompts aus dem Red-Team-Workshop. Das deckt den Mittelteil der Verteilung ab. Den Rand - die wirklich seltsamen, teuren Failures - sehen wir erst, wenn echte User sich am System abarbeiten.

Synthetische Tests decken Bekanntes ab. Reale Incidents decken Unbekanntes auf. Beides braucht es. Aber nur eines wächst von selbst.

Ein altes Pattern, neuer Kontext

Die Idee ist nicht neu. Martin Fowler hat sie 2005 für klassische Codebases formuliert:

“The usual reaction of a team using self-testing code is to first write a test that exposes the bug, and only then to try to fix it.”

M
Self Testing Code
martinfowler.com

Der Grund: damit der Bug, einmal gefixt, gefixt bleibt. Aus dieser Disziplin ist die populäre Verkürzung “every bug becomes a test case” entstanden. Bei Fowler steht sie so nicht wörtlich - die Haltung dahinter aber sehr wohl: Jeder Bug ist nicht nur ein Versagen des Codes, sondern auch eines der Test-Suite.

Genau diese Haltung fehlt in vielen AI-Eval-Setups. Es gibt eine Suite. Sie läuft im CI. Sie ist grün. Und sie altert leise vor sich hin, während Production längst neue Failure-Modes serviert.

Was die Praxis längst sagt

Hamel Husain bringt das auf den Punkt: “remove all friction from the process of looking at data.” Das ist keine Empfehlung, das ist eine Voraussetzung. Wer nicht in echte Traces schaut, sieht den Long Tail nicht. Und was ich nicht sehe, kann ich nicht in mein Eval-Set heben.

Anthropic beschreibt das Pattern in Demystifying Evals for AI Agents als Lebenszyklus eines einzelnen Evals: Capability-Evals “graduate to become a regression suite”, sobald ein Verhalten zuverlässig läuft. Die Suite wird zum Schutzschild dessen, was schon einmal funktioniert hat. Das ist Fowlers Logik, nur mit anderem Vokabular.

Drei Klassen, die kein Eval-Set vorhersieht

Es gibt mindestens drei Failure-Modes, die in einer rein synthetisch gebauten Eval-Suite strukturell fehlen:

Die Gemeinsamkeit: Jeder dieser Fälle ist im Nachhinein trivial in einen Test zu gießen. Vorher denkt niemand daran. Genau deshalb braucht die Suite einen Kanal von Production zurück ins Repo. Die Autor:innen von Applied LLMs - Yan, Husain, Bischof, Frye, Liu, Shankar - formulieren das als Mindestbedingung: “When we spot a new issue, we can immediately write an assertion or eval around it.”

O
What We Learned from a Year of Building with LLMs (Part II)
oreilly.com

Ohne diesen Kanal passiert das, was Software-Suiten ohne Bug-Regression auch immer schon passiert ist: Sie messen mit jedem Release weniger von dem, was tatsächlich kaputt geht.

Synthetische Tests altern. Incident-Tests tun das auch - aber sie wachsen wenigstens mit.

Wie der Weg von einem Postmortem zu einem konkreten Regressionstest aussieht - in sieben Schritten - klären wir im nächsten Kapitel.

Der Postmortem-zu-Eval-Loop

Wenn ein Incident im Team-Chat zerredet, mit einem Hotfix beerdigt und am Freitag vergessen wird, bezahlt der nächste User dafür. Der Loop, den ich hier zeige, ist die Disziplin, die das verhindert. Eugene Yan beschreibt diesen Rhythmus als Eval-Driven Development - Observe, Annotate, Hypothesize, Experiment, Measure - und genau dieser Zyklus wird hier auf Production-Incidents angewendet.

E
An LLM-as-Judge Won't Save The Product—Fixing Your Process Will
Applying the scientific method, building via eval-driven development, and monitoring AI output.
eugeneyan.com
Incident → Trace fixieren → Failure-Mode → Erwartung → Sanitize → Suite → Fix

Sieben Schritte. Sie bauen aufeinander auf. Lässt du einen aus, kippt der nächste. Als roter Faden läuft das Account-Beispiel aus dem Intro mit: User wollte Marketing-Mails abbestellen, der Agent hat den Account gelöscht.

1. Incident erkannt - woher kommt das Signal?

Egal woher: Sampling-Eval schlägt aus, Drift-Detector zeigt eine Verschiebung in Tool-Call-Verteilungen, ein Support-Ticket landet im Queue, ein User postet einen Screenshot. Der Trigger ist beliebig. Was zählt, ist eine Trace-ID. Ohne sie existiert der Incident nur als Anekdote, und Anekdoten landen nicht in der Suite.

Im Account-Beispiel: Ein User schreibt “Mein Account ist weg, ich wollte doch nur die Marketing-Mails abbestellen”. Der Support öffnet den zugehörigen Trace - Conversation-ID matcht.

2. Trace fixieren - was reproduziert den Fall?

Jetzt zahlt sich der Observability-Stack aus. Snapshot ziehen: vollständigen Trace, Prompt-Version, Modell-Pin, Tool-Description-Snapshot, Tool-Outputs, System-Prompt, Retrieval-Hits. Alles, was in zwei Wochen den Fall noch reproduzierbar macht. Wer hier auf “wir schauen morgen nochmal” verschiebt, hat morgen nichts mehr zum Anschauen, weil der Prompt inzwischen ausgerollt und das Modell automatisch aktualisiert wurde.

Observability für AI-Features: Welche Spans, Events und IDs du wirklich brauchst
Ohne Traces, Modell- und Promptversionen, Tool-Spans und Kostenmetriken bleibt jeder Incident Spekulation. Ein Leitfaden für Teams, die AI-Features in Produktion betreiben.
rubeen.dev/blog/observability-ai-features

Im Beispiel: Trace zeigt User-Message “Bitte deaktiviert die Marketing-Mails an meine Adresse”, direkt danach Tool-Call delete_account(user_id=...) → { "success": true }. Assistant-Output: “Ich habe deinen Wunsch umgesetzt.” Strukturell falsches Tool, technisch erfolgreich, irreversibel. Reproduzierbar falsch.

3. Failure-Mode benennen - was genau ist kaputt?

“Der Agent war komisch” ist keine Failure-Mode. “Destruktives Tool aufgerufen, obwohl der User-Intent reversibel war” ist eine. Eine Failure-Mode ist eine kurze, kategoriale Bezeichnung, die in der Suite als Tag wiederfindbar ist - z.B. routing/destructive-on-soft-intent, false-success-on-tool-failure, tool-misuse-on-restricted-action.

Sobald die Failure-Mode einen Namen hat, kannst du nach ihr suchen, sie aggregieren und den nächsten ähnlichen Trace innerhalb von Sekunden zuordnen. Vorher nicht. Halte das Vokabular über alle Tests konsistent - sonst wird die Suite später nicht aggregierbar.

4. Erwartung formulieren - was hätte passieren sollen?

Ohne Erwartung kein Test. Ohne Test kein Loop. Die Frage ist nicht “war das gut?”, sondern “wie genau hätte sich das System verhalten müssen?”. Welche Tool-Calls in welcher Reihenfolge? Welche Antwort-Form? Refusal oder Eskalation?

Im Beispiel: Bei der User-Message “Marketing-Mails deaktivieren” muss der Agent (a) unsubscribe_marketing aufrufen statt delete_account, (b) bei jeder Mehrdeutigkeit zwischen reversibler und destruktiver Aktion eine Rückfrage stellen, statt zu raten, und (c) destruktive Tools nie ohne explizite Bestätigung des Users triggern - selbst wenn er sie scheinbar bestellt hat. Drei prüfbare Bedingungen, alle aus der Trace ableitbar. Das ist die Eval-Spezifikation.

5. Sanitize - was muss raus, was bleibt?

Production-Traces enthalten PII: E-Mails, Bestellnummern, manchmal Zahlungsdaten. Die wandern nicht in eine Test-Suite, die jeder Engineer im Repo lesen kann. Reduktion auf das Minimum: ersetze identifizierende Felder durch synthetische, behalte Struktur und Edge-Case-Charakteristika. Ein leerer String an einer relevanten Stelle bleibt ein leerer String. Eine kaputte Order-ID bleibt strukturell kaputt.

Faustregel: Der sanitisierte Datenpunkt muss den Fehler weiterhin auslösen. Wenn der Test mit Fake-Daten plötzlich grün ist, hast du das Wesentliche entfernt.

6. In die Suite schreiben - welcher Grader greift?

Jetzt wird aus dem Incident ein Datenpunkt. Tags: incident:<ticket-id>, failure-mode:<name>, optional severity:<...>. Ohne Tags findest du den Test in einem halben Jahr nicht wieder.

Dann die Grader-Frage: Code-based, LLM-as-Judge oder Workflow-Eval? Faustregel: Tool-Use-Treue → Code-Grader (deterministisch, billig, schnell). Tonalität, Refusal-Qualität, Erklär-Verhalten → LLM-as-Judge mit präziser Rubric. Multi-Step-Verhalten über mehrere Turns → Workflow-Eval, die den ganzen Trace bewertet.

Im Account-Fall: Code-Grader prüft Tool-Wahl (assert tool_calls[0].name in {"unsubscribe_marketing", "ask_clarification"} and "delete_account" not in tool_calls). LLM-Judge prüft, ob bei Mehrdeutigkeit eine sinnvolle Rückfrage gestellt wurde und der Tonfall zur User-Intention passt. Zwei Grader, ein Datenpunkt.

Evals in die CI: Wenn AI-Features aufhören, Prompts zu sein
... und anfangen, Software zu werden - mit den entsprechenden Testpflichten
rubeen.dev/blog/ai-evals-ci-pipeline

7. Fix entwickeln - mit dem Test als Gate

Der Test ist jetzt rot. Gut. Erst jetzt wird gefixt. Optionen, die hier auf der Hand liegen:

Welche Kombination du wählst, hängt von der Failure-Mode ab. Wichtig ist nur eines: Der Test wird grün, weil das System den Bug reproduzierbar verhindert - nicht weil der Bug zufällig nicht mehr triggert. Genau das meinen Applied LLMs mit “spot a new issue, write the assertion immediately” - sofort, nicht später.

Wer den Incident löst, ohne die Suite anzupassen, kauft sich Zeit auf Kredit.

Der Loop schließt sich, wenn der grüne Test im nächsten CI-Run läuft - und im übernächsten, und im hundertsten. Jeder Incident, der diesen Pfad geht, hinterlässt ein permanentes Stück Wissen im System.

Was eine gute Incident-Eval ausmacht

Der Loop aus dem letzten Kapitel produziert Tests. Jeder Incident wird einer. Und genau da fängt das nächste Problem an: Wenn jeder reflexartig in die Suite wandert, erstickt sie an sich selbst. Flaky Tests, Doppelungen, Karteileichen, die niemand mehr versteht. Eine Incident-Eval, die ihren Zweck verfehlt, ist schlimmer als keine - sie verbrennt Vertrauen in die Suite.

Was hält einen Incident-Test überlebensfähig? Sechs Eigenschaften, die ich pro Test durchgehe, bevor er gemergt wird.

Ein Incident-Test, der nur einen Output-String prüft, fällt um, sobald das Modell höflicher wird.

Drei Varianten desselben Incidents

Konkret wird das erst, wenn wir denselben Failure dreimal verschieden testen. Annahme: Ein Support-Agent hat einem User die Kündigungsmodalitäten verweigert mit dem Satz “Das darf ich nicht beantworten” - obwohl die Information öffentlich ist. Der Refusal war der Bug, nicht die Antwort.

Der dritte Test überlebt Modell-Updates, weil er die Failure-Mode trifft - nicht ihre damalige Ausprägung. Genau das ist der Unterschied zwischen einer Suite, die mit der Realität wächst, und einer, die sie konserviert.

PII, Sicherheit und der Datenschutz-Reflex

Spätestens an dieser Stelle kommt der Einwand. Meistens aus der Legal-Ecke, manchmal aus dem eigenen Bauch:

“Wir können doch keine Production-Daten ins Test-Set packen.”

Stimmt. Sollen wir auch nicht. Der Reflex ist richtig - die Schlussfolgerung “dann eben keine Incident-Tests” ist falsch.

Achtung

Production-Traces gehören niemals roh ins Repo. Nicht der echte Trace ist der Testfall, sondern die Failure-Struktur, die er offenlegt.

Ein Trace aus Produktion ist Rohmaterial. Daraus extrahieren wir, was den Failure ausmacht: Routing, Tool-Sequenz, Tool-Result, falsche Annahme des Modells. Was nicht in den Testfall gehört: customer_id, Bestellnummern, E-Mail-Adressen, IP-Adressen, Postanschriften, Freitext aus User-Messages. Der Test prüft Verhalten, nicht den Datensatz.

Drei Strategien, die in der Praxis funktionieren

from langfuse import Langfuse

def mask(data):
    if isinstance(data, str) and data.startswith("SECRET_"):
        return "REDACTED"
    if isinstance(data, dict):
        return {k: mask(v) for k, v in data.items()}
    if isinstance(data, list):
        return [mask(x) for x in data]
    return data

langfuse = Langfuse(mask=mask)
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine

analyzer, anonymizer = AnalyzerEngine(), AnonymizerEngine()

def redact(inputs):
    r = analyzer.analyze(text=inputs["text_block"],
        entities=["PERSON", "EMAIL_ADDRESS", "PHONE_NUMBER"],
        language="en")
    inputs["text_block"] = anonymizer.anonymize(
        text=inputs["text_block"], analyzer_results=r).text
    return inputs

# Inside a weave.Model subclass:
@weave.op(postprocess_inputs=redact)
async def predict(self, text_block: str): ...

Vom Account-Trace zum Testfall

Konkret: Der Self-Service-Agent hat bei “Bitte deaktiviert die Marketing-Mails” delete_account aufgerufen statt unsubscribe_marketing. Der Original-Trace enthält user_id=usr_8a72f..., eine echte Mail-Adresse, IP, Browser-Fingerprint und die vollständige User-Message mit Klarname am Ende.

Der Testfall enthält das nicht. Er enthält:

User-Message-Struktur, Tool-Katalog, Mehrdeutigkeit, Assertion - alles, was die Failure-Mode reproduziert - bleibt intakt. Der echte User ist raus.

Failure-Struktur ist nicht Production-Daten. Wer beides verwechselt, baut weder Tests noch Datenschutz.

Der Compliance-Hebel, den dieser Loop nebenbei zieht

Wer ein High-Risk-System unter dem EU AI Act betreibt, baut diesen Loop ohnehin - bewusst oder unbewusst. Art. 72 verlangt, “actively and systematically collect, document and analyse relevant data … on the performance of high-risk AI systems throughout their lifetime”. Genau das ist der Kanal von Production zurück ins Eval-Set. Ein dokumentierter Postmortem-zu-Test-Pfad ist Post-Market-Monitoring in Code.

Und wenn ein Serious Incident auftritt: Art. 73 setzt 15 Tage Standard-Frist, 2 Tage bei einer weitverbreiteten Rechtsverletzung oder einer Disruption kritischer Infrastruktur, 10 Tage bei Tod einer Person. In dieser Zeit wollen wir keine Evidence mehr zusammensuchen - wir wollen sie haben. Der anonymisierte Testfall mit Trace-ID, Hypothese, Fix und Datum ist Teil der Beweiskette.

Welche Artikel der EU AI Act der Loop konkret abdeckt
  • Art. 9 - Risk Management: der Incident-zu-Test-Loop ist eine kontinuierliche Risk-Mitigation-Maßnahme über den Lebenszyklus.
  • Art. 12 - Record-Keeping: Trace-IDs, Test-IDs, Postmortem-Referenzen sind genau die automatisch generierten Logs, die der Artikel verlangt.
  • Art. 17 - Quality Management: ein dokumentierter Pfad von Incident zu Regressionstest ist Teil des QMS.
  • Art. 72 Abs. 2 - Post-Market Monitoring: “actively and systematically collect, document and analyse relevant data … on the performance of high-risk AI systems throughout their lifetime” - der Loop ist dieses System.
  • Art. 73 - Reporting of Serious Incidents: Reporting-Pflicht bei Serious Incidents (15 Tage Standard, 2 Tage bei weitverbreiteter Rechtsverletzung oder Disruption kritischer Infrastruktur, 10 Tage bei Tod). Der Testfall mit Postmortem-Link ist Teil der Evidence.

Wer es ohnehin tun muss, sollte es so tun, dass es wirkt - statt es als Compliance-Fassade nebenher zu erledigen.

Tooling: was den Workflow stützt

Der Loop steht und fällt nicht mit einem Tool. Aber wer “Trace → Annotation → Dataset” jeden Sprint von Hand zusammenklebt, verbrennt Zeit, die in die Failure-Mode-Analyse gehört. Etliche Plattformen bauen genau diese Mechanik inzwischen direkt in die UI ein. Sechs davon im Schnelldurchlauf, damit du das Muster beim nächsten Tool-Eval wiedererkennst.

{"input": "What is the capital of France?", "ideal": "Paris"}
{"input": "Who wrote Romeo and Juliet?", "ideal": ["William Shakespeare", "Shakespeare"]}

Egal mit welcher Plattform du kuratierst: Am Ende sollten die Tests in dieses Format exportierbar sein. Es ist der kleinste gemeinsame Nenner aller Eval-Suiten - und der Pfad zurück, falls du das Tooling tauschst.

Fast alle bauen denselben Loop: Trace selektieren, annotieren, ins Dataset, versionieren. Die Reihenfolge ist nicht Zufall - andersrum bekommst du Production-Daten nicht sauber in ein Test-Set.

Mini-Implementierung: JSONL-Schema und Vitest-Loader
import { describe, it, expect } from "vitest";
import { readFileSync, readdirSync } from "node:fs";
import { runAgent } from "../src/agent";

const cases = readdirSync("evals/cases/incidents")
  .filter((f) => f.endsWith(".jsonl"))
  .flatMap((f) =>
    readFileSync(`evals/cases/incidents/${f}`, "utf8")
      .trim()
      .split("\n")
      .map((line) => ({ file: f, ...JSON.parse(line) })),
  );

describe("Incident regressions", () => {
  for (const c of cases) {
    it(`${c.file}: ${c.input.slice(0, 40)}`, async () => {
      const out = await runAgent(c.input);
      const ideals = Array.isArray(c.ideal) ? c.ideal : [c.ideal];
      expect(ideals.some((i) => out.includes(i))).toBe(true);
    });
  }
});

Der Loader macht hier Substring-Match (entspricht dem Includes-Template aus OpenAI Evals, nicht Match) - bei stochastischen LLM-Outputs ist das die robustere Default-Wahl. Das ist der Startpunkt. Erst wenn die Annotation zur Engstelle wird - nicht der Loader - lohnt das Upgrade auf eine Plattform.

Was heißt das für die Tool-Wahl? Wer 50 Incidents im Quartal annotiert und ein Team von zehn Reviewern koordiniert, profitiert von Queues, Pinning und unveränderlichen Versionen. Wer als Engineering-Team von drei Leuten zwei Incidents im Monat hat, braucht das nicht - da reicht das Pattern aus dem Snippet oben: ein Repo mit evals/cases/incidents/<id>.jsonl plus ein simpler Vitest-Loader. Der Loop wird nicht besser durch teurere Tools - er wird besser durch Disziplin in der Annotation.

Tooling skaliert mit Volumen, nicht mit Anspruch.

Betriebsmodell: wer macht das, wann, wie oft

Was den Anspruch durchsetzt, ist der Rhythmus. Eine Suite wächst nicht, weil ein Tool sie kann - sie wächst, weil jemand verbindlich dafür verantwortlich ist. Ohne festen Rhythmus landet der Loop auf der gleichen Halde wie “wir machen mal Postmortems, wenn wir Zeit haben”. Das Team weiß, was passiert: nichts.

Wenn du den Loop ins reale Setup einbettest, brauchst du drei Rhythmen. Jeder mit Trigger, Output und Owner.

💡
Tipp

Eine Maßnahme mit großem Hebel und winzigem Aufwand: Mach das Feld Eval-Case-ID im Postmortem-Template zur Pflicht. Kein abgeschlossenes Postmortem ohne entweder einen verlinkten neuen Eval-Case oder eine kurze schriftliche Begründung, warum keiner gebraucht wird. Damit zwingst du den Loop in den existierenden Incident-Workflow - ohne neuen Prozess, ohne neues Meeting.

Pro Incident: jedes Postmortem schreibt einen Test

Die Reihenfolge ist wichtig: erst der Test, der den Failure offenlegt, dann der Fix - oder zumindest beides im selben PR. Wer zuerst fixt und dann “wenn Zeit ist” einen Test schreibt, kommt nie dazu. Ein gefixter Bug fühlt sich erledigt an. Der Test, der ihn fixiert, fühlt sich nach Mehrarbeit an. Deshalb muss er Pflicht sein.

Ein Postmortem ohne neuen Testfall ist ein Tagebucheintrag, keine Verbesserung.

Pro Woche: Triage offener Production-Failures

Der Sinn dieses Termins ist nicht Vollständigkeit. Es geht darum, die offenen Failures der Woche durchzugehen, solange jeder sich noch erinnert. Was diese Woche nicht eingeordnet wird, kommt nächste Woche dreimal so groß zurück - und keiner erinnert sich mehr an die Details.

Pro Quartal: Suite-Review

Das Quartals-Review ist der einzige Termin, in dem Tests überhaupt wieder rausfliegen dürfen. Nicht aus dem Bauch, sondern aus dieser Liste, mit Begründung im Commit.

Verantwortung beim Feature-Team, nicht beim AI-Team

Wer das Feature betreibt, schreibt die Tests - nicht eine zentrale Eval-Crew. On-Call sieht den Incident, On-Call kennt die Failure-Mode am genauesten, On-Call schreibt den Test. Applied LLMs formuliert genau das als Voraussetzung für eine lebende Suite: Diese Haltung muss in der Organisation verankert werden, indem die Annotation von In- und Outputs Teil der On-Call-Rotation wird.

Das AI-Team baut die Infrastruktur, reviewt die Test-Qualität, hilft beim Format. Aber es ersetzt nicht das Domänenwissen des Teams, das den Failure auf dem Tisch hatte.

Anti-Pattern: der Eval-Champion

In jedem Team gibt es eine Person, die “das mit den Evals” sehr gut versteht und gern macht. Wenn du nichts gegensteuerst, landet die ganze Suite-Pflege auf dieser Person. Funktioniert eine Weile - bis sie krank wird, das Team wechselt, oder ausbrennt. Dann kennt niemand sonst den Aufbau, niemand traut sich an einen Test ran, und die Suite versteinert in genau dem Zustand, in dem der Champion sie verlassen hat.

Bus-Faktor 1 ist kein Betriebsmodell. Drei Heuristiken dagegen: Pairing beim Schreiben jedes Tests. Rotierende Triage statt fester Owner. Quartals-Review im ganzen Team, nicht im 1:1 zwischen Champion und Lead.

Postmortem-Template mit Eval-Pflichtfeld
# Postmortem: <kurzer Titel>

- **Trigger**: Was hat den Incident ausgelöst? (User-Input, Modell-Update, Tool-Change, ...)
- **Trace-ID**: <Link zum vollständigen Trace im Observability-Tool>
- **Failure-Mode**: Welche Kategorie? (Refusal, Halluzination, Tool-Misuse, Format-Drift, ...)
- **Eval-Case-ID**: <Link zum neuen Eval-Case in der Regression-Suite>
  - *Wenn leer: Begründung, warum kein Eval-Case möglich ist - blockiert sonst den Abschluss.*
- **Fix**: Was wurde geändert? (Prompt, Tool-Schema, Retrieval, Modell, ...)
- **Rollout-Stufe**: Canary / Staged / Full
- **Owner**: <feature-team>

Typische Gegenargumente

Wer den Loop zum ersten Mal vor einem Team aufzieht, bekommt eine Reihe Einwände zurück, die sich zwischen Firmen kaum unterscheiden. Die meisten sind berechtigt - sie führen aber selten zu “brauchen wir nicht”, sondern zu “brauchen wir kleiner”. Hier die wiederkehrenden sechs, mit jeweils einer Antwort, die du selbst formulieren kannst.

”Wir bekommen unser Eval-Set damit unwartbar groß.”

Größe ist hier das Ziel, nicht der Defekt. Eine Suite, die nach einem Jahr Betrieb genauso schlank ist wie am ersten Tag, hat keine Realität gesehen. Wartbarkeit lösen wir über Tags und Sub-Suites: regression, tool-contract, routing, pii-redaction. Pre-Merge läuft nur die kritische Auswahl, nightly läuft alles - und der teure Lauf ist genau deshalb teuer, weil er etwas wert ist.

”Production-Daten gehören nicht ins Repo.”

Korrekt. Sollen sie auch nicht. Genau dafür gibt es Redaction in der Trace-Pipeline und synthetische Tests, die nur die Failure-Struktur konservieren - das war Thema des PII-Kapitels. Die Testfall-Datei kennt customer_id="cust_demo_001", nicht den echten User. Wer “keine Production-Daten ins Repo” als “keine Incident-Tests” liest, hat zwei verschiedene Dinge zu einem gemacht.

”Postmortems schaffen wir kaum, jetzt soll noch ein Test dazu?”

Der Test ist der Postmortem-Output. Nicht ein Zusatzartefakt daneben, sondern das, was vom Postmortem übrig bleibt, wenn das Meeting vorbei ist. Wenn ein Postmortem keinen reproduzierbaren Test hervorbringt, hat es nichts hervorgebracht - dann war es eben nur ein Meeting, kein Postmortem. Das Pflichtfeld im Template ist die billigste Form von Disziplin, die hier funktioniert.

”Bei nicht-deterministischen Systemen ist ‘reproduzieren’ eh schwer.”

Reproduzierbar ist nicht der exakte Output - reproduzierbar sind Trigger und erwartete Failure-Mode. Der Test fragt nicht “kommt Token-für-Token dasselbe raus”, er fragt “tritt die Failure-Klasse noch auf”. Statt eines einzelnen Laufs nutzen wir eine Pass-Rate über mehrere Läufe und definieren eine Schwelle: 8 von 10 grün heißt grün, 2 von 10 grün heißt rot. Wer flaky ist, ist halt flaky - das wird durch Wegschauen nicht besser. Nicht-Determinismus ist ein Argument für mehr Statistik im Eval, nicht für weniger Eval.

”Wir haben kein Sampling-Eval, also keine Incidents.”

Doch, sie kommen. Über Support-Tickets, Sales-Eskalationen, den CTO direkt und die eine Nachricht von außen. Das ist kein Sampling im technischen Sinne, aber es ist Signal aus Produktion - und genau dieses Signal muss in den Loop fließen, bevor es im Ticketsystem versandet. Bis ein echtes Sampling-Eval steht, ist das Support-Postfach das Eval. Tag im Ticketsystem, Pflichtfeld “Failure-Mode” beim Schließen, wöchentliches Triage-Meeting mit Eval-Ownership. Mehr Mechanik braucht es zum Anfangen nicht.

”Das ist Compliance-Fassade.”

Wäre es, wenn der Loop nur als Audit-Vorbereitung existieren würde. Hier ist es umgekehrt: Der Loop ist die Substanz - kontinuierliche Verbesserung der Suite gegen reale Failure-Modes - und der Compliance-Nachweis fällt nebenbei ab. Wie im PII-Kapitel gezeigt, deckt der Pfad genau das ab, was Art. 72/73 des EU AI Act verlangen. Fassade wäre, das Ergebnis zu dokumentieren, ohne den Loop zu fahren.

Wer ein “ja, aber” zur Suite hat, hat meistens kein Problem mit der Suite, sondern mit dem Postmortem.

Die Suite atmet mit der Realität

Eine gute Eval-Suite ist kein Artefakt vom Projektstart. Sie ist ein lebendiges Inventar dessen, was schon einmal kaputtging - und damit dessen, was nicht wieder kaputtgehen darf. Sie atmet mit der Realität, weil die Realität sich nicht an unsere Test-Pläne hält. Jeder Incident, den wir ernst nehmen, lässt sie ein Stück größer und ein Stück präziser werden.

Damit schließt sich der Bogen zu den vorherigen Posts der Serie. Keiner davon reicht für sich alleine - zusammen ergeben sie ein Betriebsmodell:

AI Systems Architecture beginnt dort, wo Modelloutput Teil der Produktlogik wird
Wie ich aus einem GitHub Issue einen testbaren, beobachtbaren Umsetzungsplan mache
rubeen.dev/blog/ai-systems-architecture
Approval ist kein UX-Bug: Warum Agent-Systeme wissen müssen, wann sie fragen
Human in the Loop ist keine Schwäche, sondern Sicherheitsarchitektur. Warum Pause/Resume bei kritischen Agent-Aktionen das vielleicht wichtigste Feature ist.
rubeen.dev/blog/human-in-the-loop
Release-Disziplin für AI-Features: Canary, Shadow Runs und automatischer Rollback
Eval grün im PR, zwei Wochen später rote Zahlen in Produktion - ohne neuen Deploy. Warum AI-Features zwischen Merge und 100% Traffic eigene Gates brauchen.
rubeen.dev/blog/ai-rollout-canary-shadow

Die Bausteine wirken nur zusammen. Der Incident-zu-Test-Pfad ist unter ihnen der einzige, der die anderen mit der Zeit besser werden lässt, statt sie altern zu lassen.

Jede Art von Änderung muss durch diesen Loop. Code-Refactor im Agent. Geänderter System-Prompt. Neues Modell, weil das alte abgekündigt wurde. Eine MCP-Tool-Description, die ein Vendor leise umformuliert hat. Ein Datensatz, der für RAG neu indiziert wurde. Wenn auch nur eines davon ohne Lauf gegen die Suite live geht, hat die Suite ihre Aufgabe nicht erfüllt - dann ist sie wieder Fassade.

Und dann gibt es noch einen anderen Hebel, den dieser Post nicht mehr abdeckt: Statt im hundertsten Test den Agent vor delete_account zu schützen, könnten wir ihm das Tool an der Stelle gar nicht erst in die Hand geben. Wenn der Flow als Struktur vorgegeben ist und das Modell nur an klar abgegrenzten Punkten entscheidet, treten ganze Klassen von Routing-Failures gar nicht mehr auf - und keiner braucht den hundertsten Test. Aber das ist ein anderer Post.

Hoffnung ist keine Strategie. Eine Suite, die mit jedem Incident wächst, schon.