#engineering

Mehr als ein Chatbot: ein Multi-Tenant-RAG-Assistent, dessen Garantien in der Architektur liegen

Für ein Technologie-Dienstleistungsunternehmen haben wir eine Plattform für dokumentenbasierte Assistenten entworfen, die vielen Kunden angeboten wird: Die Datenisolation ist eine Eigenschaft jeder Abfrage, die teure Arbeit der Modelle wird nicht doppelt bezahlt, und ein Deployment unterbricht keine laufende Indexierung.

Softwarearchitektur · AWS · Django · FastAPI · LangGraph · Celery · pgvector

Einen Chatbot zu bauen, der auf Grundlage einiger PDFs antwortet, ist die Arbeit eines Nachmittags. Eine Plattform zu bauen, auf die viele Organisationen große Mengen an Dokumenten hochladen und die sie über die Web-App, ein Widget auf ihrer eigenen Website, eine Messaging-App und eine API abfragen, ist ein ganz anderes Handwerk. Dort betreffen die schwierigen Fragen nicht das Sprachmodell: Was verhindert, dass die Dokumente eines Kunden in der Antwort an einen anderen landen, was verhindert, dass derselbe Modellaufruf zweimal bezahlt wird, was passiert mit einer langen Indexierung, wenn eine neue Version ausgerollt wird.

Für ein Technologie-Dienstleistungsunternehmen haben wir die Architektur einer solchen Plattform entworfen und umgesetzt, die das Unternehmen seinen Kunden als Service anbietet. Jede Organisation arbeitet in einem oder mehreren Wissensräumen, jeder mit eigenen Dokumenten, eigenen Berechtigungen und eigener Konfiguration von Modellen und Retrieval. Wir sind einem einzigen Prinzip gefolgt: In einem Multi-Tenant-KI-Produkt müssen Isolation, Kosten und Servicekontinuität Eigenschaften der Architektur sein, nicht Sorgfaltspflichten, die jeder einzelnen Funktion überlassen bleiben. Eine Garantie, die davon abhängt, dass jemand an einen Filter denkt, fällt früher oder später weg.

Zwei Services, aber nur ein Eigentümer für Identität und Schema

In den ersten Versionen lebte alles in einem einzigen Django-Backend, einschließlich der Konversations-Pipeline und der Indexierung. Am Anfang ist das die richtige Wahl, und Django bleibt für uns das passendste Werkzeug für eine Domäne mit vielen Modellen und Berechtigungen. Mit dem Wachstum des Produkts begannen die beiden Hälften jedoch, in entgegengesetzte Richtungen zu ziehen: Die Steuerung der Plattform besteht aus kurzen, transaktionalen Anfragen, der KI-Teil hält Anfragen während des gesamten Streamings offen, verbringt seine Zeit mit Aufrufen an die Modellanbieter und wechselt Abhängigkeiten in einem deutlich schnelleren Takt. Wir haben sie in zwei Services aufgeteilt.

Service Zuständigkeit Stack
Control Plane Identität, Organisationen, Teams, Berechtigungen, Räume, Dokumente, API-Schlüssel, Kanäle, Kontingente Django, Gunicorn mit gevent
KI-Runtime Streaming-Konversation, Agenten, Retrieval, Ingestion der Dokumente, Evaluierungen asynchrones FastAPI, LangChain, LangGraph, Celery

Die Aufteilung in zwei Services ist der einfache Teil. Das eigentliche Risiko besteht darin, auch die Hoheit aufzuteilen: Zwei Services, die jeweils selbst entscheiden, wer der Nutzer ist, oder die jeweils das Schema ändern, laufen früher oder später auseinander, und die Abweichung zeigt sich erst in Produktion. Deshalb teilen sich die beiden Services dasselbe PostgreSQL, aber die Hoheit bleibt bei der Control Plane.

Das Schema gehört den Django-Migrationen: Die KI-Runtime greift über asynchrone SQLAlchemy-Modelle auf die Datenbank zu, die nach jeder Migration aus dem Schema neu generiert und nicht von Hand geschrieben werden, und gemeinsame Enumerationen liegen in einem Paket, das beide importieren. Auch die Identität hat nur einen Eigentümer. Wenn eine Anfrage bei der Runtime eintrifft, gleich über welchen Kanal, signiert die Runtime mit einem asymmetrischen Schlüssel ein extrem kurzlebiges Service-Token, das die ursprünglichen Zugangsdaten des Nutzers enthält, und bittet die Control Plane, diese in Nutzer, Raum und effektive Berechtigungen aufzulösen; sie selbst ergänzt lediglich Prüfungen im Sinne von Defense in Depth.

