DEV Community

Cover image for Nicht-deterministische KI-Agenten testen: Wenn Temperatur=0 nicht ausreicht
Emre Demir
Emre Demir

Posted on • Originally published at apidog.com

Nicht-deterministische KI-Agenten testen: Wenn Temperatur=0 nicht ausreicht

Ihr Test am Montag war erfolgreich. Gleiche Eingabe, gleicher Code, temperature=0. Am Dienstag schlug er fehl, obwohl Sie nichts geändert hatten. Die Assertion prüfte auf eine exakte Zeichenkette, und das Modell formulierte dieselbe Antwort nur anders. Der Test ist rot, der Agent funktioniert, und Sie debuggen die Testsuite statt des Produkts.

Apidog heute ausprobieren

Das ist der Preis für Tests von LLM-Ausgaben. Selbst bei temperature=0 erhalten Sie über mehrere Durchläufe hinweg nicht zuverlässig byte-identische Antworten. Dieser Leitfaden zeigt, wie Sie Assertions schreiben, die bei variierender Formulierung stabil bleiben. Er vertieft Fehlermodus drei aus unserem Leitfaden dazu, warum KI-Agenten in der Produktion scheitern.

Warum temperature=0 nicht Determinismus bedeutet

Die Temperatur steuert das Sampling des nächsten Tokens. Bei 0 wählt das Modell jeweils das wahrscheinlichste Token. Das wirkt reproduzierbar, ist es aber nicht zwingend.

Die Ursachen liegen unterhalb des Modells:

  • Gleitkomma-Arithmetik auf GPUs ist nicht assoziativ.
  • Unterschiedliche Reihenfolgen bei Berechnungen können minimale Rundungsabweichungen erzeugen.
  • Diese Abweichungen können die Token-Rangfolge verändern.
  • Anbieter bündeln Requests unterschiedlich, wechseln Hardware, aktualisieren Inferenzbibliotheken oder routen in andere Regionen.

Eine lange vLLM-Diskussion erklärt, warum selbst ein fester Seed mit temperature=0 keine bitgenaue Reproduzierbarkeit garantiert.

Determinismus ist eine Eigenschaft des gesamten Serving-Stacks, nicht nur ein Flag in Ihrer Modellanfrage.

Behandeln Sie identische Ausgabe daher nicht als Vertrag. Testen Sie stattdessen, ob die Antwort fachlich und strukturell korrekt ist.

Warum exakte String-Assertions Ihre Suite unzuverlässig machen

Die klassische Falle:

assert.equal(response, "Ihre Gesamtsumme beträgt $42.00.");
Enter fullscreen mode Exit fullscreen mode

Dieser Test schlägt fehl, obwohl die Antwort korrekt sein kann:

Ihr Bestellwert liegt bei $42.00.
Enter fullscreen mode Exit fullscreen mode

Ein Test, der bei korrektem Verhalten fehlschlägt, ist gefährlicher als kein Test:

  1. Das Team erlebt Fehlalarme.
  2. Tests werden wiederholt ausgeführt, bis sie zufällig grün sind.
  3. Echte Regressionen gehen im Rauschen unter.
  4. Das Vertrauen in die gesamte Testsuite sinkt.

Wir haben bereits beschrieben, was unzuverlässige Tests verursacht und warum sie sich ausbreiten. Nicht-deterministische Modellantworten sind eine besonders schnelle Quelle für Flakiness.

Die Lösung ist nicht, noch mehr Text einzufrieren. Drehen Sie die Richtung um: Testen Sie den Vertrag, nicht die Formulierung.

Auf Struktur und Bedeutung prüfen, nicht auf exakten Text

Ein Support-Agent kann eine Rückerstattung auf viele Arten bestätigen. Jede gültige Antwort sollte aber dieselben fachlichen Fakten enthalten:

  • Betrag der Rückerstattung
  • Bestell-ID
  • Status
  • erwartete Datentypen und Formate

Fragen Sie nicht:

Hat das Modell diesen Satz exakt so geschrieben?

Fragen Sie stattdessen:

Enthält die Antwort die erwartete Struktur, gültige Werte und zulässige Grenzen?

Damit überstehen Ihre Tests Umformulierungen, erkennen aber weiterhin echte Fehler wie fehlende Felder, falsche Datentypen, ungültige Payloads oder Werte außerhalb erlaubter Bereiche.

Die Antwort mit einem JSON Schema validieren

Wenn Ihr Agent strukturierte Daten zurückgibt, definieren Sie ein JSON Schema und validieren Sie jede Antwort dagegen.

