#engineering

Ein LLM im kritischen Pfad eines Marktplatzes: drei Jahre in Produktion, dieselbe Architektur

Seit März 2023 bewertet ein Sprachmodell jede Nachricht zwischen Käufern und Anbietern, bevor sie zugestellt wird, um den Austausch von Kontaktdaten vor dem Kauf zu verhindern. Der Filter hat zwei Modellfamilien durchlaufen, ohne dass die Pipeline neu geschrieben werden musste, denn das Modell ist ein austauschbares Signal innerhalb einer stabilen Architektur.

Künstliche Intelligenz · Trust & Safety · LLM · Node.js · PostgreSQL

In einem Marktplatz ist der Chat zwischen Käufern und Anbietern unverzichtbar. Vor dem Kauf möchte ein Kunde die Details des Angebots klären, prüfen, ob es zum eigenen Bedarf passt, um eine Variante oder einen anderen Termin bitten. Doch derselbe Chat ist auch die Tür, durch die der Verkauf verschwindet: Eine Telefonnummer, eine E-Mail-Adresse oder ein „Such mich auf Instagram“ genügt, und die Verhandlung geht anderswo weiter, ohne Provision für die Plattform und ohne die Schutzmechanismen, die die Plattform beiden Seiten bietet. Die Nutzungsbedingungen verbieten den Austausch von Kontaktdaten vor dem Kauf, doch eine geschriebene Regel allein reicht nicht aus, um sie durchzusetzen.

Für einen Marktplatz, auf dem Kunden die Angebote unabhängiger Anbieter kaufen, haben wir das System entworfen und gebaut, das diese Regel durchsetzt. Es bewertet jede Nachricht vor der Zustellung und nutzt in seinem Kern ein Sprachmodell, das die Absicht des Verfassers beurteilt, nicht die Form des Textes. Es ist seit März 2023 in Produktion: Wir haben es in den Versandpfad der Nachrichten eingebaut, wenige Wochen nachdem Sprachmodelle der neuen Generation für Entwickler per API zugänglich geworden waren.

Einen umsatzrelevanten Ablauf einer Technologie anzuvertrauen, die erst wenige Wochen alt ist, wirkt wie ein Wagnis. Das wäre es auch gewesen, hätten wir das System um das Modell herum gebaut. Wir haben es genau umgekehrt gebaut: Das Sprachmodell ist ein austauschbares Signal innerhalb eines Systems, das jedes Ergebnis entscheidet, aufbewahrt und erklärt, unabhängig davon, welches Modell dahintersteht. Genau diese Entscheidung hat es dem Filter ermöglicht, mehr als drei Jahre Produktion, zwei Modellfamilien und ein Fine-Tuning-Experiment zu durchlaufen, ohne dass die Bewertungspipeline neu geschrieben werden musste.

Warum Schlüsselwörter nicht ausreichen

Das erste Werkzeug, das einem einfällt, ist eine Liste verbotener Wörter und ein paar reguläre Ausdrücke für E-Mail-Adressen und Telefonnummern. Das funktioniert bei allen, die nicht versuchen, sich zu verstecken, und ist tatsächlich nach wie vor eines der Signale des Systems. Doch wer die Verhandlung von der Plattform weg verlagern will, lernt schnell, und der Kreativität, mit der sich Kontaktdaten tarnen lassen, sind praktisch keine Grenzen gesetzt. Jede neue Regel provoziert eine neue Variante, und die Liste wächst, ohne das Problem je zu lösen.

Hinzu kommt der umgekehrte Fehler, und er ist ebenso kostspielig. In einem Verkaufsgespräch sind Zahlen allgegenwärtig (Preise, Mengen, Termine, Uhrzeiten), ebenso Links zu Quellen, die bei der Entscheidung helfen. Eine Regel, die weit genug gefasst ist, um jede getarnte Kontaktangabe abzufangen, würde auch die Fragen blockieren, die zum Kauf führen. Ein Filter, der legitime Gespräche ausbremst, schadet dem Marktplatz ebenso wie einer, der Kontaktdaten durchlässt.

