Ce qu’on a appris en construisant notre propre widget de support (chat avec votre contenu)
Un teardown technique de ce qui compte vraiment quand on construit un widget de support : modes d’échec de la retrieval, navigation “docs comme des fichiers”, cache, contrôle d’accès et garde-fous pour des réponses fiables.
Ce qu’on a appris en construisant notre propre widget de support (chat avec votre contenu)
Nous avons construit notre propre widget de support : une bulle de chat à intégrer sur un site pour que les visiteurs posent des questions et obtiennent des réponses à partir de votre contenu (docs, base de connaissance, pages services, politiques).Si vous en avez déjà livré un, vous savez que la promesse est simple et la réalité, beaucoup moins.Les problèmes que vous rencontrez sont rarement des « problèmes de modèle ». Ce sont des problèmes d’ingénierie :
la retrieval casse de manière prévisible
la latence détruit la confiance avant même le premier token
le contrôle d’accès est facile à rater
l’injection de prompt devient un « empoisonnement du contenu »
les réponses doivent être auditables, sinon vous créez un risque support
Ce billet est un teardown détaillé : architecture, modèle de données, stratégie de retrieval, design des outils, cache, RBAC, patterns UI, et garde-fous qui ont rendu notre widget fiable.
Ce qu’on construisait (exigences et contraintes)
On a défini quelques exigences non négociables dès le départ :
Fast first token : l’utilisateur doit voir une réponse rapidement, même si l’assistant doit « chercher » en arrière-plan.
Réponses ancrées : les réponses doivent être supportées par le contenu que l’on expédie réellement.
Read-only par défaut : l’assistant ne doit pas modifier le contenu.
Multi-locale : notre site est localisé ; l’assistant ne doit pas mélanger les langues au hasard.
Escalade sûre : quand il ne peut pas répondre, il doit orienter vers le contact/support sans halluciner.
À partir de ça, on a dérivé une définition simple du succès :
L’assistant doit se comporter comme un ingénieur prudent qui lit notre site, pas comme un écrivain créatif qui improvise.
Un modèle mental qui marche : « support bot = search + navigation + synthèse »
Un bon assistant support fait trois choses, dans cet ordre :
Search : identifier des pages/sections candidates.
Navigate : ouvrir des pages, scanner les titres, suivre des liens, affiner la recherche.
Synthesize : produire une réponse courte et correcte, avec des pointeurs vers les sources utilisées.
La plupart des implémentations ne font que (1) et (3). Elles sautent (2), et c’est là que la précision s’effondre.
Leçon 1 : un RAG “chunks only” échoue quand la réponse s’étale sur plusieurs pages
La retrieval Top-K de chunks casse toujours de la même manière :
la réponse est répartie sur deux pages
la « syntaxe exacte » est dans un bloc de code qui n’a pas remonté
la bonne réponse est sur une page avec un faible recouvrement lexical
la question est trop vague, donc le match embedding est « proche »
Quand le modèle ne voit que quelques chunks, il peut paraître confiant tout en étant incomplet. L’expérience utilisateur ressemble à :
le ton est bon
les détails sont faux
aucune manière de vérifier
Notre conclusion : la retrieval ne suffit pas. Il faut de l’exploration.
Leçon 2 : donner à l’assistant des primitives déterministes (docs comme des fichiers)
Les humains ne répondent pas à une question doc en lisant cinq paragraphes pris au hasard. On :
find la page
l’open
scanne les titres
cherche des tokens exacts
suit des liens internes
répète jusqu’à pouvoir prouver la réponse
On a donc façonné l’interface d’outils de l’assistant comme un mini filesystem au-dessus de notre contenu :
chaque page est un « fichier »
les dossiers représentent des sections (ou des segments de chemin d’URL)
l’assistant peut ls, find, cat et grep
Ce n’est pas un gimmick. Ça donne au modèle des primitives :
auditables (vous pouvez logguer les appels d’outils)
déterministes (même requête, mêmes lectures)
scalables (il peut explorer sans que vous codiez des flows à la main)
Architecture (la version la plus simple qui marche)
À haut niveau, on s’est retrouvé avec trois couches :
Normalize : compacter les espaces, retirer les query strings de tracking des liens.
Segment :
un enregistrement au niveau page pour navigation et citations
des enregistrements au niveau chunk pour la retrieval
Annotate :
locale
visibilité (public, client-only, internal)
outline de titres
liens internes sortants
Leçon principale : indexer le site rendu, pas seulement le repo source, sinon vous manquez ce que l’utilisateur voit réellement et vous finissez par citer du contenu qui n’existe pas en production.
Retrieval : combiner lexical et vector avant de « demander au modèle »
On ne fait pas confiance à une seule stratégie de retrieval.On fait :
recherche lexicale pour tokens exacts (identifiants, acronymes, codes d’erreur)
recherche vectorielle pour match sémantique (questions vagues)
Puis on fusionne les candidats et on applique quelques checks :
rejeter les pages qui ne correspondent pas à la locale utilisateur (sauf demande explicite)
préférer les pages canoniques aux pages tag / listings dupliqués
préférer les pages avec un bon recouvrement entre titres et termes de la requête
Seulement ensuite, on laisse l’assistant ouvrir des pages et synthétiser.
Design des outils : ce que l’assistant peut faire (et ce qu’il ne peut pas)
On a implémenté une surface d’outils petite et stricte :Autorisé :
lister des dossiers : ls/services
chercher des chemins : find-name"billing"
lire une page entière : cat/services/seo
chercher dans le contenu : grep-ri"canonical"/
Interdit :
écrire ou éditer du contenu
fetcher des URLs arbitraires
faire des appels réseau arbitraires
Le gain sécurité est énorme : ça transforme « l’injection de prompt » en problème de qualité de contenu, pas en compromission système.
Latence : un vrai filesystem est trop lent pour un chat interactif
Si vous démarrez un sandbox/container par session pour fournir un vrai filesystem :
le cold start devient visible
le chat donne l’impression d’être cassé
vous êtes tenté d’ajouter des complexités type warm pools
Dans un widget support, l’utilisateur est face à l’UI. Vous voulez des outils instantanés.On a donc virtualisé les opérations filesystem au-dessus de l’index :
ls et find se résolvent via l’arbre de chemins en cache
cat recompose la page depuis les chunks stockés (triés par chunk_index)
les résultats sont cachés par session (et en partie globalement, quand c’est safe)
L’assistant a l’illusion d’un shell sur des fichiers, mais il n’y a aucun fichier réel.
Mettre en cache ce qui se répète : arbre, pages et « cibles de grep »
Mettre en cache des « réponses » est fragile, parce que les questions varient.Ce qui se répète dans les vraies conversations :
lister les mêmes sections
ouvrir les mêmes 5-10 pages importantes
greper les mêmes tokens
On a donc caché :
l’arbre de chemins
les pages reconstruites
des candidats de grep récents
Ça a compté plus que d’optimiser des embeddings au micro niveau, parce que ça améliore la vitesse des follow-ups et réduit les boucles « searching… ».
RBAC : le contrôle d’accès doit être structurel, pas prompt-based
Si certaines docs ne sont pas publiques (brouillons, notes internes, docs client-only), vous ne pouvez pas vous appuyer sur le prompting.On a appliqué le RBAC avant le moindre tool call :
construire un arbre de chemins scoped à l’utilisateur
pruner tout ce qu’il ne peut pas accéder
appliquer le même filtre à chaque requête et lecture de page
Si l’assistant ne peut pas ls un fichier, il ne peut pas le cat, et il ne peut pas le citer.C’est le seul modèle mental qui tient sous pression.
Garde-fous : comment on a arrêté les réponses confiantes mais fausses
Quelques règles ont réduit fortement les mauvaises réponses :
Règle 1 : pas de preuve, pas de réponse
Si l’assistant ne trouve pas de contenu support avec les outils, il doit :
dire qu’il n’a pas pu vérifier
montrer ce qu’il a vérifié (pages ou sections)
proposer une prochaine étape (contact/support)
Règle 2 : préférer citer plutôt que paraphraser pour les détails critiques
Quand la réponse dépend de la formulation exacte (exigences, limitations, mentions légales) :
citer la/les phrase(s) pertinente(s) depuis la page lue
garder la synthèse minimale
Règle 3 : être strict sur la locale et les pages canoniques
Si l’utilisateur est sur une route de locale, l’assistant doit :
préférer le contenu de cette locale
éviter de mélanger les langues dans une seule réponse
revenir à la locale par défaut uniquement si la page de la locale n’existe pas
Le « problème grep » : la killer feature a besoin d’un plan en deux phases
Un grep récursif naïf est lent s’il lit tout sur le réseau.On a utilisé une approche en deux phases :
coarse filter via l’index (quelles pages pourraient contenir le token)
fine filter en mémoire sur le texte page en cache pour extraire les matches exacts + contexte
Ça donne l’impression que l’assistant cherche comme un dev, pas qu’il devine comme un chatbot.
UX du widget : l’UI fait ou défait la confiance
On a livré plusieurs itérations UI avant que le widget paraisse fiable.Ce qui a le plus compté :
streaming de tokens avec un layout stable (éviter le reflow “jumpy”)
états clairs :
« Searching… »
« Reading page… »
« Answering… »
citations courtes dans la conversation :
« From: /services/seo »
« From: /legal/privacy »
fallbacks qui ne ressemblent pas à un échec :
« J’ai vérifié X et Y mais je n’ai pas pu confirmer ; voici comment nous contacter. »
Ça se combine bien avec une UI qui rend le “travail” visible. Les utilisateurs pardonnent beaucoup plus un bot qui dit « je n’ai pas pu confirmer » qu’un bot qui invente une réponse confiante.
Observability : logger les lectures, pas seulement les tokens
Pour améliorer la qualité, il faut savoir ce qui s’est passé.On a loggué :
tool calls (chemins listés/lus/recherchés)
quelles pages ont servi de preuves
longueur de réponse et buckets de latence
taux de « could not verify »
taux d’escalade (clics contact)
Ça permet de répondre à des questions concrètes :
quelles pages manquent d’informations importantes ?
quelles questions ne trouvent jamais de preuve ?
quelles pages créent de la confusion et doivent être restructurées ?
Une checklist pragmatique (ce qu’on referait)
Si vous construisez un widget support qui chatte avec votre contenu, on commencerait par :
indexer le site rendu et stocker des enregistrements au niveau page
combiner retrieval lexicale + vectorielle
ajouter une couche d’exploration (files + grep)
pruner le contenu via RBAC avant le début des sessions
garder les outils read-only par défaut
ajouter une règle « pas de preuve, pas de réponse »
rendre “searching/reading/answering” visible dans l’UI
logguer les tool calls pour débugger les échecs
Si vous voulez qu’on vous aide à implémenter ça (widget + indexation + architecture sûre) :