Die meisten Chat-Funktionen in Business-Software folgen demselben Muster: Kontext sammeln, in einen Prompt legen, das Modell ausführen und hoffen, dass die Antwort geerdet bleibt.
Für eine Demo kann das funktionieren. Riskant wird es, wenn dieser Kontext ein laufender Immobilien-Deal in Millionenhöhe ist.
Jedes Beispiel in diesem Beitrag nutzt einen öffentlichen Demo-Deal: Moosstrasse 4a, 6a und 8a in Abtwil (SG), drei Mehrfamilienhäuser mit 46 Wohnungen, angeboten als Paket für indikative CHF 19'000'000 bei 4.08% Bruttorendite. Das Exposé umfasst 48 Seiten: Mieterspiegel pro Einheit, Grundbuch-Dienstbarkeiten, Grundrisse, Baubeschriebe, drei Ölheizungen von 1989, 1993 und 2005 und ein Detail, das später wichtig wird: Haus 6a hat keinen Lift.
Quelle: Baubeschrieb, Seite 9. Markierung: H6a hat keinen Personenlift.
In AssetOS ist ein Deal wie Abtwil kein einzelnes Dokument. Er ist ein komplexer, lebender Graph aus Objekten, Parzellen, Eigentümern, Angeboten, Maklerfeldern, Gebäudeplänen, E-Mails, internen Notizen und Due-Diligence-Findings. Manche Daten sind sauber strukturiert, manche sind chaotischer Text. Manche dürfen bearbeitet werden, manche sind in Stein gemeisselt.
Die erste Version unseres Agenten behandelte diesen Deal wie einen grossen Prompt. Die heutige Version behandelt ihn als begrenzte Anwendungsoberfläche. Dieser Wechsel machte aus einem Zusammenfassungswerkzeug einen Agenten, der kontrollierte Aktionen in AssetOS ausführen kann.
Was Sie in diesem Beitrag erwartet
Dieser Beitrag dreht sich um eine Designentscheidung: Ein vertrauenswürdiger Deal-Agent kann nicht einfach ein grösserer Prompt sein. Er braucht eine begrenzte Anwendungsoberfläche, mit Tools zum Lesen, Suchen und Schreiben.
Wir zeigen den Unterschied anhand von drei konkreten Fehlern:
- Die falsche Rendite: Ein Modell leitet aus ähnlich klingenden Finanzzahlen etwas ab, statt berechnete KPIs aus Code zu nutzen.
- Die falsche Lift-Antwort: Ein Dokumenteninventar sagt dem Agenten, dass eine Datei existiert, aber nicht, was auf Seite 9 steht.
- Das erfundene Aufgaben-Update: Ein Modell sagt «erledigt», obwohl kein Plattformbefehl ausgeführt wurde.
Die Architektur dahinter, von Read-Tools bis Response-Contracts, existiert, damit genau diese drei Fehler nicht bei einem Transaktionsteam landen.
01 Warum der Riesen-Snapshot scheiterte
Am Anfang war die Versuchung real, einfach alles zu laden:
// Der "einfache" Weg (der scheitert)
const dealContext = await loadEverythingUseful(dealId)
const answer = await model.generateText({
system: 'Antworte nur aus diesem Deal-Kontext.',
prompt: JSON.stringify(dealContext),
})
Das scheiterte auf drei vorhersehbare Arten.
Jede Frage wurde zur Alle-Daten-Frage. Wenn ein Nutzer eine schnelle KPI-Frage zu Abtwil stellt, muss das Modell nicht die letzten acht Makler-E-Mails lesen oder die Geometrie von Parzelle Nr. 1496 analysieren.
Es brachte zu viele ähnliche Informationen in denselben Kontext. Immobilien-Deals enthalten viele ähnlich klingende Kennzahlen. Allein das Abtwil-Exposé enthält einen Verkaufspreis, drei Versicherungswerte, drei Nettomieten pro Gebäude, Bruttomieten und Quadratmeterwerte. Ein Modell die «Rendite» daraus ableiten zu lassen, ist unzuverlässig.
Probieren Sie es selbst:
Spielen Sie das ungeführte LLM
Alle sechs Zahlen unten stehen im Abtwil-Exposé. Wählen Sie die zwei, die Sie für die Bruttorendite dividieren würden, wie ein Modell mit einem riesigen Prompt.
Wählen Sie einen «Preis»
Wählen Sie eine «Miete»
Wählen Sie einen «Preis»
Wählen Sie eine «Miete»
Bruttorendite
0 von 9 Kombinationen ausprobiert
-
Ein ungeführtes Modell sieht alle sechs Zahlen in einem Prompt. calculateDealKpis sieht genau zwei und liefert jedes Mal 4.08%.
Jede Zahl in diesem Spiel steht im echten Exposé. Nur eine von neun Kombinationen ist richtig. Ein ungeführtes Modell hat keinen zuverlässigen Grund, sie zu bevorzugen.
Das System sollte die Geschäftslogik definieren, nicht das LLM. Darauf kommen wir in Abschnitt 07 zurück.
Snapshots können nichts verändern. «Füge das zu meinen internen Notizen hinzu» ist kein Textgenerierungsproblem. Es ist ein Anwendungsbefehl, und ein JSON-Blob kann ihn nicht ausdrücken.
Um alle drei Probleme zu lösen, haben wir die Fähigkeiten des Agenten in drei klar getrennte Bereiche aufgeteilt:
- Read-Tools für begrenzte Deal-Fakten.
- Search-Tools für Dokument- und Marktbelege.
- Write-Tools für explizite Plattformänderungen.
Der Rest dieses Beitrags zeigt, wie jeder dieser Bereiche funktioniert und wie wir ihn absichern.
02 Der Sicherheitsperimeter
Bevor die AI-Runtime startet, validiert unser API-Endpoint den Request, authentifiziert den Nutzer, prüft den Organisationszugriff, verifiziert den Chat-Thread und lädt kompaktiertes Memory. Erst dann starten wir den Agenten.
Vereinfacht sieht das so aus:
const thread = await dealChatCommands.ensureActiveThread({
organizationId,
dealId,
userId,
threadId,
})
const context = await dealChatQueries.getThreadContext({
organizationId,
dealId,
threadId: thread.data.threadId,
userId,
historyLimit: MAX_HISTORY_MESSAGES,
})
const runResult = await runAssistant({
scope: { mode: 'deal', organizationId, dealId },
userId,
prompt,
messages: context.data.messages,
threadMemory: context.data.memory,
attachments,
formattingPreferences,
timeZone,
})
Ein architektonisches Detail ist hier zentral:
Der Kontext des Agenten kommt direkt aus der Datenbank, niemals aus der optimistischen Chat-UI des Browsers. Das Frontend entscheidet nicht, was das Modell weiss.
Wenn das Modell behaupten will «Ich habe die Aufgabe erstellt», kann die Runtime in der Datenbank nachsehen, ob die Aufgabe wirklich existiert. Ein Kontext aus dem lokalen Browser-State kann diese Garantie nicht geben. Die UI würde ihren eigenen optimistischen Zustand bestätigen.
03 Kleine Read-Tools
Statt dem Modell einen grossen Datendump zu geben, haben wir ihm ein spezifisches Toolset gegeben. Jedes Tool wirkt als Runtime-Grenze, die einen Ausschnitt der Daten lädt, klare Quellenangaben zurückgibt und eine Audit-Spur hinterlässt.
Wählen Sie eine Frage zum Abtwil-Deal und schauen Sie, wie sie geroutet wird:
Routen Sie die Frage
Wählen Sie eine Frage zum Abtwil-Deal und sehen Sie, welche Read-Tools der Planner lädt und welche Teile des Deals unberührt bleiben.
Wählen Sie oben eine Frage, oder tippen Sie auf ein Tool, um zu sehen, was es tut.
Eine KPI-Frage berührt nie das E-Mail-Tool. Eine E-Mail-Frage lädt nie Parzellengeometrie. Begrenzung ist Teil des Produktverhaltens.
Jedes Tool liefert:
- Kontext-Sektionen für das Modell;
- Response-Quellen für Zitate;
- Count-Metadaten für Fortschritts-UI und Traces;
- optionale Grounding-Dokumente für die spätere Dokumentsuche;
- optionale Shared Data, die ein anderes Tool wiederverwenden kann.
Entscheidend: Diese Tools kooperieren. Wenn load_deal_kpis Kern-Objektdaten braucht, die load_deal_overview im selben Lauf bereits geladen hat, verwendet es diese Daten aus dem Shared State wieder, statt die Datenbank ein zweites Mal anzufragen.
04 Token-Disziplin
Grosse Kontextfenster ersetzen keine Auswahl. Sie verändern den Fehlermodus. Ein Modell kann mehr Tokens annehmen, muss aber trotzdem die richtigen Belege beachten, das richtige Tool wählen und veraltete oder irrelevante Fakten ignorieren.12
Das Prompt-Budget ist deshalb zuerst ein Zuverlässigkeitsbudget und erst danach ein Kostenbudget. Ein grosser Prompt kann die Antwort langsamer und teurer machen. Er kann auch den relevanten Satz in lauten Kontext einbetten und das Modell zur falschen Quelle führen.34
Das Prompt-Budget
Prompt-Budget ist ein Zuverlässigkeitsbudget. Eine breite Frage lädt zuerst einen gerankten Ausschnitt und verengt danach bei Bedarf.
Eine laute Quelle kann das Budget verbrauchen und die Belege verdecken, die die Frage braucht.
Deshalb begrenzen unsere Context-Builder hochkardinale Daten. Das Limit ist kein blinder Schnitt. Es ist ein Retrieval-Vertrag: einen gerankten ersten Ausschnitt laden, ihn als unvollständig markieren und den nächsten Retrieval-Pfad explizit machen.56
Hat ein Deal zum Beispiel 73 aktuelle E-Mails, fügt load_deal_emails nicht alle 73 in den Prompt ein. Das Tool lädt einen gerankten Ausschnitt, markiert ihn als unvollständig und bietet Folgepfade für tiefere Abfragen. Stellt der Nutzer eine breite Statusfrage zum Deal, können die wichtigsten E-Mails reichen. Fragt der Nutzer nach einem bestimmten Mieter, Makler, Thema oder Zeitraum, kann der Agent ein engeres Such-Tool aufrufen und relevante E-Mails ausserhalb des ersten Ausschnitts laden.7
Die zentrale Regel ist einfach: Begrenzter Kontext darf nie so tun, als wäre er vollständig.
Ein Ausschnitt sollte dem Modell sagen, was geladen wurde, was nicht geladen wurde und welches Tool es aufrufen soll, wenn die Antwort vom fehlenden Rest abhängt.
Das ist der Unterschied zwischen Token-Disziplin und Datenverlust. Ziel ist nicht, E-Mails 9-72 zu verstecken. Ziel ist, sie nicht standardmässig zu laden und sie dann gezielt abzurufen, wenn die Frage es verlangt.
05 Die Runtime: ein flexibler Loop mit hartem Fallback
Der Agent hat jetzt Tools und ein Budget. Die Runtime muss trotzdem sicherstellen, dass benötigte Tools verwendet werden. Das geschieht über ein Zwei-Pass-System: einen Agent-first-Tool-Loop und einen deterministischen Fallback-Pfad.
Der primäre Pfad (AI-SDK-Tool-Loop): Das Modell bewertet den Prompt des Nutzers, prüft seinen Tool-Katalog und ruft Tools auf. Es darf bis zu 9 Schritte verwenden, muss aber vor der finalen Antwort genau einmal einen strikten deal_response_contract aufrufen.
Der bewachte Fallback: Ignoriert das Modell ein nötiges Tool oder erzeugt eine falsche Fähigkeitsverweigerung, etwa «Ich habe keinen Zugriff auf Aufgaben», obwohl es ihn hat, fängt die Runtime das ab. Ein Action Planner und ein Source Planner laden die fehlenden Daten über einen deterministischen Wasserfall nach.
Der deal_response_contract gibt der Runtime strukturelle Integrität:
{
requestType: 'read_only' | 'platform_write' | 'clarification' | 'other',
platformMutationRequested: boolean,
platformMutationApplied: boolean,
confidence: 'low' | 'medium' | 'high'
}
Warum ist ein Objekt mit vier Feldern wichtig?
Ohne diesen Contract kann ein Modell einen überzeugenden Satz wie «Ich habe die Aufgabe für Sie aktualisiert!» ausgeben, selbst wenn es das zugrunde liegende Write-Tool nicht aufgerufen hat. Der Contract gibt unserer Runtime ein strukturiertes Objekt zum Prüfen und Validieren statt eines Satzes, dem sie vertrauen muss.
So sieht das aus der Perspektive der Runtime aus:
Gleicher Satz, andere Wahrheit
Beide Läufe unten enden mit exakt derselben Chat-Nachricht. Wechseln Sie zwischen ihnen und beobachten Sie, was die Runtime sieht.
Was der Nutzer sieht
Write-Tool-Log
update_work_item({ dueDate: '2026-06-19' }) → ok
Response-Contract
{
requestType: 'platform_write',
platformMutationRequested: true,
platformMutationApplied: true,
confidence: 'high'
}Runtime-Urteil
Verified: Contract stimmt mit dem Tool-Log überein. Die Antwort geht an den Nutzer.
Der Nutzer kann diese zwei Läufe nicht unterscheiden. Die Runtime schon, weil der Contract ein strukturiertes Objekt ist, das sie prüfen kann, und kein Satz, dem sie vertrauen muss.
Der Nutzer sieht einen Satz. Die Runtime sieht eine überprüfbare Behauptung.
Für regionale Feinheiten enthält unser Source Planner zusätzlich eine lexikalische Absicherung. Fragt ein Nutzer auf Deutsch nach Begriffen wie Kaufpreisfaktor oder Bruttoanfangsrendite, wartet das System nicht darauf, dass das Modell alles von Grund auf herleitet. Es routet die Frage zu den richtigen Read-Tools.
Mit der Architektur können wir drei Bereiche betrachten, in denen sie relevant wird.
06 Dokumente brauchen eine zweite Schicht
Das Dokumentlisten-Tool allein kann keine Fragen zum Dokumentinhalt beantworten. Das Abtwil-Exposé zeigt, warum. Probieren Sie die Lift-Frage in beiden Varianten:
Hinter den Kulissen
- Das Modell sieht nur das Dokumenteninventar: eine Datei «Baubeschrieb.pdf» und eine Übersicht, die Lifte erwähnt.
- Es verallgemeinert von 4a und 8a auf das ganze Paket. Die eigentliche Seite wurde nie gelesen.
Issue: Falsch. Die 6a hat keinen Lift. Aus einer beiläufigen Mieterfrage wurde eine falsche Deal-Auskunft.
Das Exposé sagt es in einer Zeile auf Seite 9: «H6a verfügt über keinen Personenlift.» Ein Dokumenteninventar weiss, dass die Datei existiert. Nur die zweite Schicht liest die Zeile.
load_deal_documents gibt dem Agenten das Dokumenteninventar und baut durchsuchbare Dokument-Metadaten. Der Suchtext enthält den Dokumentnamen und die Namen der übergeordneten Ordner. Das hilft, wenn der Dateiname generisch, aber der Ordner aussagekräftig ist.
Stellt der Nutzer eine Frage zum Dokumentinhalt, leistet search_deal_documents mehr Arbeit:
- Dokumenteninventar laden, falls noch nicht geschehen.
- Grounding-Chunks für die aktuelle Frage bauen.
- Direkten Dokumentzugriff für explizit genannte Dokumente versuchen.
- Einen Dokumentfragen-Spezialisten starten, wenn direkte Dokumentlesungen verfügbar sind.
- Auszugsquellen und Direkt-Dokument-Quellen zusammenführen.
- Dokumentzitate neu mappen, damit dasselbe Dokument einen stabilen
[D#]-Tag behält.
Es gibt noch eine Absicherung: Widersprechen sich die allgemeine Antwort und die direkte Dokumentantwort, wählt ein kleiner Selektor, welche gewinnt. Er bevorzugt die direkte Dokumentantwort, wenn die allgemeine Antwort verweigert, auslässt oder den Dokumentbelegen widerspricht.
Ein Modell kann einen direkten Mietvertragsauszug lesen und trotzdem aus einer schwächeren Übersichtszusammenfassung antworten. Der Selektor lässt Dokumentbelege die generische Antwort überstimmen.
07 KPIs: aus Code berechnet, nicht von der KI geschätzt
Immobilien-KPIs sind genau der Ort, an dem LLMs nicht improvisieren sollten. Dieselbe Falle mit neun Kombinationen aus Abschnitt 01 kann auch im Chat auftreten:
Hinter den Kulissen
- Der Prompt enthält den Verkaufspreis, drei Versicherungswerte, drei Nettomieten und m²-Werte. Sie klingen alle ähnlich.
- Das Modell nahm die Summe der Versicherungswerte als «den Wert». Die Rechnung stimmt; die Inputs nicht.
Issue: 6.4% liesse Abtwil wie ein Schnäppchen aussehen. Das Exposé sagt 4.08%.
Ein falscher Nenner, und ein 4.08%-Deal sieht aus wie ein 6.4%-Deal. Das ist kein Rundungsfehler. Es ist eine andere Investitionsentscheidung.
Das KPI-Tool ruft calculateDealKpis über den strukturierten Deal, die Objekte, Parzellen und Formatierungspräferenzen auf. Es liefert eine [K]-Quelle mit dem berechneten KPI-Payload und dem für die Berechnung verwendeten Miet-Snapshot.
Der Agent-Prompt ist explizit: Für KPI-Fragen rufe load_deal_kpis auf.
Die Runtime prüft manche KPI-Antworten zusätzlich nach der Generierung. Wurde das KPI-Tool geladen und zitiert die Antwort [K], klassifiziert eine Bewertung, ob die Antwort informativ ist, eine fehlende Quelle meldet, eine Fähigkeitsverweigerung darstellt oder etwas anderes. Sagt das Modell «Das kann ich nicht berechnen», obwohl das KPI-Tool geladen war, kann die Runtime weitermachen, statt die Antwort zu akzeptieren.
Dasselbe Muster existiert für Work Items. Hat load_deal_work_items Fälligkeiten oder Aufgaben geliefert und das Modell behauptet trotzdem, keinen Zugriff zu haben, kann die Runtime eine direkte Fallback-Antwort aus den geladenen Quellen erzeugen.
Das ist nicht der sichtbare Teil des Produkts. Es ist Runtime-Logik, die verhindert, dass schlechte Verweigerungen beim Nutzer landen.
08 Mutationen laufen über strikte Commands
Read-Tools können parallel laufen. Write-Tools sind sensibler. Der Agent unterstützt direkte Plattform-Aktionen wie Aufgaben erstellen, Fälligkeiten ändern oder Notizen schreiben, aber streng reglementiert. Das Abtwil-Exposé liefert einen konkreten Testfall: Die Ölheizung in der 8a stammt von 1989. Das ist ein Punkt, den ein Käufer mit dem Makler klären könnte.
Hinter den Kulissen
- Es wurde kein Write-Tool aufgerufen. Der Satz ist reine Textgenerierung.
- Zehn Minuten später tippt der Nutzer «gibt es Updates?». Das Modell liest die Historie, sieht die alte Anfrage und erstellt die Aufgabe doppelt.
- «Nächsten Freitag» wurde nie aufgelöst. Welches Datum in der Datenbank landet, ist Zufall.
Issue: Ein überzeugender Satz ohne Garantien und mit Replay-Risiko.
Ein fehlgeschlagener Read verschwendet eine Frage. Ein fehlgeschlagener Write kann falsche Daten in den Deal schreiben. Beide brauchen unterschiedliche Leitplanken.
Drei Regeln gelten für jeden Write:
- Keine Replays: Der Action Planner betrachtet nur die aktuelle Nachricht und ignoriert die Chat-Historie für Writes. So wird ein altes «Erstelle eine Aufgabe» von vor zehn Minuten nicht versehentlich erneut ausgeführt, wenn der Nutzer «gibt es Updates?» tippt.
- Datumsauflösung: Formulierungen wie «nächsten Freitag» werden automatisch in harte ISO-8601-Zeitstempel in der lokalen Zeitzone des Nutzers aufgelöst, bevor irgendetwas die Datenbank berührt.
- Strikte Serialisierung: Reads sind schnell und parallel; Writes werden sequenziell in eine Queue gestellt, um absolute Datenbankreihenfolge zu garantieren:
let writeToolQueue: Promise<void> = Promise.resolve()
const runSerializedWriteTool = async (operation) => {
const resultPromise = writeToolQueue.then(operation, operation)
writeToolQueue = resultPromise.then(
() => undefined,
() => undefined
)
return resultPromise
}
Nach dem Write schliesst der Response-Contract aus Abschnitt 05 den Kreis: platformMutationApplied muss zu dem passen, was wirklich passiert ist.
09 Die Anatomie einer vertrauenswürdigen Antwort
Wenn das Modell seine Arbeit beendet, verpackt unser Finalizer das Ergebnis. Er führt Zitate aus allen Tools zusammen, bewertet die Grounding-Konfidenz und baut ein Telemetrie-Objekt.
Die sichtbare Antwort ist bewusst kompakt:
Bruttorendite: 4.08%: CHF 774'780 Nettomiete p.a. ÷ CHF 19'000'000 indikativer Preis. [K]
Hinter diesem einen Satz behält die Runtime die Belege, die ihn vertrauenswürdig machen:
{
requestType: 'read_only',
platformMutationRequested: false,
platformMutationApplied: false,
confidence: 'high',
sources: ['K'],
toolsUsed: ['load_deal_kpis']
}
Bei einer Aufgabenmutation macht dasselbe Muster aus einem riskanten Satz eine überprüfbare Behauptung:
Aufgabe erstellt: «Makler auf Ölheizung ansprechen (8a, 1989)». Fällig Fr, 19. Juni 2026. [T1]
Diese Antwort darf nur durch, wenn ein Write-Command tatsächlich gelaufen ist, das aufgelöste Datum gespeichert wurde und der Response-Contract platformMutationApplied: true meldet.
Weil unser Backend Quellen über ein strukturiertes Schema statt über ein flaches Textlabel verfolgt, kann unser Frontend interaktive UI-Tags wie [KPI], [Dok 1] oder [Aufgabe 2] rendern, ohne die zugrunde liegenden Daten zu verlieren.
Eine schlechte Antwort wird dadurch einfacher zu debuggen. Die Traces zeigen den Bruchpunkt:
- Hat der Source Planner das falsche Tool gewählt?
- Hat die Dokumentsuche leere Auszüge geliefert?
- Hat das Modell eine verfügbare Datenquelle ignoriert?
- Hat der Write Planner eine Frage für einen Befehl gehalten?
Das sind Engineering-Probleme, die wir tatsächlich verfolgen, reproduzieren und beheben können.
10 Das Fazit
Einen tool-gestützten Agenten zu bauen ist komplex. Wir mussten Planner, Resolver, Contracts und Fallbacks bauen, um eine Chat-Oberfläche zu betreiben.
Diese Komplexität hat einen klaren Zweck. Sobald ein Agent von Reden zu Handeln wechselt, reichen lockere Prompts nicht mehr. Ein Snapshot-gefüttertes Modell kann die Abtwil-Rendite raten, einen Lift in der 6a erfinden oder behaupten, eine Aufgabe erstellt zu haben, die es nie geschrieben hat. Jeder dieser Fehler erzeugt Risiko in einer echten CHF-19-Millionen-Entscheidung.
Indem wir die KI auf ein einzelnes Geschäftsobjekt begrenzen, Berechnungen in nativen Code zwingen, Writes als strikte Anwendungsbefehle behandeln und einen deterministischen Fallback-Pfad pflegen, haben wir einen Agenten gebaut, dem AssetOS-Nutzer in der Transaktionsarbeit vertrauen können.
Sie möchten den Agenten auf einem Live-Deal sehen?