Warum “sieht richtig aus” kein Test fĂŒr einen Prompt ist

Sie schĂ€rfen die Formulierung eines Prompts, der Tickets routet, testen ihn an den drei Beispielen, die in Ihrem Scratch-Notebook offen sind, und bringen ihn live. Eine Woche spĂ€ter landen Rechnungsbeschwerden, die frĂŒher in der richtigen Warteschlange ankamen, plötzlich bei “Frage”. Nicht die drei FĂ€lle, die Sie geprĂŒft haben: die anderen vierhundert, die Sie nicht geprĂŒft haben. Nichts stĂŒrzt ab. Es gibt keinen Stacktrace. Der Prompt klingt, laut vorgelesen, immer noch gut.

Code-Review und ein paar manuelle Checks ĂŒbersehen das, weil derselbe Prompt beim selben Input nicht zweimal denselben Text liefert. Sie können zwei DurchlĂ€ufe nicht Byte fĂŒr Byte vergleichen und eine Abweichung als Bug bezeichnen, wie Sie es bei einer reinen Funktion tun wĂŒrden. Was Sie prĂŒfen können, ist, ob jede Ausgabe die Eigenschaften hat, die Sie interessieren: die richtige Kategorie, eine Zusammenfassung unter dem LĂ€ngenlimit, kein Feld, das die Quelle nie erwĂ€hnt hat. Ein Eval fĂŒhrt diese Checks bei jeder Änderung von Prompt oder Modell ĂŒber eine Menge echter Inputs aus, sodass die Regression in Ihrem Terminal auffĂ€llt statt im Ticket eines Kunden.

Was sind die Teile eines LLM-Evals

Jedes Eval, egal mit welchem Tool, hat drei Teile: ein Dataset reprĂ€sentativer Inputs, eine Assertion, die jede Ausgabe gegen das prĂŒft, was der jeweilige Input eigentlich produzieren sollte, und eine Pass-Rate, die Sie von Durchlauf zu Durchlauf verfolgen.

Stellen Sie sich das als Testsuite fĂŒr eine Funktion vor, die jedes Mal einen leicht anderen Satz zurĂŒckgibt. Die Assertion kann keine exakte Übereinstimmung prĂŒfen, also prĂŒft sie eine Eigenschaft des Satzes: Nennt er die richtige Kategorie, lĂ€sst sich die Zahl darin parsen. Sobald Sie akzeptieren, dass der Test prĂŒft “gilt das” statt “ist das identisch”, ist das Schreiben eines solchen Tests gewöhnliches Engineering.

Wie Sie ein Golden-Dataset fĂŒr ein LLM-Eval aufbauen

Nehmen Sie einen Prompt, der eine rohe Support-E-Mail in ein strukturiertes Ticket verwandelt:

1
2
3
4
5
6
7
8
9
TRIAGE_PROMPT = """Read this support email and return JSON with exactly these keys:
- category: one of "billing", "bug", "question"
- summary: the complaint in 12 words or fewer
- urgent: true if the customer can't use the product right now, else false

Email:
{email_text}

Return only the JSON object, no other text."""

Das Golden-Dataset ist eine Menge echter E-Mails, gepaart mit der Antwort, die Sie bereits als korrekt kennen. Echt, nicht erfunden, damit der Prompt gut aussieht:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
golden_set = [
    {
        "email": "I was charged twice for my October invoice, can you refund the duplicate?",
        "category": "billing",
        "urgent": False,
    },
    {
        "email": "The app crashes every time I try to export a PDF, I have a deadline in an hour.",
        "category": "bug",
        "urgent": True,
    },
    {
        "email": "Does the Pro plan include API access, or do I need Enterprise?",
        "category": "question",
        "urgent": False,
    },
]

FĂŒnf bis zehn FĂ€lle reichen zum Start. Die GrĂ¶ĂŸe zĂ€hlt weniger als die Herkunft der spĂ€teren FĂ€lle: echte FehlschlĂ€ge aus der Produktion.

Wie Sie deterministische Assertions fĂŒr LLM-Ausgaben schreiben

Das meiste, was bei diesem Prompt schiefgeht, hat nichts mit Geschmack zu tun. Das Modell liefert fehlerhaftes JSON, erfindet eine vierte Kategorie, oder schreibt vierzig Wörter, wo Sie zwölf verlangt haben. Normaler Code fÀngt das alles ab, ohne ein zweites Modell:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import json