Die Pipeline macht diese Regeln überprüfbar. Die Datenbanktests der Runtime laufen auf dem Schema, das aus den Migrationen der Control Plane entsteht, und eine Prüfung verhindert, dass eine bereits integrierte Migration geändert, umbenannt oder gelöscht wird, sofern keine genehmigte Ausnahme vorliegt: Eine Abweichung fällt im Pull Request auf, nicht in Produktion. Beim Deployment laufen die Migrationen vor den Services, und die Runtime startet erst, wenn das Schema aktualisiert ist. Einen Kanal hinzuzufügen bedeutet zudem, einen weiteren aufzulösenden Anfragetyp hinzuzufügen, nicht die Zugriffsregeln zu duplizieren.

Serverloses Frontend Services auf ECS Asynchrone Verarbeitung Identität Web-App und Widget REST-API Messaging PostgreSQL mit pgvector Dokumente auf S3 Wissensgraph Modellanbieter CloudFront und S3 Rendering auf Lambda KI-Runtime Control Plane SQS-Queues Worker auf ECS

Die Isolation zwischen Kunden ist eine Eigenschaft jeder Abfrage

In einem RAG-System stecken die Daten eines Kunden auch in den Chunks der Dokumente und in deren Vektoren. Die verbreitetste Lösung besteht darin, der Anwendungsdatenbank eine dedizierte Vektordatenbank zur Seite zu stellen, mit einem Kundenfilter bei jeder Suche. Das funktioniert, verlagert die Isolation aber in ein zweites System, mit anderen Transaktionen und Garantien als beim Rest der Daten, und macht jede Löschung zu einer Operation, die zwischen zwei Speichern koordiniert werden muss.

Wir haben uns entschieden, Vektoren, Chunks und Anwendungsdaten in derselben Aurora-PostgreSQL-Datenbank zu halten, mit pgvector. Die Isolation wird über einen Join auf die Dokumenttabelle ausgedrückt, in derselben Transaktion und mit denselben Garantien wie der Rest der Daten. Ein Dokument zu löschen ist eine Operation auf einer einzigen Datenbank. Die Volltextsuche, die die semantische Suche ergänzt, nutzt dieselbe Engine. Auf der Isolation aufbauend legt ein Berechtigungssystem auf Basis von Zugriffsgruppen fest, welche Anwendungen, Tools und Agenten jede Organisation und jeder Nutzer verwenden dürfen.

Jeder Chunk hält außerdem fest, welches Embedding-Modell seinen Vektor erzeugt hat. Ein kleines Detail mit einer wichtigen Folge: Vektoren verschiedener Modelle bestehen nebeneinander, und ein Modellwechsel in einem Raum macht die übrigen nicht ungültig.

In der Praxis ist ein neuer Kunde eine Organisation und ein paar Räume, kein neues Deployment, und es gibt keinen zweiten Speicher, der synchron gehalten werden muss.

Die Qualität einer Antwort entscheidet sich zu zwei Zeitpunkten

Ein RAG-System antwortet nur dann gut, wenn es die richtigen Passagen findet, und die richtigen Passagen werden vorbereitet, wenn das Dokument hereinkommt, nicht erst, wenn die Frage eintrifft.

Beim Eingang: Kontext und Metadaten, Modelle nur dort bezahlt, wo sie nötig sind

Die Datei gelangt über eine signierte URL direkt aus dem Browser nach S3, und ab dort läuft die Verarbeitung vollständig asynchron. Wo die Funktion für die Organisation aktiviert ist, durchlaufen nur PDFs mit relevanten Bildern ein Vision-Modell, das Tabellen, Diagramme und Scans lesen kann; die übrigen werden Seite für Seite als Text extrahiert, und andere Formate als PDF werden mit MarkItDown zu Markdown. Die Kosten des Vision-Modells fallen nur dort an, wo es gebraucht wird.

Das nächste Problem ist weniger sichtbar: Ein Chunk aus einem langen Dokument steht oft nicht für sich allein. „Die Frist beträgt dreißig Tage“ nützt nichts, wenn man nicht weiß, um welchen Vertrag und welche Klausel es geht. Vor der Berechnung der Vektoren wird daher jeder Chunk mit Informationen angereichert, die ihn im Dokument verorten, nach der Technik des Contextual Retrieval und mit pro Raum konfigurierbaren Parametern: Ein Vertragsarchiv und eine Sammlung von Produktdatenblättern indexiert man nicht auf dieselbe Weise.

