Petit projet autonome montrant un pipeline RAG complet : documents → chunks → embeddings (Voyage AI) → recherche vectorielle → réponse Claude, avec une chatbox HTML simple.
docs/ → vos fichiers texte (.txt) à indexer
src/chunk.ts → découpage des documents en chunks avec chevauchement
src/embed.ts → appel à l'API d'embeddings Voyage AI
src/store.ts → store vectoriel minimal (JSON + similarité cosinus)
src/ingest.ts → script CLI d'indexation (npm run ingest)
src/server.ts → serveur Express : endpoint /api/chat avec retrieval + streaming Claude
public/index.html→ chatbox front (pas de build, JS pur)
npm install
cp .env.example .env
# remplissez ANTHROPIC_API_KEY et VOYAGE_API_KEY dans .env- Clé Anthropic : https://console.anthropic.com
- Clé Voyage AI (gratuite pour tester) : https://voyageai.com
- Mettez vos documents texte dans
docs/(deux exemples fournis sur Betaflight et les shaders). - Indexez-les :
Cela génère
npm run ingest
vector-store.jsonà la racine (à ne pas commiter si vos docs sont sensibles — ajoutez-le à.gitignore). - Lancez le serveur :
npm run dev
- Ouvrez http://localhost:3000 et posez vos questions.
RAG = Retrieval-Augmented Generation ("génération augmentée par la recherche").
Le problème qu'il résout : un LLM comme Claude ne connaît pas vos documents privés (vos PDF, votre wiki interne, votre code...). Deux solutions existent pour lui donner cette connaissance :
- Le fine-tuning — réentraîner le modèle sur vos données. Coûteux, lent, à refaire à chaque mise à jour des données.
- Le RAG — à chaque question, aller chercher automatiquement les passages pertinents dans vos documents, et les glisser dans le prompt envoyé au modèle. Le modèle "lit" ces extraits juste avant de répondre, comme si vous les aviez collés vous-même dans la conversation.
Ce projet implémente la solution 2, à la main, sans framework (pas de LangChain, pas de LlamaIndex) — pour bien voir chaque brique.
Le principe central : on ne peut pas mettre tous vos documents dans le prompt (trop de texte, trop cher, trop lent). Il faut donc un mécanisme de recherche qui ne retient que les quelques passages utiles à cette question précise. C'est ce que fait le pipeline ci-dessous.
Le pipeline se déroule en trois temps, deux offline et un en direct :
| Phase | Quand ? | Ce qui se passe |
|---|---|---|
| 1. Indexation | Une fois, en amont (npm run ingest) |
Vos documents sont découpés et transformés en vecteurs, puis stockés |
| 2. Recherche | À chaque question posée | La question est transformée en vecteur, comparée à ceux des documents pour trouver les plus proches |
| 3. Génération | Juste après | Les extraits trouvés sont envoyés à Claude, qui rédige la réponse en s'appuyant dessus |
Fichiers concernés : src/ingest.ts, src/chunk.ts, src/embed.ts, src/store.ts
a. Chunking (découpage) Un document entier est trop long et trop peu ciblé pour être comparé efficacement à une question. On le découpe donc en petits segments de quelques centaines de tokens ("chunks"), avec un léger chevauchement entre segments voisins pour éviter de couper une idée en plein milieu.
b. Embeddings (vectorisation) Chaque chunk est envoyé à un modèle d'embeddings — ici Voyage AI, le partenaire recommandé par Anthropic — qui le transforme en un vecteur numérique (une liste de nombres). L'idée clé : deux textes de sens proche produisent des vecteurs numériquement proches. C'est ce qui permettra plus tard de "chercher par le sens" plutôt que par mot-clé exact.
c. Stockage vectoriel Ces vecteurs (accompagnés du texte d'origine et de sa source) sont sauvegardés dans une base vectorielle. Ici, par souci de simplicité pédagogique, c'est un simple fichier JSON avec une similarité cosinus calculée à la main (src/store.ts). En production, on utiliserait plutôt une vraie base vectorielle : pgvector (si vous êtes déjà sur Postgres), Qdrant, Chroma, Pinecone ou Weaviate.
Fichiers concernés : src/server.ts, src/embed.ts, src/store.ts
Quand l'utilisateur pose une question dans la chatbox :
- La question elle-même est transformée en vecteur, avec le même modèle d'embeddings qu'à l'indexation.
- Ce vecteur est comparé à tous ceux stockés en Phase 1, pour retrouver les chunks dont le sens est le plus proche de la question — c'est la recherche sémantique (par opposition à une recherche par mot-clé classique).
- On garde les meilleurs résultats (les 4 premiers, ici).
Optionnellement, dans un système plus avancé, on ajouterait un reranking : un second modèle, plus précis mais plus lent, qui réordonne ces résultats avant de les transmettre — utile quand la recherche vectorielle seule manque de finesse.
Fichier concerné : src/server.ts
Les extraits retrouvés sont insérés dans le prompt système envoyé à Claude via l'API /v1/messages, avec une instruction claire : répondre uniquement à partir de ces extraits. Claude génère alors sa réponse en streaming, en s'appuyant sur ce contexte plutôt que sur sa seule mémoire d'entraînement.
Pour aller plus loin, l'API Claude propose aussi un mécanisme de citations : chaque affirmation de la réponse peut être reliée précisément à l'extrait source qui la justifie — utile quand la vérifiabilité compte (support client, domaine juridique, médical...).
┌─────────── PHASE 1 : INDEXATION (une fois) ───────────┐
docs/*.txt ──► chunking ──► embeddings (Voyage AI) ──► vector-store.json
└─────────────────────────────────────────────────────┘
┌──────────── PHASE 2 : RECHERCHE (par question) ────────────┐
question ──► embedding de la question ──► comparaison vectorielle ──► top 4 chunks
└──────────────────────────────────────────────────────────┘
┌──────────── PHASE 3 : GÉNÉRATION ────────────┐
top 4 chunks + question ──► prompt Claude ──► réponse streamée
└──────────────────────────────────────────────┘
Ces techniques ne sont pas implémentées dans ce projet (volontairement simplifié), mais elles sont mentionnées dans la section TODO comme pistes d'amélioration :
- Contextual Retrieval — technique d'Anthropic qui fait générer par un LLM un court résumé de contexte pour chaque chunk avant de l'embedder, afin d'améliorer la pertinence de la recherche.
- BM25 — algorithme de recherche lexicale classique (une évolution du TF-IDF), basé sur les mots-clés plutôt que le sens. Combiné à la recherche vectorielle, on parle de recherche hybride.
- Reranking — passage des résultats de recherche dans un second modèle, plus coûteux mais plus précis, pour affiner leur ordre avant de les envoyer au LLM.
- Prompt caching — mise en cache côté API d'Anthropic du system prompt (ou d'une partie du contexte) quand il est réutilisé sur plusieurs requêtes, pour réduire coût et latence.
Le code ne publie rien de lui-même, mais il fait transiter vos documents vers plusieurs endroits — chacun avec un niveau de risque différent.
- Voyage AI (à l'indexation) — chaque chunk est envoyé à
api.voyageai.compour être vectorisé (src/embed.ts:21). - Anthropic (à chaque question) — seuls les 4 extraits les plus pertinents pour la question posée sont envoyés, pas le document entier (src/server.ts:44).
- Local, en clair — le texte intégral de tous vos chunks (pas juste des vecteurs) est écrit dans
vector-store.jsonà la racine du projet (src/store.ts).
- Git / GitHub — le risque le plus immédiat. Si
docs/ouvector-store.jsonsont commités puis le repo poussé sur un remote (public, ou même privé mais partagé), le contenu confidentiel devient accessible à quiconque a accès au repo — indépendamment de toute politique d'un fournisseur d'IA. Le README:36 recommande déjà d'exclurevector-store.json, maisdocs/lui-même n'est pas dans.gitignorepar défaut : à ajouter si vos fichiers sources sont sensibles. vector-store.jsonen clair sur disque. Ce fichier n'est pas "juste des nombres anonymes" : il contient le texte original de chaque chunk. C'est donc une copie quasi complète de vos documents, en clair, que n'importe qui ayant accès au poste ou au serveur peut lire directement.- Absence d'authentification sur le serveur. src/server.ts n'a aucun contrôle d'accès sur
/api/chat. Si ce serveur est exposé au-delà delocalhost(déployé sur une machine accessible depuis l'extérieur, par exemple), n'importe qui peut l'interroger et faire ressortir le contenu confidentiel via des questions ciblées. - Fournisseurs tiers (Voyage AI, Anthropic). Chacun a sa propre politique de conservation et d'utilisation des données, distincte de ce que fait ce code :
- Anthropic : sur un compte API commercial standard, les entrées/sorties ne sont en principe pas utilisées pour entraîner les modèles, mais peuvent être conservées un temps limité à des fins de sécurité/abus. C'est une politique commerciale d'Anthropic, pas une garantie de ce projet — vérifiez les conditions actuelles sur la Console Anthropic si les documents sont réellement sensibles.
- Voyage AI : société tierce (partenaire d'Anthropic, mais distincte), avec ses propres conditions d'utilisation à vérifier séparément.
- Ajoutez
docs/à.gitignore(en plus devector-store.json), ou utilisez un dossier hors du repo. - Ne déployez jamais ce serveur tel quel, sans authentification, sur une adresse accessible publiquement.
- Vérifiez les conditions d'utilisation actuelles de Voyage AI et d'Anthropic si un cadre réglementaire s'applique (RGPD, secret professionnel, secret industriel...).
- Envisagez un chiffrement au repos pour
vector-store.jsonsi le poste ou serveur qui l'héberge n'est pas lui-même de confiance.
- OpenAI text-embedding-3-small / -large — simple, bon rapport qualité/prix, très répandu
- Cohere embed-v4 — bon support multilingue, a aussi un mode "compressé" (int8/binary) pour réduire le stockage
- Google gemini-embedding-001 — via l'API Gemini
- Mistral mistral-embed — si vous voulez rester sur un fournisseur européen
- Ollama avec nomic-embed-text ou mxbai-embed-large — tourne en local, zéro coût par requête, mais latence CPU/GPU dépendante
- Transformers.js (@xenova/transformers) avec un modèle type bge-small-en — embeddings 100% en Node, sans serveur externe
Le modèle d'embeddings (voyage-3.5) et Claude sont tous deux nativement multilingues, donc le cas simple — documents et questions dans plusieurs langues — fonctionne déjà en grande partie sans rien changer : une question en français peut retrouver un chunk en anglais par proximité sémantique, et Claude répond naturellement dans la langue de la question.
Deux points à surveiller si votre corpus n'est pas monolingue :
- Langue de la réponse non garantie : Claude peut parfois répondre dans la langue du document source retrouvé plutôt que dans celle de la question de l'utilisateur. Pour forcer la langue de réponse, ajoutez une consigne explicite dans le
systemPrompt(src/server.ts:39), du type "réponds toujours dans la langue de la question de l'utilisateur". - Précision de la recherche cross-langue : la recherche vectorielle entre langues très différentes (ex. français ↔ japonais) est mécaniquement moins précise qu'une recherche même-langue. Si votre corpus mélange ce type de langues en grand volume, envisagez de stocker un champ
langpar chunk dans le store (src/store.ts) et de filtrer/booster la recherche par langue, ou de traduire la question vers la langue dominante du corpus avant de l'embedder.
- Base vectorielle en production : remplacez
src/store.tspar pgvector (si vous êtes déjà sur Postgres), Qdrant ou Chroma. L'interfacesearch()/addAll()reste la même, seule l'implémentation change. - Meilleure précision de recherche : combinez la recherche vectorielle avec du BM25 (recherche lexicale) et un reranking — c'est ce qu'Anthropic appelle la "Contextual Retrieval". Utile si vous avez beaucoup de documents ou du vocabulaire très technique/spécifique.
- PDF/Word/HTML en entrée : ajoutez un parseur (ex:
pdf-parse,mammothpour du .docx) avant le chunking — le reste du pipeline ne change pas. - Citations vérifiables : l'API Claude supporte un mécanisme de citations sur les documents fournis en contexte, utile si vous voulez que chaque affirmation soit reliée à un extrait source précis.
- Images/vidéos en pièce jointe : associez chaque média à sa description/texte environnant à l'ingestion, stockez son chemin en métadonnée par chunk, puis affichez-le côté front quand son chunk source ressort dans les résultats — Claude ne génère pas de médias, il peut seulement les analyser (vision) ou les citer.
- Coût/latence : pensez au prompt caching côté API si votre system prompt (ou une partie de votre contexte) est réutilisé sur plusieurs requêtes — ça réduit fortement coût et latence.