Die richtige Frage lautet also nicht „Enthält dieser Text eine Nummer?“, sondern „Versucht der Verfasser, außerhalb der Plattform kontaktiert zu werden?“. Es geht um Sprachverständnis. Anfang 2023 waren Sprachmodelle gerade erst leistungsfähig und günstig genug geworden, um diese Frage bei jeder einzelnen Nachricht zu stellen, und wir haben beschlossen, sie sofort einzusetzen, und zwar in einem geschäftskritischen Ablauf, statt abzuwarten, bis sich die Technologie gefestigt hat. Gerade weil sie sich noch nicht gefestigt hatte, musste jedoch der gesamte Rest des Systems so entworfen werden, dass er nicht von ihr abhängt.

Die Prüfung erfolgt vor der Zustellung, nicht danach

Die zweite Weichenstellung betrifft den Zeitpunkt der Prüfung. Der bequemste Weg ist die nachträgliche Moderation: Die Nachricht geht sofort raus, ein Hintergrundprozess analysiert sie und entfernt sie bei Bedarf. Das verzögert den Versand nicht und hält das Modell aus dem kritischen Pfad heraus. Hier nützt es allerdings nichts: Eine Telefonnummer, die ein paar Minuten lang sichtbar war, ist bereits abgeschrieben, und eine Löschung kommt immer erst nach dem Schaden. Deshalb haben wir den anspruchsvolleren Weg gewählt: Die Prüfung erfolgt synchron, innerhalb der Sendeanfrage, bevor die Nachricht den Empfänger erreicht.

Auch der Ort zählt. Ein Filter, der im Endpoint des Chats steckt, schützt diesen Endpoint, und an dem Tag, an dem das Produkt einen neuen Weg hinzufügt, der Gegenseite zu schreiben, muss jemand daran denken, ihn nachzubauen. Unsere Prüfung sitzt dagegen in den Domain-Services, die Nachrichten erzeugen: Jeder Weg, auf dem ein Nutzer in einer Konversation der Gegenseite schreibt, nutzt dieselbe Bewertung. Ein neuer Kontaktpunkt zwischen Käufer und Anbieter erbt den Filter, statt ihn neu zu implementieren.

alt [Nachricht zugestellt] [Nachricht zurückgehalten] Versand der Nachricht Bewertung Ergebnis und Aufschlüsselung der Signale Speicherung mit der Bewertung Event „Neue Nachricht“ nach dem Commit E-Mail-Benachrichtigung Hinweis auf Nichtzustellung Absender GraphQL-API Filter PostgreSQL Domain-Events Empfänger

Ein Detail macht den Mechanismus zuverlässig und nicht bloß funktionsfähig: Auch die Nebenkanäle müssen das Ergebnis des Filters respektieren. Die E-Mail-Benachrichtigung geht nicht vom Resolver aus, sondern von einem Domain-Event, das nur für zugestellte Nachrichten und erst nach dem Commit der Transaktion ausgelöst wird. Eine zurückgehaltene Nachricht oder eine zurückgerollte Transaktion kann daher keine E-Mail mit ihrem Inhalt erzeugen: Der Nebenkanal kann nicht durchlassen, was der Chat zurückgehalten hat.

Das Modell stimmt mit, entscheidet aber nicht allein

Steht erst einmal ein Sprachmodell zur Verfügung, ist die Versuchung groß, ihm die gesamte Entscheidung zu überlassen. Das wäre ein Designfehler. Manche Informationen sollte man nicht aus dem Text ableiten, weil sie bereits mit Sicherheit bekannt sind: Ob der Käufer den Kauf abgeschlossen hat, sagt zum Beispiel die Datenbank, nicht der Ton der Nachricht. Und ein System, in dem ein einziges Urteil alles entscheidet, lässt sich nicht Stück für Stück korrigieren. Die klassische Alternative, eine Kette von if-Abfragen, die mit jedem neuen Fall wächst, verrät nach ein paar Jahren nicht mehr, welche Bedingung über das Ergebnis entschieden hat.

