Das übliche Versprechen für einen KI-Support-Bot lautet so: Verbinde deine Datenquellen, wir indexieren sie, und dein Bot antwortet daraus.
Lies das noch einmal als Entwickler. Verbinde deine Datenquellen heißt: Bau einen Export. Wir indexieren sie heißt: Deine Kundendaten liegen jetzt auch im System eines Anbieters. Und „antwortet daraus“ heißt: Er antwortet aus dem, was die letzte Synchronisierung kopieren konnte.
Für Dokumentation geht diese Rechnung auf: Artikel ändern sich wöchentlich, und ein Index passt gut zu ihnen. Für alles, was sich laufend ändert, geht sie nicht auf. Preise ändern sich. Lagerbestände ändern sich. Der Bestellstatus ändert sich, während der Kunde noch tippt. Ein Index ist ein Foto von etwas, das sich bewegt.
Die andere Möglichkeit
Hier bietet sich eine viel ältere Idee an, und bei ihr wandern deine Daten nirgendwohin: Gib dem Modell eine Funktion und lass es sie aufrufen.
Genau das ist Tool Calling, seit es das gibt. Ungewöhnlich ist, wo die Funktion läuft. In den meisten Implementierungen läuft sie auf den Servern des Anbieters. Dann gibst du ihm wieder Zugangsdaten, setzt seine IPs auf eine Allowlist und baust eine Integration.
Ich führe sie im Browser des Besuchers aus.
KookeeChat.init({
apiKey: 'YOUR_API_KEY',
tools: [{
name: 'search_orders',
description: "Look up the signed-in customer's orders.",
parameters: { type: 'object', properties: { status: { type: 'string' } } },
handler: ({ status }) =>
fetch('/api/my/orders?status=' + status, { credentials: 'include' })
.then((r) => r.json()),
}],
});
Sieh dir an, was dieser handler ist. Ein fetch an deine API, mit credentials: 'include', ausgeführt auf deiner Seite, im Namen des Besuchers, der schon angemeldet ist.
Daraus folgen drei Dinge, und um genau die geht es:
Du hast keine Integration gebaut. Kein ausgetauschter API-Schlüssel, keine IP-Allowlist, kein Webhook, keine OAuth-App. Der Endpunkt, den du aufrufst, ist einer, den dein Frontend ohnehin aufruft.
Kookee behält deine Daten nicht. Name, Beschreibung und Parameterschema des Tools gelangen zu Kookee, gerade genug, damit das Modell weiß, dass es das Tool gibt und was es übergeben muss. Der Handler läuft im Browser; sein Ergebnis geht über Kookee an das Modell, nur für diese eine Antwort, und wird nicht gespeichert. Deine Bestelltabelle liegt nicht in meiner Datenbank, weil ich nie nach ihr gefragt habe.
Die Authentifizierung ist schon gelöst. Die schwierigste Frage bei „KI, die Kundendaten sehen kann“ lautet: welcher Kunde? Hier stellt sie sich gar nicht. Die Anfrage schickt das Session-Cookie des Besuchers selbst mit, also liefert sie seine Bestellungen und die von niemandem sonst. Das sichert derselbe Autorisierungscode ab, der den Rest deiner App schützt.
So sieht es für den Besucher aus
Besucher: wo ist meine letzte Bestellung?
Bestellungen werden geprüft …
Assistent: Deine Bestellung #4821 wurde am 21. August verschickt und kommt am Dienstag an. Sendungsverfolgung: DHL 0034…
Die Pause in der Mitte ist der interessante Teil. Die Antwort hält tatsächlich an: Das Modell fordert ein Tool an, der Text stoppt, der Browser führt deine Funktion aus, das Ergebnis geht zurück, und der Text läuft weiter. Für den Besucher ist es eine durchgehende Antwort mit einer Statuszeile.
Wie ich bei diesem Aufbau gelandet bin
Mein erster Entwurf schickte den Gesprächsverlauf zwischen den beiden Hälften an den Browser zurück und ließ den Client ihn wieder mitsenden. Zustandslos, kein Speicher auf dem Server, elegant auf dem Whiteboard.
Zwei Reviewer haben ihn gekippt, aus Gründen, die ich hier gern wiederhole:
Er gab zu viel preis. Meine eigene Suche in den Hilfeartikeln läuft auf dem Server und liefert den vollständigen Artikeltext, auch von chatbot_only-Artikeln, die nicht im öffentlichen Hilfecenter erscheinen. Wären die Nachrichten der Antwort durch den Browser gelaufen, hätte jeder sie im Netzwerk-Tab der Devtools lesen können.
Er war fälschbar. Hält der Client den Gesprächszustand, kann der Client ihn bearbeiten. Ein paar Einträge löschen, und der Rundenzähler springt zurück: eine endlose Schleife von Modellaufrufen auf fremde Kosten.
Deshalb liegt die pausierte Antwort auf dem Server, in Redis, hinter einem Einmal-Token mit kurzer Laufzeit. Der Browser bekommt nur, was er braucht: welche Funktionen er mit welchen Argumenten aufrufen soll, und ein opakes Token, mit dem er fortsetzt. Die Tool-Ergebnisse des Servers sieht er nicht, und die Antwort umschreiben kann er auch nicht.
Die Prüfung des Tokens hatte ihre eigene Tücke, und an der bin ich zuerst gescheitert: Der ursprüngliche Code löschte das Token, bevor er prüfte, zu welchem Projekt es gehört. Ein zufälliges 24-Byte-Token kann niemand erraten, aber eine Anfrage aus dem falschen Projekt hätte trotzdem eine legitime, gerade laufende Antwort zerstört. Lesen, prüfen, dann löschen.
Schutzmechanismen und wo sie enden
Einiges erzwinge ich:
Tool-Ergebnisse werden als nicht vertrauenswürdige Daten gekennzeichnet, nie als Anweisungen.
Zitiermarkierungen werden aus ihnen entfernt, damit ein Tool-Ergebnis keine Quelle erfinden kann.
Handler brechen nach 15 Sekunden ab; Ergebnisse sind auf 4000 Zeichen begrenzt.
Wirft ein Tool einen Fehler, geht der Fehler an das Modell zurück, statt die ganze Antwort abstürzen zu lassen.
Und eines mache ich bewusst nicht: Vor dem Ausführen eines Tools gibt es keine automatische Bestätigung. Registrierst du etwas mit Nebenwirkungen, etwa eine Bestellung stornieren oder eine E-Mail senden, läuft es, sobald das Modell entscheidet, es aufzurufen.
Ich habe überlegt, für jedes Tool einen Bestätigungsdialog einzubauen, und mich dagegen entschieden. Eine Bestätigung bei einer reinen Produktsuche ist Reibung ohne Nutzen, und welches Tool nur liest und welches schreibt, kann ich an einem JSON-Schema nicht erkennen. Du schon. Also: Registriere lesende Tools ohne Bedenken und setz ein window.confirm in jeden Handler, der schreibt.
Wann du das nicht nutzen solltest
Steht die Antwort in deiner Dokumentation, schreib einen Artikel. Tools sind für Daten, die sich schneller ändern, als du veröffentlichen kannst: Preise, Lagerbestand, Bestellstatus, Kontostatus. Alles Stabile ist als Hilfeartikel günstiger, schneller und zuverlässiger, und es wird mit einem Link zitiert, den der Besucher öffnen kann.
Meine Faustregel: Müsstest du neu deployen, um die Antwort zu ändern, ist es ein Tool. Würdest du eine Seite bearbeiten, ist es ein Artikel.
Ab sofort in @kookee/react 1.5.0 und @kookee/sdk 1.5.0 und im Script-Tag ganz ohne Installation. Zehn Tools pro Widget, mehr, als irgendein Support-Assistent, den ich gesehen habe, tatsächlich braucht.