Wie wir einen Agenten gebaut haben, der deine Umsatzsteuer einreicht
Autofiling bereitet deine UStVA vollständig vor und reicht sie ein. Das Schwere war nie das Modell – sondern einen stochastischen Agenten sicher genug zu machen, um eine echte Steuererklärung abzugeben. So haben wir es gebaut.
- Kategorie
- Allgemein
- Aktualisiert
- Autor:in
- Stan Kharlap
Jedes Quartal – oder jeden Monat, wenn du größer bist – muss ein deutsches Unternehmen seine Umsatzsteuervoranmeldung über ELSTER ans Finanzamt schicken. Sie hat einen festen Abgabetermin, sie verzeiht keine Rechenfehler, und ein Fehler kostet echtes Geld. Die meiste Buchhaltungssoftware hilft dir, das Formular auszufüllen. Wir wollten, dass die Software die Anmeldung einfach macht – die Zahlen sammeln, prüfen und einreichen – während der Mensch bei dem einen Moment, auf den es ankommt, fest die Kontrolle behält.
Diese Funktion heißt Autofiling, und das Interessante am Bau hatte fast nichts mit dem Sprachmodell zu tun. Ein Modell, das Belege lesen und Umsatzsteuer aufaddieren kann, ist die Grundvoraussetzung. Das eigentliche Problem war das Gegenteil einer Demo: Wie lässt man einen stochastischen Agenten einen unwiderruflichen rechtlichen Akt im Namen einer anderen Person ausführen – und kann hinterher beweisen, dass er genau das Richtige getan hat?
Eine Steuererklärung ist ein Rechtsakt, kein Chat
Der erste Reflex bei jeder Agenten-Funktion ist, ein Modell an ein paar Tools zu hängen und es laufen zu lassen. Für einen Chatbot: in Ordnung. Für eine Steuererklärung ist dieses Design eine Haftung:
- Das Einreichen ist nicht idempotent. Eine UStVA zweimal an ELSTER zu senden ist keine doppelte Zeile, die man später aufräumt – es sind zwei Anmeldungen. Ein Absturz zur falschen Millisekunde darf nicht zu einer erneuten Abgabe führen.
- „Wahrscheinlich richtig" ist keine Messlatte. Das Ergebnis muss einsehbar und exakt sein. Niemand unterschreibt eine Zahl, die er nicht sehen kann.
- Es ist fristgebunden. Der Agent darf nicht stillschweigend hängen bleiben. Wenn etwas fehlt, muss das als konkrete, lösbare Aufgabe auftauchen – jetzt, nicht am Abgabetag.
Autofiling ist deshalb kein Prompt mit angehängten Tools. Es ist eine State Machine mit einem Agenten, der darin arbeitet – und jeder Übergang, der die Außenwelt berührt, ist unter der Annahme entworfen, dass der Prozess an jeder Stelle abstürzen kann.
Eine Pipeline, keine Blackbox
Ein Autofiling-Lauf durchläuft explizite, persistierte Zustände – SCHEDULED → COLLECTING → RECONCILING → READY_FOR_APPROVAL → APPROVED → SUBMITTING → FILED – mit zwei Notausgängen, NEEDS_INPUT und FAILED, für den Fall, dass die Realität nicht mitspielt. Der Zustand liegt in der Datenbank, nicht im Kopf des Agenten, also ist ein Lauf immer fortsetzbar und immer prüfbar.
Bereitschaft statt Bauchgefühl
Bevor eine Anmeldung eine Vorschau wert ist, geht der Lauf in RECONCILING – und hier verdient sich der Agent seinen Platz, indem er ein guter Pessimist ist. Statt selbstbewusst einzureichen, was er hat, sucht er nach Gründen, warum die Anmeldung falsch sein könnte, und macht aus jedem einen typisierten Blocker.
Blocker sind keine Freitext-Warnungen. Sie sind eine geschlossene Menge maschinenlesbarer Typen – uncategorized_transactions, unverified_transactions, possible_duplicates, unclaimed_input_vat, vat_conflict, expired_bank_sync, missing_tax_settings, elster_validation_error und ein Dutzend mehr – jeweils mit einer Schwere von info, warning oder blocking. Ein blocking-Blocker stoppt den Lauf und schiebt ihn nach NEEDS_INPUT mit einer konkreten, lösbaren Aufgabe („14 Transaktionen in diesem Zeitraum haben keine Kategorie") statt einem vagen „irgendwas stimmt nicht". Informative Blocker reisen auf der Vorschau mit, sodass der Mensch sie sieht, aber nicht gestoppt wird.
Der Punkt ist: Der Agent wird dafür belohnt, Probleme sichtbar zu machen, nicht dafür, sie hinter einer selbstsicheren Antwort zu verstecken. Eine Anmeldung, die noch nicht abgegeben werden sollte, erreicht nie den Punkt, an dem sie es könnte.
Du gibst die Payload frei, nicht das Versprechen
Ist ein Lauf sauber, rendert er eine Vorschau: exakt die ELSTER-Payload, die übertragen würde, erzeugt durch dieselbe Report-Engine, die auch die echte Einreichung macht – nur im Testmodus. Diese Payload wird gehasht, und der Hash wird als preview_payload_hash am Lauf gespeichert.
Die Freigabe bindet sich an diesen Hash. Wenn ein Mensch freigibt, speichern wir approved_payload_hash zusammen mit dem Wer und Wann. Ab diesem Moment müssen beide übereinstimmen. Ändert sich irgendetwas davor – ein später Beleg trifft ein, eine Kategorie wird korrigiert, USt-Einstellungen verschieben sich – rendert die Payload zu einem anderen Hash, die gespeicherte Freigabe wird ungültig, und der Lauf weigert sich, auf Basis veralteter Zustimmung weiterzumachen. Du gibst nie „den Plan des Agenten" frei. Du gibst genau diese Zahlen frei – und das System setzt das wortwörtlich durch.
Ein Einreichen, das nicht doppelt abgeben kann
Dieser Teil gefällt mir am besten, weil er auf genau die richtige Art langweilig ist. Der Einreich-Schritt ist bewusst nicht in eine einzige große Transaktion gepackt. Hier ist der echte Docstring aus dem Runner:
def submit_approved_run(run: AutoFilingRun) -> AutoFilingRun:
"""Submit an approved run to ELSTER.
Deliberately NOT one big transaction: the ELSTER POST is a non-idempotent
external effect. SUBMITTING is committed *before* the external call, and
the result is committed in its own transaction afterwards — so a crash
can never roll the DB back to a state that would re-file the same UStVA.
"""
Der Ablauf, der Reihe nach:
- In einer Transaktion wird unter einem Row-Lock alles erneut geprüft – Status, dass der freigegebene Hash noch zur aktuellen Payload passt, dass das Konto noch berechtigt ist – dann wird der Lauf auf
SUBMITTINGgesetzt und committet. - Erst danach erfolgt der externe ELSTER-Aufruf, außerhalb jeder Transaktion.
- Das Ergebnis – eingereicht oder ein konkretes Scheitern – wird in einer eigenen Transaktion committet.
Weil SUBMITTING vor dem Netzwerkaufruf dauerhaft ist, wacht ein Prozess, der mitten im Einreichen stirbt, in einem Zustand auf, der sagt „das war bereits unterwegs" – und der Recovery-Pfad prüft, ob ELSTER den Report schon akzeptiert hat, bevor er je erneut senden würde. Ein vorübergehender Provider-Fehler setzt den Lauf auf APPROVED zurück, damit ein Sweeper ihn erneut versucht, bis zu einer begrenzten Anzahl an Versuchen, ohne je die Freigabe zu verlieren. Das Einzige, was niemals passieren kann, ist dieselbe Anmeldung zweimal abzugeben. Alles andere ist wiederherstellbar.
Jeder Schritt ist aktenkundig
Unter Autofiling liegt unsere Agenten-Schicht, und ihre ganze Aufgabe ist es, den Agenten nachvollziehbar zu machen. Jede Einheit Modellarbeit – ein Dokument lesen, eine Korrektur entwerfen, die Anmeldung vorbereiten – wird als AgentRun festgehalten: zu welchem Workflow sie gehörte, welcher Provider und welches Modell lief, ein Hash von Eingabe und Ausgabe, die Kosten, die Latenz und die exakte PromptVersion, die sie erzeugt hat. Die Modellwahl pro Workflow ist selbst Policy – eine ModelPolicy mit Obergrenzen für Kosten, Latenz und Risikostufe plus Fallback-Modell – kein in den Code vergrabener Konstantwert.
Diese Buchführung ist keine Zeremonie. Sie ist das, was uns Monate später die Frage „warum hat dieser Lauf genau diese Zahl erzeugt?" mit einer echten Antwort beantworten lässt statt mit einem Schulterzucken. Sie macht Kosten und Latenz zu einem Dashboard statt zu einer Überraschung. Und weil jeder Prompt versioniert ist und jede echte Korrektur zu einem Evaluationsfall eingefroren werden kann, ist sie das Fundament, das den Agenten über die Zeit messbar besser werden lässt – wobei ein Mensch, nie das Modell, entscheidet, was ausgerollt wird.
Die langweiligen Teile sind das Produkt
Es wäre leicht gewesen, eine spektakulärere Version davon zu bauen: eine Chatbox, in der du den Agenten bittest, „meine Umsatzsteuer einzureichen", und er tut es. Es wäre auch das Falsche gewesen, denn in dem Moment, in dem eine Anmeldung falsch ist, wird der Demo-Zauber zum Problem von jemandem mit dem Finanzamt.
Also lehnt sich das Design in die andere Richtung. Der Agent macht das mühsame, fehleranfällige Sammeln und Prüfen, das kein Mensch gern tut. Er weigert sich zu raten, wenn er fragen sollte. Er zeigt dir die exakte Payload und macht dich zu dem, der sie unterschreibt. Und er behandelt den Akt des Einreichens mit der Paranoia, die eine rechtliche Anmeldung verdient. So hat jeder Agent Gestalt, den wir bei Norman bauen: die langweilige Arbeit vollständig automatisieren, den Menschen bei der Entscheidung mit dem Risiko behalten und jeden Schritt dazwischen beweisen können.
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.