Wir haben einen dritten Weg gewählt: eine Reihe unabhängiger Regeln, von denen jede die Nachricht aus einem anderen Blickwinkel betrachtet und einen Beitrag zu einem gemeinsamen Score liefert. Die Regeln laufen parallel, sodass die synchrone Prüfung auf die langsamste wartet und nicht auf die Summe aller, und die Summe der Beiträge entscheidet über das Ergebnis.

Familie Was sie betrachtet Stärke
Inhalt erkennbare Muster und pro Konto konfigurierte Begriffslisten sofort, kostenlos, vollständig nachvollziehbar
Absicht die Bedeutung der Nachricht, bewertet von einem Sprachmodell erkennt, was keine geschriebene Regel vorhersieht
Kontext Bestellstatus und Transaktionsphase unterscheidet denselben Text in verschiedenen Phasen

Die Signale haben nicht alle dasselbe Gewicht, und sie weisen auch nicht alle in dieselbe Richtung: Manche drängen zur Sperrung, andere zur Zustellung. Insbesondere der Kontext kann Vorrang vor dem Inhalt haben. Hat der Käufer den Kauf abgeschlossen, ist der Austausch von Kontaktdaten kein Versuch mehr, die Plattform zu umgehen, sondern der natürliche Weg, das Gekaufte abzuwickeln. Umgekehrt ist das Zeitfenster, in dem eine Konversation offen bleibt, an den Lebenszyklus der Bestellung gebunden: Jenseits einer bestimmten Phase hat sie keine Daseinsberechtigung mehr.

In der Praxis sorgt das Score-Modell dafür, dass sich das System leicht weiterentwickeln und gut verteidigen lässt. Eine Regel hinzuzufügen bedeutet, der Summe einen Term hinzuzufügen, ohne die anderen anzufassen. Jede Entscheidung lässt sich Posten für Posten in die Beiträge zerlegen, die zu ihr geführt haben. Und die Begriffslisten werden im Backoffice für jeden einzelnen Anbieter oder Käufer verwaltet: Stellt das Operations-Team bei einem Konto ein wiederkehrendes Verhalten fest, greift es sofort ein, ohne auf ein Release zu warten.

Der Prompt ist die Policy, geschrieben wie eine Spezifikation

Das Herzstück des Systems ist die Regel, die das Sprachmodell abfragt, und die typische Schwachstelle einer solchen Regel ist der Prompt. Ein Modell, das nur gebeten wird, „Kontaktdaten zu finden“, neigt dazu, sie überall zu sehen: in einer Uhrzeit, einem Preis, dem Namen einer verbreiteten App. Unser Prompt ist keine allgemeine Frage: Er ist die Policy der Plattform, in natürlicher Sprache formuliert, mit derselben Präzision wie eine Spezifikation. Er beschreibt die Rolle des Modells, was verboten ist und vor allem, was ausdrücklich erlaubt ist.

Gerade dieser letzte Teil macht in der Produktion den Unterschied. Ein Modell, dem erklärt wird, was in einem Verkaufsgespräch normal ist, kann die praktische Frage vom Versuch unterscheiden, die Plattform zu verlassen. Erst eine genaue Definition des Erlaubten macht es möglich, beim Unerlaubten streng zu sein, ohne die Gespräche auszubremsen, die zum Kauf führen.

