Aller au contenu

Construire un Chatbot IA RAG - Notre Retour d'Expérience

Un chatbot assistant IA : RAG, embeddings, grounding et anti-hallucination

Construire un Chatbot IA RAG - Notre Retour d'Expérience

Sur rou9e.com, le bouton de chat en bas à droite n'est pas un gadget : c'est un véritable assistant en production qui répond aux questions des visiteurs sur nos services, nos délais, nos technos et nos tarifs, en français, à toute heure. Pas un chatbot générique branché sur un modèle qui improvise n'importe quoi : un système RAG (Retrieval Augmented Generation) construit maison, qui ne parle que de ce qu'il connaît, cite ses sources, et quand il ne sait pas, il escalade vers un humain plutôt que d'inventer. Le défi était de faire tenir un assistant IA crédible sur une stack légère, sans framework lourd, sans se ruiner en appels d'API, et sans jamais laisser le modèle déborder hors de son périmètre. Cet article raconte comment nous l'avons construit, de l'ingestion du corpus jusqu'au filtre anti-hallucination, avec les chiffres réels et des limites assumées. Le bot tourne sur le site que vous lisez en ce moment : testez-le en bas de page, et si le sujet vous intéresse pour votre propre site de marque, on en discute en fin de l'article 😎.

Pourquoi un chatbot RAG, et pas un chatbot branché sur un LLM

Un chatbot qui écrit directement dans le modèle, sans contexte, a de très fortes chances d'halluciner : il invente des prix, des délais, des compétences ou des réponses sortis de son chapeau. Sur le site d'une agence, c'est pire que pas de chatbot, c'est de la désinformation commerciale. Le RAG inverse le problème : au lieu de demander au modèle ce qu'il sait, on lui fournit le contenu pertinent (nos pages, nos articles, notre FAQ, nos brochures PDF) et on lui demande de synthétiser ses réponses à partir de ce corpus uniquement. Le modèle devient un rédacteur qui cite ses sources, pas un oracle. Concrètement, le pipeline tient en trois blocs : une phase offline d'ingestion qui transforme le site en index vectoriel, une phase runtime qui retrouve les bons morceaux à chaque message, et un orchestrateur qui pilote le modèle via le function calling d'OpenAI. L'intérêt d'une archi en trois classes de service (Ingestion, Retrieval, ChatOrchestrator) plutôt qu'en microservices : pour un corpus d'agence, découper en microservices n'apporte que de la complexité opérationnelle pour zéro gain de scalabilité. Le monolithe modulaire reste la version crédible, en prod comme en entretien.

TL;DR : Le RAG fournit au modèle un corpus délimité et lui interdit d'inventer ; pas d'hallucination, des guardes-fous et un control total sur les réponses.

L'ingestion : transformer un site entier en base vectorielle

Tout commence hors ligne, via une commande CLI dédiée. L'étape d'Ingestion parcourt une whitelist de fichiers : pages du site, fichiers de données (FAQ, équipe, clients, secteurs), articles de blog, documentations techniques, brief commercial, etc. Rien d'autre en dehors de cette liste n'est lu ou ingéré. Chaque page est découpée en sections selon ses sections, puis chacune est découpée en chunks. Le Chunking est hybride : si une section tient sous un seuil de tokens, on la garde entière ; sinon, on la recoupe en fenêtres de taille fixe avec un chevauchement, pour ne pas couper une idée en deux. Chaque chunk porte un en-tête qui rappelle son titre de page et son titre de section, pour que l'embedding sache de quoi on parle. Vient l'Embedding : on envoie les textes à OpenAI par lots, et on récupère un vecteur par chunk, via le modèle text-embedding-3-large projeté en 1536 dimensions. Ces vecteurs représentent le sens du texte : deux chunks au sens proche pointent dans la même direction. Au final, l'index contient plus de 1300 documents sources, plus de 8000 chunks, pour un fichier d'environ 250 Mo. C'est cet index, précalculé une fois, que le runtime lira à chaque message : le site n'est jamais relu en direct pendant une conversation.

TL;DR : Ingestion + Chunking + Embedding transforment le site et les contenus annexes en index vectoriel précalculé, lu au runtime uniquement.

Le runtime : retrieval, scoring et réponse ancrée

