Kapitel

kontinent / evalsKapitel 9

Evals in der CI

Wie die Tests bei jeder Codeänderung automatisch laufen, ohne ständig grundlos fehlzuschlagen.

Ein Eval, das nur läuft, wenn jemand daran denkt, ist ein Dokument. Ein Eval, das automatisch läuft, ist Infrastruktur. Der Unterschied entscheidet, ob Sie Regressionen finden, bevor Nutzer sie finden.

Befund: Prompt-Änderungen abzusichern ist keine Frage der Ordnung. Es ist empirisch begründet. When Generic Prompt Improvements Hurt (2026) zeigt: Allgemeine Zusätze zum Prompt bringen keine gleichmäßige Verbesserung. Strengere Vorgaben zum Ausgabeformat verbesserten die strikte Extraktion bei beiden getesteten Modellen. Dieselben allgemeinen Regeln drückten bei einem Modell die Zitiertreue im RAG von 26/30 auf 9/30 bestandene Fälle. Auf dem Schluss der Autoren baut dieses Kapitel auf: „prompt changes should be treated as potential regression risks and tested against task-specific suites before deployment“.

Quelle: When Generic Prompt Improvements Hurt, 2026

Eine Prompt-Änderung ist eine Auslieferung. Das ist das ganze Argument dafür, Evals in die CI zu nehmen.

Das Problem beim Bau: LLM-Evals verletzen die Annahmen, auf denen CI beruht. Sie sind langsam. Sie kosten pro Lauf Geld. Und sie liefern nicht jedes Mal dasselbe Ergebnis. Wer Unit-Test-Gewohnheiten einfach überträgt, bekommt daher eine teure und unzuverlässige Pipeline. Irgendwann schaltet sie jemand ab.

Die Suite staffeln

Drei Stufen mit drei verschiedenen Aufgaben. Diese Form funktioniert.

StufeWannGrößeGraderBlockiert Merge?
SmokeBei jedem Push20–50 BeispieleNur code-basiertJa
RegressionBei jedem PR100–300 BeispieleCode + günstige JudgesJa, bei harten Fehlern
VollNachts / vor ReleaseAllesAlle, auch teure JudgesNein, berichtet nur

Smoke fängt die kaputte Pipeline ab, nicht die schlechtere Antwort. Geprüft wird: Stimmt das Schema? Lassen sich die Tool-Aufrufe parsen? Gibt es leere Antworten? Ist der System-Prompt durchgesickert? Liegt die Latenz in der richtigen Größenordnung? Das ist deterministisch, dauert Sekunden und kostet praktisch nichts. Diese Stufe fängt das meiste ab, was bei einer Änderung wirklich kaputtgeht.

Regression ist Ihre gepflegte Sammlung echter Vorfälle aus der Vergangenheit plus Abdeckung der Kernpfade. Jeder behobene Produktionsfehler sollte hier ein Beispiel hinzufügen. Diese Stufe lohnt sich am meisten. Sie ist eine Testsuite aus Dingen, die wirklich schiefgegangen sind. Hier liegen zwei Arten von Fällen, und Sie lesen sie verschieden. Die benannten Vorfälle müssen bei 100 % stehen. Ein neuer Fehlschlag darunter ist ein Vorfall, kein Rauschen. Die Kernpfad-Fälle und die vom Judge bewerteten Fälle sind eine Quote mit Spanne. Im durchgerechneten Beispiel sind das 71 %. Diese Quote wird berichtet, nie als Gate benutzt. Tag 5 meint mit „Regressionssuite“ genau das.

Voll ist der Ort für Kennzahlen je Teilmenge, teure Judges und statistische Vergleiche. Diese Stufe läuft nach Zeitplan, weil ein Aufruf zu viel kostet, um ihn an jeden Push zu hängen. Sie berichtet, statt zu blockieren, weil ein Mensch ihr Signal deuten muss.

Nur an deterministischen Prüfungen scheitern

Der schwere Teil ist die Frage, was einen Build scheitern lässt. Ein Fehler in beide Richtungen ist teuer. Zu streng: Das Team schaltet die Prüfung ab. Zu locker: Die Prüfung ist Dekoration.

