Zurück zu Technology

Wie wir jeden KI-Agenten-Lauf nachverfolgen, ohne deine Daten zu speichern

Jeder Modellaufruf in Norman hinterlässt eine Prüfspur: welcher Workflow, welches Modell, was es gekostet hat, wie lange es gedauert hat. Was er nicht hinterlässt, sind deine Belege und Bankdaten. Wir speichern Hashes, keine Payloads. So sieht die Observability-Schicht hinter unseren Agenten aus.

Kategorie
Allgemein
Aktualisiert
Autor:in
Stan Kharlap

Jede KI-Funktion, die wir ausliefern, lässt ein Modell auf etwas Privates los. Die Kategorisierung liest eine Banktransaktion. OCR liest einen Beleg. Die Abstimmung vergleicht eine Rechnung mit einer Zahlung. Autofiling stellt eine ganze Umsatzsteuervoranmeldung zusammen. Wenn du Agenten in Produktion betreiben und trotzdem ruhig schlafen willst, musst du noch Monate später beantworten können: „Was genau hat dieser Lauf getan, und warum?" Der naive Weg zu dieser Antwort ist, alles zu protokollieren: jeden Prompt, jede Antwort, jedes Dokument, das das Modell gesehen hat.

Wir machen das Gegenteil. Wir verfolgen Zehntausende Agenten-Läufe, und wir behalten bewusst fast keine der Daten. Was wir speichern, ist die Form eines Laufs, seine Kosten, sein Anbieter und ein kryptografischer Fingerabdruck von Ein- und Ausgabe. Die Belege und die Beträge bleiben in den Tabellen, in die sie gehören. In diesem Beitrag geht es um diesen Handel, und um den kleinen Haufen unspektakulärer Mechanik, die ihn trägt.

Ein Lauf, eine Zeile

Unter jeder KI-Funktion sitzt eine einzige App, deren einzige Aufgabe es ist, Modellarbeit nachvollziehbar zu machen. Jede Einheit dieser Arbeit wird zu einer Zeile, einem Agenten-Lauf. Die interessanten Spalten sind nicht die Payload, denn es gibt keine. Grob sieht ein Lauf so aus:

AgentRun
  workflow            ocr | categorization | reconciliation | autofiling | ...
  status              running | needs_input | succeeded | failed | ...
  provider, model     wer ihn ausgeführt hat
  input_hash          Fingerabdruck dessen, was hineinging
  output_hash         Fingerabdruck dessen, was herauskam
  estimated_cost      in Micro-USD
  latency_ms
  prompt_version      welcher Prompt ihn erzeugt hat

Ein Lauf hat Kinder. Jeder Schritt ist ein Modell- oder Tool-Aufruf darin, mit eigenen Token-Zahlen und eigenen Kosten. Jedes Artefakt ist eine strukturierte Ausgabe, die es wert ist, behalten zu werden (die Felder, die OCR extrahiert hat, die Antwort, die der Copilot zurückgegeben hat). Die Aufrufstellen bauen davon nichts von Hand. Sie umschließen den Modellaufruf mit einem Context Manager und setzen das Ergebnis beim Verlassen, grob:

with traced_agent_run(workflow="categorization", input_payload=payload) as trace:
    result = call_model(payload)
    trace.set_output(result, response=response)

Das ist der ganze Vertrag. Starte einen Lauf, mach die Arbeit, gib die Ausgabe zurück. Die Schicht protokolliert den Schritt, rollt Kosten und Latenz auf, schließt den Lauf ab und schreibt ein Artefakt, falls du danach gefragt hast. Sechs echte Workflows in Produktion gehen heute durch genau diesen Pfad.

Wir speichern den Hash, nicht die Daten

Sieh dir input_payload oben noch einmal an. Wir geben die tatsächliche Eingabe des Modells an den Tracer, und trotzdem hält die Zeile immer nur einen input_hash. Das ist der ganze Kniff. Bevor irgendetwas geschrieben wird, wird die Payload auf einen Fingerabdruck reduziert, grob:

input_hash = sha256(canonical_json(payload))

Das Wort, das das Gewicht trägt, ist kanonisch: wir serialisieren mit sortierten Schlüsseln, sodass dieselbe logische Eingabe immer dieselben Bytes und damit denselben Fingerabdruck erzeugt, unabhängig von der Reihenfolge im Dictionary. Ein 64 Zeichen langer Digest ersetzt einen Beleg, einen Namen eines Geschäftspartners oder eine ganze Transaktionsliste.