Für die verbotenen Fälle beschreibt der Prompt in Form allgemeiner Muster, wie sich eine Information verstecken lässt. Er listet keine Zeichenketten auf, nach denen gesucht werden soll, denn damit kehrte die Schwäche der Schlüsselwörter zurück: Er bringt dem Modell bei, das Muster zu erkennen, nicht die einzelne Variante. Er verlangt auch nicht, dass die Kontaktangabe vollständig erscheint: Eine erkennbare Absicht genügt. Diese Strenge kann sich das System gerade deshalb leisten, weil das Erlaubte ebenso präzise definiert ist.

Der Rest des Vertrags macht die Antwort für ein System verwertbar. Das Modell antwortet mit einem binären Urteil in einem festen Format, das der Code eindeutig interpretiert. Die Temperatur steht auf null, damit dieselbe Nachricht eine möglichst stabile Einschätzung erhält. Nachrichten kommen in jeder beliebigen Sprache an, und das mehrsprachige Sprachverständnis ist einer der Gründe, warum ein Sprachmodell das richtige Werkzeug ist: Es braucht kein eigenes Regelwörterbuch für jede Sprache.

Schließlich liegt der Prompt in der Konfiguration der Anwendung, nicht im Code. Die Policy zu ändern bedeutet, einen Text zu bearbeiten, der dasselbe Review durchläuft wie jede andere Änderung, ohne die Logik des Filters anzufassen.

Drei Modellkonfigurationen, eine einzige Architektur

Der Filter entstand im März 2023 auf einem universellen Sprachmodell, und in den ersten Wochen nahm der Prompt die oben beschriebene Form an: ein Urteil in festem Format mit einer kurzen Begründung, der Kontext der Plattform, die erlaubten Fälle.

Anfang 2024 haben wir den Weg erprobt, den viele für die natürliche Weiterentwicklung bei einer so spezifischen Aufgabe halten: Fine-Tuning, mit zwei eigens dafür trainierten Varianten desselben Modells. Im April haben wir uns für das allgemeine Modell mit einem reichhaltigeren Prompt entschieden. Ein trainiertes Modell friert die Policy zum Zeitpunkt des Trainings ein, während sich ein Prompt mit einer gewöhnlichen Konfigurationsänderung aktualisieren lässt, wenn sich die Policy weiterentwickelt oder eine neue Verschleierungstechnik auftaucht. Bei einem Problem, bei dem sich die Regeln häufiger ändern als die Modelle, ist Flexibilität mehr wert als Spezialisierung.

Die Entscheidung erwies sich auch aus einem grundsätzlicheren Grund als richtig. Ein trainiertes Modell ist eine Variante eines bestimmten Basismodells und teilt dessen Lebenszyklus: Anbieter nehmen ältere Modelle regelmäßig außer Betrieb, und wird ein Basismodell eingestellt, werden auch die darauf trainierten Varianten nicht mehr bereitgestellt. Beim Fine-Tuning zu bleiben hätte bedeutet, bei jedem Generationswechsel neu zu trainieren. Mit dem Prompt sind wir 2025 auf ein Modell einer neueren Generation umgestiegen und haben die Anweisungen um neue Verschleierungsmuster erweitert: Ein leistungsfähigeres Modell nutzt ausführlichere Anweisungen besser, und der Wechsel erforderte nichts weiter.

Dass jeder Wechsel so leichtgewichtig war, liegt am Vertrag der Regel, der seit den ersten Wochen derselbe geblieben ist: eine Nachricht als Eingabe, ein Urteil und die Rohantwort des Modells als Ausgabe. Modellname, Temperatur und Prompt liegen in der Konfiguration: Ein Modellwechsel ist eine Konfigurationsänderung. Der Rest der Pipeline hat davon nie etwas bemerkt. Wenn ein besseres oder günstigeres Modell erscheint, ist seine Einführung eine Produktentscheidung, kein Projekt.

Eine zurückgehaltene Nachricht verschwindet nicht