Hart scheitern bei deterministischen Prüfungen. Schemaverstöße, nicht parsebare Tool-Aufrufe, Treffer der Richtlinien-Regexe, Abstürze. Diese Prüfungen sind binär und reproduzierbar. Behandeln Sie sie genau wie Unit-Tests.

Nie hart scheitern, nur weil der Judge-Wert etwas gefallen ist. Ein Beispiel: 200 Fälle, 75 % bestanden. Diese Zahl hat eine Spanne von rund ±6 Punkten, allein weil 200 Fälle wenig sind. Ein Gate mit der Regel „der Wert darf nicht fallen“ schlägt dann mehrmals pro Woche an, ohne dass sich etwas geändert hat. Das Team lernt, den Lauf so lange zu wiederholen, bis er grün ist. Danach ignoriert es das Gate. Ein Gate, das alle ignorieren, ist schlechter als keines.

Was das Gate stattdessen prüft: den gepaarten Vergleich. Zwei Läufe liegen als zwei Ergebnisdateien vor, die Baseline und der neue Lauf, jede mit einer Zeile je Fall, so wie run.py aus Ein Eval in fünf Dateien sie schreibt. Gepaart heißt: Sie legen die beiden Dateien nebeneinander und vergleichen Zeile für Zeile denselben Fall. Je Fall gibt es vier Möglichkeiten:

BaselineNeuer LaufZählt als
bestandenbestandenunverändert
durchgefallendurchgefallenunverändert
bestandendurchgefallenschlechter
durchgefallenbestandenbesser

Nur die letzten zwei Zeilen entscheiden. Aus ihnen berechnet das Gate zwei Zahlen. Den Rückgang: schlechter minus besser, geteilt durch alle Fälle. Und das Rauschen: wie viel Rückgang allein dadurch entstehen kann, dass einzelne Fälle mal so und mal so ausfallen. Der Build scheitert nur, wenn der Rückgang größer ist als das Rauschen plus ein Spielraum, den Sie festlegen:

# Beide Läufe auf denselben Fällen; verglichen wird Fall für Fall.
worse  = sum(1 for c in cases if baseline[c].passed and not new[c].passed)
better = sum(1 for c in cases if new[c].passed and not baseline[c].passed)
n = len(cases)

drop  = (worse - better) / n                    # beobachteter Rückgang
noise = 1.96 * math.sqrt(worse + better) / n    # was allein das Umkippen einzelner Fälle erklärt

assert drop <= noise + TOLERANCE

Mit den Zahlen aus dem Praxisbeispiel: 147 Fälle, 11 besser, 7 schlechter. Rückgang = (7 − 11) / 147 = −2,7 Punkte, also eine Verbesserung. Rauschen = 1,96 × Wurzel(18) / 147 = 5,7 Punkte. Erst ein Rückgang über 5,7 Punkte plus Spielraum lässt den Build scheitern. Warum die Rechnung so geht, erklärt Statistik.

Dafür muss die Baseline als Datei mit einer Zeile je Fall vorliegen, nicht als eine Prozentzahl. Eine Prozentzahl lässt sich mit nichts paaren. Die Baseline ist der letzte Lauf auf dem Hauptbranch mit denselben Versionen von Modell, Judge und Datensatz. Nach jedem Merge ersetzt der neue Lauf sie. Ändert sich Modell, Judge oder Datensatz, läuft sie neu.

Was das Gate nicht fängt: einen echten Rückgang, der kleiner ist als Rauschen plus Spielraum. Im Praxisbeispiel sind das bis zu 5,7 Punkte. Das ist der Preis dafür, dass das Gate nicht bei Rauschen anschlägt. Wer kleinere Rückgänge sehen will, braucht mehr Fälle, kein strengeres Gate.

Berichten Sie die Bestehensquote selbst mit Spanne, und sie liegt nahe 100 %, dann nehmen Sie das Wilson-Intervall statt der einfachen Formel aus Statistik. Die einfache Formel ergibt bei 100 von 100 eine Spanne von null und bei 98 von 100 eine Obergrenze über 100 %. Beides ist falsch. In Python: proportion_confint(k, n, method="wilson") aus statsmodels.

