Was wir beim Bau unseres eigenen Support-Bot-Widgets gelernt haben (Chat mit deinem Content)
Wir haben unser eigenes Support-Bot-Widget gebaut: die Chat-Blase, die du auf einer Website einbettest, damit Besucher Fragen stellen und Antworten aus deinem Content bekommen (Docs, Knowledge Base, Service-Seiten, Policies). Auf dem Papier klingt das simpel. In der Praxis ist es messy. Die Probleme sind selten "Model-Probleme". Es sind Engineering-Probleme:- Retrieval bricht vorhersehbar
- Latenz zerstoert Vertrauen, bevor die erste Antwort landet
- Access Control ist leicht falsch zu machen
- Prompt Injection wird zu "Content Poisoning"
- Antworten muessen auditierbar sein, sonst wird der Bot zum Support-Risiko
Was wir gebaut haben (Requirements und Constraints)
Wir hatten ein paar nicht verhandelbare Anforderungen:- Schnelles First Token: Nutzer sollen schnell eine Reaktion sehen, auch wenn der Assistant im Hintergrund erst "suchen" muss.
- Grounded Answers: Antworten muessen durch Content gestuetzt sein, den wir wirklich ausliefern.
- Read-only by default: der Assistant darf Content nicht mutieren.
- Multi-Locale ready: unsere Site ist lokalisiert; der Assistant soll Locales nicht beliebig mischen.
- Safe Escalation: wenn er nicht sicher antworten kann, soll er sauber eskalieren (Contact/Support), ohne zu halluzinieren.
Der Assistant soll sich wie ein vorsichtiger Engineer verhalten, der unsere Website liest, nicht wie ein kreativer Writer, der improvisiert.
Ein funktionierendes Modell: "Support Bot = Search + Navigation + Synthesis"
Ein guter Support-Assistant macht drei Dinge in Reihenfolge:- Search: Kandidaten-Seiten/Sections identifizieren.
- Navigate: Seiten oeffnen, Headings scannen, Links folgen, Query verfeinern.
- Synthesize: eine kurze, korrekte Antwort liefern, mit Pointern, woher sie kommt.
Lesson 1: Chunk-only RAG faellt auseinander, wenn die Antwort ueber mehrere Seiten geht
Top-K Chunk Retrieval bricht in konstanten Mustern:- die Antwort ist auf zwei Seiten verteilt
- die "exakte Syntax" steckt in einem Codeblock, der nicht gerankt wurde
- die Frage ist underspecified, Embedding Match ist "close enough"
- die richtige Seite hat wenig lexikalische Ueberschneidung
Lesson 2: Gib dem Assistant deterministische Primitives (Docs-as-Files)
Menschen beantworten Docs-Fragen nicht, indem sie fuenf zufaellige Absaetze lesen. Wir:finddie Seiteopensie- scannen Headings
- suchen nach exakten Tokens
- folgen internen Links
- wiederholen, bis wir die Antwort beweisen koennen
- jede Seite ist ein "File"
- Directories repraesentieren Sections (oder URL-Pfadsegmente)
- der Assistant kann
ls,find,catundgrep
- auditierbar sind (Tool Calls lassen sich loggen)
- deterministisch sind
- skalierbar sind (Exploration ohne Hardcoding)
Architektur (die einfachste Version, die funktioniert)
High-level sind wir bei drei Layern gelandet:Browser widget
-> /api/support-chat (stream)
-> Retrieval layer (lexical + vector)
-> Tool layer ("filesystem" over content)
-> Answer synthesis (grounded prompt + citations)
-> Observability (logs + traces + eval hooks)
Der Kern: "unseren Content lesen" ist ein Tool, nicht nur ein Nebenprodukt von Retrieval.
Datenmodell: pages, chunks und ein path tree
Wir haben die Representation bewusst boring gehalten:page: canonical URL/slug + title + locale + visibility + lastmod + headingschunk:{ page, chunk_index, text, embedding, tokens, hash }path_tree:{ "services/seo": { isPublic: true, groups: [] }, ... }
- der Assistant braucht eine Map: "was existiert"
- Access Control wird strukturell (Paths vor Sessionstart prunen)
ls/findwird schnell und gut cachebar
Ingestion-Pipeline: eine Website zu einer verlaesslichen Knowledge-Surface machen
Das war der groesste Zeitfresser. "Indexiere deine Docs" klingt leicht, bis du realen Content siehst:- Marketing Copy + Components
- MDX und Headings, die nicht sauber auf HTML mappen
- Listing-Seiten, die Content-Bloecke wiederholen
- Locale-Varianten, die teilweise divergieren
- Pages discovern: Sitemap + bekannte Route-Liste.
- Rendered HTML fetchen: was Nutzer (und Crawler) wirklich sehen.
- Main Content extrahieren: Nav, Footer, Cookie Banner, wiederholte UI entfernen.
- Normalisieren: Whitespace, Tracking-Query-Strings aus Links entfernen.
- Segmentieren:
- page-level Record fuer Navigation und Zitate
- chunk-level Records fuer Retrieval
- Annotieren:
- locale
- visibility (public, client-only, internal)
- headings outline
- outbound internal links
Retrieval: lexical und vector kombinieren, bevor du "das Model fragst"
Wir vertrauen keiner einzelnen Retrieval-Strategie. Wir kombinieren:- lexical search fuer exakte Tokens (Identifier, Akronyme, Error Codes)
- vector search fuer semantische Naehe (vage Fragen)
- Pages mit falscher Locale ablehnen (ausser explizit angefragt)
- canonical Pages gegenueber Tag-/Listing-Duplikaten bevorzugen
- Pages bevorzugen, deren Headings gut zur Query passen
Tool-Design: was der Assistant darf (und was nicht)
Wir haben eine kleine Tool-Oberflaeche implementiert und sie strikt gemacht: Erlaubt:- Directories listen:
ls /services - Pfade suchen:
find -name "billing" - eine ganze Seite lesen:
cat /services/seo - innerhalb von Content suchen:
grep -ri "canonical" /
- Content schreiben oder editieren
- beliebige URLs fetchen
- beliebige Network Calls
Latenz: echte Filesystems sind zu langsam fuer interaktiven Chat
Wenn du pro Session einen Sandbox/Container startest, um ein echtes Filesystem zu bieten:- Cold Start wird sichtbar
- Chat fuehlt sich kaputt an
- du wirst zu Komplexitaet wie Warm Pools verleitet
lsundfindlaufen ueber den gecachten Path Treecatsetzt eine Page aus gespeicherten Chunks zusammen (sortiert nachchunk_index)- Ergebnisse werden per Session gecacht (und teilweise global, wenn safe)
Cache, was sich wiederholt: path tree, pages und "grep targets"
Antworten cachen ist schwach, weil Fragen variieren. Was sich in echten Conversations wiederholt:- dieselben Sections listen
- dieselben 5-10 Kernseiten oeffnen
- dieselben Tokens greppen
- den Path Tree
- rekonstruierte Full Pages
- recente Grep-Kandidaten
RBAC: Access Control muss strukturell sein, nicht prompt-basiert
Wenn ein Teil deiner Docs nicht public ist (Drafts, interne Notes, client-only Docs), kannst du nicht auf Prompting vertrauen. Wir enforce RBAC, bevor der Assistant einen einzigen Tool Call macht:- user-scoped Path Tree bauen
- alles prunen, was der User nicht sehen darf
- denselben Filter auf jede Query und jeden Page Read anwenden
lsen kann, kann er es nicht caten und nicht zitieren.
Guardrails: wie wir "confident wrong" Antworten reduziert haben
Ein paar Regeln haben bad answers drastisch reduziert:Regel 1: Kein Evidence, keine Antwort
Wenn der Assistant keinen stuetzenden Content findet, soll er:- sagen, dass er es nicht verifizieren konnte
- zeigen, was er geprueft hat (Pages/Sections)
- einen naechsten Schritt anbieten (Contact/Support)
Regel 2: Bei kritischen Details lieber zitieren als paraphrasieren
Wenn exakte Formulierungen zaehlen (Requirements, Limits, rechtliche Copy):- relevante Saetze aus der gelesenen Page zitieren
- Synthesis minimal halten
Regel 3: Strikt bei Locale und canonical Pages bleiben
Wenn der User in einer Locale ist, soll der Assistant:- die Locale bevorzugen
- Sprachen nicht mischen
- nur auf Default-Locale fallbacken, wenn die Locale-Page nicht existiert
Das "grep problem": Killer-Feature, aber nur mit Plan
Naives recursive Grep ist langsam, wenn du alles ueber das Netzwerk lesen musst. Wir nutzen einen Two-Phase-Ansatz:- coarse filter ueber den Index (welche Pages koennten das Token enthalten)
- fine filter in memory ueber gecachten Page Text, um exakte Treffer + Kontext zu extrahieren
Widget-UX: die UI entscheidet ueber Vertrauen
Wir haben mehrere UI-Iterationen gebraucht, bis das Widget sich verlaesslich anfuehlte. Was am meisten zaehlte:- Streaming Tokens mit stabilem Layout (keine jumpy Reflows)
- klare States:
- "Searching..."
- "Reading page..."
- "Answering..."
- kurze, inline Zitate:
- "From: /services/seo"
- "From: /legal/privacy"
- Fallbacks, die nicht wie ein Fail wirken:
- "Ich habe X und Y geprueft, konnte es aber nicht bestaetigen; hier ist, wie du uns erreichst."
Observability: logge Reads, nicht nur Tokens
Wenn du Qualitaet verbessern willst, musst du wissen, was passiert ist. Wir loggen:- Tool Calls (welche Pfade gelistet/gelesen/gesucht wurden)
- welche Pages als Evidence genutzt wurden
- Answer length und Latenz-Buckets
- "could not verify" Raten
- Escalation-Raten (Contact Clicks)
- welche Pages fehlen wichtige Informationen?
- welche Fragen finden nie Evidence?
- welche Pages verwirren Nutzer und muessen umstrukturiert werden?
Ein praktischer Build-Checklist (was wir wieder so machen wuerden)
Wenn du ein Support-Widget baust, das mit deinem Content chattet:- indexiere die gerenderte Site und speichere page-level Records
- kombiniere lexical + vector Retrieval
- addiere eine Exploration-Tool-Layer (files + grep)
- prune Content fuer RBAC vor Sessionstart
- halte Tools read-only by default
- setze die Regel "no evidence, no answer"
- mache "searching/reading/answering" in der UI sichtbar
- logge Tool Calls, damit du Failures debuggen kannst