Wat We Leerden bij het Bouwen van Onze Eigen Support Bot Widget (Chat met Je Content)
We bouwden onze eigen support bot widget: een chat bubble die je in een website embedt zodat bezoekers vragen kunnen stellen en antwoorden krijgen uit je content (docs, knowledge base, servicepagina’s, policies). Als je er ooit eentje hebt shipped, dan weet je: de pitch is makkelijk en de realiteit is rommelig. De problemen waar je tegenaan loopt zijn zelden “modelproblemen”. Het zijn engineering problemen:- retrieval breekt op voorspelbare manieren
- latency breekt vertrouwen voordat het eerste antwoord landt
- access control is makkelijk om fout te doen
- prompt injection wordt “content poisoning”
- antwoorden moeten auditbaar zijn, anders krijg je support-risico
Wat we bouwden (requirements en constraints)
We hebben upfront een paar niet-onderhandelbare requirements gezet:- Fast first token: users moeten snel iets zien, zelfs als de assistant op de achtergrond moet “zoeken”.
- Grounded answers: antwoorden moeten ondersteund worden door content die we echt shippen.
- Read-only by default: de assistant mag content niet muteren.
- Multi-locale ready: onze site is gelokaliseerd; de assistant mag talen niet lukraak mixen.
- Safe escalation: als hij het niet weet, moet hij kunnen doorverwijzen naar contact/support zonder te hallucineren.
De assistant moet zich gedragen als een zorgvuldige engineer die onze website leest, niet als een creatieve schrijver die improviseert.
Een werkbaar model: “support bot = search + navigation + synthesis”
Een goede support assistant doet drie dingen op volgorde:- Search: kandidaatpagina’s/secties identificeren.
- Navigate: pagina’s openen, headings scannen, links volgen, search verfijnen.
- Synthesize: een kort, correct antwoord produceren met aanwijzingen waar het vandaan komt.
Les 1: Chunk-only RAG faalt zodra het antwoord over meerdere pagina’s verspreid is
Top-K chunk retrieval breekt op consistente manieren:- het antwoord is verdeeld over twee pagina’s
- de “exacte syntax” staat in een code block dat niet hoog rankte
- het juiste antwoord staat op een pagina met zwakke lexical overlap
- de vraag van de user is te vaag, dus de embedding match is “ongeveer goed”
- juiste toon
- foute details
- geen manier om te verifiëren
Les 2: Geef de assistant deterministische primitives (docs-as-files)
Mensen beantwoorden docs-vragen niet door vijf willekeurige paragrafen te lezen. We:findde paginaopenhem- scannen headings
- zoeken op exacte tokens
- volgen interne links
- herhalen tot we het antwoord kunnen bewijzen
- elke pagina is een “file”
- directories representeren secties (of URL path segments)
- de assistant kan
ls,find,catengrep
- auditbaar zijn (je kunt tool calls loggen)
- deterministisch zijn (zelfde query geeft dezelfde reads)
- schaalbaar zijn (het kan exploreren zonder dat jij flows hardcodeert)
Architectuur (de simpelste versie die werkt)
Op hoog niveau kwamen we uit op drie lagen: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)
De kern: we behandelen “onze content lezen” als een tool, niet als een bijeffect van retrieval.
Datamodel: pagina’s, chunks en een path tree
We hielden de contentrepresentatie bewust saai:page: canonical URL/slug + title + locale + visibility + lastmod + headingschunk:{ page, chunk_index, text, embedding, tokens, hash }path_tree:{ "services/seo": { isPublic: true, groups: [] }, ... }
- de assistant heeft een kaart nodig van “wat bestaat”
- access control wordt structureel (prune paths voordat sessies beginnen)
ls/findwordt snel en cacheable
Ingestion pipeline: maak van een website een betrouwbaar knowledge surface
Dit was de grootste tijdslokker. “Index je docs” klinkt makkelijk totdat je ziet hoe echte content eruitziet:- marketing copy + componenten
- MDX en headings die niet netjes mappen naar HTML
- navigatiepagina’s die contentblokken herhalen
- locale varianten die deels uiteenlopen
- Discover pages: sitemap + bekende route list.
- Fetch rendered HTML: wat users en crawlers echt zien.
- Extract main content: strip nav, footers, cookie banners, herhaalde UI.
- Normalize: whitespace samenvoegen, tracking query strings uit links halen.
- Segment:
- page-level record voor navigatie en citaten
- chunk-level records voor retrieval
- Annotate:
- locale
- visibility (public, client-only, internal)
- headings outline
- outbound interne links
Retrieval: combineer lexical en vector vóór je “het model vraagt”
We vertrouwen niet op één retrieval-strategie. We doen:- lexical search voor exacte tokens (goed voor identifiers, acroniemen, error codes)
- vector search voor semantische match (goed voor vage vragen)
- reject pagina’s die niet matchen met de locale van de user (tenzij expliciet gevraagd)
- prefer canonical pages boven tag pages / duplicate listings
- prefer pagina’s met sterke heading overlap met query terms
Tool design: wat de assistant kan (en niet kan)
We implementeerden een klein tool surface area en maakten het strikt: Allowed:- directories lijsten:
ls /services - paths zoeken:
find -name "billing" - een volledige pagina lezen:
cat /services/seo - binnen content zoeken:
grep -ri "canonical" /
- content schrijven of editen
- willekeurige URLs ophalen
- arbitraire network calls
De latency les: echte filesystems zijn te traag voor interactieve chat
Als je per sessie een sandbox/container opzet om een echt filesystem te bieden:- cold start wordt zichtbaar
- chat voelt kapot
- je krijgt de neiging om complexiteit toe te voegen zoals warm pools
lsenfindresolven uit de gecachte path treecatreconstrueert de pagina uit opgeslagen chunks (gesorteerd opchunk_index)- resultaten worden per sessie gecached (en deels globaal, waar veilig)
Cache wat herhaalt: directory tree, pagina’s en “grep targets”
Caching “answers” is zwak omdat vragen variëren. Wat herhaalt in echte gesprekken:- dezelfde secties lijstend
- dezelfde 5-10 belangrijke pagina’s openen
- greppen naar dezelfde tokens
- de path tree
- gereconstrueerde volledige pagina’s
- recente grep candidates
RBAC: access control moet structureel zijn, niet prompt-based
Als sommige docs niet publiek zijn (drafts, interne notities, client-only docs), kun je niet op prompting vertrouwen. We enforceen RBAC voordat de assistant één tool call uitvoert:- bouw een user-scoped path tree
- prune alles waar de user geen toegang toe heeft
- pas dezelfde filter toe op elke query en page read
ls-en, kan hij hem niet cat-en en kan hij hem niet citeren.
Dat is het enige mentale model dat standhoudt onder druk.
Guardrails: hoe we zelfverzekerd foute antwoorden stopten
We voegden een paar regels toe die slechte antwoorden drastisch reduceerden:Regel 1: Geen evidence, geen antwoord
Als de assistant geen ondersteunende content kan vinden met tools, moet hij:- zeggen dat hij het niet kon verifiëren
- laten zien wat hij checkte (pagina’s of secties)
- een volgende stap bieden (contact/support)
Regel 2: Citeer liever dan parafraseer bij kritieke details
Als het antwoord gevoelig is voor exacte wording (requirements, limitations, legal copy):- quote de relevante zin(nen) van de pagina die hij las
- houd de synthese minimaal
Regel 3: Wees streng op locale en canonical pages
Als de user op een locale route zit, moet de assistant:- content van die locale preferen
- talen niet mixen in één antwoord
- alleen terugvallen naar de default locale als de locale pagina niet bestaat
Het “grep probleem”: de killer feature heeft een two-phase plan nodig
Naive recursive grep is traag als het alles over het netwerk moet lezen. We gebruikten een two-phase aanpak:- coarse filter met de index (welke pagina’s kunnen het token bevatten)
- fine filter in memory over gecachte page text om exacte matches + context te pakken
Widget UX: de UI maakt of breekt vertrouwen
We shipten meerdere UI iteraties voordat de widget betrouwbaar voelde. Wat het meest telde:- streaming tokens met een stabiele layout (vermijd jumpy reflow)
- duidelijke states:
- “Searching…”
- “Reading page…”
- “Answering…”
- korte, inline citaten:
- “From: /services/seo”
- “From: /legal/privacy”
- fallbacks die niet als falen voelen:
- “I checked X and Y but couldn’t confirm; here’s how to reach us.”
Observability: log reads, niet alleen tokens
Als je kwaliteit wilt verbeteren, moet je weten wat er gebeurde. We logden:- tool calls (paths listed/read/searched)
- welke pagina’s als evidence gebruikt werden
- answer length en latency buckets
- “could not verify” rates
- escalation rates (contact clicks)
- welke pagina’s missen belangrijke informatie?
- welke vragen vinden nooit evidence?
- welke pagina’s zorgen voor verwarring en moeten herstructureerd worden?
Een praktische build checklist (wat we opnieuw zouden doen)
Als je een support widget bouwt die met je content chat, zouden we starten met:- indexeer de gerenderde site en sla page-level records op
- combineer lexical + vector retrieval
- voeg een exploration tool layer toe (files + grep)
- prune content voor RBAC voordat sessies starten
- houd tools read-only by default
- voeg een “no evidence, no answer” regel toe
- maak “searching/reading/answering” zichtbaar in de UI
- log tool calls zodat je failures kunt debuggen