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 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.
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.
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 »
- le ton est bon
- les détails sont faux
- aucune manière de vérifier
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 :findla page- l’
open - scanne les titres
- cherche des tokens exacts
- suit des liens internes
- répète jusqu’à pouvoir prouver la réponse
- chaque page est un « fichier »
- les dossiers représentent des sections (ou des segments de chemin d’URL)
- l’assistant peut
ls,find,catetgrep
- 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 :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)
Le point clé : on traite la « lecture de notre contenu » comme un outil, pas comme un effet secondaire de la retrieval.
Modèle de données : pages, chunks et un arbre de chemins
On a volontairement gardé une représentation du contenu très simple :page: URL canonique/slug + titre + locale + visibilité + lastmod + plan de titreschunk:{ page, chunk_index, text, embedding, tokens, hash }path_tree:{ "services/seo": { isPublic: true, groups: [] }, ... }
- l’assistant a besoin d’une carte de « ce qui existe »
- le contrôle d’accès devient structurel (on prune les chemins avant la session)
ls/finddevient rapide et cacheable
Pipeline d’ingestion : transformer un site en surface de connaissance fiable
C’est ce qui a pris le plus de temps. « Indexer vos docs » paraît simple jusqu’à ce qu’on voie à quoi ressemble du vrai contenu :- copy marketing + composants
- MDX et titres qui ne se mappent pas proprement à l’HTML
- pages de navigation qui répètent des blocs
- variantes de locale qui divergent partiellement
- Discover pages : sitemap + liste de routes connues.
- Fetch rendered HTML : ce que les utilisateurs et les crawlers voient vraiment.
- Extract main content : retirer nav, footers, bannières cookies, UI répétée.
- 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
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)
- 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
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" /
- écrire ou éditer du contenu
- fetcher des URLs arbitraires
- faire des appels réseau arbitraires
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
lsetfindse résolvent via l’arbre de chemins en cachecatrecompose la page depuis les chunks stockés (triés parchunk_index)- les résultats sont cachés par session (et en partie globalement, quand c’est safe)
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
- l’arbre de chemins
- les pages reconstruites
- des candidats de grep récents
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
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
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. »
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)
- 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