Handbücher · Eval Engineering

Build: Die Suite

Lektion 7/910 Min. LesezeitProfi

Lernziele

  • Du lässt dein Golden Set mit einem Skript laufen, das Code-Checks und einen Judge kombiniert.
  • Du kalibrierst den Judge an zehn eigenen Labels und liest jede Abweichung.
  • Du entscheidest, ab wann ein Framework wie promptfoo oder Inspect mehr bringt als die eigene Datei.

Aus Build: Traces lesen hast du golden.jsonl. Diese Lektion macht daraus einen Testlauf: eine einzige Python-Datei, ohne Framework. Code-Checks bewerten alles, was sie bewerten können. Ein enger Judge · engl. LLM as a judgeEin Sprachmodell, das die Ausgabe eines anderen Modells nach einer festen Frage bewertet, etwa ob eine Antwort auf der gefundenen Notiz beruht. Ein Judge ist ein Messgerät und muss gegen eigene Labels geprüft werden.Im Glossar übernimmt den Rest, wie in LLM als Judge beschrieben. Das getestete Produkt ist der Support-Assistent aus app.py. Für dein eigenes Produkt tauschst du run_target gegen den Aufruf deiner Anwendung.

Das Skript

Speichere die Datei als eval_suite.py neben app.py. Sie braucht pip install anthropic (getestet mit 2.8.0) und die Umgebungsvariable ANTHROPIC_API_KEY. Der Judge läuft absichtlich auf einem anderen Modell als das Produkt.

import argparse
import json
import re
import sys

import anthropic

from app import answer  # das Produkt unter Test, siehe app.py

client = anthropic.Anthropic()

JUDGE_MODEL = "claude-sonnet-5-5"  # bewusst ein anderes Modell als das Produkt

JUDGE_PROMPT = """You are grading one output from an invoicing assistant.

Statement to verify:
{expectation}

Assistant output:
<output>
{output}
</output>

First write one or two sentences of reasoning. Then, on the final line, write
exactly VERDICT: pass if the statement is true of the output, or VERDICT: fail
if it is not. Do not reward length, politeness, or effort.
"""


def run_target(case: dict) -> str:
    return answer(case["input"])


def grade_code(output: str, checks: dict) -> tuple[bool, str]:
    for needle in checks.get("must_contain", []):
        if needle.lower() not in output.lower():
            return False, f"missing required text: {needle!r}"
    for needle in checks.get("must_not_contain", []):
        if needle.lower() in output.lower():
            return False, f"contains forbidden text: {needle!r}"
    return True, "code checks passed"


def grade_judge(output: str, expectation: str) -> tuple[bool, str]:
    response = client.messages.create(
        model=JUDGE_MODEL,
        max_tokens=4000,
        output_config={"effort": "low"},
        messages=[{
            "role": "user",
            "content": JUDGE_PROMPT.format(expectation=expectation, output=output),
        }],
    )
    text = "".join(b.text for b in response.content if b.type == "text")
    match = re.search(r"VERDICT:\s*(pass|fail)", text, re.IGNORECASE)
    passed = bool(match) and match.group(1).lower() == "pass"
    return passed, text.strip().splitlines()[0]


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("golden", help="path to golden.jsonl")
    parser.add_argument("--limit", type=int, default=0, help="run only the first N cases")
    parser.add_argument("--threshold", type=float, default=0.0, help="exit 1 below this pass rate")
    args = parser.parse_args()

    with open(args.golden, encoding="utf-8") as f:
        cases = [json.loads(line) for line in f if line.strip()]
    if args.limit:
        cases = cases[: args.limit]

    results = []
    for case in cases:
        output = run_target(case)
        if case.get("checks"):
            passed, reason = grade_code(output, case["checks"])
        else:
            passed, reason = grade_judge(output, case["judge_expectation"])
        results.append({"id": case["id"], "passed": passed, "reason": reason, "output": output})
        print(f"{'PASS' if passed else 'FAIL'}  {case['id']}  {reason}")

    rate = sum(r["passed"] for r in results) / len(results)
    print(f"\npass rate: {rate:.0%}")
    with open("results.json", "w", encoding="utf-8") as f:
        json.dump({"pass_rate": rate, "results": results}, f, indent=2)
    if args.threshold and rate < args.threshold:
        sys.exit(1)


if __name__ == "__main__":
    main()

Zeilen 8-12

Das Produkt kommt aus app.py und läuft auf Opus 5.5. Der Judge ist Sonnet 5.5, also ein anderes Modell. So bewertet sich das Produkt nicht selbst.

Starte die Suite mit python eval_suite.py golden.jsonl. Sie gibt eine Zeile pro Fall aus, schreibt results.json und endet mit --threshold bei einem schlechten Lauf mit einem Code ungleich null.

Unsere drei ersten Läufe gegen das Beispielprodukt ergaben 52, 48 und 52 Prozent. Die Code-Checks fanden die zwei häufigsten Fehlerarten in jedem Lauf in 8 von 10 Fällen wieder. Ein Lauf brauchte 66 bis 69 Sekunden für 21 Aufrufe des Produkts und 6 Aufrufe des Judges. Dann haben wir den Prompt in app.py um drei Sätze ergänzt:

    "Answer only the question that was asked. Mention other invoices, cancelling or "
    "refunds only when the user asks about them. Do not guess beyond the context, "
    "not even with 'may' or 'usually'.\n\n"