Immer hart scheitern bei einem Vorfall. Ein Vorfall ist ein Fall aus einem echten Ticket, bei dem das System einmal falsch lag. Sein erwartetes Verhalten ist ein Vertrag. Prüfen Sie ihn deterministisch, wo es geht. Für Vorfälle gibt es keine Spanne: Ein Vorfall, der gestern bestanden hat und heute nicht, ist ein Rückfall, kein Rauschen.

Mit Flakiness umgehen

Dass Ergebnisse schwanken, ist eine Eigenschaft des Systems, kein Defekt der Pipeline. Gehen Sie ausdrücklich damit um:

  • Infrastrukturfehler von Bewertungsfehlern trennen. Ein 503 ist keine falsche Antwort. Wiederholen Sie Infrastrukturfehler. Wiederholen Sie nie einen bewerteten Fehlschlag.
  • Bei hohen Fehlerraten den Lauf scheitern lassen, nicht den Wert. Ein Lauf mit 10 % API-Fehlern ist ungültig. Melden Sie ihn so, nicht als Regression um 10 %.
  • Quarantäne statt Löschen. Ein Beispiel, das zwischen Läufen umkippt, sagt Ihnen: Hier ist etwas wirklich grenzwertig. Verschieben Sie es auf eine geführte Quarantäneliste mit einer verantwortlichen Person. Löschen entfernt das Signal und die Erinnerung.
  • Generierungen cachen und temperature=0 setzen, wo nur die Form geprüft wird. Das Offline-Harness erklärt, wann Produktionsparameter die bessere Wahl sind. Viele scheinbar zufällige Ausfälle in der CI kommen daher, dass das Modell ohne Cache jedes Mal neu würfelt.

Kostenkontrolle

  • Generierungen über Läufe hinweg cachen, mit einem Schlüssel aus Modell, Parametern und Prompt. Ein PR, der nur den Grader ändert, sollte nicht für neue Generierungen zahlen.
  • Die teure Stufe beim Merge auf main laufen lassen, nicht bei jedem Push auf einen Branch.
  • Ausgaben pro Lauf deckeln. Ist die Grenze erreicht, soll der Lauf laut scheitern, statt still abzubrechen. Ein abgebrochener Lauf meldet einen Wert für einen Datensatz, der nicht vollständig bewertet wurde.
  • Kosten pro Lauf über die Zeit verfolgen. Suiten wachsen nur in eine Richtung. Niemand merkt es, bis die Rechnung es zeigt.

Festnageln in der CI

In der CI richtet eine nicht festgenagelte Modellversion den größten Schaden an. Die Pipeline meldet dann eine Änderung, zu der es keinen Commit gibt.

Nageln Sie Modellversion, Judge-Version und Datensatzversion in einer Konfiguration fest, die im Repository liegt. Dann wird ein Modell-Upgrade zu einem Pull Request. Es ist sichtbar, prüfbar und lässt sich mit den Eval-Ergebnissen vergleichen, die es verändert. Genau das wollen Sie: Das Upgrade erscheint als Änderung der Werte, die an einer Änderung im Code hängt.

Überanpassung an das Eval-Set verhindern

Am Ende dieses Wegs droht ein Fehlermodus: eine Suite, die komplett grün ist und nichts mehr vorhersagt. Jede Iteration gegen ein festes Set bringt Sie ein Stück näher daran, dieses Set auswendig zu lernen.

Umstritten: Wie stark das wirklich zuschlägt, ist offen. Die ehrliche Antwort ist weniger beunruhigend als die Legende. Ein zurückgehaltenes Testset immer wieder adaptiv zu benutzen ist ein bekanntes statistisches Problem. Dwork et al. bauten dafür den Mechanismus Thresholdout. Er fügt Rauschen hinzu und verträgt beweisbar quadratisch mehr adaptive Abfragen als naive Wiederverwendung. Recht et al. bauten aber das ImageNet-Testset von Grund auf neu und bewerteten jedes veröffentlichte Modell noch einmal. Sie fanden einen absoluten Rückgang der Genauigkeit. Der Anteil adaptiver Überanpassung daran war „limited to non-existent“. Die Rangfolge hielt. Spätere Arbeiten erklären diese Robustheit teils damit, dass ImageNet sehr viele Klassen hat. Das macht die Wiederverwendung eines Testsets viel schwerer auszunutzen.

