Retour au blog
Guides

Comment construire un pipeline RAG en Python : guide technique 2026

Racine AI

Dernière mise à jour le 9 janvier 2026

Un pipeline RAG permet d’interroger vos documents internes en combinant recherche sémantique et génération par LLM. Cette architecture évite le fine-tuning coûteux tout en gardant vos connaissances à jour. Voici une implémentation complète en Python avec PostgreSQL et pgvector, testée en production sur des corpus de plus de 100 000 documents.

Architecture complète d'un pipeline RAG

Le RAG représente une alternative sérieuse au fine-tuning

Le fine-tuning d’un LLM coûte cher en compute, nécessite des données annotées de qualité et implique une maintenance lourde des différentes versions du modèle. Pire encore, il ne permet pas de mise à jour en temps réel des connaissances. Le RAG résout ces deux problèmes fondamentaux. Vous indexez vos documents dans une base vectorielle, et le LLM génère des réponses en s’appuyant sur les passages récupérés dynamiquement.

“RAG enables LLMs to access external knowledge without retraining, achieving comparable or superior performance to fine-tuning on knowledge-intensive tasks while being more cost-effective and updatable.”

— Gao et al., RAG Survey (arXiv:2506.00054)

Selon le benchmark RAG-QA de 2025, un système RAG correctement configuré atteint 89% de la performance d’un modèle fine-tuné sur des tâches de question-answering. Le tout pour un dixième du coût de développement initial.

L’architecture se décompose en deux phases distinctes

Un pipeline RAG production comprend une phase d’ingestion qui s’exécute hors ligne et une phase d’inférence qui répond aux requêtes en temps réel.

La phase d’ingestion commence par l’extraction du texte brut depuis vos fichiers PDF, DOCX ou HTML. Ce texte passe ensuite dans un module de chunking qui le découpe en segments de 512 à 1024 tokens. Chaque chunk est vectorisé par un modèle d’embedding puis stocké dans la base vectorielle avec ses métadonnées.

La phase d’inférence démarre quand un utilisateur pose une question. Cette question est elle-même vectorisée puis comparée aux chunks indexés pour trouver les k plus similaires par similarité cosinus. Un re-ranker optionnel réordonne ces résultats par pertinence fine. Enfin, le LLM synthétise une réponse cohérente à partir des passages récupérés.

Configurer PostgreSQL avec l’extension pgvector

pgvector ajoute le support natif des vecteurs et des recherches par similarité à PostgreSQL. Pour des corpus de moins de 5 millions de vecteurs, ses performances rivalisent avec Pinecone sans les coûts cloud associés.

import psycopg2
from pgvector.psycopg2 import register_vector

def setup_database():
    """Configure PostgreSQL avec l'extension pgvector."""
    conn = psycopg2.connect(
        host="localhost",
        database="rag_db",
        user="rag_user",
        password="your_secure_password"
    )

    with conn.cursor() as cur:
        cur.execute("CREATE EXTENSION IF NOT EXISTS vector")

        cur.execute("""
            CREATE TABLE IF NOT EXISTS documents (
                id SERIAL PRIMARY KEY,
                content TEXT NOT NULL,
                embedding vector(384),
                metadata JSONB,
                source_file VARCHAR(500),
                chunk_index INTEGER,
                created_at TIMESTAMP DEFAULT NOW()
            )
        """)

        cur.execute("""
            CREATE INDEX IF NOT EXISTS documents_embedding_idx
            ON documents
            USING hnsw (embedding vector_cosine_ops)
            WITH (m = 16, ef_construction = 64)
        """)

    conn.commit()
    register_vector(conn)
    return conn

Les paramètres de l’index HNSW méritent quelques explications. Le paramètre m=16 définit le nombre de connexions par noeud dans le graphe. Une valeur plus élevée améliore le recall mais consomme plus de mémoire. Le paramètre ef_construction=64 contrôle la qualité de l’index lors de sa construction.

Le chunking intelligent préserve le contexte sémantique

Le découpage du texte en chunks constitue une étape critique. Des chunks trop petits perdent le contexte nécessaire à la compréhension. Des chunks trop grands introduisent du bruit dans le retrieval et diluent l’information pertinente.

from typing import List, Dict
import re

class SemanticChunker:
    """Chunking intelligent avec préservation du contexte."""

    def __init__(
        self,
        chunk_size: int = 768,
        overlap: int = 100,
        min_chunk_size: int = 100
    ):
        self.chunk_size = chunk_size
        self.overlap = overlap
        self.min_chunk_size = min_chunk_size

    def chunk_document(self, text: str, metadata: Dict = None) -> List[Dict]:
        """Decoupe un document en chunks avec métadonnées."""
        text = re.sub(r'\n{3,}', '\n\n', text)
        text = re.sub(r' {2,}', ' ', text)

        paragraphs = text.split('\n\n')
        chunks = []
        current_chunk = ""
        current_tokens = 0

        for para in paragraphs:
            para_tokens = len(para.split())

            if current_tokens + para_tokens <= self.chunk_size:
                current_chunk += para + "\n\n"
                current_tokens += para_tokens
            else:
                if current_tokens >= self.min_chunk_size:
                    chunks.append({
                        'content': current_chunk.strip(),
                        'metadata': metadata or {},
                        'token_count': current_tokens
                    })

                overlap_text = self._get_overlap(current_chunk)
                current_chunk = overlap_text + para + "\n\n"
                current_tokens = len(current_chunk.split())

        if current_tokens >= self.min_chunk_size:
            chunks.append({
                'content': current_chunk.strip(),
                'metadata': metadata or {},
                'token_count': current_tokens
            })

        return chunks

    def _get_overlap(self, text: str) -> str:
        """Extrait les derniers tokens pour l'overlap."""
        words = text.split()
        if len(words) <= self.overlap:
            return text
        return ' '.join(words[-self.overlap:]) + ' '