def run_triage_case(case, call_model):
    raw = call_model(TRIAGE_PROMPT.format(email_text=case["email"]))
    try:
        output = json.loads(raw)
    except json.JSONDecodeError:
        return {"passed": False, "reason": "not valid JSON"}

    if output.get("category") not in {"billing", "bug", "question"}:
        return {"passed": False, "reason": f"bad category: {output.get('category')!r}"}

    if len(output.get("summary", "").split()) > 12:
        return {"passed": False, "reason": "summary over 12 words"}

    if not isinstance(output.get("urgent"), bool):
        return {"passed": False, "reason": "urgent is not a boolean"}

    if output["category"] != case["category"]:
        return {"passed": False, "reason": f"expected {case['category']}, got {output['category']}"}

    return {"passed": True, "reason": None}

FĂŒhren Sie das ĂŒber das gesamte Dataset aus, und Sie erhalten eine Pass-Rate, kein einfaches Ja oder Nein:

1
2
results = [run_triage_case(c, call_model) for c in golden_set]
pass_rate = sum(r["passed"] for r in results) / len(results)

Diese Checks kosten nichts und schwanken nie zufÀllig: dieselbe schlechte Ausgabe schlÀgt jedes Mal auf dieselbe Weise fehl. Schreiben Sie sie zuerst. Sie fangen die meisten echten Regressionen ab, etwa eine SchemaÀnderung oder eine Zusammenfassung, die sich unbemerkt verdoppelt hat.

Wann Sie LLM-as-Judge einsetzen und wie Sie es kalibrieren

Kategorie und Dringlichkeit sind objektiv, und das Dataset enthĂ€lt bereits die richtige Antwort. Die QualitĂ€t der Zusammenfassung nicht. “RĂŒckerstattung nötig, doppelte Abbuchung” und “Kunde hat eine Rechnungsfrage” sind beide gĂŒltiges JSON, beide unter 12 Wörtern, und nur eine der beiden beschreibt wirklich die E-Mail. Keine Regex fĂ€ngt das ab, also bewertet ein zweites Modell das anhand einer Rubrik:

1
2
3
4
5
6
7
8
9
JUDGE_PROMPT = """Email: {email}
Summary: {summary}

Does the summary accurately capture the main complaint in the email?
Answer with only "yes" or "no"."""

def judge_summary(email, summary, judge_model):
    verdict = judge_model(JUDGE_PROMPT.format(email=email, summary=summary), temperature=0)
    return verdict.strip().lower() == "yes"

Bevor Sie diesem Urteil ĂŒber das ganze Dataset vertrauen, lassen Sie den Judge auf 15-20 FĂ€llen laufen, die eine Person schon von Hand bewertet hat, und zĂ€hlen Sie, wie oft sie ĂŒbereinstimmen. Ein Judge, der bei jedem dritten Fall von Menschen abweicht, ist eine schwache Assertion, die sich als Modell-AutoritĂ€t ausgibt.

Zwei Verzerrungen lohnt es sich zu beobachten. Judges bewerten lĂ€ngere, ausgefeiltere Antworten tendenziell besser, selbst wenn eine kĂŒrzere genauso richtig ist. Und ein Modell, das seine eigenen Ausgaben bewertet, bewertet sie gĂŒnstiger, als es ein unabhĂ€ngiges Modell tĂ€te. Nutzen Sie, wo möglich, ein anderes Modell als Judge, und halten Sie den Prompt eng: eine Ja/Nein-Frage pro Aufruf, nicht fĂŒnf Bewertungen in einer Anfrage.

Deterministische AssertionLLM-as-Judge
Kosten pro FallKostenlosEin zusÀtzlicher Modellaufruf
Was sie abfÀngtSchema, Format, bereits im Dataset bekannte FaktenSubjektive QualitÀt, Relevanz, Ton
WiederholbarkeitGleiche Ausgabe, gleiches UrteilEtwas Drift selbst bei Temperatur 0
EinrichtungskostenCheck schreiben, fertigBraucht vorherige Kalibrierung an von Menschen bewerteten Beispielen

In Begriffen der Testsuite sind die deterministischen Checks Ihre Unit-Tests: schnell und exakt, und sie laufen bei jeder Änderung mit. Der Judge kommt einem Abnahme-Check nĂ€her, dem, was eine Person sonst durch Lesen jeder Ausgabe machen wĂŒrde. Die AbwĂ€gungen zwischen diesen Ebenen sind dieselben wie in Unit-Tests vs Integrationstests vs End-to-End-Tests.

Wie Sie LLM-Evals in CI laufen lassen, um Regressionen abzufangen

