Alle Notizen
18. Mai 202611 Min. Lesedauerai-agentsevalsllmtestingengineering

Temperature zero wird dich nicht retten

Dieselbe Frage an denselben Agenten am selben Tag liefert eine andere Antwort. Dein Test-Framework hat das noch nicht aufgeholt.

2025 verbrachte ein Entwickler in einem Microsoft-Community-Forum zwei Tage damit, einen Copilot-Studio-Agenten zu debuggen, der "zufällig" aufhörte, das richtige Topic zu triggern. Der Agent war unverändert. Der Prompt war unverändert. Das verbundene SharePoint-Dokument war von einer Kollegin "zur Lesbarkeit" bearbeitet worden, und die Triggerphrase, auf die der Agent gematcht hatte, war leise verschwunden. Es gab keinen fehlschlagenden Test. Es gab keinen Alarm. Es gab einen Nutzer, der dieselbe Frage wie letzte Woche tippte und eine höflich nutzlose Antwort bekam. Das ist die Form jedes Agent-Fehlers, den ich in Produktion gesehen habe. Still, quellenlos und immun gegen jeden Test, der auf Output-Strings assertet.

Ein Agent-Test ist eine Hypothese über eine Verhaltensverteilung, bewertet an einer statistischen Pass Rate, nicht eine Assertion auf einen String. Die Annahme "gleicher Input, gleicher Output", auf der dreißig Jahre Softwaretests ruhten, ist weg, und die Pass Condition muss ihre Form mit ihr ändern. Ich habe den strategischen Fall für die Eval Bank als überlebendes Artefakt in Deine Eval Suite ist der Agent, nicht das Modell ausgearbeitet. Dieser Beitrag ist der technische Begleiter: warum selbst Temperature zero dich nicht rettet und wie das YAML tatsächlich aussieht, sobald du aufhörst, so zu tun, als könnte sie es.

In diesem Artikel


Temperature zero wird dich nicht retten

Warum Temperature zero kein Fix ist

Der Standardreflex ist, Temperature auf null zu setzen, die Assertion zu schreiben und weiterzumachen. Ich habe genau diesen Code geschrieben. Er ist falsch, und er ist auf eine Art falsch, die es lohnt zu verstehen, weil das Missverständnis strukturell ist, nicht schlampig. Temperature ist ein Sampling-Parameter auf der Output-Verteilung. Setzt du sie auf null, kollabiert Sampling zu Greedy Decoding: bei jedem Schritt gewinnt das Token mit der höchsten Wahrscheinlichkeit. Zwei identische Prompts bei Temperature zero sollten im Prinzip identische Token-Streams produzieren.

In der Praxis tun sie das nicht. Drei Mechanismen brechen die Garantie.

Der erste ist, dass Mixed-Precision Inference (BF16- oder FP16-Weights und -Activations) bei jeder Matrixmultiplikation Rundungsfehler einführt. Zwei Prompts, die ein Präfix teilen, können an einer Position mit unterschiedlichen "höchstwahrscheinlichen" Tokens landen, an der die Top-zwei-Kandidaten durch einen Abstand getrennt sind, der kleiner als das Präzisionsfloor ist. Das Paper Understanding and Mitigating Numerical Sources of Nondeterminism in LLM Inference misst die Rate, mit der das über Llama-, Qwen- und DeepSeek-Modellfamilien hinweg passiert, und stellt fest, dass das nicht selten ist.

Der zweite ist Batch-Nichtassoziativität. Wenn Inference-Server deinen Request für Durchsatz mit Requests anderer Tenants batchen, ändert sich die Reihenfolge der Floating-Point-Additions in jedem Kernel mit der Batch Size. Die Thinking-Machines-Lab-Analyse verfolgt das direkt: gleicher Input, andere Batch-Komposition am Server, anderer Output. Du hast aus deinem Test heraus keine Kontrolle über die Batch-Komposition. Non-Determinism of "Deterministic" LLM Settings misst die resultierende Varianz über Anbieter hinweg im "deterministischen" Setting und findet sie überall ungleich null.

