Systeme bauen, die ihre eigenen Entscheidungen erklären können
Ein Staffel-Abschluss darüber, warum jede Pipeline dieser Plattform ihre eigene Begründung zusammen mit ihrem Output ausliefert — und eine Lücke, die ich bewusst offengelassen habe, statt so zu tun, als sei sie geschlossen
Engineering·Experte·8 Min. Lesezeit · 6. Juli 2026
Der Anfang
Eine Pipeline mit benannten Stufen löst, wer entscheiden darf, was wahr ist — die deterministische Hälfte entscheidet, Claude schreibt. Sie löst nicht automatisch ein verwandtes, aber separates Problem: Kann ich im Nachhinein tatsächlich sagen, warum eine bestimmte Entscheidung so ausgefallen ist, wie sie ausgefallen ist, nicht nur dass sie es hat? Ein Score von 14 sagt einem, dass ein Evidenzstück hoch gerankt wurde. Er sagt einer künftigen Version von mir, sechs Monate später, die debuggt, warum ein bestimmtes Anschreiben einen Job überproportional betont hat, nicht, was genau diese Zahl angetrieben hat.
Die erste Lösung
Anfangs behandelte ich den Score selbst als ausreichend. scoreCandidateEvidence() produziert eine Zahl pro Eintrag, absteigend sortiert, und eine Weile fühlte sich das ausreichend an — das Ranking ist deterministisch, also ist es doch sicher schon per Definition nachvollziehbar. Auditierbar bedeutete "nicht zufällig", und dabei bin ich stehen geblieben.
Was kaputtging
Eine nackte Zahl übersteht den Kontakt mit einer echten "Warum"-Frage nicht, und jede Domäne dieser Plattform bekam irgendwann eine gestellt. "Warum hat dieses Anschreiben diesen Job über jenen betont" wird nicht durch "14 > 9" beantwortet — es wird beantwortet, indem man weiß, welche Schlüsselwörter in welchem Feld übereinstimmten und warum dieses Feld das Gewicht trägt, das es trägt. "Warum hat der Assistent gesagt, diese beiden Fotos seien auf derselben Reise entstanden" wird nicht durch eine Ähnlichkeits-Fließkommazahl beantwortet — es wird beantwortet, indem man weiß, welche gespeicherte Tatsache — ein gemeinsames taken_at-Fenster, eine übereinstimmende Ortsangabe — diese Behauptung tatsächlich hervorgebracht hat. Ein Score ist eine Schlussfolgerung. Er ist keine Erklärung, und ich hatte die beiden als dasselbe behandelt.
Die Entscheidung
Die Lösung existierte bereits in der Codebasis, bevor ich das Muster, das ich anwendete, vollständig verstand — fast jede Bewertungsstufe dieser Plattform hatte von selbst ein Feld entwickelt, dessen ganzer Job es ist, den Grund weiterzutragen, nicht das Ergebnis. EvidenceItem trägt nicht nur einen score; es trägt einen reason-String, pro Eintrag generiert — "Matches 3 keyword(s) in tech stack and responsibilities" — und ein matchedKeywords-Array, das genau benennt, welche Begriffe die Zahl angetrieben haben. Foto-Suchergebnisse tragen matchedBy[], das benennt, welcher Retrieval-Pfad — Tag, Ort, Stimmung, Semantik — tatsächlich beigetragen hat. matchPercentage ist bewusst als keine Wahrscheinlichkeit dokumentiert; es ist nur relativ zum Top-Ergebnis in diesem Set, ein ehrlicher Vorbehalt statt einer selbstbewusst klingenden Zahl, die mehr verspricht, als sie misst.
Die Evidence Relationships des Fotografie-Assistenten trieben dieselbe Idee eine Ebene tiefer. Der type einer Beziehungsbehauptung nutzt EvidenceRelationshipKind wörtlich, statt ein separates Vokabular dafür zu erfinden, was Claude "sagen darf" versus was der Graph tatsächlich enthält — sodass eine Behauptung nie eine Beziehungsart behaupten kann, die strukturell nicht existiert. Und als ich entscheiden musste, wie eine Behauptung verifiziert wird, erwog ich die naheliegende Abkürzung: Claude bitten, seine eigene Begründung in einem Folgeturn zu erklären, oder seiner angegebenen Rechtfertigung für ein Zitat zu vertrauen. Das habe ich gezielt verworfen, weil die eigene Darstellung eines LLM über seine Begründung nur mehr unverifizierte Prosa ist — ein Modell zu bitten, sich selbst zu erklären, fügt keine Prüfung hinzu, es fügt ein weiteres Ding hinzu, das eine braucht. validateClaims() ist stattdessen reiner Graph-Vergleich: keine Sprachanalyse, kein Regex, keine Embeddings, kein zweiter Claude-Aufruf. Die Foto-IDs jeder Behauptung müssen im Paket existieren, das Claude tatsächlich bekommen hat, die behauptete Beziehung muss in beide Richtungen zwischen jedem Paar gelten, geprüft gegen Daten, die der deterministische Evidence Relationship Builder bereits berechnet hat — nicht angenommen, nicht aus Prosa neu abgeleitet.
Ich musste auch entscheiden, was zu tun ist, wenn Claude eine wahre Behauptung wiederholt, statt eine falsche zu erfinden — dieselbe Beziehung zweimal erzählen und für jede Erwähnung eine strukturierte Behauptung ausgeben. Die gesamte Antwort dafür abzulehnen, hätte eine korrekte Antwort für eine Formatierungseigenheit bestraft, also fasst normalizeClaims() nur exakte Duplikate zusammen, bevor die Validierung läuft, und rührt sonst nichts an — es führt keine ähnlichen-aber-unterschiedlichen Behauptungen zusammen, schreibt den Typ einer Behauptung nicht um, rettet keine tatsächlich ungültige. Auditierbarkeit heißt, genau zu wissen, wo Deduplizierung endet und Korrektheitsprüfung beginnt, nicht die beiden zu einem nachsichtigen Durchgang zu verwischen.
Die neue Architektur
Jede Bewertungs-/Einbeziehungsentscheidung liefert ihre eigene Erklärung mit, nicht nur ihr Ergebnis:
EvidenceItem.score + EvidenceItem.reason, matchedKeywords[]
PhotoSearchResult.finalScore + matchedBy[]
matchPercentage (dokumentiert: relativ zum Top-Ergebnis, keine Wahrscheinlichkeit)
AssistantClaim.type (nutzt EvidenceRelationshipKind — kann sich nicht auf eine
Beziehung beziehen, die der Graph nicht tatsächlich enthält)
validateClaims(): reiner Graph-Vergleich, kein LLM, kein Regex, keine Embeddings —
die Prüfung von Claudes Behauptungen ist nie Claudes eigene Selbstdarstellung.
Was ich gelernt habe
Das Wichtigste, was ich über dieses Muster sagen kann, ist keine Erfolgsgeschichte — es ist ein Eingeständnis. Das Citation Gate und der Claim Validator schließen die Lücke für alles, was Claude über den strukturierten claims-Kanal behauptet. Sie schließen sie nicht für eine Beziehung, die nur in freiem Text behauptet wird, ohne eine passende strukturierte Behauptung dahinter. Diese Restlücke wird verfolgt, nicht versteckt — sie ist ein echter, derzeit offener Punkt im eigenen Sicherheits-Tracking dieser Plattform, verengt durch die Arbeit an strukturierten Behauptungen, aber ausdrücklich nicht vollständig geschlossen, ruhend auf einer weichen Verteidigung (einer Systemprompt-Anweisung) statt auf einer harten, beweisbaren Prüfung. Ich hätte diesen Artikel so schreiben können, als sei jede Lücke geschlossen. Die nützlichere Version von "erklärbar" schließt ein, zuzugeben, welche Teile des Systems es noch nicht sind — denn ein System, das seine Entscheidungen erklären kann, muss auch bereit sein, die zu erklären, die es noch nicht vollständig verifizieren kann.
Der Blick nach vorn
Staffel 1 begann mit einer Datenbank, die eine Rendering-Seite bediente, und endete mit denselben Zeilen, bewertet, gerankt, gedeckelt, validiert und erklärt über drei unabhängige Domänen hinweg, die nie eine Zeile Prompt-Text geteilt haben. Blicke ich auf die Fäden zurück, die diese Staffel absichtlich statt aus Versehen offengelassen hat: Die Verhaltensprofil-Generierung wurde noch nicht zu structuredGenerate() migriert, weil bisher nichts die Frage erzwungen hat. Das Knowledge-Builder-Proposal-Framework existiert vollständig mit null registrierten Generatoren. HUSKY=0 bleibt ein dokumentierter, legitimer Weg, die Hooks zu überspringen, die eigentlich das Sicherheitsnetz sein sollen. Und die oben genannte Freitext-Behauptungslücke ist noch offen. Keins davon sind Fehler, so sehr sie das eigentliche Ausgangsmaterial der nächsten Staffel sind — jeder einzelne ist eine Stelle, an der die Plattform derzeit etwas vertraut (meinem Gedächtnis, einer Konvention, einer weichen Anweisung), das eine spätere Entscheidung irgendwann durch eine strukturelle Garantie ersetzen könnte, genauso wie es bei jedem anderen Vertrau-Claude-Standard in dieser Codebasis bereits geschehen ist.
Fazit
Nichts in dieser Staffel hat das Modell klüger gemacht. Jede Korrektur, vom ersten gemeinsamen structuredGenerate()-Wrapper bis zum letzten Claim Validator, war dieselbe Bewegung, erneut an einer neuen Stelle ausgeführt: aufhören, einer Zahl, einem Prompt oder der Selbstdarstellung eines Modells zu vertrauen, und sie durch etwas ersetzen, auf das man zeigen, das man inspizieren und das man — wenn es falsch ist — abfangen kann, bevor es jemanden erreicht. Das ist kein Feature, das ich am Ende hinzugefügt habe. Es ist das, wofür diese ganze Plattform, von der ersten Tabelle an, eigentlich gedacht war.