Eine Nachricht zu blockieren wirft ein Problem auf, das meist unterschätzt wird: was mit ihr geschehen soll. Die naheliegendsten Antworten sind beide falsch. Sie zu löschen vernichtet den einzigen Beleg dafür, was passiert ist: Der Support kann die Entscheidung nicht mehr erklären, und es lässt sich nicht überprüfen, ob der Filter richtig lag. Sie stillschweigend verschwinden zu lassen und den Verfasser im Glauben zu lassen, sie sei zugestellt worden, erzeugt Konversationen, die grundlos abbrechen, Frust und Support-Tickets.

Unsere Entscheidung lautet: Eine zurückgehaltene Nachricht wird unversehrt aufbewahrt, ist für den Empfänger konstruktionsbedingt unsichtbar und wird ihrem Verfasser erklärt. Sie wird ohne Änderungen zusammen mit ihrer Bewertung gespeichert und als nicht zugestellt markiert.

„Konstruktionsbedingt“ bedeutet, dass die Sichtbarkeitsregel im Read Model liegt, nicht in den Oberflächen. Die Query, die die Konversationen liest und von beiden Anwendungen gemeinsam genutzt wird, gibt eine zurückgehaltene Nachricht nur an ihren Verfasser zurück. Dasselbe gilt für die Vorschauen der Konversationen in der Nachrichtenliste. Läge der Filter in den einzelnen Ansichten, wäre jede neue Ansicht eine Gelegenheit, ihn zu vergessen; so müsste man, um die Nachricht dem Empfänger anzuzeigen, die Query selbst umgehen.

Der Verfasser hingegen sieht klar, was passiert ist. In der App für Käufer ist die Nachricht als nicht gesendet gekennzeichnet, mit einer Erklärung: Die Kontaktdaten des Anbieters kommen mit dem Kauf, und für alle weiteren Anliegen steht der Kundenservice zur Verfügung. In der App für Anbieter erscheint die Nachricht durchgestrichen, begleitet von einem Hinweis. Wer eine legitime Frage hatte, kann sie umformulieren, und alle werden daran erinnert, dass die Plattform beide Seiten gerade deshalb schützt, weil die Transaktion auf ihr stattfindet.

Jedes Ergebnis lässt sich Posten für Posten rekonstruieren

Eine Entscheidung, an der auch ein Sprachmodell beteiligt ist, lässt sich nicht verteidigen, wenn von ihr nur das Endurteil bleibt. Fragt ein Nutzer, warum seine Nachricht nicht angekommen ist, kann der Support nur antworten: „Das hat der Algorithmus entschieden.“ Und bei einer Änderung am Prompt kann niemand sagen, ob sie die Dinge verbessert oder die Fehler nur verlagert hat.

Deshalb trägt jede Nachricht die vollständige Aufschlüsselung ihrer eigenen Bewertung mit sich, in einem JSONB-Feld in PostgreSQL: den Beitrag jeder einzelnen Regel und, für die modellbasierte Regel, die gesendete Anfrage und die erhaltene Antwort. Der Gesamtscore ist nur die Zusammenfassung; dahinter steht immer die Erklärung.

Ein Filter, der die Konversationen zwischen Käufern und Anbietern liest, verarbeitet naturgemäß personenbezogene Daten, und jede Bewertung enthält einen Teil davon. Deshalb war Datenschutz vom ersten Tag an eine Designanforderung, gleichrangig mit der Wirksamkeit des Filters: Was an das Modell gesendet wird, was gespeichert wird und wer darauf zugreifen kann, sind Entscheidungen, die mit dem Schutz der Nutzer als Randbedingung getroffen und nicht erst im Nachhinein überprüft wurden.

Im Backoffice erscheinen die zurückgehaltenen Nachrichten markiert in der Konversation, mit ihren Scores, und der Support kann genau nachprüfen, warum sie zurückgehalten wurden. Dieselben Informationen ermöglichen es, die Auswirkungen einer Policy-Änderung auf echte Nachrichten zu bewerten statt auf am Schreibtisch konstruierte Beispiele, und zu bemerken, wenn sich eine neue Verschleierungstechnik abzeichnet. Genau das macht es sicher, den Prompt weiterzuentwickeln: Jede Änderung wird am echten Traffic gemessen.