Der dritte sind stille Vendor-Updates. Die Weights hinter gpt-4o oder claude-sonnet-4-7 oder einem beliebigen kommerziellen Endpoint sind dienstags im Allgemeinen nicht dieselben wie montags. Selbst pinned-version-Endpoints bekommen Fine-Tuning-Patches, Safety-Updates und Kernel-Level-Optimierungen, die Outputs am Rand verschieben. Dein "deterministischer" Test reproduziert einen Snapshot der Vendor-Infrastruktur, der nicht mehr existiert.

Hör auf, für Determinism zu kämpfen. Determinism ist nicht die Eigenschaft, die du eigentlich willst. Die Eigenschaft, die du willst, ist eine stabile, messbare Verteilung des Verhaltens über eine Test Bank, mit einer bekannten Akzeptanzschwelle und genug Samples, damit diese Schwelle etwas bedeutet. Das ist ein anderes Artefakt, und es ist das, das überlebt.

💡 Ein Agent-Test ist eine Hypothese über eine Verhaltensverteilung, bewertet an einer statistischen Pass Rate, nicht eine Assertion auf einen String.

Die vier Schichten, mit anderer Pass Condition

Die Zerlegung, die ich verwende, sind Nivian Foss' vier Schichten (Prompt und Intent, Knowledge und Grounding, Actions und Connectors, Conversation Flow), die der Schwester-Beitrag vollständig auslegt. Der Punkt, den ich hier wiederholen will, ist der, der spezifisch für Nicht-Determinismus ist: die Kategorien überleben den Wechsel aus dem Software-Testing, die Pass Condition tut es nicht. Du assertest nicht mehr, dass agent.respond("how much does it cost?") gleich einem fixen String ist. Du assertest, dass dieselbe Phrase über N Samples das Pricing-Topic in einem ausreichend hohen Anteil der Runs feuert, sodass die Suite eine Regression auffängt, die dich interessiert. Die Einheit wird statistisch, und die Schwelle muss gegen die Varianz der Metrik dimensioniert werden, nicht nach Gefühl gewählt.

Kleines N ist ehrlich, aber kein Gate

Hier ist der Teil, den die meisten Eval-Write-ups überspringen. Wenn Trigger-Accuracy bei einem einzelnen Case auf demselben Tag über aufeinanderfolgende Fünf-Sample-Läufe zwischen 80 % und 96 % springt, dann ist eine 0,95-Schwelle auf einem Fünf-Sample-Lauf statistisches Theater. Mit N=5 sind die erreichbaren Pass Rates 0, 0,2, 0,4, 0,6, 0,8, 1,0. Mit N=3 sind es 0, 0,33, 0,66, 1,0. Ein pass_threshold: 0.66 auf drei Samples ist "2 von 3 müssen bestehen" mit zusätzlichen Nachkommastellen. Der Bruchteil lässt es prinzipientreu aussehen. Ist er nicht.

Es gibt zwei ehrliche Züge. Der erste ist, N substanziell zu erhöhen, in den 20-bis-50-Samples-pro-Case-Bereich, wenn ein Case tatsächlich ein Release gated, und die Kosten zu akzeptieren, und ein Konfidenzintervall zu berichten statt einer einzelnen Quote. Der zweite ist, kleines N für ein Entwicklungssignal beizubehalten und diese Bänke explizit als "richtungsweisend, nicht gate-tauglich" zu labeln. Beides ist okay. So zu tun, als wäre eine Fünf-Sample-Pass-Rate von 0,95 ein Release Gate, ist es nicht. Das YAML unten zeigt die Kleines-N-Variante, weil die meisten Teams damit starten; behandle die Schwellen als Entwicklungssignal, bis N wächst.

Wie der Eval Loop in der Praxis aussieht

Der kleinste glaubwürdige Eval Loop für einen Copilot-Studio-Agenten oder ein Äquivalent, dieselbe Form funktioniert für LangSmith, Promptfoo oder ein handgeschriebenes Python-Skript gegen das Anthropic SDK, startet mit der Bank.

