RAG en production : assistant documentaire à l’INSEI
Un assistant qui répond aux questions des personnels sur l’intranet, entièrement hébergé par l’établissement, et le harnais d’évaluation qui a piloté ses versions successives.
- Python 3.11
- FastAPI
- LangChain
- Qdrant
- Ollama
- Docker
- Ce que c’est
Un assistant documentaire qui répond aux questions des personnels sur les procédures, les contacts et les ressources internes, en service sur deux sites.
- Ce qui était dur
Rien ne sort de l’établissement et rien n’est payé. Les solutions reposant sur un service externe étaient écartées d’emblée.
- Ce que ça prouve
Un jeu de questions de référence et un juge automatique, posés avant les optimisations : c’est ce qui permet de dire qu’un changement a amélioré quelque chose.
L’INSEI est un établissement public d’enseignement supérieur installé à Suresnes, qui forme les professionnels de l’éducation inclusive et mène la recherche associée : scolarisation des élèves en situation de handicap, accessibilité, adaptation pédagogique. Il accueille des étudiants, forme des personnels en exercice, et publie des ressources pour le terrain.
Mon alternance y dure d’avril 2023 à septembre 2026. Le projet décrit ici a été mené à deux.
Des réponses qui existent déjà, introuvables
Les agents cherchent des informations qui existent déjà. Procédures internes, contacts, ressources RH : tout est sur l’intranet, et tout y est difficile à retrouver. La demande est un assistant qui répond en langage naturel, en citant ses sources.
Un prototype qui répond à peu près se construit vite, à partir de briques documentées. Ce qui suit est ce qui l’en sépare d’un système qu’un établissement accepte d’exploiter : des contraintes d’hébergement, des contenus qui ne sont pas propres, et un moyen de dire si une version vaut mieux que la précédente.
Rien ne sort, rien n’est payé
Rien ne sort de l’établissement. Modèle de langage, modèle d’embeddings, base vectorielle, observabilité, journalisation : tout tourne sur les serveurs internes. Ce n’était pas une préférence mais une condition d’acceptation, et elle écarte d’emblée toute solution reposant sur un service hébergé ailleurs.
Budget nul. Aucune licence, aucun service payant, aucun appel facturé à la requête.
Le contenu source n’est pas propre. Un intranet est fait de pages hétérogènes, de profils d’annuaire et de documents joints, avec un certificat auto-signé et une structure de page qui varie d’un type de contenu à l’autre.
Comment l’assistant est construit
Architecture de l’assistant documentaire
Ajouter un site, c’est écrire un fichier
À l’ingestion, une fois
- Sites
- pages, profils, documents joints
- Adaptateurs
- un fichier par site
- Le CMS déclare où lire
- la zone utile de chaque page, fournie par la source
- Moteur générique
- découpage par section, vectorisation
- Base vectorielle
- le sens des passages
- Index de mots
- les termes exacts
À chaque question posée
- Question
- posée dans une interface de discussion
- Aiguillage
- annuaire ou contenu, deux index et deux pondérations
- Récupération hybride
- mots-clés et sens combinés
- extraits plafonnés par page
- Reranking
- implémenté, désactivé par défaut, jamais mesuré
- Modèle de langage, hébergé sur place
- rédige la réponse et cite ses sources
- Cache et observabilité
- réponses mises en cache, requêtes tracées
- Réponse
- avec les pages d’où elle vient
Trois couches, séparées pour une raison précise : ce qui change d’un site à l’autre ne doit pas contaminer ce qui n’en change pas.
Une couche d’adaptateurs. Un contrat abstrait définit ce qu’un site doit fournir : comment s’authentifier, comment transformer une page en documents indexables, quel prompt système, quelle collection. Chaque site est un fichier qui remplit ce contrat, et l’API les découvre au démarrage. Ajouter un site, c’est écrire un fichier.
Un moteur générique. Ingestion, découpage, vectorisation, recherche : rien de ce code ne connaît un site en particulier. Il sait qu’il existe des profils de personnes et des pages de contenu, et il se replie proprement sur les pages quand un site n’a pas d’annuaire.
Une API compatible avec le standard du domaine, ce qui permet de la brancher dans une interface de discussion existante plutôt que d’en écrire une. Une interface maison a existé : elle n’est plus dans le dépôt, et la documentation la décrit encore.
14briquesproductionL’assistant n’est pas un programme mais quatorze : la base qui stocke les documents, le modèle d’IA qui rédige, le cache qui accélère, la supervision qui surveille, et tout ce qui les relie. Chacun tourne dans sa propre boîte, sur les serveurs de l’établissement. composent la pile déployée : base vectorielle, modèle de langage, cache, observabilité, métriques de conteneurs, proxy, API.
Sur l’intranet de l’établissement, l’index porte de l’ordre de 300profils environproductionProfils de contacts indexés : 300 profils environ (production). et 700pages environproductionPages et documents indexés : 700 pages environ (production).. Ce sont deux natures de contenu, et le pipeline les traite par deux routes distinctes, ce qui explique la suite.
artefact reproductibleL’ingestion est idempotente et incrémentale : réindexer ne duplique rien, et les pages supprimées de la source disparaissent de l’index.
Les décisions qui ont compté
Le CMS dit au moteur de recherche où regarder
Cette décision n’a pas été prise dans le code, et c’est ce qui la rend intéressante.
Extraire le contenu utile d’une page web demande de savoir quelle zone lire. La solution réflexe est d’écrire des sélecteurs dans le code du moteur, ce qui a été fait d’abord. Elle a un défaut qui n’apparaît qu’avec le temps : chaque changement de gabarit côté CMS casse silencieusement l’ingestion, et la personne qui fait le changement n’a aucune raison de savoir qu’un autre système en dépend.
Le choix retenu déplace la responsabilité : le CMS publie lui-même, pour chaque page, l’emplacement de sa zone de contenu, et le moteur d’ingestion le consomme. Cela suppose une modification côté CMS, donc une négociation avec l’équipe qui en a la charge, et une convention à tenir des deux côtés. En échange, changer un gabarit ne casse plus rien en aval, et le moteur reste ignorant des sites qu’il sert : brancher une source revient à écrire un adaptateur, pas à toucher au moteur.
Mesurer avant d’optimiser
Un jeu de 88questionsjeu de testQuestions du jeu d’évaluation : 88 questions (jeu de test). a été constitué et versionné, chacune avec sa réponse attendue et la source où elle doit se trouver, dont 3questionsjeu de testContrôles négatifs dans le jeu d’évaluation : 3 questions (jeu de test). qui vérifient que le système répond qu’il ne sait pas plutôt que d’inventer. Un juge automatique lit le contexte récupéré et tranche en binaire : la réponse attendue s’y trouve, ou non.
C’est ce harnais, posé avant les optimisations, qui rend la suite lisible. Sans lui, comparer deux versions du pipeline revient à comparer deux impressions.
L’hybride est né d’un échec
Une recherche purement sémantique confondait des concepts proches : deux documents dont les titres se ressemblent et dont les contenus n’ont rien à voir. La combinaison avec une recherche par mots-clés a réglé le problème, avec des pondérations différentes selon qu’on cherche une personne ou une procédure. Ce n’est pas un raffinement théorique, c’est la réponse à une panne observée, et le harnais a dit si elle marchait.
Une mémoire à trois fenêtres
Le premier comportement conservait tout l’historique, ce qui faisait dériver le sujet dès qu’un utilisateur changeait de question. La règle actuelle distingue trois cas : une question contenant un pronom est un suivi et voit un historique large, une question autonome ouvre un nouveau sujet et n’en voit presque rien, et la reformulation travaille sur une fenêtre intermédiaire.
arbitrage documentéRecherche hybride lexicale et sémantique adoptée après l’échec d’une approche purement sémantique, avec des pondérations différentes selon qu’on cherche une personne ou une procédure.
contrainte tenueTraitement entièrement sur les serveurs de l’établissement, aucun service externe dans la pile, budget nul.
Ce que le harnais a mesuré
Ce que le passage à la recherche hybride a changé
Sur le jeu de référence, la part des questions dont le contexte récupéré contenait effectivement la réponse est passée de 27%jeu de testQuestions dont le contexte récupéré contenait la réponse, au départ : 27 % (jeu de test). à 86%jeu de testL’assistant retrouve le bon document neuf fois sur dix, contre une fois sur quatre au début du projet.. Entre les deux : le passage de la recherche purement sémantique à la recherche hybride, et un changement de modèle d’embeddings.
C’est le résultat du projet, et c’est aussi sa méthode : le chiffre n’a de sens que parce que le harnais existait avant les changements qu’il mesure.
Une réserve l’accompagne. Les deux runs portent sur le jeu de questions versionné dans le dépôt, et le second date de début juin 2026, mais aucun des deux résultats n’a été conservé : les chiffres sont consignés dans le dossier d’architecture du projet, pas dans des fichiers rejouables.
Une mesure indépendante, sur ce qui était mesurable hors du réseau
Le pipeline aiguille chaque question vers l’une de deux routes, annuaire ou contenu, avec des pondérations, des filtres et des index distincts. Cet aiguillage est une fonction de texte pur, qui ne consulte ni la base vectorielle ni un modèle : il est donc mesurable hors du réseau de l’établissement, alors que tout le reste ne l’est pas. Il envoie 96,47% des questionsjeu de testJustesse du routage : 96,47 % des questions (jeu de test, n = 85). du bon côté.
La route attendue est dérivée par moi de la source consignée dans le jeu. C’est une hypothèse explicite, pas une vérité terrain du projet.
Deux nuances que la documentation dilue
Le reranker est implémenté mais pas activé par défaut : il faut donc dire « disponible », pas « le pipeline reranke ». Et l’identification de l’utilisateur dans les traces relève de l’attribution, utile pour savoir qui a posé quelle question ; ce n’est pas de l’authentification, et la page ne le présente pas comme telle.
Ce que ces chiffres ne disent pas
Il n’y a aucun plancher. La progression mesurée dit que l’hybride fait mieux que le sémantique seul. Elle ne dit pas ce que ferait une recherche par mots-clés seule, qui coûterait bien moins cher. Sans ce troisième point, on sait que l’hybride a réglé un symptôme, pas ce qu’il apporte réellement.
Le reranker n’a jamais été mesuré. Il est désactivé par défaut, ce qui est un choix de coût défendable, mais pris sans chiffre.
La mémoire longue n’existe pas, l’historique est perdu à chaque rechargement. L’interface atteint le premier niveau d’accessibilité, pas le second. Et aucun indicateur de gain métier n’a été posé : on ne sait pas combien de temps l’assistant fait gagner, faute d’avoir mesuré le temps qu’il fallait avant.
Si je recommençais
Je me méfierais des heuristiques qui marchent. Mesurer l’aiguillage a sorti un défaut de mon propre code : la règle qui reconnaît une question d’annuaire se déclenche, entre autres, sur deux mots capitalisés consécutifs, un motif censé attraper un nom et un prénom, qui attrape en réalité n’importe quel couple de mots capitalisés. Le défaut est resté invisible tant que personne n’a compté, parce qu’une question mal aiguillée renvoie quand même quelque chose de plausible. La mesure m’a donné un bug plutôt qu’un score.
Je tiendrais la documentation au rythme du code, ou pas du tout. Le document d’architecture décrit encore une interface qui n’existe plus. Une documentation en retard est pire qu’une documentation absente, parce qu’elle est crue : quelqu’un qui reprend le projet cherchera un fichier disparu et perdra la confiance qu’il avait dans le reste. Sur un projet conçu pour être repris par d’autres, c’est l’incohérence la plus coûteuse, et la seule que personne ne signale, parce qu’elle ne casse aucun test.
Je mesurerais l’usage avant de construire. L’indicateur de gain métier aurait dû être posé au début, quand il était encore possible de chronométrer une demande traitée à l’ancienne. Une fois l’assistant en place, la référence a disparu.
Preuves
arbitrage documentéLe CMS indique lui-même au moteur de recherche quelle zone de page contient le contenu utile, au lieu de sélecteurs codés en dur côté RAG.arbitrage documentéRecherche hybride lexicale et sémantique adoptée après l’échec d’une approche purement sémantique, avec des pondérations différentes selon qu’on cherche une personne ou une procédure.contrainte tenueTraitement entièrement sur les serveurs de l’établissement, aucun service externe dans la pile, budget nul.artefact reproductibleIngestion idempotente et incrémentale : réindexer ne duplique rien, et les pages supprimées de la source disparaissent de l’index.artefact reproductibleExplorateur web de ce qui est réellement indexé, avec filtres, recherche et export. Il permet de vérifier l’ingestion sans interroger la base à la main.Chiffres et méthode
10 chiffres et la façon dont chacun a été mesuré
- Code de la plateforme
- 3 232lignes de PythonproductionDécompte des modules livrés, hors dépendances : couche d’adaptateurs par site, moteur générique d’ingestion et de récupération, API, scripts d’évaluation.
- Briques logicielles qui font tourner l’assistant
- 14briquesproductionL’assistant n’est pas un programme mais quatorze : la base qui stocke les documents, le modèle d’IA qui rédige, le cache qui accélère, la supervision qui surveille, et tout ce qui les relie. Chacun tourne dans sa propre boîte, sur les serveurs de l’établissement.Décompte des services déclarés dans la composition Docker de production, dont deux services ponctuels activés à la demande.
- Questions du jeu d’évaluation
- 88questionsjeu de testJeu versionné dans le dépôt, une ligne par question avec sa réponse attendue et la source où elle doit se trouver.Le jeu contient des données d’annuaire réelles : aucune question ni réponse attendue n’est reproductible ici.
- Contrôles négatifs dans le jeu d’évaluation
- 3questionsjeu de testQuestions dont la source attendue est « aucune » : elles vérifient que le système répond qu’il ne sait pas, au lieu d’inventer.
- Questions dont le contexte récupéré contenait la réponse, au départ
- 27%jeu de testPremière mesure du harnais d’évaluation, sur une récupération purement sémantique, sur le jeu de questions versionné dans le dépôt. Un juge automatique lit le contexte récupéré et tranche en binaire : la réponse attendue s’y trouve, ou elle ne s’y trouve pas.Le résultat du run n’a pas été conservé : le chiffre est consigné dans le dossier d’architecture du projet, pas dans un fichier rejouable.
- Questions dont l’assistant retrouve la bonne source
- 86%jeu de testL’assistant retrouve le bon document neuf fois sur dix, contre une fois sur quatre au début du projet.Même jeu de questions, même harnais, même juge automatique, après remplacement de la recherche purement sémantique par une combinaison de recherche par mots-clés et par sens, et changement du modèle d’embeddings. Relevé début juin 2026. C’est cette progression qui a piloté les itérations du pipeline.Même réserve que la mesure de départ : le résultat du run n’a pas été conservé, et la comparaison repose donc sur deux chiffres rapportés, pas sur deux fichiers rejouables.
- Profils de contacts indexés
- 300profils environproductionVolume que j’ai relevé sur l’index de l’intranet de l’établissement. Ces profils suivent la route annuaire du pipeline, avec son index et ses pondérations propres.Ordre de grandeur donné de mémoire, sans requête de comptage ni date de relevé : ce n’est pas une mesure, et la page ne s’en sert que pour situer l’échelle.
- Pages et documents indexés
- 700pages environproductionVolume que j’ai relevé sur l’index de l’intranet de l’établissement. Pages de contenu et documents joints, qui suivent la route contenu. Le second site servi par la plateforme ne porte que des pages.Même réserve que pour les profils : ordre de grandeur donné de mémoire, sans requête de comptage ni date de relevé.
- Justesse du routage
- 96,47% des questionsjeu de test · calculéLe routage est une fonction de texte pur : il ne consulte ni la base vectorielle ni un modèle de langage. Il a donc été extrait du module de production par analyse syntaxique, donc le code réel et non une réimplémentation, puis appliqué aux 85 questions positives du jeu. La route attendue est dérivée de la source attendue consignée dans le jeu : si la réponse vit dans un profil d’annuaire, la route annuaire aurait dû se déclencher.C’est moi qui ai dérivé cette vérité terrain, elle n’a pas été annotée à la main par le projet : c’est une hypothèse explicite, pas une mesure du projet. Le taux est en outre dominé par la route pages, qui pèse 80 des 85 questions, et masque donc l’essentiel des erreurs sur l’annuaire.
- Questions aiguillées vers l’annuaire
- 4questions sur 88jeu de test · calculéComptage des questions du jeu que le routage de production envoie vers la route annuaire, obtenu en appliquant ce routage aux 88 questions.Effectif trop faible pour qu’un score de recherche par route soit interprétable : une question y pèserait 25 points.