Cette implémentation utilise un chunking sémantique basé sur les paragraphes avec overlap. L’overlap de 100 tokens entre chunks consécutifs assure qu’une information située à la frontière entre deux chunks ne sera pas perdue.

Sentence Transformers vectorise efficacement vos textes

Pour du contenu technique en français, le modèle paraphrase-multilingual-MiniLM-L12-v2 offre un excellent compromis entre qualité et vitesse. Pour l’anglais, all-MiniLM-L6-v2 reste la référence du domaine.

from sentence_transformers import SentenceTransformer
import numpy as np

class EmbeddingService:
    """Service de vectorisation des textes."""

    def __init__(self, model_name: str = "sentence-transformers/all-MiniLM-L6-v2"):
        self.model = SentenceTransformer(model_name)
        self.dimension = self.model.get_sentence_embedding_dimension()

    def embed_texts(self, texts: List[str], batch_size: int = 32) -> np.ndarray:
        """Vectorise une liste de textes par batch."""
        embeddings = self.model.encode(
            texts,
            batch_size=batch_size,
            show_progress_bar=True,
            convert_to_numpy=True,
            normalize_embeddings=True
        )
        return embeddings

    def embed_query(self, query: str) -> np.ndarray:
        """Vectorise une query unique."""
        return self.model.encode(
            query,
            convert_to_numpy=True,
            normalize_embeddings=True
        )

L’option normalize_embeddings=True normalise les vecteurs à une norme unitaire. Cette normalisation permet d’utiliser le produit scalaire à la place de la similarité cosinus pour les comparaisons, ce qui accélère les calculs sans changer les résultats.

Les benchmarks sur notre corpus de test révèlent des performances solides

Sur un corpus de 50 000 documents techniques mélangeant PDF, DOCX et TXT, voici les performances mesurées sur un MacBook M1 Pro.

MétriqueValeurConfiguration
Temps d’ingestion45 minutes50K documents
Throughput embedding1200 docs/minbatch_size=32
Latence retrieval p5028mspgvector HNSW, top_k=10
Latence retrieval p9967msidem
Latence re-ranking18msMiniLM cross-encoder
Recall@100.87Test set 1000 queries
Precision@50.72Après re-ranking

Le bottleneck réside clairement dans la génération LLM avec 1.2 seconde en moyenne pour gpt-4o-mini et 2.8 secondes pour gpt-4o.

La recherche hybride améliore le recall de 5 à 8%

Combiner recherche lexicale BM25 et recherche sémantique par embeddings produit de meilleurs résultats sur les corpus techniques.

cur.execute("""
    SELECT id, content,
           (0.7 * (1 - (embedding <=> %s))) +
           (0.3 * ts_rank(to_tsvector('french', content), plainto_tsquery('french', %s)))
           as hybrid_score
    FROM documents
    WHERE to_tsvector('french', content) @@ plainto_tsquery('french', %s)
       OR embedding <=> %s < 0.5
    ORDER BY hybrid_score DESC
    LIMIT %s
""", (query_emb, query, query, query_emb, top_k))

Cette requête combine un score sémantique pondéré à 70% avec un score lexical pondéré à 30%. Ces proportions fonctionnent bien pour la plupart des cas d’usage mais méritent d’être ajustées selon votre corpus.

Pour aller plus loin

Cette implémentation couvre les fondamentaux d’un RAG production-ready. Plusieurs pistes permettent d’aller plus loin.

L’Agentic RAG ajoute des agents capables de reformuler les requêtes, de choisir les sources pertinentes ou de valider les réponses. Le paper arXiv:2501.09136 détaille cette approche.

Le RAG multimodal intègre images et tableaux grâce à des VLM comme SmolVLM ou Qwen2-VL, particulièrement utile pour les documents techniques illustrés.

L’évaluation continue via des outils comme RAGAS permet de monitorer la qualité du système en production et de détecter les régressions.

Racine AI propose Pi-Search, une solution RAG déployable on-premise pour les entreprises ayant des contraintes de souveraineté sur leurs données. Contactez-nous pour une démonstration sur vos documents.

Newsletter technique

1 article par mois sur l'IA documentaire. Pas de spam.

14 - 13 =

On nous demande souvent

Quelle taille de chunk optimale pour des documents techniques de 200+ pages ?

Pour des documents techniques denses, on recommande des chunks de 512 à 1024 tokens avec un overlap de 10 à 15%. Sur nos tests avec des manuels de maintenance industriels, 768 tokens avec 100 tokens d'overlap donnent le meilleur recall@10 à 0.89.

Faut-il utiliser des embeddings multilingues si mon corpus est uniquement en français ?

Non, les modèles multilingues sacrifient de la précision pour la généralisation. Pour un corpus 100% français, CamemBERT-base ou multilingual-e5-large-instruct surpassent les modèles multilingues génériques de 3 à 5 points sur le recall.

pgvector vs Pinecone vs Qdrant, lequel choisir pour moins de 1M documents ?

pgvector sans hésiter. En dessous d'un million de vecteurs, les performances restent équivalentes avec moins de 50ms de latence p99 et vous gardez tout dans PostgreSQL.

Discutons de

Votre Projet.

IA Documents, automatisation legacy, inspection terrain. Nous deployons des solutions qui passent en production.

Decrivez votre projet et recevez une reponse sous 48h.

Nous contacter