RagEmbeddingsLlmVector-StoreRetrieval-Augmented-GenerationOpenaiPgvectorPineconeChunkingRecherche-SemantiqueIa-GenerativeLangchainKnowledge-Base
Comprenez et implémentez le RAG pour enrichir vos LLM avec vos données : pipeline d'embeddings, vector stores, retrieval et intégration dans vos apps.
Qu'est-ce que le RAG — architecture complète
RAG (Retrieval-Augmented Generation) enrichit les réponses d'un LLM avec des données externes récupérées dynamiquement. Sans RAG, le modèle est limité à ses données d'entraînement — il hallucine sur des faits récents ou spécifiques à votre domaine.
Flux RAG complet :
- Indexation offline — documents découpés en chunks → embeddings → stockés dans le vector store
- Requête utilisateur → embedding de la question → recherche par similarité dans le vector store
- Reranking — les top-K résultats sont reclassés par pertinence fine
- Génération — question + contexte récupéré → prompt → LLM → réponse avec sources citées
| Approche | Forces | Limites | Cas d'usage |
|---|---|---|---|
| LLM seul | Simple, pas d'infra | Hallucinations, données périmées | Chat général, créativité |
| Fine-tuning | Modèle spécialisé, rapide | Coûteux, pas de MAJ temps réel | Style d'écriture, format fixe |
| RAG | Précis, sourcé, données fraîches | Latence, complexité infra | FAQ, documentation, support |
| RAG + Fine-tuning | Meilleure précision combinée | Coût élevé | Produits IA production critiques |
Chunking et préparation des documents
La qualité du chunking conditionne directement la qualité du RAG. Des chunks trop grands = bruit ; trop petits = perte de contexte.
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';
import { DirectoryLoader } from 'langchain/document_loaders/fs/directory';
// Charger des documents depuis un dossier
const loader = new DirectoryLoader('./docs', {
'.pdf': (path) => new PDFLoader(path),
'.txt': (path) => new TextLoader(path),
'.md': (path) => new TextLoader(path),
});
const rawDocs = await loader.load();
// Découpage récursif — respecte les structures naturelles (paragraphes, phrases)
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 800, // Taille max d'un chunk en caractères
chunkOverlap: 100, // Chevauchement pour préserver le contexte entre chunks
separators: ['\n\n', '\n', '. ', ' ', ''], // Ordre de préférence des séparateurs
});
const chunks = await splitter.splitDocuments(rawDocs);
// Enrichir les métadonnées pour le filtrage
const enrichedChunks = chunks.map((chunk, i) => ({
...chunk,
metadata: {
...chunk.metadata,
chunkIndex: i,
processedAt: new Date().toISOString(),
wordCount: chunk.pageContent.split(' ').length,
}
}));
console.log(`${rawDocs.length} documents → ${enrichedChunks.length} chunks`);
Règles pratiques : Pour de la documentation technique : chunk de 500-800 caractères avec overlap 100-150. Pour des textes narratifs : chunk de 1000-1500 avec overlap 200. Toujours inclure des métadonnées (
source, page, date) pour pouvoir filtrer et citer.
Embeddings et modèles disponibles
| Modèle | Dimensions | Prix (1M tokens) | Usage recommandé |
|---|---|---|---|
OpenAI text-embedding-3-small | 1536 | $0.02 | Production, bon rapport qualité/prix |
OpenAI text-embedding-3-large | 3072 | $0.13 | Haute précision, contenu complexe |
Cohere embed-v3 | 1024 | $0.10 | Multilingue, performant en français |
Mistral mistral-embed | 1024 | $0.10 | Souveraineté européenne, français |
Local nomic-embed-text | 768 | Gratuit | Dev/test, données sensibles |
import { OpenAIEmbeddings } from '@langchain/openai';
import { CohereEmbeddings } from '@langchain/cohere';
// OpenAI embeddings (recommandé en prod)
const openaiEmbeddings = new OpenAIEmbeddings({
model: 'text-embedding-3-small',
dimensions: 1536, // Peut être réduit pour les petits projets (ex: 512)
});
// Cohere — meilleur pour le français
const cohereEmbeddings = new CohereEmbeddings({
model: 'embed-multilingual-v3.0',
inputType: 'search_document', // 'search_query' pour les requêtes
});
// Test manuel d'un embedding
const vector = await openaiEmbeddings.embedQuery('Angular Signal Inputs');
console.log(`Dimensions: ${vector.length}`); // 1536
console.log(`Sample: [${vector.slice(0, 4).map(v => v.toFixed(4)).join(', ')}...]`);
Vector Store — choix et configuration
| Solution | Type | Scalabilité | Idéal pour |
|---|---|---|---|
| Chroma | Open source, local | Moyenne | Dev, prototypes, petits projets |
| Pinecone | Cloud managé | Très haute | Production, millions de vecteurs |
| pgvector | Extension PostgreSQL | Haute | Apps existantes sur PostgreSQL |
| Qdrant | Open source, self-hosted | Très haute | Données sensibles, self-hosted |
| Supabase | PostgreSQL + pgvector managé | Haute | Stack moderne tout-en-un |
import { Chroma } from '@langchain/community/vectorstores/chroma';
import { OpenAIEmbeddings } from '@langchain/openai';
import { Document } from '@langchain/core/documents';
const embeddings = new OpenAIEmbeddings({ model: 'text-embedding-3-small' });
// Créer le vector store depuis les chunks
const vectorStore = await Chroma.fromDocuments(enrichedChunks, embeddings, {
collectionName: 'angular-docs',
url: 'http://localhost:8000', // Chroma local
});
// Avec Pinecone pour la production
import { PineconeStore } from '@langchain/pinecone';
import { Pinecone } from '@pinecone-database/pinecone';
const pinecone = new Pinecone({ apiKey: process.env.PINECONE_API_KEY! });
const index = pinecone.index('angular-docs');
const pineconeStore = await PineconeStore.fromDocuments(enrichedChunks, embeddings, {
pineconeIndex: index,
namespace: 'production-v1',
textKey: 'text',
});
Recherche sémantique et hybride
// Recherche sémantique de base
const semanticResults = await vectorStore.similaritySearch(
'Comment créer un service injectable Angular ?',
5 // top-5 chunks
);
// Recherche avec filtres métadonnées
const filteredResults = await vectorStore.similaritySearch(
'Angular signals',
3,
{ source: 'angular-docs.pdf' } // Filtrer par source
);
// Recherche avec scores
const withScores = await vectorStore.similaritySearchWithScore('Angular 19', 5);
withScores.forEach(([doc, score]) => {
console.log(`Score: ${(1 - score).toFixed(3)} | ${doc.pageContent.substring(0, 80)}...`);
});
// Hybrid search — combiner vecteurs + BM25 (mots-clés)
// LangChain EnsembleRetriever
import { EnsembleRetriever } from 'langchain/retrievers/ensemble';
import { BM25Retriever } from '@langchain/community/retrievers/bm25';
const vectorRetriever = vectorStore.asRetriever({ k: 5 });
const bm25Retriever = BM25Retriever.fromDocuments(enrichedChunks, { k: 5 });
// Fusion des deux approches avec pondération
const ensembleRetriever = new EnsembleRetriever({
retrievers: [vectorRetriever, bm25Retriever],
weights: [0.7, 0.3], // 70% sémantique, 30% mots-clés
});
Génération avec contexte et citations
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
async function answerWithRAG(question: string, vectorStore: any): Promise<{
answer: string;
sources: string[];
}> {
// 1. Rechercher les chunks pertinents
const results = await vectorStore.similaritySearchWithScore(question, 4);
const relevantChunks = results.filter(([, score]) => score < 0.3); // Seuil de pertinence
if (relevantChunks.length === 0) {
return { answer: 'Je ne trouve pas d\'information sur ce sujet dans ma base de connaissances.', sources: [] };
}
// 2. Construire le contexte avec sources
const contextWithSources = relevantChunks.map(([doc], i) =>
`[SOURCE ${i + 1}] ${doc.metadata.source} :\n${doc.pageContent}`
).join('\n\n---\n\n');
const sources = [...new Set(relevantChunks.map(([doc]) => doc.metadata.source))];
// 3. Prompt avec instructions de citation
const response = await client.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
system: `Tu es un assistant expert. Réponds UNIQUEMENT en te basant sur les sources fournies.
Si l'information n'est pas dans les sources, dis-le clairement.
Cite les sources avec [SOURCE N] dans ta réponse.`,
messages: [{
role: 'user',
content: `Sources :\n${contextWithSources}\n\nQuestion : ${question}`
}]
});
return {
answer: response.content[0].type === 'text' ? response.content[0].text : '',
sources,
};
}
Pipeline RAG LangChain.js complet
import { ChatAnthropic } from '@langchain/anthropic';
import { createRetrievalChain } from 'langchain/chains/retrieval';
import { createStuffDocumentsChain } from 'langchain/chains/combine_documents';
import { ChatPromptTemplate } from '@langchain/core/prompts';
// Modèle LLM
const model = new ChatAnthropic({ model: 'claude-sonnet-4-6', maxTokens: 1024 });
// Prompt template avec contexte
const prompt = ChatPromptTemplate.fromTemplate(`
Tu es un assistant expert. Réponds à la question en utilisant uniquement le contexte fourni.
Si la réponse n'est pas dans le contexte, dis que tu ne sais pas.
Contexte :
{context}
Question : {input}
`);
// Chain de combination de documents
const documentChain = await createStuffDocumentsChain({ llm: model, prompt });
// Retriever depuis le vector store
const retriever = vectorStore.asRetriever({
k: 4,
searchType: 'similarity', // 'mmr' pour la diversité
});
// Pipeline RAG complet
const retrievalChain = await createRetrievalChain({
retriever,
combineDocsChain: documentChain,
});
// Invocation
const result = await retrievalChain.invoke({
input: 'Comment utiliser les linkedSignals dans Angular 19 ?',
});
console.log(result.answer);
console.log('Sources:', result.context.map(doc => doc.metadata.source));
RAG avancé : re-ranking et query expansion
// Re-ranking avec Cohere Rerank
import { CohereRerank } from '@langchain/cohere';
import { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression';
const cohereRerank = new CohereRerank({
apiKey: process.env.COHERE_API_KEY,
topN: 3, // Garder les 3 meilleurs après re-ranking
model: 'rerank-v3.5',
});
// Compression + re-ranking en une étape
const rerankedRetriever = new ContextualCompressionRetriever({
baseCompressor: cohereRerank,
baseRetriever: vectorStore.asRetriever({ k: 10 }), // Récupérer plus, re-ranker ensuite
});
// Query expansion — générer plusieurs formulations de la question
async function expandQuery(question: string): Promise<string[]> {
const response = await client.messages.create({
model: 'claude-haiku-4-5-20251001',
max_tokens: 200,
messages: [{
role: 'user',
content: `Génère 3 formulations alternatives de cette question pour améliorer la recherche.
Retourne uniquement les 3 questions, une par ligne.
Question originale: ${question}`
}]
});
const text = response.content[0].type === 'text' ? response.content[0].text : '';
return [question, ...text.split('\n').filter(q => q.trim())];
}
Évaluation et métriques de qualité
// Évaluation RAG avec RAGAS (Retrieval Augmented Generation Assessment)
// npm install ragas
// Métriques clés à mesurer :
// - Faithfulness : la réponse est-elle fidèle aux chunks récupérés ?
// - Answer Relevancy : la réponse répond-elle à la question ?
// - Context Recall : tous les faits nécessaires sont-ils dans les chunks ?
// - Context Precision : les chunks récupérés sont-ils pertinents ?
// Test simple sans librairie
async function evaluateRAG(testCases: Array<{question: string; expectedAnswer: string}>) {
let correctCount = 0;
for (const testCase of testCases) {
const { answer } = await answerWithRAG(testCase.question, vectorStore);
// Évaluation avec LLM juge
const evaluation = await client.messages.create({
model: 'claude-opus-4-6',
max_tokens: 100,
messages: [{
role: 'user',
content: `La réponse générée correspond-elle à la réponse attendue ?
Réponse attendue: ${testCase.expectedAnswer}
Réponse générée: ${answer}
Réponds uniquement par "OUI" ou "NON".`
}]
});
const isCorrect = evaluation.content[0].type === 'text' &&
evaluation.content[0].text.includes('OUI');
if (isCorrect) correctCount++;
}
return { accuracy: correctCount / testCases.length * 100 };
}
Conclusion
Le RAG est la technique de référence pour ancrer les LLMs dans des données spécifiques à votre domaine. Les points clés :
- Chunking qualitatif (800 chars, overlap 100) — la base de tout RAG performant
text-embedding-3-smallOpenAI pour l'équilibre qualité/coût- Recherche hybride (vecteurs + BM25) pour +20-30% de précision
- Re-ranking Cohere après retrieval pour filtrer le bruit
- Citer les sources dans le prompt système — réponses traçables
- Évaluer régulièrement avec un jeu de tests connu
Questions fréquentes
Le RAG combine deux systèmes : un moteur de recherche vectorielle (retrieval) et un modèle de langage génératif (generation). Quand une question arrive, elle est convertie en vecteur via un modèle d'embedding (OpenAI text-embedding-3, Cohere, etc.), puis les documents les plus similaires sont récupérés depuis une base vectorielle (Pinecone, Weaviate, Chroma). Ces documents pertinents sont injectés dans le contexte du prompt envoyé au LLM, qui génère une réponse ancrée dans ces sources. Contrairement à un LLM seul, le RAG peut accéder à des données privées, récentes ou spécifiques à un domaine sans nécessiter de fine-tuning coûteux.
Le choix dépend du volume et de l'infrastructure : Pinecone est le leader managé, idéal pour démarrer rapidement sans gestion d'infrastructure, avec une latence faible garantie. Weaviate offre un filtrage hybride (vectoriel + keyword) et un déploiement on-premise. Chroma est parfait pour le développement local et les POC Python. Qdrant se distingue par sa haute performance sur de grands volumes avec payload filtering. Pour une intégration PostgreSQL existante, pgvector permet d'ajouter la recherche vectorielle sans nouvelle base. Le critère clé en production est le compromis entre rappel (tous les documents pertinents trouvés) et précision (qualité des résultats retournés).
Le chunking (découpage des documents) est critique : des chunks trop petits perdent le contexte, trop grands noient l'information pertinente. Les stratégies recommandées incluent le recursive character text splitting avec chevauchement (overlap de 10-20% entre chunks pour éviter les coupures de contexte), le semantic chunking qui regroupe les phrases par similarité sémantique plutôt que par nombre de caractères, et le parent-document retrieval qui stocke de petits chunks pour la recherche mais injecte les parents plus larges comme contexte. Avec LangChain ou LlamaIndex, tester différentes tailles (512, 1024, 2048 tokens) sur votre dataset spécifique reste la meilleure approche empirique.
L'évaluation RAG se décompose en deux axes : la qualité du retrieval et la qualité de la génération. Pour le retrieval, on mesure le recall@k (proportion de documents pertinents dans les k résultats retournés) et la précision@k. Pour la génération, RAGAS (framework open-source) mesure la faithfulness (la réponse est-elle fondée sur les contextes récupérés ?), l'answer relevancy (la réponse répond-elle à la question ?) et le context precision. Des outils comme TruLens ou DeepEval permettent d'automatiser ces évaluations. Un pipeline RAG performant atteint typiquement un faithfulness > 0.85 sur des datasets de questions métier bien définis.