Anthropics Demystifying evals (Januar 2026) schlägt 20 bis 50 Cases aus echten Failures als richtiges Floor vor. Foss akzeptiert 10 bis 15 als praktisches Minimum für einen neuen Agenten. Ich nutze 12 als Starter für einen Drei-Topic-Agenten, weil das jedes Topic dreifach auf dem Happy Path plus einen Flow Case, einen adversarialen Case und Unhappy Paths abdeckt. Aber die Bank sollte in der Woche, in der der Agent ausgeliefert wird, auf 20 bis 50 wachsen. Baue die Bank aus drei Quellen: bekannte Produktionsfehler (die signalstärksten Cases, die du je schreiben wirst), Synonyme und Akronyme von High-Traffic-Phrasen und ein adversarialer Satz, generiert von einem Nutzer, der den Agenten nie gesehen hat.

# evals/prospect-agent.yaml
cases:
  - id: pricing-direct
    layer: prompt-intent
    input: "How much does the enterprise plan cost?"
    expected_topic: pricing-and-plans
    samples: 5
    min_passes: 5            # 5/5: High-Traffic-Phrase, null Toleranz
  - id: pricing-typo
    layer: prompt-intent
    input: "wht does the enterprize plan cost"
    expected_topic: pricing-and-plans
    samples: 5
    min_passes: 4            # 4/5: Typo-Pfad, einen Miss tolerieren
  - id: grounding-fresh
    layer: knowledge-grounding
    input: "What is the current enterprise tier price?"
    expected_citation_url_contains: "/pricing-2026"
    samples: 3
    min_passes: 3            # 3/3: Citation-Korrektheit ist binär
  - id: connector-demo
    layer: actions-connectors
    input: "I want to book a demo for next Tuesday"
    expected_action: book_demo
    expected_parameters:
      meeting_type: "demo"
      preferred_day_set: true
    samples: 3
    min_passes: 3            # 3/3: Action-Kontrakt ist binär
  - id: flow-unhappy
    layer: conversation-flow
    input_sequence:
      - "how much does it cost?"
      - "I don't like that answer"
      - "actually never mind"
    expected_outcome: graceful_exit
    samples: 3
    min_passes: 2            # 2/3: nur richtungsweisend; N erhöhen, bevor gegated wird

Jeder Case benennt seine Schicht, deklariert die zu prüfende strukturelle Eigenschaft und setzt die Pass Condition als Ganzzahl erfolgreicher Samples. Ganzzahlen, weil N klein ist. Der Runner sampled den Agenten N-mal pro Case, bewertet jedes Sample mit dem passenden Grader (Exact Match auf den Topic-Namen, URL-Substring für Citations, Schema-Check für Connector-Parameter, LLM-as-Judge für Outcome-Level-Flow-Checks) und berichtet Per-Case-Pass-Counts plus ein Aggregat. Bei 200+ Cases splittest du das YAML nach Schicht (evals/layer-1-prompt-intent.yaml und drei Geschwister); Single-File-Lesbarkeit bricht um die 50-Case-Marke zusammen.

LLM-as-Judge verdient eine saubere Einschränkung. Das Zheng et al. NeurIPS-Paper ist die Zitation, die die Kategorie legitimiert: im MT-Bench-Setup erreichte GPT-4 ungefähr 80 % Agreement mit menschlichen Ratern, vergleichbar mit Inter-Human-Agreement bei denselben Aufgaben. Dieses Ergebnis ist ein nützlicher Proof of Concept, keine domänenübergreifende Garantie; die Agreement-Rate auf deiner Domäne wird anders ausfallen, oft substanziell. Eugene Yans Follow-up ist die Zitation, die es ehrlich hält: ein Judge kann Prozessvernachlässigung nicht kompensieren. Nutze einen Judge für Outcomes, die sich programmatischen Checks entziehen (war die Antwort relevant? war der Ton angemessen?), und paare ihn mit einem kleinen kalibrierten Human-Review-Set bei jedem Lauf, sodass der Judge selbst gegen deine Domäne benotet wird.

Worked Example: der Prospect-Engagement-Agent