Beispiel für eine Rückerstattungsantwort:

{
  "type": "object",
  "required": ["order_id", "status", "amount"],
  "properties": {
    "order_id": {
      "type": "string",
      "pattern": "^ORD-[0-9]+$"
    },
    "status": {
      "type": "string",
      "enum": ["refunded", "pending", "denied"]
    },
    "amount": {
      "type": "number",
      "minimum": 0
    }
  },
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

Mit JavaScript und Ajv könnte die Assertion so aussehen:

import Ajv from "ajv";
import assert from "node:assert/strict";

const ajv = new Ajv();
const validate = ajv.compile(refundSchema);

const valid = validate(agentResponse);

assert.equal(valid, true, JSON.stringify(validate.errors));
Enter fullscreen mode Exit fullscreen mode

Das Schema prüft:

  • erforderliche Felder,
  • Datentypen,
  • zulässige Enum-Werte,
  • ID-Formate,
  • unerwartete Zusatzfelder.

Es prüft nicht, ob der Freitext exakt gleich formuliert ist. Genau das macht es robust.

Laden Sie Ihr Antwortschema in Apidog und validieren Sie Live-Antworten dagegen. Bei einer Abweichung sehen Sie das konkrete fehlerhafte Feld statt eines langen String-Diffs.

Tool-Aufrufe auf Ziel, Methode und Payload prüfen

Wenn ein Agent ein Tool aufruft, testen Sie den Tool-Aufruf selbst, nicht den Satz, der ihn ausgelöst hat.

Prüfen Sie mindestens drei Dinge:

  1. Wurde das richtige Tool gewählt?
  2. Wurden HTTP-Methode und Ziel korrekt gesetzt?
  3. Entspricht die Payload dem Tool-Schema?

Beispiel für einen Buchungsagenten:

assert.equal(toolCall.method, "POST");
assert.equal(toolCall.path, "/reservations");

assert.equal(Number.isInteger(toolCall.body.guests), true);
assert.match(toolCall.body.date, /^\d{4}-\d{2}-\d{2}$/);
Enter fullscreen mode Exit fullscreen mode

Noch robuster wird es mit einem Schema:

{
  "type": "object",
  "required": ["guests", "date"],
  "properties": {
    "guests": {
      "type": "integer",
      "minimum": 1
    },
    "date": {
      "type": "string",
      "format": "date"
    }
  },
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

Validieren Sie dabei auch, dass keine erfundenen Felder in der Payload landen. Die End-to-End-Methode zum Testen der API-Aufrufe eines Agenten behandelt das Erfassen und Prüfen solcher Tool-Schemas.

Numerische Bereiche statt exakter Werte verwenden

Für Zahlen, die ein Modell erzeugt oder weitergibt, sind Bereichsprüfungen meist besser als Gleichheit.

Ein Warenkorb-Agent berechnet beispielsweise eine Gesamtsumme. Sie kennen vielleicht nicht jeden exakten Wert über alle Steuerregeln und Versandoptionen hinweg. Sie kennen aber fachliche Grenzen:

assert.ok(response.total >= 0);
assert.ok(response.total <= cartSubtotal + maxShipping + maxTax);
Enter fullscreen mode Exit fullscreen mode

Damit erkennen Sie relevante Fehler:

  • negative Gesamtsummen,
  • unrealistisch hohe Werte,
  • 0 bei einem gefüllten Warenkorb,
  • Werte außerhalb des Geschäftsrahmens.

Bereichsprüfungen eignen sich auch für:

  • Konfidenzwerte,
  • Artikelanzahlen,
  • Token-Verbrauch,
  • Latenzbudgets,
  • Rabatte,
  • abgeleitete Kennzahlen.

Wählen Sie die breiteste Grenze, die bei einem echten Fehler noch zuverlässig fehlschlägt.

Erforderliche Schlüssel prüfen und verbotene Felder ausschließen

Zwei einfache Assertions liefern besonders viel Wert:

  1. Felder, auf die Ihr Code angewiesen ist, müssen vorhanden und nicht null sein.
  2. Felder, die niemals sichtbar sein dürfen, müssen fehlen.

Beispiel:

assert.ok(response.resolution);
assert.equal(response.resolution, null, false);

assert.equal("internal_notes" in response, false);
assert.equal("raw_prompt" in response, false);
Enter fullscreen mode Exit fullscreen mode

Für eine klarere Prüfung:

assert.ok(Object.hasOwn(response, "resolution"));
assert.notEqual(response.resolution, null);

assert.ok(!Object.hasOwn(response, "internal_notes"));
assert.ok(!Object.hasOwn(response, "raw_prompt"));
Enter fullscreen mode Exit fullscreen mode

Diese Prüfungen sind unabhängig von der Formulierung. Gleichzeitig schützen sie gegen Datenschutzlecks, bei denen ein Modell versehentlich interne Notizen oder Prompt-Inhalte in eine Kundenausgabe übernimmt.

Freitext mit semantischen und Schwellenwertprüfungen testen

Manchmal ist die Antwort bewusst Prosa. Dann können Sie trotzdem stabile Eigenschaften prüfen.

Beispiel:

assert.ok(responseText.includes(orderId));
assert.ok(responseText.length <= 500);
assert.ok(!blockedPhrases.some((phrase) => responseText.includes(phrase)));
Enter fullscreen mode Exit fullscreen mode

Typische Freitext-Assertions:

  • Enthält die Antwort die übergebene Bestellnummer?
  • Bleibt sie unter einem Längenlimit?
  • Enthält sie keine gesperrten Phrasen?
  • Enthält sie erwartete Fakten oder Links?
  • Vermeidet sie interne Daten?

Wenn Sie die semantische Nähe zu einer Referenzantwort prüfen müssen, verwenden Sie einen Embedding-Ähnlichkeitsscore mit einem Schwellenwert:

assert.ok(similarityScore >= 0.85);
Enter fullscreen mode Exit fullscreen mode

Behandeln Sie das als grobes Tor, nicht als Beweis für sachliche Korrektheit. Eine semantisch ähnliche Antwort kann immer noch einen subtilen Faktenfehler enthalten. Kombinieren Sie solche Prüfungen daher mit Schema-, Feld- und Bereichs-Assertions.

Bereichs-Snapshots statt Text-Snapshots verwenden

Snapshot-Tests bleiben nützlich, wenn Sie nur stabile Teile einfrieren.

Speichern Sie beispielsweise:

  • Schlüsselsatz,
  • Typen,
  • erlaubte Enum-Werte,
  • Wertebereiche,
  • erwartete Objektform.

Speichern Sie nicht den vollständigen Freitext als unveränderlichen Blob.

Ein sinnvoller Snapshot beschreibt eher:

{
  "requiredKeys": ["order_id", "status", "amount"],
  "statusEnum": ["refunded", "pending", "denied"],
  "amountRange": {
    "min": 0,
    "max": 10000
  }
}
Enter fullscreen mode Exit fullscreen mode

Dann bricht der Test bei einer strukturellen Änderung, die geprüft werden muss — nicht bei einem Synonym.

Zustand und Gedächtnis erschweren Tests zusätzlich

Bisher galt: eine Anfrage hinein, eine Antwort heraus. Zustandsbehaftete Agenten funktionieren anders. Sie tragen Erinnerungen über mehrere Runden mit.

Die Antwort kann davon abhängen:

  • welche Dokumente abgerufen wurden,
  • welche Daten in früheren Runden gespeichert wurden,
  • in welcher Reihenfolge Tool-Aufrufe liefen,
  • wie frühere Gespräche zusammengefasst wurden.

Zwei Durchläufe derselben Konversation können deshalb auseinanderlaufen, selbst wenn die letzte Nutzereingabe identisch ist. Unser Erklärer dazu, wie das Gedächtnis von KI-Agenten funktioniert, zeigt, wo dieser Zustand lebt und wie er aufgebaut wird.

Machen Sie zustandsbehaftete Tests mit zwei Gewohnheiten beherrschbar.

1. Testzustand vor jedem Test zurücksetzen

Initialisieren Sie den Speicher vor jedem Test auf einen bekannten Zustand:

beforeEach(async () => {
  await agentMemory.clear();
  await agentMemory.seed({
    userId: "test-user-1",
    preferences: [],
    reservations: []
  });
});
Enter fullscreen mode Exit fullscreen mode

So testen Sie nicht gleichzeitig unterschiedliche Modellausgaben und unterschiedliche Ausgangszustände.

2. Pfadunabhängige Invarianten prüfen

Prüfen Sie Eigenschaften, die unabhängig vom Gesprächspfad gelten müssen:

assert.ok(account.balance >= 0);
assert.equal(reservations.length, 1);
Enter fullscreen mode Exit fullscreen mode

Beispiele für Invarianten:

  • Ein Guthaben darf nie negativ werden.
  • Eine abgeschlossene Flugbuchung erzeugt genau eine Reservierung.
  • Eine Bestellung darf nicht zweimal belastet werden.
  • Ein Ticket muss am Ende einen gültigen Status besitzen.

Solche Assertions überleben zustandsbehaftete, nicht-deterministische Abläufe deutlich besser als exakte Gesprächs-Snapshots.

Abhängigkeiten mocken, damit Tests wiederholbar bleiben

Sie können zuverlässige Tests nicht allein auf Live-APIs Dritter aufbauen. Externe APIs können:

  • rate-limiten,
  • Daten verändern,
  • temporär ausfallen,
  • unterschiedliche Antworten liefern.

Damit fügen sie neben dem Modell eine zweite Zufallsquelle hinzu.

Mocken Sie daher die Abhängigkeiten, die nicht Gegenstand des aktuellen Tests sind:

const paymentApiMock = {
  charge: async () => ({
    id: "pay_test_123",
    status: "approved",
    amount: 42.0
  })
};

const searchApiMock = {
  search: async () => [
    { id: "doc-1", title: "Rückerstattungsrichtlinie" },
    { id: "doc-2", title: "Bestellstatus" },
    { id: "doc-3", title: "Versandinformationen" }
  ]
};
Enter fullscreen mode Exit fullscreen mode

Damit liefern Zahlungs- und Such-API bei jedem Testlauf dieselben kontrollierten Antworten. Sie können außerdem gezielt Randfälle erzwingen:

const declinedPaymentMock = {
  charge: async () => ({
    id: "pay_test_456",
    status: "declined",
    reason: "insufficient_funds"
  })
};
Enter fullscreen mode Exit fullscreen mode

Dann prüfen Sie, ob der Agent korrekt reagiert:

assert.equal(agentResponse.status, "payment_failed");
assert.ok(agentResponse.message);
Enter fullscreen mode Exit fullscreen mode

Richten Sie Apidog auf die Abhängigkeiten Ihres Agenten aus, um Mocks mit stabilen, kontrollierbaren Response-Bodies einzurichten. Kombinieren Sie diese Mocks mit Schema-Assertions. Das gehört zur breiteren Praxis des Agentic AI Testing, in der Mocking und Assertions zusammenarbeiten.

Wo Apidog passt — und wo nicht

Ordnen Sie das Tool korrekt ein: Apidog ist eine Plattform für API-Design, API-Tests und Mocking.

Es ist nicht:

  • ein Agent-Framework,
  • ein Modell-Host,
  • eine Agent-Laufzeitumgebung,
  • eine Orchestrierungsplattform,
  • eine Evaluations- oder Observability-Plattform.

Apidog baut Ihren Agenten nicht, führt ihn nicht aus und bewertet nicht seine Argumentation.

Es passt an die API-Grenze, an der Ihr Agent kommuniziert:

  1. Antwortverträge testen: Validieren Sie API-Antworten des Agenten gegen Schemas, Bereiche, erforderliche und verbotene Schlüssel.
  2. Abhängigkeiten mocken: Stellen Sie reproduzierbare Antworten für externe APIs bereit.

Das ist die Schnittstelle, die Apidog abdeckt: Verträge über Requests und Responses — nicht das Modell, das sie erzeugt.

Testen Sie den Vertrag, nicht die Formulierung

Nicht-Determinismus ist kein Fehler, den Sie mit temperature=0 wegkonfigurieren können. Er ist eine Eigenschaft von LLM-Serving in der Praxis.

Ersetzen Sie fragile Assertions wie diese:

assert.equal(responseText, expectedText);
Enter fullscreen mode Exit fullscreen mode

durch Assertions auf stabile Eigenschaften:

assert.ok(validateSchema(response));
assert.ok(response.total >= 0);
assert.ok(response.total <= maxAllowedTotal);
assert.ok(Object.hasOwn(response, "order_id"));
assert.ok(!Object.hasOwn(response, "internal_notes"));
Enter fullscreen mode Exit fullscreen mode

Teams mit zuverlässigen Agenten testen, was konstant bleibt:

  • Schema,
  • Struktur,
  • Datentypen,
  • Wertebereiche,
  • erforderliche Felder,
  • verbotene Felder,
  • Tool-Call-Payloads,
  • pfadunabhängige Invarianten.

Die Formulierung darf variieren. Ihre Testsuite bleibt dadurch grün, wenn sich nur Text bewegt — und wird rot, wenn wirklich etwas kaputtgeht.

Nehmen Sie diese Woche eine unzuverlässige String-Assertion aus Ihrer Suite. Schreiben Sie sie als Schema- und Bereichsprüfung um. Verwenden Sie Apidog, um Agent-Antworten gegen Verträge zu validieren und Abhängigkeiten für wiederholbare Tests zu mocken.

Top comments (0)