Cosa abbiamo imparato costruendo il nostro widget di supporto (chat con i tuoi contenuti)
Abbiamo costruito il nostro widget di supporto: una chat bubble che integri in un sito web cosi i visitatori possono fare domande e ottenere risposte dai tuoi contenuti (docs, knowledge base, pagine servizi, policy). Se ne hai mai rilasciato uno, sai che il pitch e semplice e la realta e complicata. I problemi che incontri raramente sono “problemi di modello”. Sono problemi di ingegneria:- il retrieval si rompe in modi prevedibili
- la latenza distrugge la fiducia prima che arrivi la prima risposta
- il controllo accessi e facile da sbagliare
- la prompt injection diventa “avvelenamento dei contenuti”
- le risposte devono essere verificabili, altrimenti aumenti il rischio di supporto
Cosa stavamo costruendo (requisiti e vincoli)
Abbiamo fissato alcuni requisiti non negoziabili fin dall’inizio:- Primo token veloce: gli utenti devono vedere una risposta rapidamente, anche se l’assistente deve “cercare” in background.
- Risposte grounded: le risposte devono essere supportate dai contenuti che pubblichiamo davvero.
- Read-only di default: l’assistente non deve modificare i contenuti.
- Pronto per piu lingue: il sito e localizzato; l’assistente non deve mischiare lingue con leggerezza.
- Escalation sicura: quando non puo rispondere, deve instradare l’utente verso contatto/supporto senza allucinare.
L’assistente deve comportarsi come un ingegnere prudente che legge il nostro sito, non come uno scrittore creativo che improvvisa.
Un modello mentale utile: “support bot = ricerca + navigazione + sintesi”
Un buon assistente di supporto fa tre cose, in sequenza:- Cerca: identifica pagine/sezioni candidate.
- Naviga: apre pagine, scansiona heading, segue link, raffina la ricerca.
- Sintetizza: produce una risposta breve e corretta con indicazioni su da dove arriva.
Lezione 1: un RAG “solo chunk” fallisce quando la risposta e distribuita su piu pagine
Il retrieval top-K di chunk si rompe in modi consistenti:- la risposta e divisa tra due pagine
- la “sintassi esatta” e in un blocco di codice che non e entrato in ranking
- la risposta giusta e su una pagina con scarsa sovrapposizione lessicale
- la domanda dell’utente e troppo vaga, quindi il match embedding e “abbastanza vicino”
- tono corretto
- dettagli sbagliati
- nessun modo per verificare
Lezione 2: dare all’assistente primitive deterministiche (docs-come-file)
Gli esseri umani non rispondono alle domande su documentazione leggendo cinque paragrafi casuali. Noi:findla pagina- la
open - scorriamo gli heading
- cerchiamo token esatti
- seguiamo i link interni
- ripetiamo finche possiamo provare la risposta
- ogni pagina e un “file”
- le directory rappresentano sezioni (o segmenti dell’URL)
- l’assistente puo fare
ls,find,categrep
- auditabili (puoi loggare le tool call)
- deterministiche (la stessa query produce le stesse letture)
- scalabili (puo esplorare senza che tu debba hardcodare i flussi)
Architettura (la versione piu semplice che funziona)
Ad alto livello, siamo arrivati a tre livelli: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)
Il punto chiave: trattiamo “leggere i nostri contenuti” come uno strumento, non come un effetto collaterale del retrieval.
Modello dati: pagine, chunk e un albero di path
Abbiamo mantenuto la rappresentazione dei contenuti intenzionalmente noiosa:page: URL/slug canonico + titolo + locale + visibilita + lastmod + headingchunk:{ page, chunk_index, text, embedding, tokens, hash }path_tree:{ "services/seo": { isPublic: true, groups: [] }, ... }
- l’assistente ha bisogno di una mappa di “cosa esiste”
- il controllo accessi diventa strutturale (pruni i path prima che inizi la sessione)
ls/finddiventano veloci e cacheabili
Pipeline di ingestion: trasformare un sito in una superficie di conoscenza affidabile
E stato il maggiore sink di tempo. “Indicizza la documentazione” sembra facile finche non vedi com’e fatto il contenuto reale:- copy marketing + componenti
- MDX e heading che non mappano pulitamente su HTML
- pagine di navigazione che ripetono blocchi di contenuto
- varianti di lingua che divergono solo in parte
- Scoperta pagine: sitemap + lista di route note.
- Fetch HTML renderizzato: cio che utenti e crawler vedono davvero.
- Estrazione contenuto principale: rimuovi nav, footer, cookie banner, UI ripetuta.
- Normalizzazione: collassa spazi, rimuovi query di tracking dai link.
- Segmentazione:
- record a livello pagina per navigazione e citazioni
- record a livello chunk per retrieval
- Annotazione:
- locale
- visibilita (pubblico, client-only, interno)
- outline degli heading
- link interni in uscita
Retrieval: combinare lessicale e vettoriale prima di “chiedere al modello”
Non ci fidiamo di una sola strategia di retrieval. Facciamo:- ricerca lessicale per token esatti (ottima per identificatori, acronimi, codici errore)
- ricerca vettoriale per match semantico (ottima per domande vaghe)
- scarta pagine che non corrispondono al locale dell’utente (a meno che lo chieda esplicitamente)
- preferisci pagine canoniche rispetto a tag page / listing duplicati
- preferisci pagine con forte overlap tra heading e termini della query
Design degli strumenti: cosa puo fare l’assistente (e cosa no)
Abbiamo implementato una superficie strumenti piccola e l’abbiamo resa rigorosa: Consentito:- elencare directory:
ls /services - cercare path:
find -name "billing" - leggere una pagina completa:
cat /services/seo - cercare nel contenuto:
grep -ri "canonical" /
- scrivere o modificare contenuti
- fetch di URL arbitrari
- chiamate di rete arbitrarie
La lezione sulla latenza: filesystem reali sono troppo lenti per una chat interattiva
Se avvii un sandbox/container per sessione per fornire un filesystem reale:- il cold start diventa visibile
- la chat sembra rotta
- sei tentato di aggiungere complessita (pool caldi, ecc.)
lsefindrisolvono dal path tree cacheatocatricompone la pagina dai chunk salvati (ordinati perchunk_index)- i risultati vengono cacheati per sessione (e in parte globalmente, quando e sicuro)
Cache di cio che si ripete: albero, pagine e “target di grep”
Cacheare “risposte” e fragile perche le domande variano. Cio che si ripete nelle conversazioni reali e:- listare le stesse sezioni
- aprire le stesse 5-10 pagine importanti
- fare grep sugli stessi token
- il path tree
- pagine ricostruite complete
- candidati recenti per grep
RBAC: il controllo accessi deve essere strutturale, non basato sul prompt
Se alcune docs non sono pubbliche (draft, note interne, docs client-only), non puoi affidarti al prompting. Abbiamo applicato RBAC prima che l’assistente esegua una singola tool call:- costruisci un path tree per utente
- pruni tutto cio a cui l’utente non puo accedere
- applica lo stesso filtro a ogni query e lettura pagina
ls su un file, non puo fare cat e non puo citarlo.
E l’unico modello mentale che regge sotto pressione.
Guardrail: come abbiamo fermato risposte sbagliate ma “sicure”
Abbiamo aggiunto alcune regole che hanno ridotto drasticamente le risposte cattive:Regola 1: niente evidenza, niente risposta
Se l’assistente non trova contenuto di supporto con gli strumenti, deve:- dire che non ha potuto verificare
- mostrare cosa ha controllato (pagine o sezioni)
- proporre un next step (contatto/supporto)
Regola 2: preferire citazioni a parafrasi per dettagli critici
Quando la risposta e sensibile alla formulazione esatta (requisiti, limitazioni, copy legale):- cita la/le frase/i rilevanti della pagina letta
- mantieni la sintesi minimale
Regola 3: essere rigorosi su locale e pagine canoniche
Se l’utente e su una route di lingua, l’assistente deve:- preferire il contenuto di quel locale
- evitare di mischiare lingue nella stessa risposta
- fare fallback sul locale di default solo se la pagina in quel locale non esiste
Il “problema grep”: la feature killer richiede un piano in due fasi
Un grep ricorsivo naive e lento se legge tutto via rete. Abbiamo usato un approccio in due fasi:- filtro grossolano usando l’indice (quali pagine potrebbero contenere il token)
- filtro fine in memoria sul testo pagina cacheato per estrarre match esatti + contesto
UX del widget: la UI fa o disfa la fiducia
Abbiamo rilasciato piu iterazioni UI prima che il widget risultasse affidabile. Cosa ha contato di piu:- streaming di token con layout stabile (evita reflow “saltellante”)
- stati chiari:
- “Cercando…”
- “Leggendo la pagina…”
- “Rispondendo…”
- citazioni brevi inline:
- “Da: /services/seo”
- “Da: /legal/privacy”
- fallback che non sembrano un fallimento:
- “Ho controllato X e Y ma non ho potuto confermare; ecco come contattarci.”
Osservabilita: loggare le letture, non solo i token
Se vuoi migliorare la qualita, devi sapere cosa e successo. Abbiamo loggato:- tool call (path listati/letti/cercati)
- quali pagine sono state usate come evidenza
- lunghezza della risposta e bucket di latenza
- tasso “non ho potuto verificare”
- tasso di escalation (click su contatto)
- quali pagine mancano di informazioni importanti?
- quali domande non trovano mai evidenza?
- quali pagine generano confusione e richiedono ristrutturazione?
Una checklist pratica di build (cosa rifaremmo)
Se stai costruendo un widget di supporto che chatta con i tuoi contenuti, partiremmmo da:- indicizzare il sito renderizzato e salvare record a livello pagina
- combinare retrieval lessicale + vettoriale
- aggiungere un layer di esplorazione (file + grep)
- prunare i contenuti per RBAC prima che inizi la sessione
- mantenere gli strumenti read-only di default
- aggiungere la regola “niente evidenza, niente risposta”
- rendere visibile in UI “cercando/leggendo/rispondendo”
- loggare le tool call per poter debuggare i failure