Der Demo-Agent, den Foss in ihrem Talk durchgeht, ist ein Copilot-Studio-Prospect-Engagement-Assistant für eine fiktive Firma namens Lummetra, ein kleines B2B-SaaS mit Pricing Page, Product Page und Demo-Booking-Flow. Der Agent macht im Scope drei Dinge: Produktfragen beantworten, einen Pricing Tier basierend auf Teamgröße empfehlen und eine Demo buchen, indem er die nötigen Parameter sammelt. Drei Topics: product-info, pricing-and-plans, book-demo.

Die Starter-Bank umfasst zwölf Cases. Drei pro Topic auf dem Happy Path (eine direkte Phrasierung, ein Synonym, ein Typo). Zwei auf dem Unhappy Path über Topics hinweg (Nutzer ändert seine Meinung, Nutzer fragt out of scope). Ein Conversation-Flow-Case über alle drei Topics (Pricing → Products → Book). Ein adversarialer Case (Nutzer versucht, den Agenten dazu zu bringen, einen Wettbewerber zu diskutieren). Summe: zwölf Cases, jeder drei- bis fünfmal gesampled. Zwölf ist ein Floor, kein Ziel; treib das in der Woche, in der der Agent live geht, über 20.

Beim ersten Lauf gegen den Agenten, der Foss' Walkthrough nachspielt, scheitern zwei Cases. Die Typo-Pricing-Phrase feuert das product-info-Topic in 3 von 5 Samples statt Pricing. Der Connector-Test für book_demo lässt in einem von drei Samples den preferred_day_set-Parameter aus, weil der Agent entschieden hat, den Tag in einer Folgefrage zu bestätigen. Der erste Fehler ist eine Triggerphrase-Coverage-Lücke; der Fix ist, die Typo-Variante zu den Triggerphrasen für pricing-and-plans hinzuzufügen. Der zweite ist ein Fixture-Problem im Test: das erwartete Verhalten ist konversationell, und der Test hat zu früh assertet.

Das ist es, was zwölf Cases dir bringen. Zwei reale Findings, eines ein Agent Bug, eines ein Test Bug. Beide wert, behoben zu werden. Keines davon wäre durch manuelles Tippen in den Test Canvas aufgetaucht, weil der Test Canvas das erste Sample auffängt und weitergeht. Foss' Analytics-Dashboard hätte sie irgendwann aufgedeckt, in Produktion, nachdem Nutzer sie getroffen hätten.

Dieselbe YAML-Bank läuft ohne Änderungen aus einem Promptfoo-CLI-Step in einem GitHub-Actions-Workflow und aus einem Power-Automate-Flow innerhalb der Copilot-Studio-Power-Platform-Pipeline. Ich habe beides gegen denselben Agenten gefahren; die Per-Case-Pass-Counts stimmen innerhalb des Rauschfloors überein, den man bei N=5 erwartet. Die Bank ist das Artefakt. Die Runner sind austauschbar, was zählt, weil der Runner, mit dem du startest, selten der ist, mit dem du endest.

Wie du das in deinem eigenen Stack anwendest

Fünf konkrete Züge, keine Allgemeinplätze.

Erstens: benenne die vier Schichten explizit in deinem Repo. Lege evals/layer-1-prompt-intent.yaml und die drei Geschwister an, sobald die Bank ~50 Cases überschreitet. Die Schicht eines fehlschlagenden Tests ist die Schicht, in der der Fix landet; Schichten in einer Bank zu mischen, macht das Failure-zu-Fix-Routing schwerer.

Zweitens: setze Per-Case-Sample-Counts und Pass-Thresholds als Ganzzahlen, nie als globale Dezimalzahlen. Ein Typo-Case für ein Low-Traffic-Topic darf bei 3/5 bestehen; eine direkte Phrasierung für dein höchsttraffisches Topic sollte bei 5/5 bestehen. Eine einzelne globale Schwelle kollabiert den Unterschied.