Ein Hash lässt sich nicht in ein Dokument zurücklesen, und genau das wollen wir. Aber nutzlos ist er nicht. Er beantwortet die Fragen, die in Produktion tatsächlich aufkommen:

  • Hat sich die Eingabe geändert? Wird ein Lauf erneut ausgelöst und der input_hash stimmt überein, hat sich stromaufwärts nichts bewegt. Ist er anders, dann eben doch. Das ist die Grundlage für Idempotenz und für das Erkennen von stillem Drift.
  • Hat das Modell zweimal dieselbe Antwort gegeben? Gleiche output_hash-Werte bedeuten identische Ausgabe, ohne zwei Blöcke Kundendaten zu vergleichen.
  • Welche Korrektur gehört zu welchem Lauf? Wenn ein Nutzer eine Kategorisierung korrigiert, können wir die Korrektur per Fingerabdruck an genau den Lauf binden, der sie erzeugt hat, und müssen dafür nie speichern, was das Modell gesehen hat.

Jede Trace-Zeile trägt außerdem ihr eigenes Policy-Label, damit die Absicht selbstdokumentierend ist und an einer Stelle durchgesetzt wird: eine Aufbewahrungsrichtlinie von „nur Metadaten und Hashes", und eine Payload-Richtlinie, die besagt, dass Ein- und Ausgaben gehasht statt gespeichert werden. Artefakte sind die eine Ausnahme, und eine bewusste. Wenn wir doch eine strukturierte Ausgabe behalten, dann ist es ein kleines Objekt bekannter Form, das wir selbst erzeugt haben (extrahierte Felder, eine kurze Zusammenfassung), niemals das rohe Quelldokument, und auch das wird gehasht. Der rohe Beleg liegt bereits in seiner eigenen Tabelle mit eigenen Zugriffsrechten. Die Trace bekommt keine zweite Kopie.

Ein nachverfolgter Lauf: die Modelleingabe links (Belegbild, Geschäftspartner, Betrag, Umsatzsteuer) läuft durch einen SHA-256-Hash und wird verworfen, während die gespeicherte AgentRun-Zeile rechts nur Workflow, Modell, Ein- und Ausgabe-Hash, Kosten, Latenz und Prompt-Version behält. Eine Fußzeile nennt die Aufbewahrungsrichtlinie metadata_and_hashes_only.
Was das Modell sieht, wird gefingerprintet und verworfen. Was wir speichern, ist die Form des Laufs: Workflow, Modell, Kosten, Latenz, Prompt-Version und zwei Hashes.

Tracing darf niemals kaputt machen, was es nachverfolgt

Eine Observability-Schicht, die die Funktion zum Absturz bringen kann, die sie beobachtet, ist schlimmer als gar keine Schicht. Deshalb ruht der ganze Tracer auf einer Regel: ein Tracing-Fehler ist für den Aufrufer unsichtbar. Er zeigt sich an drei Stellen.

Erstens lässt sich die Schicht komplett abschalten, und jeder Einstiegspunkt prüft dieses Flag, bevor er die Datenbank berührt. Zweitens ist jeder Schreibvorgang defensiv: ein Fehler beim Protokollieren wird gefangen, geloggt und in ein „keine Trace"-Ergebnis verwandelt statt in eine Ausnahme, sodass ein fehlerhafter Schreibvorgang in die Trace-Tabelle niemals nach oben in die Kategorisierung durchschlagen kann. Drittens behandelt jeder nachgelagerte Helfer einen fehlenden Lauf als „Tracing findet nicht statt" und macht still gar nichts.

Das Einzige, was der Tracer nicht verschluckt, ist dein Fehler. Wirft der Modellaufruf im Block, wird der Lauf als fehlgeschlagen protokolliert und die Ausnahme unverändert an dich weitergereicht. Wir verstecken unsere eigenen Fehler, niemals deine. Das Ergebnis ist, dass die Kategorisierung weiterläuft, egal ob die Trace-Tabelle einen guten Tag hat oder nicht, und das ist das einzig akzeptable Verhalten für etwas, das jeden Modellaufruf im Produkt umschließt.

Kosten sind eine Spalte erster Klasse, keine monatliche Überraschung

Weil die Schicht jeden Modellaufruf sieht, ist sie der natürliche Ort, um zu zählen, was sie kosten. Jeder Schritt erfasst Eingabe-, Ausgabe- und Cache-Token-Zahlen und rechnet sie in geschätzte Kosten in Micro-USD um, nicht in Dollar: eine einzelne Kategorisierung kann einen winzigen Bruchteil eines Cents kosten, und ganzzahlige Mikrodollar lassen uns Millionen davon summieren, ohne Rundungsdrift bei Gleitkommazahlen.