Eine Runde Schwungrad (eigene Läufe mit Opus 5.5 und Sonnet 5.5, 14.11.2026, gekürzt)1/7
  1. $python eval_suite.py golden.jsonl

Die glatten Fälle bestanden auch nach der Änderung. Der Assistent erklärt das Stornieren also weiterhin, wenn jemand danach fragt. 100 Prozent bei 21 Fällen heißt aber nur: Diese Suite findet nichts mehr. Neue Traces zeigen die nächsten Fehler, und damit beginnt die nächste Runde aus Warum Evals.

Den Judge kalibrieren

Bevor du dem Judge traust, bewertest du zehn Ausgaben selbst. Leg dazu labels.jsonl an, eine Zeile pro Ausgabe mit id, output, expectation und deinem eigenen label (true oder false). Die Ausgaben ziehst du aus results.json. Dann misst du die Übereinstimmung:

import json

from eval_suite import grade_judge

with open("labels.jsonl", encoding="utf-8") as f:
    labels = [json.loads(line) for line in f if line.strip()]

agree = 0
for row in labels:
    verdict, reason = grade_judge(row["output"], row["expectation"])
    agree += verdict == row["label"]
    if verdict != row["label"]:
        print(f"disagree on {row['id']}: judge={verdict} you={row['label']} - {reason}")
print(f"agreement: {agree}/{len(labels)}")

Lies jede Abweichung. Meist ist der Satz in judge_expectation schwammig, manchmal dein eigenes Label. Bei uns kam beides zusammen:

Kalibrierung an zehn eigenen Labels (14.11.2026)1/7
  1. $python calibrate.py

Beide Abweichungen betrafen denselben Satz: „makes no general claim about where bank details are usually printed“. Die Antwort sagte, das Rechnungsdokument „may“ die Bankdaten enthalten. Wir hatten das als zulässig gelabelt, der Judge nicht. Beim Lesen wurde klar: Auch ein Hinweis mit „may“ geht über den Kontext hinaus, und genau das soll das Produkt lassen. Also haben wir den Erwartungssatz geschärft („does not suggest, not even with ‘may’ or ‘usually’, where else they could be found“) und vier Labels auf false gesetzt.

Zähl die Übereinstimmung pro Klasse. In der ersten Runde stimmte der Judge bei unseren 7 Ausgaben mit true fünfmal zu und bei den 3 mit false dreimal. Rohe 8 von 10 verdecken, dass alle Fehler auf einer Seite lagen. Zehn Labels sind die Version für einen Nachmittag. Das volle Verfahren mit mehr Labels beschreibt LLM als Judge, und die Kurslektion Evals in der Praxis setzt die Grenze bei acht von zehn: Darunter überarbeitest du die Rubrik.

Wann sich ein Framework lohnt

Diese Datei reicht für ein Produkt, einen Judge und einige hundert Fälle. Zu promptfoo greifst du, wenn du mehrere Prompts oder Modelle nebeneinander vergleichen willst, ohne die Matrix selbst zu schreiben. Inspect lohnt sich, wenn du Sandboxen für Agenten, Solver und eine Bewertung pro Schritt brauchst. Beide stellt Eval-Werkzeuge vor. Beide lassen deine Suite in größerem Maßstab laufen. Die Arbeit an der Taxonomie bleibt bei dir.

Merksätze

  • Eine Python-Datei genügt für den Anfang: Code-Checks, wo es geht, ein enger Judge für den Rest, ein Exit-Code für die CI.
  • Der Judge läuft auf einem anderen Modell als das Produkt. Bei Modellen mit Nachdenken braucht er Platz in max_tokens.
  • Vor dem ersten Einsatz bewertest du zehn Ausgaben selbst und liest jede Abweichung.
  • Abweichungen zeigen schwammige Erwartungssätze oder uneinheitliche eigene Labels, oft beides.
  • 100 Prozent heißt, dass die Suite nichts mehr findet. Neue Traces liefern die nächsten Fälle.

Prüf dich selbst

Denkfrage

Warum läuft der Judge auf einem anderen Modell als das Produkt?

Denkfrage

Der Judge stimmt in 8 von 10 Fällen mit dir überein. Reicht das?

Prüf dich selbst

  1. 1Ein Fall in golden.jsonl hat checks und judge_expectation ist null. Was macht eval_suite.py?
  2. 2Warum steht max_tokens beim Judge auf 4.000?
  3. 3Die Kalibrierung zeigt zwei Abweichungen. Was tust du zuerst?
  4. 4Wann lohnt sich promptfoo mehr als die eigene Datei?

Selbst nachweisen

0 von 4 erledigt

Quellen

  1. Using LLM-as-a-Judge For Evaluation: A Complete Guide · Hamel Husain, Okt. 2024, aktualisiert Sept. 2026

    Kalibrierung an Labels einer Fachperson, Raten pro Klasse.

  2. Effort · Anthropic, Stand: Nov. 2026

    effort-Stufen und ihre Standardwerte.

Dein Fortschritt bleibt nur in diesem Browser gespeichert.