kontinent / evalsKapitel 4
Code-basierte und strukturierte Evals
Vieles lässt sich mit normalem Programmcode prüfen statt mit einem Modell. Das ist billiger und verlässlicher.
Bevor Sie ein Modell holen, um ein Modell zu bewerten, prüfen Sie eines: Kann gewöhnlicher Code das erledigen? Erstaunlich oft kann er das. Und dann ist er in jeder wichtigen Hinsicht besser. Er ist deterministisch, sofort fertig und kostenlos. Und eine selbstbewusst formulierte falsche Antwort kann ihn nicht umstimmen.
Die Regel ist einfach. Nehmen Sie den billigsten Grader, den die Aufgabe zulässt. Steigen Sie nur dann eine Stufe höher, wenn der billigere Grader die gewünschte Eigenschaft wirklich nicht prüfen kann.
Die Eskalationsleiter
| Grader | Determinismus | Kosten | Einsetzen, wenn |
|---|---|---|---|
| Exakter / normalisierter Abgleich | Vollständig | Kostenlos | Es genau eine richtige Antwort gibt |
| Schema- oder Typvalidierung | Vollständig | Kostenlos | Die Form der Ausgabe vorgeschrieben ist |
| AST- oder Strukturabgleich | Vollständig | Kostenlos | Die Ausgabe ein Aufruf, eine Query oder ein Ausdruck ist |
| Eigenschafts-/Invariantenprüfung | Vollständig | Kostenlos | Korrektheit eine Regel ist, keine Antwort |
| Zustandsprüfung der Umgebung | Vollständig | Günstig | Das System etwas tun sollte |
| LLM-Judge | Nein | Pro Aufruf | Man für die Korrektheit Prosa lesen muss |
| Mensch | Nein | Teuer | Alles darüber kalibriert werden muss |
Die meisten Teams starten unten in dieser Tabelle und schauen nie nach oben. Das ist verkehrt herum. Und es ist doppelt teuer: Sie zahlen pro Aufruf, und Sie zahlen mit Vertrauen.
Exakter und normalisierter Abgleich
Manchmal gibt es genau eine richtige Antwort. Ein Ticket bekommt eine von fünf Kategorien. Aus einer E-Mail wird eine Bestellnummer gezogen. Eine Anfrage geht an eine Abteilung. Dann ist das ganze Eval ein Vergleich: Ist die Antwort des Modells gleich der erwarteten?
Fast. Angenommen, die richtige Kategorie ist billing, und das Modell schreibt
Billing.. Das ist richtig, nur mit großem B und einem Punkt am Ende. Ein simpler
Vergleich sagt „falsch“. Also putzen Sie beide Seiten, bevor Sie vergleichen: alles
klein, einfache Leerzeichen, keine Satzzeichen am Ende. Das nennt man
normalisieren. Danach sind die beiden gleich:
def normalise(text: str) -> str:
return " ".join(text.lower().split()).strip(".,!?\"'")
assert normalise(prediction) == normalise(expected)
Die Gefahr steckt im Putzen. Jede Regel entfernt etwas. Entfernt sie etwas mit
Bedeutung, wird eine falsche Antwort plötzlich gleich der richtigen. Ein Beispiel: Sie
entfernen alle Bindestriche statt nur der Satzzeichen am Ende. Dann werden die
Bestellnummern A-22-31 und A-2231 beide zu A2231. Eine falsche Nummer besteht,
jedes Mal, und niemand merkt es. Schreiben Sie deshalb jede Regel auf: Groß- und
Kleinschreibung, Leerraum, Satzzeichen am Ende, Einheiten, Artikel. Und testen Sie jede
Regel einzeln. Nehmen Sie ein Paar, das gleich sein soll, und ein Paar, das verschieden
bleiben muss.
Zwei Sonderfälle. Zahlen vergleichen Sie als Zahlen, mit einer Toleranz, die Sie
hinschreiben. 1,0, 1.0 und 1 sind dasselbe. 1 und 1,4 vielleicht auch, je nach
Aufgabe. Daten wandeln Sie vor dem Vergleich in ein Datum um. 2026-03-09 und
9.3.2026 sind derselbe Tag. Ein Textvergleich sieht das nicht.
Schema-Validierung
Gibt Ihr System JSON aus, ist das Schema ein kostenloses Eval. Und es fängt eine Fehlerklasse, die Judges erstaunlich schlecht bemerken.
from typing import Literal
from pydantic import BaseModel, ValidationError
class RefundRequest(BaseModel):
order_id: str
amount_cents: int
currency: Literal["EUR", "USD", "CHF"]
try:
RefundRequest.model_validate_json(output)
valid = True
except ValidationError:
valid = False
Structured Output und Constrained Decoding machen harte Schemaverstöße selten. Das verleitet Teams dazu, die Prüfung wegzulassen. Behalten Sie sie. Sie kostet nichts. Und wenn sie auslöst, hat sich etwas an einer Stelle geändert, die Sie nicht im Blick hatten: ein Modellwechsel, ein API-Standardwert, eine abgeschnittene Antwort.
Umstritten: Ob erzwungene strukturierte Ausgabe Denkleistung kostet. Let Me Speak Freely? (EMNLP 2024, Industry Track) berichtet einen deutlichen Rückgang der Schlussfolgerungsfähigkeit unter Formatvorgaben. Strengere Vorgaben schaden stärker. Als Mechanismus nennt die Arbeit die falsche Reihenfolge der Ausgabe: Ein starres Schema zwingt das Modell, das Antwortfeld auszugeben, bevor es zu Ende gedacht hat. Dagegen stehen Ablationen, die ein Schema im Prompt gegen schema-beschränktes Dekodieren stellen. Sie finden nur geringe Unterschiede. Das legt nahe: Der Effekt hängt stark davon ab, wie das Schema geschnitten ist, und weniger von der Beschränkung selbst.
Die handlungsleitende Fassung lautet nicht „strukturierte Ausgabe vermeiden“. Sie lautet: Geben Sie dem Modell ein Begründungsfeld vor dem Antwortfeld. Und behandeln Sie die Feldreihenfolge als etwas, das Sie prüfen, statt es anzunehmen. Haben Sie ein JSON-Schema ergänzt und die Genauigkeit fiel, prüfen Sie das zuerst.
Erfassen Sie semantische Schemafehler getrennt von syntaktischen. total_cents: 0
parst einwandfrei und ist meistens falsch.
AST- und Strukturabgleich für Tool-Aufrufe
Ein Tool-Aufruf ist eine Antwort, die kein Text ist, sondern eine Anweisung: Rufe dieses Tool mit diesen Werten auf. Sie wollen prüfen, ob es das richtige Tool mit den richtigen Werten war. Erwartet ist:
issue_refund(order_id="A-2231", amount=89)
Das Modell schreibt dieselbe Anweisung mal so, mal so:
issue_refund(amount=89, order_id='A-2231')
issue_refund( order_id = "A-2231", amount = 89 )
Als Text verglichen ist keine der beiden gleich der erwarteten. Andere Reihenfolge, andere Anführungszeichen, andere Leerzeichen. Gemeint ist aber dreimal dasselbe. Also vergleichen Sie nicht den Text. Nehmen Sie stattdessen den Aufruf auseinander: Wie heißt das Tool, und welches Argument hat welchen Wert? Das Auseinandernehmen erledigt Python für Sie. Die zerlegte Form heißt AST (abstrakter Syntaxbaum):
import ast
def _parts(call: ast.Call):
name = ast.unparse(call.func) # "search" oder "client.search"
positional = [ast.dump(a) for a in call.args]
keywords = {k.arg: ast.dump(k.value) for k in call.keywords}
return name, positional, keywords
def call_matches(predicted: str, expected: str) -> bool:
try:
p = ast.parse(predicted).body[0].value
e = ast.parse(expected).body[0].value
except (SyntaxError, IndexError):
return False # kein Aufruf ist ein Fehlschlag, kein Absturz
if not (isinstance(p, ast.Call) and isinstance(e, ast.Call)):
return False
return _parts(p) == _parts(e)
Liefert Ihre API den Aufruf schon als JSON, also Name plus ein Objekt mit den Argumenten, brauchen Sie nicht einmal das. Lesen Sie das JSON auf beiden Seiten ein und vergleichen Sie die beiden Objekte.
Befund: Genau so bewertet das Berkeley Function Calling Leaderboard (BFCL) Funktionsaufrufe. Es zerlegt den vorhergesagten Aufruf und die Referenz und vergleicht die zerlegten Formen. Den Aufruf führt es nicht aus. Das Leaderboard ist Ebene A und für Ihr Produkt meist irrelevant. Die Technik ist Ebene B und direkt wiederverwendbar.
Wie beim Normalisierer oben steckt das Denken in einer Frage: Was gilt als gleich? Der Code oben entscheidet so:
- Die Reihenfolge benannter Argumente ist egal.
amount=89, order_id="A-2231"ist dasselbe wie andersherum. - Die Reihenfolge unbenannter Argumente zählt.
transfer(a, b)undtransfer(b, a)sind verschiedene Aufrufe, und das ist richtig so. "A-2231"und'A-2231'sind gleich. Die Anführungszeichen fallen beim Zerlegen weg.89und89.0sind verschieden. Ob das ein Fehler sein soll, entscheiden Sie. Und Sie schreiben pro Entscheidung einen Test.
Ein Fall bleibt. Manche Tools nehmen einen Wert wahlweise mit oder ohne Namen.
f(1, b=2) und f(a=1, b=2) meinen dasselbe. Der Code oben hält sie für verschieden.
Haben Sie die Tool-Funktion selbst zur Hand, löst Python das auf:
inspect.signature(tool).bind(*args, **kwargs).arguments macht aus beiden dieselbe
Tabelle aus Name und Wert.
Dieselbe Idee lässt sich übertragen. Vergleichen Sie SQL über den geparsten Query-Baum statt über die Zeichenkette. Dann zählen Alias- und Leerraumänderungen nicht als Fehler. Vergleichen Sie generierten Code über den AST, wenn die Aufgabe eine feste Form hat. Hat sie keine, lassen Sie seine Tests laufen.
Eigenschafts- und Invariantenprüfungen
Manchmal ist Korrektheit eine Regel. Sie gilt für jede gültige Antwort. Dann prüfen Sie die Regel, ohne die Antwort zu kennen.
- Jede Zitationsmarke in der Antwort verweist auf ein Dokument, das wirklich abgerufen wurde.
- Jede Zahl in einer Zusammenfassung kommt in der Quelle vor.
- Die Antwort nennt nie einen Wettbewerber.
- Eine Übersetzung erhält alle Ziffernfolgen.
- Die Ausgabe enthält nicht den rohen System-Prompt.
Das sind die wertvollsten billigen Prüfungen. Denn sie sind referenzfrei. Sie laufen auch auf Produktionsverkehr, wo es keine Gold-Antwort gibt. Siehe Online- und Produktions-Evaluation.
Zustandsprüfungen der Umgebung
Für alles, was auf die Welt einwirkt, ist die deterministische Prüfung die entscheidende.
Befund: Anthropics Leitlinie zu Agenten-Evals stellt das an erste Stelle: „A flight agent can say 'your flight is booked' while the database shows no reservation. For agent products, the database state is usually more important than the final sentence.“
Bewerten Sie die Zeile in der Tabelle, die Datei auf der Platte, die Testsuite, die jetzt durchläuft. Die Prosa bewerten Sie danach, wenn überhaupt. Agenten-Evaluation baut das zu einem vollständigen Vorgehen aus.
Wo code-basierte Bewertung versagt
Code-Bewertung ist keine Antwort auf alles. Wer das Gegenteil behauptet, erzeugt einen eigenen Fehlermodus: eine Eval-Suite, die komplett grün ist, während die Nutzer unzufrieden sind.
Gültige Variation gilt als falsch. Zwei korrekte Zusammenfassungen haben kaum ein Token gemeinsam. Exakter Abgleich bestraft eine bessere Antwort dafür, dass sie anders formuliert ist. So täuscht ein code-basiertes Eval am häufigsten.
Die gewünschte Eigenschaft lässt sich nicht ausdrücken. „Ist diese Erklärung für Laien verständlich?“ hat keine Assertion.
Überspezifizierte Referenzen zementieren eine Implementierung. Verlangt Ihr Referenz-Tool-Aufruf Argumente in einer bestimmten Reihenfolge? Verlangt Ihr Referenz-SQL eine bestimmte Join-Strategie? Dann messen Sie Übereinstimmung mit Ihrer ersten Lösung, nicht Korrektheit.
Die Abhilfe ist nicht, code-basierte Bewertung aufzugeben. Die Abhilfe ist Ehrlichkeit: Welche Prüfungen sind notwendige Bedingungen, welche hinreichende? Eine Schemaprüfung ist notwendig und nie hinreichend. Kombinieren Sie eine billige notwendige Prüfung mit einem Judge für den hinreichenden Teil. Dann haben Sie Determinismus, wo er möglich ist, und Urteilsvermögen nur dort, wo es nötig ist.
Häufiger Fehler: Einen LLM-Judge für etwas einsetzen, das eine richtige Antwort hat. „Ist das der korrekte ISO-Ländercode?“ mit einem Sprachmodell zu bewerten, ist langsamer, teurer, nicht deterministisch und ungenauer als ein Nachschlagen in einer Tabelle. Das passiert am häufigsten, wenn ein Team eine Metrik-Bibliothek übernimmt. Deren judge-basierte Metriken sind fertig mitgeliefert, also nutzt das Team sie für alles.