Parallel dazu extrahiert ein zweiter Task die Metadaten des Dokuments mit einem LangGraph-Graphen und normalisiert die Kernfelder gegen die bereits vorhandenen Werte, damit derselbe Kunde oder dieselbe Kategorie nicht in drei verschiedenen Schreibweisen auftaucht. Die beiden Tasks sind unabhängig voneinander, und der Status des Dokuments leitet sich aus beiden ab: Der Nutzer sieht stets, welcher Teil der Verarbeitung abgeschlossen ist.

Bei der Frage: zwei Suchen, ein einziges Ranking

Die semantische Suche allein hat eine bekannte Schwachstelle: Sie findet Passagen, die dasselbe mit anderen Worten sagen, neigt aber dazu, Codes, Kürzel und Kennungen zu verwässern. Deshalb wird die Konversation zweimal umformuliert: als Satz für die Vektorsuche, als wenige Schlüsselwörter für die Volltextsuche von PostgreSQL. Beide Suchen starten parallel, und die semantische liefert Textabschnitte, die groß genug sind, um einen ganzen Gedankengang zu enthalten.

Die wichtigste Entscheidung ist, wo sortiert wird. Wer jede Suche für sich sortiert, erhält zwei Rankings, die niemand miteinander vergleicht. Wir führen die Kandidaten zusammen, deduplizieren sie und lassen sie ein einziges Mal von einem Reranking-Modell sortieren: Dort wird entschieden, welche Passagen tatsächlich beim Modell ankommen.

Metadaten fließen als harter Filter in das Retrieval ein, wenn sich die Frage ausdrücklich auf eine Dokumentkategorie bezieht, oder als Boosting, das relevante Ergebnisse nach oben schiebt, ohne die anderen auszuschließen. Eine Konversation lässt sich auf eine oder mehrere Dokumentgruppen beschränken, und ist die gewählte Gruppe leer, liefert die Suche nichts statt alles: genau die Art von Detail, die, falsch umgesetzt, einem Nutzer Dokumente zeigt, die er ausgeschlossen hatte.

Jede Phase wird protokolliert: generierte Abfragen, die Kandidaten jeder Suche, Deduplizierung, finales Ranking und Scores. Überzeugt eine Antwort nicht, lässt sich genau rekonstruieren, was das Modell gesehen hat und warum. Die Antworten zitieren die Quellen mit Titel und Seiten.

Externe Abhängigkeiten leben hinter einem Vertrag, den wir selbst definieren

Das Herzstück der semantischen Indexierung basiert auf einer Open-Source-RAG-Bibliothek mit wertvollen Retrieval-Algorithmen, die jedoch für einen anderen Kontext gedacht war: Sie verwaltete ihre Persistenz selbst, kommunizierte mit den Modellen über eine andere Schnittstelle als die der gewählten Anbieter und war nicht für viele Kunden auf einer einzigen Datenbank ausgelegt. Bei einer solchen Abhängigkeit gibt es zwei naheliegende Wege, beide teuer: sie neu schreiben oder sich weiterhin an sie anpassen.

Bevor wir uns entschieden, haben wir sie Modul für Modul zerlegt und für jedes die Kosten einer Neuimplementierung geschätzt. Die Retrieval-Logik war der wertvollste und zugleich der am teuersten neu zu implementierende Teil; viele andere Module waren Infrastruktur, die wir ohnehin ersetzen wollten. Wir haben daher entschieden, die Algorithmen zu behalten und alles zu ersetzen, was sie mit der Außenwelt verbindet: Speicher und Vektorindex auf den Tabellen der Plattform neu implementiert, mit Isolation pro Raum in jeder Abfrage; die bibliothekseigene Persistenz deaktiviert; Adapter zu den Modellen aller Anbieter, mit Token-Zählung; das Reranking hinter die Zusammenführung mit der Volltextsuche verlegt.