À chaque message utilisateur, le bot transforme la question en vecteur (même modèle d'embedding qu'à l'ingestion, sinon les vecteurs ne sont pas comparables), puis cherche les chunks les plus proches dans l'index. La métrique est la similarité cosinus : on classe les chunks par angle entre le vecteur question et le vecteur contenu, et on garde le top-k. Le Retrieval lance en fait plusieurs requêtes fusionnées (la question reformulée par le LLM et le message brut du visiteur), ce qui améliore le rappel sur les questions courtes ou mal formulées. Ensuite vient le Scoring : on applique des boosts pour remonter les sources fiables (pages institutionnelles, FAQ) et un bonus de mots-clés métier quand la question et le chunk partagent un terme technique. Le meilleur score est classé en trois tiers qui décident du comportement : réponse directe, demande de précision, ou escalade humaine. Si le contexte est suffisant, on l'injecte dans un deuxième appel au LLM avec un system prompt strict qui exige une réponse ancrée dans ce contexte uniquement, avec des sources / liens interne. Le function calling d'OpenAI gère le routage d'intention (saluer, rediriger vers le contact, chercher, clarifier, escalader) : c'est plus robuste qu'un regex et ça donne un vrai mot-clé technique crédible. La réponse est enfin re-filtrée avant d'être renvoyée au widget, avec ses liens de sources.

TL;DR : Retrieval + Scoring + Function calling = une réponse tirée du contexte, pas de la mémoire du modèle.

VectorStore : Local vs Pinecone ?

Les vecteurs résident derrière une interface VectorStore, avec deux backends différents : un store local (JSON + similarité cosinus calculée en local) et Pinecone (base vectorielle gérée). Pour un corpus d'agence, un store local en JSON est largement suffisant, sans latence réseau ni coût supplémentaire. La solution développée permet tout de même de switcher sur Pinecone pour valider deux choses : que l'archi était portable sans réécrire le runtime, et qu'il était faisable de brancher Pinecone en deux clicks. C'est d'ailleurs l'intérêt d'avoir une interface : basculer d'un backend à l'autre, sans toucher au reste. Un point technique souvent mal compris : Pinecone ne choisit pas le modèle d'embedding. C'est OpenAI qui embed, puis on upsert les vecteurs. La dimension de l'index Pinecone est fixée à sa création (immutable), c'est juste la taille du vecteur, pas le modèle. Changer de modèle ou de dimension implique un reindex complet ; changer de dimension implique aussi de recréer l'index.

TL;DR : interface VectorStore avec deux backends via une interface; Pinecone (base vectorielle gérée) vs (JSON + similarité cosinus calculée en local).

Sécurité, Grounding, Pentesting

Le risque numéro un, c'est la fuite : un bot qui recrache son system prompt ou qui lâche une clé API, c'est la cata ! Ici, le system prompt est verrouillé et invisible : le modèle n'a le droit de piocher que dans le contexte fourni, n'évoque jamais de fichier de config, de clé ou de chemin, et ne dispose d'aucun outil d'écriture. En bout de course, un filtre de grounding confronte la réponse aux chunks retrouvés : dès qu'une affirmation n'est pas couverte par le contexte, on la remplace par un template de refus plutôt que de laisser passer une invention. Un bloqueur de termes sensibles verrouille l'ensemble. Côté budget et anti-abus : un plafond mensuel en dur (fail-closed), un rate-limit, un plafond par session, une longueur max de message, et un kill switch qui coupe le bot à la volée. On a passé le bot à travers plus d'une trentaine de prompts adversariaux (injection, extraction de system prompt, demande de clé, jailbreak) : zéro fuite, refus poli à chaque fois. Côté escalade, quand le bot sèche, il demande l'accord du visiteur puis son email, et déclenche un email avec la question non résolue et le contexte de navigation, pour qu'un humain reprenne la main. Ce qu'on a sciemment écarté : pas de streaming (peu utile pour des réponses courtes), pas de retrieval hybride BM25 (le dense suffit ici), pas de multi-langue (français only pour v1), pas de fine-tuning (RAG avec grounding suffit). Trois enseignements : le métier du dev senior se déplace vers l'archi et la relecture, pas vers la frappe de boilerplate ; un side project en prod vaut mieux qu'un tuto pour valider une stack IA ; et la productivité gagne chez ceux qui ont déjà des conventions solides. Pour cadrer un chatbot comparable sur votre site, nos tarifs développement sur-mesure permettent d'isoler le périmètre dès le premier échange.

TL;DR : System prompt strict + grounding filter + fail-closed + kill switch = zéro fuite sur les prompts adversariaux.

Adrien FERREIRA
  • IA
  • RAG
  • Chatbot

Cet article vous a donné des idées pour votre projet ?

Autres articles de blog qui pourraient vous intéresser