Annexe · Environ 5 min
Pour les développeurs : le RAG que j’utilise, et les comparaisons
La recherche hybride derrière mon propre second cerveau (mots, sens et liens), étape par étape, avec ses mesures et sa comparaison avec les approches habituelles.
Cette page s’adresse à celles et ceux qui écrivent du code. Elle décrit la recherche sur laquelle tourne mon propre second cerveau chaque fois que Claude doit retrouver quelque chose : ce qu’elle fait, les choix que j’ai faits, ce que j’ai mesuré, et où elle se situe face aux approches habituelles. Pour la théorie du RAG elle-même, voir le module RAG agentique du cours agents.
Le principe : les fichiers restent la vérité
La base de données est dérivée des fichiers Markdown, jamais éditée. Obsidian, les scripts et git lisent les fichiers ; pour changer le cerveau, on change un fichier. À chaque recherche, le moteur compare la taille et la date des fichiers, recharge ce qui a changé, et ne recalcule les vecteurs que des passages modifiés (chaque vecteur est rangé sous l’empreinte de son texte). Une page écrite il y a une minute est trouvée. Perdre la base coûte une reconstruction, pas des données.
La chaîne, étape par étape
- Découper en passages. Un document est coupé là où il change de sujet : à ses intertitres, aux déclarations de code, sinon aux lignes vides quand un passage est assez long. Chaque passage garde la suite d’intertitres au-dessus de lui, parce qu’un passage dit rarement de quoi il parle ; ses intertitres, si.
- Les mots : BM25, calculé en SQL. Les termes sont normalisés (accents retirés, identifiants comme
camelCasedécoupés, mots vides écartés, mots tronqués à cinq lettres en guise de racinisation bon marché). BM25 (k1 = 1,2) est calculé dans Postgres, par document et par passage. - Le sens : des embeddings en local. Chaque passage est vectorisé avec
bge-m3(1 024 dimensions), à partir de son titre, de ses intertitres et de son texte. Le modèle tourne dans Ollama, dans son propre conteneur, joignable sur la seule boucle locale. Les vecteurs vivent dans Postgres avecpgvector; les passages les plus proches sont trouvés par distance cosinus. - Les liens : le graphe du module 1. Les wikiliens, les liens Markdown relatifs et les
references:de l’en-tête forment un graphe. Les scores se propagent des correspondances vers leurs voisines par une marche aléatoire avec retour au départ, amortie sur les hubs pour qu’une page liée de partout ne gagne pas toutes les requêtes. Une note voisine d’une correspondance remonte même si elle ne contient aucun mot de la requête. - La fusion : deux places pour le sens, une pour les mots. Les deux classements sont entrelacés, deux résultats sémantiques puis un lexical, au lieu de la fusion par rang réciproque habituelle. Le tiers lexical garde les noms propres et les identifiants que les vecteurs brouillent.
- Plusieurs requêtes, écrites par le modèle. Claude envoie trois ou quatre requêtes à la fois : la question, sa traduction, et les mots que les documents emploient probablement. C’est la façon la moins chère de couvrir les paraphrases, même avec des vecteurs.
- Un budget de tokens, pas un nombre de morceaux. Le budget va d’abord au meilleur passage de chaque document, dans l’ordre, puis aux passages suivants. Chaque ligne garde son numéro dans le fichier : chaque réponse cite
chemin:ligneet se vérifie en un clic. - Des signaux d’absence. Chaque recherche renvoie la part de la requête couverte par le meilleur passage (mots) et la meilleure similarité cosinus (sens). Sur 44 cas tirés de mon cerveau, les 7 questions sans réponse donnaient entre 0,37 et 0,51, et presque toutes les autres plus de 0,52 : en dessous, la réponse est probablement absente. C’est un indice ; c’est le texte des passages qui décide.
Ce que j’ai mesuré
- Sur 16 questions paraphrasées, le rappel est de 88 % avec 8 documents et un budget d’environ 750 tokens, contre 56 % avec 3 documents et un budget quatre fois plus petit. Trop serrer le budget coûte plus en réponses manquées qu’il n’économise.
- Une commande
evalnote un jeu de tests sans modèle : rappel, précision, présence d’une citation donnée dans le contexte, et signalement des réponses absentes. La règle : écrire les cas de test avant de regarder les résultats, sinon on mesure ce que le moteur sait déjà bien faire.
Ce que ça coûte à faire tourner
- Deux conteneurs Docker (Postgres + pgvector, Ollama). Aucun port publié, rien ne quitte la machine. Le texte et les vecteurs sont stockés en clair dans des volumes locaux.
- Une surveillance : chaque session commence par un contrôle de santé. Si un conteneur est arrêté, la recherche se replie sur les mots seuls, et le dit. Par deux fois, des parties de cette chaîne sont tombées en panne sans bruit pendant des mois, avant que j’ajoute ce contrôle.
- Une version d’Ollama figée : dans une version,
bge-m3renvoyaitNaNsur le GPU dès qu’un texte dépassait une centaine de tokens. Des embeddings qui échouent en silence, c’est le bug le plus difficile à voir. - Un repli sans dépendance : le même moteur sans la base, mots et liens seulement, qui lit le dossier directement.
Les comparaisons
| Approche | Force | Faiblesse | Face à cette méthode |
|---|---|---|---|
| Lire tout l’index (le wiki de Karpathy) | Aucune infrastructure, simple | Le coût grandit à chaque note ; aucune paraphrase | Très bien jusqu’à quelques centaines de notes, ensuite le routage et la recherche prennent le relais |
| Tout dans une longue fenêtre de contexte | Aucune recherche qui peut se tromper | Payé à chaque question ; la qualité baisse avec la longueur | Sur mon cerveau, retirer le wiki complet d’une consigne l’a fait passer d’environ 235 000 à 16 000 tokens |
grep | Exact, instantané | Déverse les lignes trouvées dans le contexte ; aucun synonyme | J’utilise un index de termes qui ne renvoie que des noms de notes, puis une recherche ciblée |
| Vecteurs seuls, morceaux de taille fixe (le tutoriel habituel) | Trouve les paraphrases | Brouille les noms et les codes ; coupes arbitraires ; pas de citation à la ligne | Coupe aux intertitres, garde un tiers lexical, cite les lignes |
| Hybride avec fusion par rang réciproque (RRF) | La façon standard de fusionner des classements | Une constante de plus à régler | J’entrelace à 2 pour 1 parce que c’est simple et que ça garde les identifiants. Je ne l’ai pas mesuré contre RRF. |
| Façon GraphRAG (un modèle extrait les entités et résume les communautés) | Répond aux questions larges sur tout un corpus | Indexation coûteuse, faite par un modèle ; le graphe dérive | Réutilise les liens déjà présents dans les notes : aucun modèle à l’indexation |
| Base vectorielle hébergée (Pinecone, Supabase…) | Rien à faire tourner | Les notes personnelles quittent la machine ; une dépendance | Tout en local, au prix de deux conteneurs à entretenir |
Les mots pour les noms, le sens pour les paraphrases, les liens pour le contexte, les fichiers pour la vérité.
Si vous construisez le vôtre
- Commencez sans base : fichiers, carte des thèmes, liens. Ajoutez une recherche quand Claude commence à rater des notes que vous savez exister.
- Écrivez d’abord votre jeu de tests, avec des questions qui n’ont pas de réponse.
- Ajoutez BM25 avant les vecteurs ; ajoutez les vecteurs quand le jeu de tests montre que les paraphrases échouent.
- Surveillez dès le premier jour : un conteneur arrêté ressemble exactement à une recherche qui n’a rien trouvé.