Die Agenten sehen die Bibliothek nicht: Sie sehen einen LangChain-Retriever wie jeden anderen. Aus demselben Grund nutzen wir LangChain und LangGraph: Sie geben Komponenten eine gemeinsame Form, die sonst mit jedem Anbieter ihre Schnittstelle ändern würden. Jedes Modell ist ein Chat-Model, jeder Agent ist ein Graph mit expliziten Knoten, die sich einzeln lesen, tracen und testen lassen, und Fähigkeiten wie Verlauf, Gedächtnis, Tracing und Evaluierung werden geerbt, indem man sie deklariert. Prompts und Modelle sind Konfiguration, die jeder Raum überschreiben kann: Ein neues Modell eines bereits integrierten Anbieters ist ein Datensatz, keine Codeänderung.

Der Token-Verbrauch schließlich wird pro Agentenschritt, Modell, Konversation und Raum erfasst und bildet die Grundlage der Kostenanalyse: Was ein Raum kostet, lässt sich aus den Daten ablesen, ohne es nachträglich rekonstruieren zu müssen.

Der Wissensgraph: eine Brücke, nicht der Eigentümer der Daten

Die Ähnlichkeitssuche beantwortet lokale Fragen gut, tut sich aber schwer, wenn die Antwort verlangt, Entitäten zu verknüpfen, die über verschiedene Dokumente verstreut sind: welche Komponenten in mehreren Projekten auftauchen, welche Verfahren wiederkehren. Für solche Fragen haben wir einen lexikalischen Graphen aus Themen, Aussagen, Fakten und Entitäten integriert, die mit den Chunks verknüpft sind, aus denen sie stammen. Er wird mit dem Open-Source-Toolkit von AWS für GraphRAG aufgebaut und per OpenCypher abgefragt, kompatibel sowohl mit Neo4j als auch mit Amazon Neptune.

Das Risiko bei einem solchen Toolkit besteht darin, es alles machen zu lassen. Das ist der schnellste Weg zum Experimentieren, aber dann berechnet das Toolkit die Embeddings selbst, fragt den Graphen ab und generiert eine Antwort, die die Anwendung anschließend erneut generieren muss, mit doppelten kostenpflichtigen Aufrufen. Deshalb ist das Toolkit eine Brücke, um den Graphen aufzubauen und zu traversieren, nicht der Eigentümer der Daten oder der Autorisierung: Persistenz, Modellaufrufe und Zugriffskontrollen bleiben bei der Plattform.

Der teure Teil wird nur einmal bezahlt

Die Ingestion des Graphen ist in zwei Phasen mit sehr unterschiedlichen Kosten aufgeteilt. Bei der Extraktion analysiert ein Sprachmodell jeden Chunk; das Ergebnis wird gegen das erwartete Schema validiert und mit seiner Herkunft in PostgreSQL gespeichert: Fingerprint des Prompts, Modell, Versionen von Parser und Toolkit. Ein Chunk, der bereits mit derselben Herkunft extrahiert wurde, wird nicht erneut extrahiert. Beim Aufbau wird der Graph vollständig aus den gespeicherten Daten neu erstellt, ohne Modellaufrufe und mit stabilen Kennungen; ein zweimaliger Neuaufbau desselben Dokuments ergibt daher denselben Graphen. Der Aufbau startet erst, wenn Metadaten und Extraktion abgeschlossen sind, und wird über einen Row-Lock reserviert, damit er nur ein einziges Mal in die Queue gestellt wird.

Auf der Vektorseite hat das Toolkit keinen eigenen Speicher: Es liest dieselbe Chunk-Tabelle wie der Rest der Plattform, über einen schreibgeschützten Index, der jeden Schreibzugriff verweigert, und bestehende Räume haben keine Datenmigration erfordert. Der Graph lässt sich daher jederzeit neu aufbauen, ohne die Extraktion erneut zu bezahlen und ohne die Vektoren zu duplizieren.

Eine Isolation, die sich nicht auf eine einzige Prüfung verlässt

Im Graphen ist jeder Wissensraum ein Tenant. Hier würde ein Isolationsfehler am meisten kosten, und hier haben wir uns entschieden, doppelt zu prüfen. Die Tenant-ID wird immer aus dem bereits autorisierten Raum abgeleitet, nie aus der Anfrage. Die Multi-Tenant-Schicht des Toolkits ist in einen eigenen Adapter gekapselt, der jede Abfrage umschreibt und sich weigert, einen anderen Raum zu bedienen als den, für den er erzeugt wurde.

Nach der Traversierung muss jede zurückgegebene Kennung zum erwarteten Tenant gehören, andernfalls schlägt die Anfrage mit einem Isolationsfehler fehl. Die verbleibenden Belege werden anschließend in SQL erneut autorisiert: aktives Dokument, aus dem richtigen Raum, mit allen Phasen abgeschlossen, passend zu den Filtern und Dokumentgruppen der Konversation und mit demselben Embedding-Modell indexiert. Die Tracing-Spans des Graphen schließlich erfassen nie den Text der Dokumente.