Die Preise sind nicht fest verdrahtet. Eine Policy-Zeile pro Modell hält die Preise pro 1000 Token, eine Latenzobergrenze, ein Risikoniveau und ein optionales Kostenbudget, sodass eine Änderung dessen, was uns ein Modell kostet, eine Daten- und keine Deploy-Änderung ist. Wenn Schritte in ihren Lauf aufgerollt werden, werden die laufenden Summen für Kosten und Latenz mit einem einzigen atomaren Inkrement geschrieben, nicht mit einem Read-Modify-Write, sodass gleichzeitige Schritte am selben Lauf sich nicht gegenseitig die Zahlen überschreiben können. Sprengt ein Lauf das Budget seiner Policy, markieren wir ihn, statt ihn mitten im Flug zu töten, damit die Anomalie im Nachhinein abfragbar ist. Darauf berechnen wir Stückkosten, die eine Finanzperson wirklich interessieren: Kosten pro tausend kategorisierten Transaktionen, Kosten pro OCR-Dokument, Kosten pro eingereichter Voranmeldung. Wenn die Frage „Was kostet uns die KI" aufkommt, ist die Antwort eine Abfrage, keine Schätzung. Bei unserem aktuellen Volumen läuft diese Abfrage über nahezu eine Million Transaktionen in den Büchern unserer Kunden.

Prompts sind Daten, keine Deploys

Es gibt noch etwas, worauf der Lauf zeigt: die genaue Prompt-Version, die ihn erzeugt hat. Prompts leben in der Datenbank, verschlüsselt nach Workflow und Version, und der aktive wird zur Aufrufzeit hinter einem kurzen Cache aufgelöst. Die Sicherheitseigenschaft ist eine kleine, aber sie zählt: eine leere Übersteuerung bedeutet „nimm den im Code eingebackenen Prompt", also ist der schlimmste Fall einer fehlkonfigurierten Prompt-Zeile, dass wir still die ausgelieferte Version verwenden. Der Code ist immer der sichere Standard; das Aktivieren oder Deaktivieren einer Zeile kann die Pipeline niemals kaputt machen.

Das bringt zwei Dinge. Du kannst am Wortlaut feilen, ohne ein Release. Und weil jeder Lauf mit der Version gestempelt ist, die ihn gemacht hat, kannst du später sagen, welcher Prompt welche Ausgabe erzeugt hat. Dieses Stempeln ist das, was aus „wir haben den Prompt geändert und es fühlt sich besser an" etwas macht, das du tatsächlich messen kannst.

Der Lohn: ein Agent, den du benoten kannst

All das existiert, um ein Ziel zu stützen, nämlich den Agenten messbar besser zu machen, ohne ihn sich selbst verändern zu lassen. Derselbe Lauf, der einen Hash und eine Prompt-Version trägt, sammelt auch menschliches Signal: ein Like oder Dislike, und strukturierte Korrekturen, wenn ein Nutzer überschreibt, was das Modell erzeugt hat. Wir behalten ein paar hundert dieser Korrekturen als typisierte Review-Zeilen, jede mit einem Grund versehen (Extraktionsfehler, Mapping-Bug, nicht unterstützter Fall, schlichte Nutzerpräferenz), sodass ein Muster echter Fehler von Rauschen unterschieden und in einen Evaluationsfall eingefroren werden kann.

Diese Eval-Schleife ist jung. Wir haben die Mechanik (Findings, Eval-Fälle, benotete Ergebnisse) und füllen den Datensatz noch, also tue ich nicht so, als würden wir schon eine ausgereifte Regressions-Suite fahren. Aber die Grundlage ist der Punkt. Weil jeder Lauf gefingerprintet, versioniert und bepreist ist, können wir in dem Moment, in dem eine Korrektur es wert ist, gelernt zu werden, sie in einen wiederholbaren Test verwandeln und beweisen, dass der nächste Prompt es besser macht, wobei ein Mensch, niemals das Modell, entscheidet, was ausgeliefert wird.

Bewusst langweilig

Nichts davon ist der aufregende Teil eines KI-Produkts. Der aufregende Teil ist das Modell, das einen Beleg liest und ihn richtig versteht. Aber das Modell, das ihn in einer Demo richtig versteht, und das Modell, das ihn in der Steuererklärung von jemandem richtig versteht, sind verschiedene Behauptungen, und der Abstand zwischen ihnen ist genau diese Art unglamouröser Buchführung: eine Zeile pro Lauf, ein Hash statt einer Payload, Kosten, die du abfragen kannst, ein Prompt, den du benennen kannst, und ein Tracer, der lieber verschwindet, als die Funktion mit sich zu reißen. Automatisiere die langweilige Arbeit, halte den Menschen bei der Entscheidung, die das Risiko trägt, und sei in der Lage, jeden Schritt dazwischen zu belegen. Die Observability-Schicht ist, wie wir das letzte Versprechen halten.

Norman übernimmt die operative Arbeit im Hintergrund

Von Rechnungen bis Buchhaltung: Norman organisiert wiederkehrende Finanzarbeit, damit du Fristen sauber einhältst und weniger manuell nachhalten musst.