Denkmaschinen · Teil 3 · Halluzinationen · 13. Juli 2026

"Commit drauf: 9d22a7c" — nur dass es diesen Commit nie gab

Teil 3 einer Artikelserie über zwei eigene Agenten-Projekte: wintermute (produktiver persönlicher KI-Agent) und agenticframework (produktiv eingesetztes Meta-Framework für autonome Entwicklungs-Pipelines).

Der Vorfall

Am 2. Mai 2026 meldete mein Agent wintermute stolz: "Commit drauf: 9d22a7c". Klang gut. Es gab nur ein Problem: Der zugrunde liegende Tool-Aufruf hatte einen strukturierten Fehler zurückgegeben. Kein Commit. Nirgends.

Am selben Tag, Variante zwei: "Test commit drauf: e30bea2" — diesmal gab es im Agent-Log nicht einmal einen fehlgeschlagenen Tool-Aufruf. Der Agent hatte die komplette Aktion frei erfunden, inklusive plausibel aussehender Commit-ID. Und Variante drei lieferte gleich eine formvollendete 40-Zeichen-SHA zu einem Commit, der nie existierte.

Alle drei Vorfälle sind in meinem Lessons-Log dokumentiert (Lesson #22), und sie führten zu dem Satz, der seither über meiner Agenten-Entwicklung steht:

"Ein Modell mit Zugriff auf Tool-Ergebnisse produziert nicht automatisch treue Zusammenfassungen dieser Ergebnisse. Ohne Abgleich ist ein Tool-nutzender Agent eine Confidence-Maschine, keine Kompetenz-Maschine."

Dieser Artikel handelt davon, was man dagegen technisch tun kann — und, genauso wichtig, wo die Grenzen liegen.

Erst das Problem verstehen: fünf Klassen von Halluzination

Bevor man einen Validator baut, lohnt eine Taxonomie. In wintermutes docs/HALLUCINATIONS.md sind fünf Klassen definiert, sortiert nach aufsteigender Heimtücke:

  1. Reine Konfabulation — eine Commit-ID aus dem Nichts. Am leichtesten zu erkennen.
  2. Geliehene Identifier — die SHA ist echt, stammt aber aus der User-Nachricht oder einem früheren Turn; der Agent behauptet, sie käme aus dem aktuellen Tool-Ergebnis.
  3. Fehlinterpretierte Tool-Ergebnisse — das Tool lief, gab {"error": ...} zurück, und der Agent berichtet einen Erfolg.
  4. Phantom-Tool-Calls — der Agent behauptet, ein Tool aufgerufen zu haben, das nie lief.
  5. Narrativ-kohärente Konfabulation — die Königsklasse: ein ganzes Gewebe erfundener Dateinamen, Klassennamen und Funktionssignaturen, alle grammatisch korrekt und untereinander konsistent. Keine einzelne verdächtige SHA weit und breit.

Die ehrlichste Passage des Dokuments erklärt, warum "sag dem Modell einfach, es soll nicht lügen" nicht reicht:

"Zu wissen, dass man verifizieren muss, stoppt nicht die Generierung eines plausiblen Berichts."

Die Disziplin-Ebene ("ich muss prüfen") und die Bericht-Ebene ("hier ist, was ich gefunden habe") laufen im Modell parallel — und die Bericht-Ebene bedient sich notfalls aus der Plausibilität. Persona-Anweisungen sind deshalb Verteidigungsschicht 1 von 4, und die schwächste.

Schicht 2: Der Post-Turn-Validator

Die erste technische Schicht ist ein Validator, der nach jeder Antwort läuft. Kernidee: SHA-förmige Tokens in der Antwort extrahieren und gegen die tatsächlichen Tool-Ergebnisse dieses Turns abgleichen:

# Match tokens that are 10..40 hexadecimal characters and not preceded by
# alphanumerics (so we don't pick up the tail of a longer identifier).
_SHA_LIKE = re.compile(r"(?<![A-Za-z0-9])([A-Fa-f0-9]{10,40})(?![A-Za-z0-9])")

Drei Design-Entscheidungen stecken in dieser einen Zeile, und jede hat eine Geschichte: Die Untergrenze liegt bei 10 statt 7 Zeichen, weil kürzere Hex-Tokens zu viele Fehlalarme produzieren. Die Lookahead/Lookbehind-Klammern verhindern, dass foo9d22a7c als SHA zählt (Teil eines längeren Identifiers). Und der Validator prüft nur SHA-förmige Tokens — UUIDs und Issue-Nummern wären nette Kandidaten, haben aber eine zu hohe False-Positive-Rate.

Findet der Validator eine unbelegte SHA, blockiert er nicht, sondern annotiert: Die Antwort bekommt eine sichtbare Framework-Fußnote ("⚠️ Framework note: I referenced identifier(s) ... that are not in this turn's tool-call trace"). Advisory statt blocking ist eine bewusste Entscheidung: Ein False Positive mit Fußnote ist verschmerzbar, eine stumm verschluckte echte Antwort nicht. Und wenn der Validator selbst einen Bug hat, fällt er offen — Exception wird geloggt, Antwort geht unverändert raus.

Schicht 3: Das Modell muss seinen Fehler selbst sehen

Die Fußnote allein hatte einen Haken, den Lesson #23 präzise benennt:

"Ein Framework-Guard, der nur die nutzerseitige Ausgabe annotiert, aber nicht in den Loop zurückführt, der die Ausgabe produziert hat, ist eine halbe Verteidigung. Das Modell muss seinen eigenen Fehler im selben Turn als Input sehen — nicht als Dekoration am ausgehenden Text — sonst ändert es sein Verhalten nicht."

Ein Rauchmelder ist kein Sprinkler. Also wurde die zweite Hälfte gebaut: Wird eine Antwort geflaggt, geht sie nicht raus, sondern zurück ins Modell — zusammen mit einem Framework-Nudge:

"[FRAMEWORK SELF-CORRECTION — not a user message] ... your previous reply "
"references identifier(s) {listed} that did not appear in any tool result "
"this turn. This is the hallucination pattern the framework guards against. "
"Do one of the following before replying again:\n"
"  1. Issue an actual tool call (e.g. `gh_list_commits`, ...) to verify ...\n"
"  2. Revise the reply to remove or qualify the unverified identifier(s) ..."

Das Modell bekommt also die Wahl: nachprüfen oder zurückrudern. Klappt es, geht die korrigierte Antwort ohne Fußnote raus. Klappt es nicht, eskaliert die Fußnote ("after a self-correction attempt the reply still references them"). In der ersten Produktionswoche griff dieser Mechanismus dreimal — zweimal korrigierte sich das Modell erfolgreich selbst, einmal blieb es stur. Zwei von drei automatisch abgefangen ist keine Perfektion, aber es ist der Unterschied zwischen "der Nutzer merkt es hoffentlich" und "das System merkt es selbst".

Die unbequeme Pointe: Die Verteidigung schlug selbst fehl

Jetzt der Teil, den man in Hochglanz-KI-Content nicht liest. Der Self-Correction-Mechanismus ging mit 13 grünen Tests in Produktion — und produzierte sofort einen 400er von der Anthropic-API: "The conversation must end with a user message." Der Nudge war als system-Message injiziert worden, die API verlangt aber user an letzter Stelle. Die Tests hatten das nicht gefangen, weil sie den API-Client gemockt hatten — und ein Mock akzeptiert klaglos jede Nachrichtenfolge (Lesson #24: gemockte Tests validieren, dass dein Code den Collaborator so aufruft, wie der Test es erwartet — nicht, dass der echte Collaborator das auch akzeptiert).

Und die zweite Grenze ist noch fundamentaler: Klasse 5, die narrativ-kohärente Konfabulation, kann dieser Validator prinzipiell nicht fangen — es gibt keine SHA-förmigen Tokens zu flaggen, sondern ein in sich stimmiges Gewebe erfundener Struktur. Dokumentiert ist eine Session, in der jede aufeinanderfolgende Konfabulation näher an echter Code-Struktur lag als die vorherige. Wer behauptet, sein Agent-System halluziniere dank Guardrails nicht mehr, hat entweder Klasse 5 noch nie gesehen — oder ist gerade mittendrin.

Dieselbe Idee eine Etage höher: agenticframework

In einer Pipeline aus mehreren Agenten stellt sich das Problem verschärft: Wenn Agent A halluziniert und Agent B darauf aufbaut, ist der Schaden strukturell. agenticframework verteidigt deshalb an den Übergabepunkten — jedes Handover-Dokument wird an der Phasengrenze gegen sein Schema validiert, und der Coding-Agent Case darf Erfolg nicht einfach behaupten:

@model_validator(mode="after")
def failed_handovers_include_errors(self) -> CodeHandover:
    if self.status == "failed" and not self.errors:
        raise ValueError("failed code handovers must include at least one error")
    return self

Dazu wird jeder von Case produzierte Unified Diff strukturell geprüft, bevor irgendetwas angewendet wird: Sind Diff-Header vorhanden? Hat jede Datei mindestens einen Hunk? Liegen alle Pfade innerhalb der erlaubten Grenzen? Ein leerer oder strukturell kaputter Diff wird zum typisierten Fehler (empty_diff, malformed_diff, forbidden_path) — und ein fehlgeschlagenes Handover ohne Fehlerangabe ist per Schema unmöglich. Die Devise ist dieselbe wie bei wintermute, nur als Prozessregel: Behauptungen von Agenten werden nicht geglaubt, sondern geprüft — und zwar von Code, nicht von einem weiteren LLM. Was tatsächlich passiert ist, steht im Run-Manifest, das jede Phase mit Artefakt-Referenzen, Gate-Entscheidungen und Fehlern fortschreibt.

Fazit und Ausblick

Halluzinationen in Tool-nutzenden Agenten sind kein Randphänomen, sondern ein strukturelles Verhalten, das man managen muss wie Speicherlecks: mit Erkennung, Rückkopplung und der Demut, dass die raffinierteste Klasse durch jedes Raster fällt. Die wirksame Architektur hat sich bei mir in vier Schichten herausgebildet — Persona-Anweisung, Post-Turn-Validator, In-Loop-Self-Correction, Verbatim-or-Nothing-Regel — plus der Prozess-Variante in agenticframework: Schema-Validierung an jeder Übergabe.

Im nächsten Teil wird es wirtschaftlich: Was kostet der Betrieb einer Agenten-Pipeline wirklich — und wie drückt man die Rechnung von 16 Dollar pro Durchlauf auf gut 4, ohne bei der Qualität zu lügen? Spoiler: Die Antwort hat mit derselben Disziplin zu tun wie dieser Artikel — messen statt glauben.


Quellen: wintermute/LESSONS.md (Lessons #22, #23, #24), wintermute/docs/HALLUCINATIONS.md (Fünf-Klassen-Taxonomie, Defense Layers, dokumentierte Incidents A–J), wintermute/docs/AGENT-LOOP-RELIABILITY.md, wintermute/agent/src/agent/tool_call_validation.py, wintermute/agent/src/agent/core.py, wintermute/tests/agent/test_tool_call_validation.py + test_tool_loop_self_correction.py; agenticframework/orchestrator/.../phases/contracts.py (Handover-Schema-Validierung), .../phases/results.py (CodeHandover), .../errors.py (Failure-Taxonomie), agenticframework/apps/intent-compiler/backend/src/agents/case/diff.py (Diff-Validierung), .../kernel/manifest.py (Run-Manifest).