Hinzu kommt ein operativer Aspekt. Die Aufrufe beim Modellanbieter erscheinen in denselben OpenTelemetry-Traces wie der Rest des Backends, und Latenz und Fehler werden mit denselben Werkzeugen beobachtet wie der Rest der Plattform, nicht in einer separaten Konsole.

Dieselbe Policy, eine eigene Maßnahme für jeden Kanal

Der Chat ist der wichtigste Kanal für den Austausch von Kontaktdaten, aber nicht der einzige. Und die Maßnahme, die im Chat funktioniert, das Zurückhalten der Nachricht, lässt sich nicht überall anwenden: Eine Bewertung etwa ist ein öffentlicher Text und kann nicht in der Schwebe bleiben. Deshalb haben wir dieselbe Policy mit einer eigenen Aktion für jeden Kanal angewendet, statt überall denselben Mechanismus zu erzwingen.

Im Backoffice der Anbieter sind Telefonnummer, E-Mail-Adresse und Anschrift des Käufers erst sichtbar, wenn die Bestellung vollständig bezahlt ist; bis dahin erscheinen Platzhalterdaten. So kommt der direkte Kontakt erst mit der abgeschlossenen Transaktion zustande, auch außerhalb des Chats. In den Bewertungen werden unzulässige Begriffe in der veröffentlichten Version unkenntlich gemacht, während der Originaltext für das Operations-Team verfügbar bleibt, und ändern sich die Regeln, werden bestehende Bewertungen automatisch neu verarbeitet. Der Nachrichtenversand schließlich ist durch eine Anti-Bot-Prüfung geschützt, die automatisierte Clients aus dem Chat fernhält.

Zurückhalten, unkenntlich machen, aufschieben oder abweisen: Jede Ebene tut das Richtige für ihren Kanal. Zusammen machen sie den Austausch von Kontaktdaten vor dem Kauf zu einem mühsamen Weg statt zu einem Hindernis, das man nur einmal umgehen muss.

Was es braucht, um ein LLM in den kritischen Pfad zu bringen

Für einen Marktplatz ist die Provision auf jeden Verkauf das Geschäftsmodell, und Disintermediation untergräbt es leise, Konversation für Konversation. Der Filter schützt diesen Wert, ohne zu opfern, was die Plattform nützlich macht: Käufer und Anbieter sprechen weiterhin frei über alles, was sie für ihre Entscheidung brauchen, und tauschen ihre Kontaktdaten aus, wenn der richtige Zeitpunkt dafür gekommen ist.

Die Methode gilt über dieses Projekt hinaus. In einem System, das ein Sprachmodell nutzt, ist das Modell der Teil, der sich am schnellsten ändert, und derjenige, den man am wenigsten unter Kontrolle hat: Neue Versionen erscheinen, alte werden außer Betrieb genommen. Deshalb behandeln wir es nicht als Zentrum der Architektur, sondern als Komponente hinter einem stabilen Vertrag, und entwerfen alles, was Bestand haben muss (wo die Prüfung stattfindet, wie die Signale kombiniert werden, wo die Policy liegt, was gespeichert und nachverfolgt wird), so, dass es nicht von ihm abhängt. Genau das hat es uns ermöglicht, eine Technologie in ihren ersten Wochen einzusetzen, in einem umsatzrelevanten Ablauf, und zuzusehen, wie sie sich über mehr als drei Jahre verbessert, ohne das System neu zu bauen.

Die Arbeit am Prompt geht mit demselben Ansatz weiter, mit dem das System von Anfang an gewachsen ist: die Policy präziser machen und die Architektur, die sie durchsetzt, unverändert lassen.