Ein Eval, das nur lĂ€uft, wenn jemand daran denkt, fĂ€ngt nichts ab. Legen Sie das Skript neben den Prompt, den es testet (evals/triage.py zusammen mit prompts/triage.py), und lassen Sie es in CI bei jeder Änderung an einer der beiden Dateien laufen, vor dem Merge. Verfolgen Sie die Pass-Rate ĂŒber die Zeit und lassen Sie den Build fehlschlagen, wenn sie unter eine von Ihnen festgelegte Baseline fĂ€llt. Ein Abfall von 95 % auf 88 % ist eine Regression, die eine Blockade wert ist, auch wenn die meisten FĂ€lle noch durchlaufen.

Halten Sie einen Regressionssatz innerhalb des Datasets: jeder Fall, der einmal ein gemeldeter Bug war, nach der Behebung dauerhaft festgenagelt. Bei einem Prompt gibt Ihnen das den Kreislauf, der in was TDD eigentlich ist beschrieben wird. Prompt Ă€ndern, Suite neu laufen lassen, genau sehen, welche Assertions umgeschlagen sind, korrigieren, wiederholen. FĂŒttern Sie das Dataset mit neuen Produktionsfehlern, sobald sie passieren, damit Sie nicht nur die Inputs testen, auf die der Prompt ursprĂŒnglich ausgelegt war.

Wie Sie Tool-Calling- und RAG-Pipelines evaluieren

Dieselben drei Teile ĂŒbertragen sich auch hierher. Bei einer Tool-Calling-Schleife paart das Dataset jeden Input mit dem richtigen Tool-Namen und den richtigen Argumenten, und die Assertion prĂŒft, ob das Modell dieses Tool mit ausgefĂŒllten Pflichtfeldern gewĂ€hlt hat. Die Mechanik der Schleife selbst steht in Function Calling und Tool Use bei LLMs.

Eine RAG-Pipeline braucht zwei getrennte Scores. Hat das Retrieval den Chunk mit der Antwort geholt? Hat die Generierung diesen Chunk korrekt genutzt? Bewerten Sie beides zusammen, können Sie nicht sagen, welche HĂ€lfte kaputt ist. Siehe wie RAG funktioniert fĂŒr die Pipeline, die Sie da auftrennen.

HĂ€ufige Fehler bei LLM-Evals

Das Dataset aus FĂ€llen bauen, die schon bestehen. FĂŒnf Beispiele, die Sie gewĂ€hlt haben, weil der Prompt sie gut handhabt, bestehen immer und sagen Ihnen nichts ĂŒber den Input, der nĂ€chsten Monat alles zum Scheitern bringt. Jeder echte Fehlschlag, der Sie erreicht, gehört noch am selben Tag ins Dataset.

Exakte Übereinstimmung auf variabler Ausgabe prĂŒfen. summary == "RĂŒckerstattung nötig wegen doppelter Abbuchung" schlĂ€gt in dem Moment fehl, in dem das Modell ein einziges Wort Ă€ndert, selbst wenn die neue Formulierung genauso korrekt ist. PrĂŒfen Sie die Eigenschaft, nicht den Text: die Wortzahl, die Kategorie, ob eine erforderliche Tatsache auftaucht.

Die Judge-Kalibrierung auslassen. Ein Judge, den Sie nie auch nur mit einem Dutzend von Menschen bewerteter Beispiele verglichen haben, liefert Ihnen seine eigenen Verzerrungen als Wahrheit, einschließlich der, die seine eigene Ausgabe begĂŒnstigt.

Nur den Fall erneut laufen lassen, den Sie gerade behoben haben. So kommt ein alter Bug unbemerkt zurĂŒck. Genau dafĂŒr gibt es den Regressionssatz.

Wann sich eine LLM-Eval-Suite nicht lohnt

Ein Prompt, den Sie nur einmal laufen lassen, oder den eine Person bei jedem Aufruf prĂŒft, braucht keinen Harness. Setzen Sie den JSON-GĂŒltigkeitscheck inline, wenn er billig ist, und machen Sie weiter. Die Suite zahlt sich aus, sobald ein Prompt wiederholt an Nutzer ausgeliefert wird oder Sie ihn mehr als ein paar Mal Ă€ndern wollen. Das ist der Moment, in dem “sah an meinen drei Beispielen gut aus” nicht mehr reicht.

Zum Start: fĂŒnf echte Inputs, eine deterministische Assertion pro Eigenschaft, die Sie interessiert, und ein LLM-Judge nur fĂŒr die Dimension, die Code nicht prĂŒfen kann. Lassen Sie es vor Ihrer nĂ€chsten Prompt-Änderung laufen, nicht nach der Beschwerde.

Verwandte Artikel