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