Auf Anwendungs-Evals für LLMs lässt sich das nicht automatisch übertragen. Und es spricht gegen Sie: Ihr Eval-Set ist klein und oft binär statt vielklassig. Und Sie iterieren pro Beispiel weit aggressiver, als es je eine ImageNet-Einreichung tat. Treffen Sie die folgenden Vorkehrungen, weil Ihre Lage die ungünstige ist. Nicht, weil ImageNet bewiesen hätte, dass sie nötig sind.

Quelle: Dwork et al., Reusable Holdout, 2015

  • Ein Testset zurückhalten, das die CI nie anfasst. Lassen Sie es vor einer Entscheidung über die Auslieferung laufen, nicht bei jedem PR. Und verwenden Sie es nicht bei jedem Release wieder. Tauschen Sie es nach der Regel aus Datensatz-Design aus.
  • Frische Produktionsbeispiele aufnehmen, im selben Takt wie die Fehleranalyse: alle 2–4 Wochen.
  • Auf steigende Werte bei gleichbleibenden Nutzermetriken achten. Dieses Auseinanderlaufen ist das Kennzeichen von Überanpassung. Es ist der nützlichste Alarm, den Sie bauen können. Sie sehen ihn in der Online-Evaluation.
  • Misstrauen Sie 100 %. Husain und Shankar sagen es direkt: Wer alle seine Evals besteht, hat zu leichte Evals. Eine Bestehensquote um 70 % trägt weit mehr Information als eine bei 99 %. Das gilt für den quotenbewerteten Teil einer Suite und für eine Fähigkeits-Suite. Es gilt nicht für die benannten Vorfall-Fälle. Die sollen bei 100 % stehen.

Umstritten: Ob Evals überhaupt Merges blockieren sollten. Dagegen spricht: Sie sind langsam und probabilistisch. Wer Merges an ein probabilistisches Signal bindet, erzieht Entwickler dazu, es zu umgehen. Dafür spricht: Eine Regressionssuite aus echten Vorfällen ist kaum probabilistischer als ein Integrationstest gegen einen unzuverlässigen externen Dienst. Und niemand fordert, dass solche Tests nur beratend sein sollen. Die tragfähige Lösung ist die Staffelung oben. Blockieren bei Deterministischem, berichten bei Probabilistischem.

Eine minimale Pipeline

Der Befehl evals unten steht für den Runner, den Sie benutzen. Es geht um die Form.

name: evals
on: [pull_request]

jobs:
  smoke:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -e .
      - run: python -m evals run --suite smoke --fail-on-error
        env:
          MODEL: ${{ vars.EVAL_MODEL_VERSION }}

  regression:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/cache@v4
        with:
          path: .eval-cache
          key: evals-${{ vars.EVAL_MODEL_VERSION }}
      - run: python -m evals run --suite regression --baseline main --gate interval
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: eval-results
          path: results/

Zwei Details leisten die meiste Arbeit. --gate interval vergleicht mit der Baseline über ein Konfidenzintervall statt über einen Punktwert. Das Hochladen der Ergebnisse mit if: always() sorgt dafür, dass auch ein gescheiterter Lauf die Zeilen je Beispiel liefert. Die braucht jemand für die Diagnose. Ohne sie sagt ein roter Build nur, dass etwas kaputt ist, und nichts darüber, was.

Häufiger Fehler: Die volle Eval-Suite bei jedem Push laufen lassen, weil man das mit Unit-Tests so macht. Sie ist langsam und teuer. Also ergänzt jemand if: label == 'run-evals'. Nach einem Monat setzt niemand mehr das Label. Staffeln Sie von Anfang an.