Der Graph-Agent ist ein LangGraph-Graph wie die anderen, mit denselben geerbten Fähigkeiten und ohne Antwortgenerierung und Reranking des Toolkits, die Aufgaben der Plattform sind. Geht etwas schief, ist der Fehler explizit: kein stiller Rückfall auf die klassische Suche, der das Problem verdecken würde. Der Graph wird Raum für Raum aktiviert.

Worker: Jeder Arbeitsrhythmus hat seine eigene Lane

Hintergrundjobs sind nicht alle gleich. Eine Nachricht aus einem Konversationskanal muss innerhalb weniger Sekunden beantwortet werden; die Indexierung eines Dokuments oder die Extraktion des Graphen kann Minuten dauern. In einer einzigen Queue verzögert ein Massen-Upload die Antwort an einen wartenden Nutzer. Wir haben die Celery-Tasks daher in drei Lanes aufgeteilt, jede mit eigener SQS-Queue und eigener Infrastruktur.

Lane Was sie ausführt Wo sie läuft
Interaktiv Verteilung frisch hochgeladener Dokumente, Nachrichten aus den Konversationskanälen eigener ECS-Service
Verarbeitung Indexierung, Metadaten, Extraktion und Aufbau des Graphen, Evaluierungen, mehrstufige Jobs eigener ECS-Service mit mehr Arbeitsspeicher
Lange Laufzeit Reserve-Lane für Jobs, die sich nicht aufteilen lassen ein eigener ECS-Task, gestartet pro Nachricht

In den ersten beiden Lanes führt jeder Worker einen Task nach dem anderen aus und reserviert nur eine einzige Nachricht. Standardmäßig reserviert ein Celery-Prozess vier: praktisch bei kurzen Tasks, aber bei Jobs, die Minuten dauern, bleibt eine Nachricht hinter einer Indexierung hängen, während ein anderer Worker frei ist. Wir verzichten auf etwas Dichte zugunsten eines vorhersehbaren Verhaltens: Die Kapazität bemisst sich in laufenden Tasks und wird durch zusätzliche ECS-Tasks erhöht.

Folglich richtet sich das Autoscaling nach der Queue, nicht nach der CPU. Die Metrik ist die Anzahl wartender Nachrichten pro aktivem Worker, mit einem eigenen Zielwert für jede der beiden Lanes: Wächst die Queue, fügt ECS zügig Worker hinzu, leert sie sich, nimmt es sie vorsichtig wieder zurück. Eine Upload-Spitze wird zu mehr Workern, und eine Leerlaufphase hält keine ungenutzten Worker am Laufen.

Kein abgebrochener Job, nicht einmal während eines Deployments

Autoscaling und Deployments haben einen gemeinsamen blinden Fleck: Um Kapazität abzubauen oder eine Version zu ersetzen, schickt ECS den Containern ein SIGTERM und beendet sie nach Ablauf des stopTimeout mit SIGKILL. ECS weiß nicht, ob ein Worker mitten in einer Indexierung steckt, und der Warm Shutdown von Celery wartet nur so lange auf das Ende des Tasks, wie ECS es zulässt.

Die verbreitetste Lösung besteht darin, den stopTimeout aller Container so weit zu erhöhen, dass er den längsten Task abdeckt. Auf Fargate liegt die Grenze jedoch bei zwei Minuten, während eine Indexierung oder eine Graph-Extraktion länger dauern kann. Und selbst wo es reichen würde, verlangsamt eine auf den ungünstigsten Fall ausgelegte Stoppzeit jedes Deployment und jeden Kapazitätsabbau, auch wenn die Worker gar nichts tun. Die Alternative, die Unterbrechung hinzunehmen und den Job neu einzureihen, bedeutet, Arbeit zu wiederholen, die bereits bezahlte Modellaufrufe enthält.

Wir haben die Verantwortung umgekehrt: Der Worker sagt ECS, wann er nicht gestoppt werden darf. Ein Supervisor umschließt den Celery-Prozess und empfängt über einen lokalen Unix-Socket die Lifecycle-Signale jedes Tasks: Start, Ende, Fehlschlag, Widerruf. Er führt ein Register der aktiven Tasks mit ihrer maximalen Ablaufzeit, die er aus dem Zeitlimit jedes Tasks ableitet, und aktiviert die task scale-in protection von ECS für genau diese Zeit. Solange der Worker arbeitet, kann der Autoscaler ihn nicht auswählen; sobald er wieder frei ist, fällt der Schutz weg.

