Zurück zu Technology

Unser Agent hat 102 Tools. Die Ingenieursarbeit steckt in dem, was er nie sieht

Jeder Reflex im Agenten-Design ist gerade additiv: mehr Tools, mehr Instruktionen, mehr Memory. Norman stellt 102 Tools und 19 Workflow-Playbooks bereit, und fast jede Architekturentscheidung war eine Subtraktion. Hier steht, was wir dem Modell wegnehmen, und warum der Agent seine Aufgabe besser erledigt, weil er nie weiß, in welchem Schritt er ist.

Kategorie
Allgemein
Aktualisiert
Autor:in
Stan Kharlap

Verbinde heute einen Client mit Normans MCP-Server, und er reicht dir 102 Tools über vierzehn Module. Rechnung erstellen, Transaktion kategorisieren, einen Gründer durch die GmbH-Gründung führen, einen Fragebogen zur steuerlichen Erfassung vorbefüllen, eine Umsatzsteuermeldung ziehen, eine Rechnung bezahlen. Auf dem Papier ist diese Zahl das Verkaufsargument: schau, wie viel der Agent kann.

In der Praxis ist diese Zahl das Problem. Alles im Agenten-Design zieht gerade in die additive Richtung: gib dem Modell mehr Tools, mehr Instruktionen, mehr Memory, mehr Autonomie, und vertrau darauf, dass ein besseres Modell das Chaos schon absorbiert. Wir sind den anderen Weg gegangen. Fast jede Entscheidung in Normans Agenten-Schicht ist eine Subtraktion, und die nützlichste Regel, bei der wir gelandet sind, lautet: Der Agent darf nie wissen, in welchem Schritt er gerade ist.

Die Skizzen unten sind auf die Form der Sache vereinfacht und nicht aus unserem Quellcode kopiert, aber die Formen sind echt.

Tool-Anzahl ist keine Fähigkeit. Sie ist ein Budget, das zweimal ausgegeben wird

Ein Tool kostet dich in zwei Währungen, und nur eine davon ist offensichtlich.

Die offensichtliche sind Token. Jede Tool-Definition geht in jedem Request mit, hundert Tools sind also eine feste Steuer auf jeden einzelnen Zug jeder Unterhaltung, egal ob der Nutzer gerade eine Firma gründet oder fragt, wann seine Umsatzsteuer fällig ist. Die weniger offensichtliche ist Aufmerksamkeit. Ab etwa fünfzig Tools werden Modelle messbar schlechter darin, das richtige auszuwählen und die dazugehörigen Instruktionen zu befolgen. Du merkst das nicht als Fehler. Du merkst es als einen Agenten, der anfängt, ungefähr das Richtige zu tun.

Die Tool-Liste ist in unserem Server deshalb keine Konstante. Sie wird pro Nutzer beim Start zusammengesetzt, aus dem, was dieser Nutzer plausibel brauchen könnte:

register(core_tools)                       # Rechnungen, Transaktionen, Steuern

if company.is_incorporated:
    register(chart_of_accounts_tools)      # für Freelancer bedeutungslos

if user.role == "tax_advisor":
    register(multi_client_review_tools)    # für Unternehmer bedeutungslos

Ein Freelancer sieht die Kontenrahmen-Tools nie, weil ein Freelancer keinen Kontenrahmen hat. Ein Unternehmer sieht die Review-Tools des Steuerberaters nie. Unsere eigenen Kommentare halten fest, was jedes dieser Auslassen wert ist, und für einen typischen Nutzer landet die Summe in der Größenordnung von fünf- bis achttausend Token, die aus jedem Request verschwinden.

Unglamouröse Rohrverlegung, und der billigste Zuverlässigkeitsgewinn im ganzen System: Der schnellste Weg, ein Modell davon abzuhalten, das falsche Tool zu rufen, ist, ihm dieses Tool nicht zu schicken.

Playbooks gehören in einen Index, nicht in den Prompt

Über die Tools hinaus pflegen wir 19 Workflow-Playbooks als Markdown-Dateien im Repo: monatliche Abstimmung, Mahnungen, fehlende Belege finden, eine Firma gründen, DATEV-Vorbereitung. Echtes prozedurales Wissen, die Art, für deren Formulierung es einen Fachexperten gebraucht hat.

Der naive Zug ist, all das in den System-Prompt zu verketten. Das sind in der Größenordnung von zehntausend Token Instruktionen bei jedem Request, die meisten davon irrelevant für das, was der Nutzer gerade gefragt hat, und alle konkurrieren mit dem, worauf es wirklich ankommt, um die Aufmerksamkeit des Modells.

Wir liefern deshalb das Inhaltsverzeichnis und lassen den Agenten das Kapitel anfordern:

# Im System-Prompt: nur Namen und Auslöser, wenige hundert Token.
for workflow in workflows:
    prompt += f"- {workflow.name}: {workflow.trigger}\n"

@tool
def get_workflow_details(name: str) -> str:
    """Schritt-für-Schritt-Body für EINEN Workflow. Vor dem Ausführen rufen."""
    return workflows[name].body

