Vad vi lärde oss när vi byggde vår egen supportbot-widget (chatta med ditt innehåll)
Vi byggde vår egen supportbot-widget: en chat-bubbla du bäddar in på en webbplats så att besökare kan ställa frågor och få svar från ditt innehåll (docs, knowledge base, tjänstesidor, policies). Om du någon gång har levererat en sådan vet du att pitchen är enkel och verkligheten är rörig. Problemen du stöter på är sällan “modellproblem”. Det är engineering-problem:- retrieval går sönder på förutsägbara sätt
- latency dödar förtroendet innan första svaret landar
- access control är lätt att göra fel
- prompt injection blir “content poisoning”
- svar måste vara auditerbara annars får du support-risk
Vad vi byggde (krav och constraints)
Vi satte några icke-förhandlingsbara krav från början:- Snabb första token: användaren ska se respons snabbt, även om assistenten behöver “söka” i bakgrunden.
- Grounded answers: svar måste stödjas av innehåll vi faktiskt publicerar.
- Read-only som default: assistenten får inte mutera innehåll.
- Multi-locale redo: vår sajt är lokaliserad; assistenten ska inte blanda språk slarvigt.
- Säker eskalering: när den inte kan svara ska den kunna lotsa vidare utan att hallucinerar.
Assistenten ska bete sig som en noggrann ingenjör som läser vår webbplats, inte som en kreativ skribent som improviserar.
En fungerande mental modell: “supportbot = sök + navigation + syntes”
En bra supportassistent gör tre saker i ordning:- Sök: identifiera kandidat-sidor/sektioner.
- Navigera: öppna sidor, skanna rubriker, följa länkar, förfina sökningen.
- Syntetisera: producera ett kort, korrekt svar med pekare till var det kommer ifrån.
Lärdom 1: Chunk-only RAG fallerar när svaret spänner över flera sidor
Top-K chunk retrieval går sönder på konsekventa sätt:- svaret är splittrat över två sidor
- den “exakta syntaxen” är i ett kodblock som inte rankade
- rätt svar finns på en sida med svag lexical overlap
- användarens fråga är underspecificerad så embedding-matchen blir “tillräckligt nära”
- rätt ton
- fel detaljer
- inget sätt att verifiera
Lärdom 2: Ge assistenten deterministiska primitiver (docs-as-files)
Människor svarar inte på docs-frågor genom att läsa fem slumpmässiga stycken. Vi:findsidanopenden- skannar rubriker
- söker efter exakta tokens
- följer interna länkar
- upprepar tills vi kan bevisa svaret
- varje sida är en “fil”
- kataloger representerar sektioner (eller URL-path segments)
- assistenten kan
ls,find,catochgrep
- auditerbara (du kan logga tool calls)
- deterministiska (samma fråga ger samma läsningar)
- skalbara (den kan utforska utan att du hårdkodar flöden)
Arkitektur (den enklaste versionen som funkar)
På hög nivå landade vi i tre lager: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)
Nyckeln: vi behandlar “att läsa vårt innehåll” som ett verktyg, inte som en bieffekt av retrieval.
Datamodell: sidor, chunks och ett path tree
Vi höll representationen medvetet tråkig:page: canonical URL/slug + title + locale + visibility + lastmod + headingschunk:{ page, chunk_index, text, embedding, tokens, hash }path_tree:{ "services/seo": { isPublic: true, groups: [] }, ... }
- assistenten behöver en karta över “vad som finns”
- access control blir strukturell (prune paths innan sessionen börjar)
ls/findblir snabbt och cachebart
Ingestionspipeline: gör en webbplats till en pålitlig knowledge surface
Det här tog mest tid. “Indexera dina docs” låter enkelt tills du ser hur verkligt innehåll ser ut:- marknadscopy + komponenter
- MDX och rubriker som inte mappar rent till HTML
- navigationssidor som upprepar content-block
- locale-varianter som divergerar delvis
- Upptäck sidor: sitemap + känd route-lista.
- Hämta renderad HTML: vad användare och crawlers faktiskt ser.
- Extrahera main content: ta bort nav, footers, cookie banners, repetitiv UI.
- Normalisera: kollapsa whitespace, ta bort tracking query strings från länkar.
- Segmentera:
- page-level record för navigation och citations
- chunk-level records för retrieval
- Annotera:
- locale
- visibility (public, client-only, internal)
- headings outline
- outbound internal links
Retrieval: kombinera lexical och vector innan du “frågar modellen”
Vi litar inte på en enda retrieval-strategi. Vi gör:- lexical search för exakta tokens (bra för identifiers, akronymer, felkoder)
- vector search för semantisk match (bra för vaga frågor)
- avvisa sidor som inte matchar användarens locale (om den inte uttryckligen ber om annat)
- föredra canonical-sidor framför tag-sidor / dubblettlistningar
- föredra sidor med stark rubrik-overlap med frågetermer
Tool-design: vad assistenten får göra (och inte får)
Vi implementerade en liten tool-yta och gjorde den strikt. Tillåtet:- lista kataloger:
ls /services - sök paths:
find -name "billing" - läs en hel sida:
cat /services/seo - sök i innehåll:
grep -ri "canonical" /
- skriva eller redigera innehåll
- hämta godtyckliga URL:er
- godtyckliga nätverksanrop
Latency-lärdomen: riktiga filesystems är för långsamma för interaktiv chat
Om du startar en sandbox/container per session för att ge ett riktigt filesystem:- cold start blir synligt
- chatten känns trasig
- du blir frestad att lägga på komplexitet som warm pools
lsochfindslår i det cachade path tree:tcatsätter ihop sidan från lagrade chunks (sorterade påchunk_index)- resultaten cachas per session (och delvis globalt när det är säkert)
Cacha det som upprepas: path tree, pages och “grep targets”
Att cacha “svar” är svagt eftersom frågor varierar. Det som upprepas i riktiga konversationer är:- lista samma sektioner
- öppna samma 5-10 viktiga sidor
- greppa efter samma tokens
- path tree:t
- rekonstruerade full pages
- senaste grep-kandidater
RBAC: access control måste vara strukturell, inte prompt-baserad
Om vissa docs inte är publika (utkast, interna anteckningar, client-only docs) kan du inte lita på prompting. Vi enforced RBAC innan assistenten gör ett enda tool call:- bygg ett user-scoped path tree
- prune:a allt användaren inte får se
- applicera samma filter på varje query och page read
ls en fil kan den inte cat den och den kan inte citera den.
Det är den enda mental modellen som håller under press.
Guardrails: hur vi stoppade självsäkra fel-svar
Vi lade till några regler som kraftigt minskade dåliga svar.Regel 1: Inget evidens, inget svar
Om assistenten inte kan hitta stödjande innehåll via tools ska den:- säga att den inte kunde verifiera
- visa vad den kollade (sidor eller sektioner)
- föreslå nästa steg (contact/support)
Regel 2: Föredra att citera framför att parafrasera för kritiska detaljer
När svaret är känsligt för exakta formuleringar (krav, begränsningar, juridisk copy):- citera relevant(a) mening(ar) från sidan den läste
- håll syntesen minimal
Regel 3: Var strikt med locale och canonical-sidor
Om användaren är på en locale-route ska assistenten:- föredra den locale:ns innehåll
- undvika att blanda språk i ett svar
- falla tillbaka till default locale bara om locale-sidan inte finns
“Grep-problemet”: killer feature kräver en tvåfasplan
Naiv rekursiv grep är långsam om den läser allt över nätet. Vi använde en tvåfas-approach:- coarse filter med indexet (vilka sidor kan innehålla token)
- fine filter i minne över cachad sidtext för att plocka exakta träffar + kontext
Widget-UX: UI:t bygger eller förstör förtroende
Vi levererade flera UI-iterationer innan widgeten kändes pålitlig. Det som spelade störst roll:- streaming tokens med stabil layout (undvik jumpy reflow)
- tydliga states:
- “Searching…”
- “Reading page…”
- “Answering…”
- korta, in-line citations:
- “From: /services/seo”
- “From: /legal/privacy”
- fallbacks som inte känns som failure:
- “Jag kollade X och Y men kunde inte bekräfta; här är hur du når oss.”
Observability: logga läsningarna, inte bara tokens
Om du vill förbättra kvalitet måste du veta vad som hände. Vi loggade:- tool calls (paths listade/lästa/sökta)
- vilka sidor som användes som evidens
- svarslängd och latency buckets
- “kunde inte verifiera”-andel
- eskaleringsgrad (contact clicks)
- vilka sidor saknar viktig information?
- vilka frågor hittar aldrig evidens?
- vilka sidor skapar förvirring och behöver omstrukturering?
En praktisk bygg-checklista (vad vi skulle göra igen)
Om du bygger en supportwidget som chattar med ditt innehåll börjar vi med:- indexera den renderade sajten och lagra page-level records
- kombinera lexical + vector retrieval
- lägg till ett exploration tool layer (filer + grep)
- prune:a innehåll för RBAC innan sessioner startar
- håll tools read-only som default
- lägg till en “inget evidens, inget svar”-regel
- gör “searching/reading/answering” synligt i UI:t
- logga tool calls så du kan debugga failure