Task gestartet, mit Zeitlimit Schutz aktiv bis zum Ablauf Kapazitätsabbau abgewiesen Task abgeschlossen Schutz aufgehoben Der Task darf gestoppt werden Celery-Prozess Supervisor ECS-Agent Autoscaling und Deployments

Derselbe Supervisor kümmert sich um die Deployments. In regelmäßigen Abständen vergleicht er seine eigene Version mit der des Deployments, das der Service als primär betrachtet; stellt er fest, dass er überholt ist, fordert er von Celery einen Warm Shutdown an: Der laufende Task wird zu Ende geführt, es werden keine weiteren angenommen, und der Prozess beendet sich. Ein wiederholtes Stoppsignal führt zu einem schrittweise härteren Herunterfahren.

Zwei Details machen den Mechanismus zuverlässig, nicht bloß funktionsfähig. Die Aufrufe an die ECS-API sind in ihrer Frequenz begrenzt und erfolgen nur, wenn sich die Ablaufzeit nennenswert ändert, sodass ein Worker mit vielen kurzen Tasks die API nicht überlastet. Und das Register verwirft von selbst Tasks, die ihre Ablaufzeit überschritten haben: Geht ein Event verloren, bleibt der Worker nicht für immer geschützt und blockiert den Kapazitätsabbau.

Das Ergebnis: Wir können jederzeit ausrollen und Kapazität abbauen, ohne eine laufende Indexierung zu unterbrechen, mit normalen Stoppzeiten für alle anderen Container. Ein Deployment braucht kein Zeitfenster, in dem niemand Dokumente hochlädt.

Kurze Tasks aus Prinzip, nicht aus Zufall

Der Schutz funktioniert umso besser, je kürzer die Tasks sind, und das Projekt hat daraus eine Regel gemacht: Ein Task, der einen Worker zu lange belegt, ist ein Designfehler, kein Konfigurationsproblem. Mehrstufige Verarbeitungen werden in kurze, idempotente Schritte zerlegt, die ihren Fortschritt in der Datenbank speichern und den nächsten Schritt in die Queue stellen. Ein Neustart kostet einen Schritt, nicht den ganzen Job, und der Schutz eines Workers dauert nie länger als nötig.

Für die seltenen Jobs, die sich nicht aufteilen lassen, gibt es die dritte Lane, die keinen Worker verwendet: Die Queue ist über EventBridge Pipes mit ECS verbunden, das für jede Nachricht einen eigenen Task startet. Der Container dekodiert den Celery-Envelope, führt den Task aus und beendet sich. Der Code des Tasks ändert sich nicht; es ändert sich nur der Prozess, der ihn ausführt, und dieser lebt genau so lange wie der Job.

Was das Produkt gewinnt

Unser Kunde bietet die Plattform als Service an, und um sie wachsen zu lassen, muss sie nicht neu gedacht werden. Ein neuer Kunde ist eine Organisation und ein paar Räume; ein neuer Kanal ist ein aufzulösender Anfragetyp in der Control Plane; ein neues Modell eines bereits integrierten Anbieters ist Konfiguration; ein neuer Agent ist ein Graph, der die vorhandenen Fähigkeiten erbt, und eine neue Retrieval-Technik wie der Wissensgraph wird Raum für Raum aktiviert. Isolation und Deployments müssen nicht bei jedem Schritt neu durchdacht werden, weil sie nicht von der Funktion abhängen, die gerade hinzukommt.

So gehen wir KI-Produkte an: Das Modell ist die Komponente, die sich am häufigsten ändert, und gerade deshalb kann es nicht der Ort sein, an dem die Garantien liegen. Die Garantien liegen in der Architektur, die es umgibt: ein einziger Eigentümer für Identität und Schema, die Isolation in jeder Abfrage, teure Arbeit, die nur einmal bezahlt wird, Worker, die wissen, wann sie nicht gestoppt werden dürfen. Jede dieser Entscheidungen kostet anfangs mehr als eine Abkürzung; zusammen ermöglichen sie es dem Produkt, mit Kunden, Dokumenten und Funktionen zu wachsen, ohne das Fundament erneut anfassen zu müssen.