Die Bodies zusammen kommen auf etwa zehntausend Token; genau einer davon ist je im Kontext. Ungefähr Faktor zwanzig weniger als alles inline, und das Modell liest die vollständige Prozedur genau dann, wenn es ihr gleich folgen wird, was auch der Moment ist, in dem es ihr am meisten Aufmerksamkeit schenkt.

Derselbe Instinkt bestimmt die Reihenfolge des Prompts selbst. Unser Modell-Anbieter cacht, wie inzwischen die meisten, ein ausreichend langes geteiltes Präfix über Requests hinweg und berechnet es mit Rabatt, womit Prompt-Layout keine Autorenfrage mehr ist, sondern eine Kostenfrage:

prompt  = STATIC_PREFIX            # für jeden Nutzer identisch, bleibt gecacht
prompt += workflow_index()         # für jeden Nutzer identisch
prompt += company_facts(company)   # unternehmensspezifisch, also immer zuletzt

Setz den Firmennamen eines Nutzers nach oben, und du hast das geteilte Präfix für jeden anderen Nutzer auf der Plattform invalidiert.

Die Regel, auf die es ankommt: verfolge deinen Fortschritt niemals selbst

Das ist die, die ich am härtesten verteidigen würde.

Drei unserer Flows sind wirklich lang: eine GmbH oder UG gründen (18 Tools), der Fragebogen zur steuerlichen Erfassung für Freelancer (12 Tools) und die Gewerbeanmeldung (10 Tools). Jeder davon ist eine Datenerhebung über fünf Abschnitte und mehrere Sitzungen, bei der ein Gründer mitten im Flow verschwindet, morgen zurückkommt, es sich bei der Rechtsform anders überlegt, einen dritten Gesellschafter hinzufügt und erwartet, dass der Agent genau weiß, wie der Stand ist.

Das verlockende Design überlässt das dem Modell. Gib ihm Memory, lass es den Fortschritt zusammenfassen, vertrau darauf, dass es sich merkt, dass das Kapital erledigt ist, der Notar-Abschnitt aber nicht. Wir machen das Gegenteil, und das Playbook sagt es in einer Zeile:

Das Backend ist die Quelle der Wahrheit. Navigiere nach sections.missing; verfolge deinen Fortschritt niemals selbst.

Jede Tool-Antwort, für was auch immer sie aufgerufen wurde, bringt den gesamten Zustand des Flows mit zurück:

{
  "status": "data_collection",
  "sections": {
    "company":      { "complete": true,  "missing": [] },
    "shareholders": { "complete": false, "missing": ["managingDirector"] },
    "capital":      { "complete": false, "missing": ["nominalSumMismatch"] }
  }
}

Fortschritt ist eine reine Funktion der Datenbank, bei jedem einzelnen Aufruf neu berechnet. Nichts davon lebt in der Unterhaltung.

Das Detail, das zeigt, warum das zählt, ist der letzte Marker. Das Stammkapital und die Summe der einzelnen Nominalbeträge der Gesellschafter müssen exakt übereinstimmen, und das ist keine Feld-für-Feld-Validierung, sondern eine Beziehung zwischen mehreren getrennten Datenstücken:

def missing_in_capital(record) -> list[str]:
    missing = [f for f in REQUIRED_FIELDS if not getattr(record, f)]

    # feldübergreifende Invariante: die Teile müssen das Ganze ergeben
    if sum(p.nominal for p in record.parts) != record.declared_total:
        missing.append("nominalSumMismatch")

    return missing

Ein Gründer, der das Stammkapital eine Stunde nach der Aufteilung der Anteile senkt, bekommt diesen Marker beim nächsten Aufruf zurück, und der Agent fragt danach, weil er direkt in der Antwort steht, die er gerade erhalten hat. Kein Maß an konversationellem Memory hätte das verlässlich erwischt.

Der Gewinn: Jede schwierige Frage zu langlaufenden Agenten hört auf, eine Agenten-Frage zu sein. Wiederaufnehmbarkeit: Der Flow läuft weiter, weil der Zustand nie in der Unterhaltung war. Gleichzeitigkeit: Ein Nutzer, der denselben Datensatz in der Web-App bearbeitet, kann den Agenten nicht desynchronisieren, weil im Agenten nichts ist, was desynchronisieren könnte. Kontextverlust: Die Historie zu kürzen kostet strukturell nichts, weil die nächste Tool-Antwort das gesamte Bild rekonstruiert. Halluzinierter Fortschritt wird strukturell unmöglich, weil das Modell nie das ist, was gefragt wird.

Wir halten das auch dort, wo es tatsächlich zwei Wahrheitsquellen gibt. Die Gründungs-Roadmap verfolgt sieben Schritte nach dem Notartermin, und jeder kann entweder vom Gründer abgehakt oder von unserem Ops-Team vorangebracht werden. Beides ist legitim, keines darf das andere überschreiben:

# Erledigt, wenn der Gründer es markiert hat ODER Ops den Meilenstein passiert hat.
# Selbst-Markierungen schreiben nie in den operativen Status, was die
# Meilenstein-Mails korrekt getimt hält und Undo zu einem sauberen Toggle macht.
done = bool(step.marked_at) or ops_status_reached(record, step.milestone)