Drittens: gate Publish an der Bank. Foss hat recht, dass "Tests müssen vor Publish bestehen", spezifiziert aber nicht, wie das durchgesetzt wird. Der Mechanismus ist die REST API oder ein Power-Automate-Flow; das Gate ist ein Power-Platform-Pipeline-Step. Gleiche Idee wie ein Braintrust PR Gate oder ein LangSmith-CI-Step. Salesforce Agentforce, ServiceNows Agent-Plattform und mehrere LangSmith-umwickelte No-Code-Tools haben ähnliche Primitives ausgeliefert; Copilot Studio ist hier nicht einzigartig, aber das, das ich End-to-End gefahren habe, weshalb dieser Beitrag sich darauf stützt. Der Plattformname ist Detail; das Gate ist Substanz.

Viertens: speichere die Per-Run-Ergebnisse. SharePoint funktioniert. Eine SQLite-Datei im Repo funktioniert. Der Punkt ist, dass ein heute bestandener Lauf nur dann nützlich ist, wenn du ihn mit dem bestandenen Lauf vom letzten Monat vergleichen und sehen kannst, ob die Trigger-Accuracy-Verteilung gedriftet ist. Ohne die Zeitreihe ist Regression Testing Wunschdenken.

Fünftens: plane pro Sprint eine 45-minütige explorative Session mit einem Nutzer, der den Agenten noch nie gesehen hat. Foss macht das zur Fußnote; es sollte ein Kalendertermin sein. Jeder adversariale Case, der in deiner Bank landet, hat als freie Session angefangen, die einen Failure Mode aufgedeckt hat, den du nicht aufgeschrieben hättest.

Takeaways

  • Behandle die Eval Bank als versioniertes Artefakt neben der Agent-Definition; die Runner sind tauschbar.
  • Bewerte Per-Case-Pass-Conditions als Ganzzahlen (4/5, 2/3), nicht als dezimale Schwellen, die das zugrundeliegende N maskieren.
  • Erhöhe N auf 20 bis 50 pro Case, bevor ein Ergebnis ein Release gaten darf; darunter labelst du die Bank "richtungsweisend, nicht gate-tauglich".
  • Benenne die vier Schichten (Prompt, Knowledge, Action, Flow) in der Dateistruktur der Bank, sodass Failures sich selbst zum Fix routen.
  • Paare LLM-as-Judge mit einem kleinen Human-Calibration-Set; Judges driften und müssen ebenfalls bewertet werden.

Offene Fragen

Bei drei Dingen bin ich nicht sicher und höre lieber von Leuten, die dagegen ausgeliefert haben.

Erstens: die richtige Stichprobengröße pro Case. Ich schreibe drei bis fünf für Entwicklung und das fühlt sich niedrig an. Die ehrliche Antwort ist vermutlich, dass die Sample-Anzahl eine Funktion der Varianz der Metrik ist und wir sie aus einer Konfidenzintervall-Rechnung wählen sollten, nicht aus Gewohnheit, aber ich habe keine saubere veröffentlichte Heuristik gesehen, die Domäne auf benötigtes N abbildet.

Zweitens: die Kalibrierungs-Kadenz für LLM-as-Judge. Einmal pro Quartal ist das, was ich tue; ich vermute, einmal pro Woche würde Judge Drift schneller fangen, aber der Human-Review-Aufwand ist real. Den richtigen Tradeoff kenne ich nicht.

Drittens: die Wechselwirkung zwischen Layer-eins-Trigger-Accuracy und den Instruction-Following-Verbesserungen des darunterliegenden Modells. Wenn das Modell upgradet, steigt Trigger-Accuracy oft "gratis" auf der ganzen Linie. Das ist Regressionsrisiko in Verkleidung: die Schwelle, die letzten Monat Fehler fing, lässt diesen Monat vielleicht alles durch, einschließlich der Fehler. Die Bank muss mit dem Floor mitwachsen, und ich habe dafür noch keinen sauberen Prozess.

Ich erwarte, dass Teile davon schlecht altern. Wenn du einen Agenten in Produktion betreibst und deine Test Bank überhaupt nicht wie die obige Form aussieht, ist der schnellste Weg, mich umzustimmen, ein konkretes Gegenbeispiel: was fängt deine Bank, was diese Form nicht fängt, und was kostet die Pflege der Suite? Melde dich, ich lese alles.