Das in einem Prompt zu versöhnen wäre hoffnungslos. Als berechneter Read über zwei unabhängige Spalten sind es etwa sechs Zeilen.

Ein Vergleich zwischen dem, was auf dem Server existiert, und dem, was das Modell in einem Zug hält. Auf der Serverseite: 102 MCP-Tools über vierzehn Module, 19 Workflow-Playbooks mit insgesamt rund zehntausend Token, und Flow-State in der Datenbank als sections und Roadmap. In der Mitte drei Subtraktionen: Tools nach Nutzerrolle und Unternehmenstyp registrieren, was rund fünf- bis achttausend Token pro Request spart, einen wenige hundert Token großen Index ausliefern und Playbook-Inhalte bei Bedarf nachladen, und in jeder Tool-Antwort den Zustand zurückgeben, damit das Modell nichts speichert. Das resultierende Working Set enthält nur die Tools dieses Nutzers, einen Workflow-Index statt der Inhalte, ein statisches cachefähiges Präfix zuerst und Unternehmensfakten zuletzt, und ausdrücklich keinen Fortschritt, keinen Schrittzähler und keine Erinnerung an den Flow. Darunter die Schleife: Das Modell ruft ein Tool, der Server berechnet die fehlenden Abschnitte aus der Datenbank, die Antwort trägt den neuen Zustand, und das Modell liest ihn und stellt die nächste Frage. Der bindende Schritt fehlt mit Absicht: Es gibt kein Submit-Tool, ein Mensch drückt Submit in der App.
Fähigkeit bleibt auf dem Server. Was beim Modell ankommt, wird pro Zug zusammengesetzt, und Fortschritt ist nicht Teil davon.

Das wichtigste Tool ist das, das wir nicht ausgeliefert haben

Der Flow für die körperschaftsteuerliche Erfassung hat neun Tools zum Sammeln und Validieren von Daten, und dann hört er auf. Nicht weil uns die Zeit ausgegangen ist:

get_registration, get_choices, create_registration,
update_company, update_details, update_people,
update_financials, update_vat_and_bank,
get_submission_link          # gibt eine URL + readyToSubmit zurück

# Absichtlich nicht vorhanden: submit()

Der Fragebogen wird über ELSTER ans Finanzamt übermittelt, und dieser letzte, bindende Schritt passiert nur in der Norman-App, wo der Nutzer eine gerenderte Vorschau jeder Antwort sieht und selbst auf Submit drückt. Die endgültige Fähigkeit des Agenten besteht darin, dem Nutzer eine Tür zu reichen.

Das ist dieselbe Linie, die wir bei Autofiling gezogen haben: Alles, was beim Finanzamt einreicht, Geld bewegt oder einen Brief verschickt, ist freigabepflichtig, und das Gate ist kein Bestätigungs-Prompt, an dem sich das Modell vorbeireden könnte. Es ist die Abwesenheit eines Tools. Es gibt keine Formulierung, keinen Jailbreak, keinen verwirrten Multi-Turn-Zustand, der den Agenten eine bindende Steuererklärung einreichen lässt, weil dieses Verb in seinem Vokabular nicht existiert. Fähigkeit, die du nie erteilst, braucht kein Guardrail.

Ich erkläre einem Nutzer lieber, warum der Agent ihm einen Link gegeben hat, als einem Gründer, warum er die falsche Rechtsform ans Finanzamt übermittelt hat.

Wie das in der Produktion aussieht

In den letzten zwei Wochen hat unsere Agenten-Schicht deutlich über hunderttausend getracete Runs ausgeführt, und die Verteilung lohnt sich: Die überwiegende Mehrheit ist Kategorisierung und OCR, hochvolumige enge Aufgaben, bei denen ein Modell eine eng gefasste Sache innerhalb deterministischen Codes tut. Der konversationelle Agent, der alle 102 Tools hält, macht ein paar hundert davon aus. Die breiteste Tool-Oberfläche trägt den dünnsten Verkehr.

Diese Umkehrung ist kein Versagen des Chat-Agenten. Es ist die Architektur, die wie beabsichtigt arbeitet. In der Unterhaltung wohnt die Ambiguität, sie bekommt deshalb die meiste Fähigkeit und die engsten Grenzen; die hochvolumigen Pfade sind konstruktionsbedingt eng und brauchen fast keine. AI-first zu sein bedeutet nicht, alles durch eine Chat-Box zu routen. Es bedeutet, präzise darin zu sein, welche Entscheidungen wirklich ein Modell brauchen, und die langweilige Maschinerie zu bauen, die diese Entscheidungen sicher macht.

Wir werden weiter Tools hinzufügen; es gibt eine lange Liste von Dingen, die Norman kann und der Agent noch nicht erreicht. Aber das Verhältnis, das wir wirklich beobachten, ist nicht die Zahl ausgelieferter Tools. Es ist, wie viel das Modell im Kopf halten muss, um sie zu benutzen, und diese Zahl halten wir mit Arbeit flach.

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.