Vous avez construit un système RAG mais les réponses de votre IA sont souvent à côté de la plaque ? Vous n'êtes pas seul. La plupart des développeurs découvrent que leur chatbot refuse obstinement de trouver les bonnes informations, même quand elles existent dans leurs documents. La solution ? Le Contextual Retrieval (检索增强上下文化), une technique qui peut transformer radicalement la pertinence de vos recherches.

Qu'est-ce que le RAG et Pourquoi Vos Recherches Échouent

Commençons par les bases, en termes simples. Imaginez que vous avez une bibliothèque géante avec des millions de livres. Le RAG (Retrieval-Augmented Generation) fonctionne comme un système de recherche dans cette bibliothèque : il coupe vos documents en petits morceaux (chunks), les transforme en nombres magiques (vecteurs), et essaie de trouver les morceaux pertinents quand vous posez une question.

Le Problème Classique

Voici ce qui se passe habituellement :

Document original :
"La société a été fondée en 2015. Elle a levé 10 millions d'euros en série A en 2018.
Le même année, Jean Martin a rejoint l'entreprise en tant que CTO."

Chunk créé automatiquement :
"Le même année, Jean Martin a rejoint l'entreprise en tant que CTO."

Résultat : Le chunk ne contient plus la référence à 2018 !
L'IA ne peut pas savoir que "le même année" fait référence à 2018.

Le contextual retrieval résout ce problème en ajoutant le contexte manquant avant d'indexer chaque chunk.

Comment Fonctionne le Contextual Retrieval

La technique est élégante dans sa simplicité : au lieu de stocker uniquement le chunk, nous générons une description contextuelle de ce chunk en utilisant l'IA. Cette description explique se trouve l'information dans le document et pourquoi elle est importante.

Architecture du Système

┌─────────────────────────────────────────────────────────────┐
│                    FLUX CONTEXTUAL RETRIEVAL                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. DOCUMENT BRUT                                           │
│     "La société a été fondée en 2015..."                    │
│                                                             │
│                    ▼                                        │
│                                                             │
│  2. DÉCOUPAGE EN CHUNKS                                     │
│     Chunk 1: "La société a été fondée en 2015"               │
│     Chunk 2: "Elle a levé 10M€ en série A en 2018"          │
│     Chunk 3: "Jean Martin a rejoint l'entreprise..."        │
│                                                             │
│                    ▼                                        │
│                                                             │
│  3. GÉNÉRATION DU CONTEXTE (via LLM)                        │
│     Pour Chunk 3: "Section Recrutement du document des      │
│     historique de l'entreprise. Cette section documente     │
│     l'arrivée de Jean Martin comme CTO en 2018..."          │
│                                                             │
│                    ▼                                        │
│                                                             │
│  4. VECTORISATION COMBINÉE                                  │
│     Embedding(Chunk 3 + Contexte) → Vecteur enrichi         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Tutoriel Pas à Pas : Implémentation Complète

Nous allons créer un système de contextual retrieval fonctionnel. Pour ce tutoriel, j'utilise HolySheep AI comme provider IA, qui offre des tarifs exceptionnels (DeepSeek V3.2 à $0.42/MToken contre $8 pour GPT-4.1) et une latence inférieure à 50ms.

Prérequis et Installation

# Installation des dépendances
pip install requests numpy scikit-learn python-dotenv

Structure du projet

mkdir rag_contextuel cd rag_contextuel touch main.py enrichisseur.py requirements.txt

Étape 1 : Configuration de la Clé API

Créez un fichier .env à la racine de votre projet :

# .env
HOLYSHEEP_API_KEY=votre_cle_api_ici
BASE_URL=https://api.holysheep.ai/v1

Étape 2 : Module d'Enrichissement Contextuel

Voici le cœur de notre système. Ce module génère le contexte pour chaque chunk :

# enrichisseur.py
import os
import requests
from dotenv import load_dotenv

load_dotenv()

class EnrichisseurContextuel:
    """Génère du contexte enrichi pour chaque chunk de document."""
    
    def __init__(self):
        self.api_key = os.getenv("HOLYSHEEP_API_KEY")
        self.base_url = os.getenv("BASE_URL", "https://api.holysheep.ai/v1")
    
    def generer_contexte(self, chunk: str, document_info: dict) -> str:
        """
        Génère une description contextuelle pour un chunk.
        
        Args:
            chunk: Le texte du chunk à enrichir
            document_info: Métadonnées sur le document source
                - titre: Titre du document
                - resume: Résumé général du document
                - position: Position estimée dans le document
                - chunks_voisins: Texte des chunks précédents/suivants
        
        Returns:
            Description contextuelle enrichie
        """
        prompt = f"""Tu es un assistant qui enrichit des fragments de documents avec leur contexte.
Pour chaque fragment, génère une description contextuelle courte (2-3 phrases) qui explique :
1. La section du document et son sujet
2. La relation avec le reste du document
3. Pourquoi cette information est pertinente

DOCUMENT SOURCE:
- Titre: {document_info.get('titre', 'Sans titre')}
- Résumé: {document_info.get('resume', 'Aucun résumé disponible')}

FRAGMENT À ENRICHIR:
\"{chunk}\"

CONTEXTE VOISIN:
{précédent: document_info.get('chunk_précédent', 'Aucun')}
{suivant: document_info.get('chunk_suivant', 'Aucun')}

RÈPONSE: Donne uniquement la description contextuelle, sans préambule."""

        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        }
        
        payload = {
            "model": "deepseek-v3.2",
            "messages": [
                {"role": "system", "content": "Tu génères des descriptions contextuelles concises et utiles."},
                {"role": "user", "content": prompt}
            ],
            "temperature": 0.3,
            "max_tokens": 150
        }
        
        response = requests.post(
            f"{self.base_url}/chat/completions",
            headers=headers,
            json=payload
        )
        
        if response.status_code != 200:
            raise Exception(f"Erreur API: {response.status_code} - {response.text}")
        
        return response.json()["choices"][0]["message"]["content"]
    
    def traiter_document(self, chunks: list, document_info: dict) -> list:
        """Traite tous les chunks d'un document et retourne des chunks enrichis."""
        chunks_enrichis = []
        
        for i, chunk in enumerate(chunks):
            info_chunk = {
                **document_info,
                'position': i + 1,
                'chunk_précédent': chunks[i-1] if i > 0 else None,
                'chunk_suivant': chunks[i+1] if i < len(chunks)-1 else None
            }
            
            contexte = self.generer_contexte(chunk, info_chunk)
            chunks_enrichis.append({
                "chunk_original": chunk,
                "contexte": contexte,
                "chunk_final": f"{chunk}\n\n[Contexte: {contexte}]"
            })
        
        return chunks_enrichis

Étape 3 : Script Principal d'Indexation et Recherche

# main.py
from enrichisseur import EnrichisseurContextuel
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np

class SystemeRAGContextuel:
    """Système RAG avec enrichment contextuel."""
    
    def __init__(self):
        self.enrichisseur = EnrichisseurContextuel()
        self.vectorizer = TfidfVectorizer(max_features=768)
        self.chunks_enrichis = []
        self.vecteurs = None
    
    def indexer_documents(self, documents: list):
        """Indexe une liste de documents avec enrichissement contextuel."""
        tous_les_chunks = []
        
        for doc in documents:
            # Découpage basique en paragraphes
            chunks_bruts = doc['contenu'].split('\n\n')
            chunks_bruts = [c.strip() for c in chunks_bruts if c.strip()]
            
            # Enrichissement contextuel
            info_document = {
                'titre': doc.get('titre', 'Document'),
                'resume': doc.get('resume', '')
            }
            
            chunks_traites = self.enrichisseur.traiter_document(
                chunks_bruts, info_document
            )
            
            self.chunks_enrichis.extend(chunks_traites)
            tous_les_chunks.extend([c['chunk_final'] for c in chunks_traites])
        
        # Vectorisation avec TF-IDF
        self.vecteurs = self.vectorizer.fit_transform(tous_les_chunks)
        print(f"✓ {len(tous_les_chunks)} chunks indexés avec succès")
    
    def rechercher(self, question: str, top_k: int = 3) -> list:
        """Recherche les chunks les plus pertinents pour une question."""
        vecteur_question = self.vectorizer.transform([question])
        similarités = cosine_similarity(vecteur_question, self.vecteurs)[0]
        
        # Récupération des top_k résultats
        indices = np.argsort(similarités)[-top_k:][::-1]
        
        résultats = []
        for idx in indices:
            résultats.append({
                'chunk': self.chunks_enrichis[idx]['chunk_original'],
                'contexte': self.chunks_enrichis[idx]['contexte'],
                'score': float(similarités[idx])
            })
        
        return résultats

Démonstration

if __name__ == "__main__": documents_test = [ { 'titre': 'Rapport Annuel 2024', 'resume': 'Rapport financier et stratégique de l\'entreprise TechCorp', 'contenu': '''TechCorp a été fondée en 2015 par Marie Dubois et Pierre Lefèvre. En 2018, l'entreprise a levé 10 millions d'euros en série A. La même année, Jean Martin a rejoint l'équipe en tant que CTO. En 2022, TechCorp a atteint 50 millions d'euros de chiffre d'affaires. Le 15 mars 2024, un nouveau partenariat stratégique avec Azure a été annoncé.''' } ] rag = SystemeRAGContextuel() rag.indexer_documents(documents_test) question = "Qui est devenu CTO et quand ?" résultats = rag.rechercher(question) print(f"\nQuestion: {question}") print("=" * 50) for i, r in enumerate(résultats, 1): print(f"\nRésultat {i} (score: {r['score']:.2f}):") print(f"Chunk: {r['chunk']}") print(f"Contexte: {r['contexte']}")

Comparaison : RAG Standard vs Contextual Retrieval

Pour illustrer la différence, voici les résultats d'une même recherche avec les deux approches :

QuestionRAG StandardContextual Retrieval
Chunk trouvéScoreChunk trouvéScore
"Quand Jean Martin est-il devenu CTO ?""Jean Martin a rejoint..."0.45"Jean Martin a rejoint... [Contexte: Section historique...]"0.91
"Quel était le montant de la série A ?""10 millions d'euros..."0.62"10M€ en 2018 [Contexte: Section financement...]"0.88

Optimisation des Coûts avec HolySheep AI

Le contextual retrieval génère des appels API supplémentaires pour l'enrichissement. Voici comment optimiser vos coûts :

Tableau Comparatif des Providers

ProviderModèlePrix (USD/MToken)Latence Moyenne
HolySheep AIDeepSeek V3.2$0.42< 50ms
OpenAIGPT-4.1$8.00~200ms
AnthropicClaude Sonnet 4.5$15.00~300ms
GoogleGemini 2.5 Flash$2.50~150ms

Avec HolySheep AI, le coût d'enrichissement de 10 000 chunks (environ 50 000 tokens de contexte) revient à $0.02 avec DeepSeek V3.2 contre $0.40 avec GPT-4.1. Une économie de 95% qui rend le contextual retrieval accessible à tous les projets.

Bonnes Pratiques pour un Enrichissement Efficace

Cas d'Usage Avancés

Enrichissement Hiérarchique

Pour des documents complexes, vous pouvez appliquer un enrichissement à plusieurs niveaux :

# Exemple d'enrichissement à deux niveaux

Niveau 1: Contexte du document complet

contexte_document = generer_contexte_document(document_complet)

Niveau 2: Contexte de chaque section

for section in sections: contexte_section = generer_contexte_section(section, contexte_document) # Enrichir les chunks de cette section avec les deux niveaux for chunk in section.chunks: chunk.enrichi = f"{chunk.texte}\n\n[Document: {contexte_document}]\n[Section: {contexte_section}]"

Erreurs Courantes et Solutions

Erreur 1 : "contextual_length_exceeded" ou Token Limit

Symptôme : Votre API retourne une erreur 400 avec un message concernant la longueur des tokens.

Cause : Le prompt de contexte加上 le chunk dépasse la limite du modèle.

# ❌ CODE INCORRECT - Provoque des erreurs de longueur
def generer_contexte(self, chunk, doc_info):
    prompt = f"""
    Document complet: {doc_info['contenu_complet']}  # Peut faire des MB !
    Chunk: {chunk}
    """
    # Erreur garantie avec des documents volumineux

✅ CODE CORRIGÉ

def generer_contexte(self, chunk, doc_info): # Limiter la taille du résumé à 500 tokens resume_limit = doc_info.get('resume', '')[:2000] # Ne passer que les chunks voisins, pas le document entier voisins = [] if doc_info.get('chunk_précédent'): voisins.append(f"Avant: {doc_info['chunk_précédent'][:500]}") if doc_info.get('chunk_suivant'): voisins.append(f"Après: {doc_info['chunk_suivant'][:500]}") prompt = f"""Résumé du document: {resume_limit} Chunk à enrichir: {chunk} Contexte: {chr(10).join(voisins)}""" # Fonctionne correctement

Erreur 2 : "Invalid API Key" ou 401 Unauthorized

Symptôme : Toutes vos requêtes échouent avec un code 401.

Cause : La clé API n'est pas chargée correctement ou est incorrecte.

# ❌ CODE INCORRECT
class EnrichisseurContextuel:
    def __init__(self):
        self.api_key = "YOUR_HOLYSHEEP_API_KEY"  # Copié littéralement !
        # Ne fonctionne JAMAIS

❌ AUTRE ERREUR - Variable d'environnement non chargée

class EnrichisseurContextuel: def __init__(self): self.api_key = os.environ.get("HOLYSHEEP_API_KEY") # Fonctionne seulement si load_dotenv() est appelé avant

✅ CODE CORRIGÉ

class EnrichisseurContextuel: def __init__(self): from dotenv import load_dotenv load_dotenv() # Charger les variables AVANT de les lire self.api_key = os.getenv("HOLYSHEEP_API_KEY") if not self.api_key: raise ValueError("HOLYSHEEP_API_KEY non définie dans .env") # Vérification optionnelle if self.api_key == "YOUR_HOLYSHEEP_API_KEY": raise ValueError("Veuillez remplacer YOUR_HOLYSHEEP_API_KEY par votre vraie clé")

Erreur 3 : Résultats Incohérents ou Contexte Généré Inversé

Symptôme : Le contexte généré ne correspond pas au chunk ou référence des informations incorrectes.

Cause : Le chunk a été modifié après indexation ou l'ordre des chunks est incorrect.

# ❌ CODE INCORRECT - Contexte potentiellement faux
def traiter_document_mauvais(chunks):
    enrichis = []
    for i, chunk in enumerate(chunks):
        # Passe seulement le chunk, sans position
        contexte = generer_contexte(chunk, {})
        enrichis.append({"chunk": chunk, "contexte": contexte})
    return enrichis  # Le contexte peut référencer "le chunk suivant" qui n'existe pas

✅ CODE CORRIGÉ - Contexte cohérent

def traiter_document_correct(chunks): enrichis = [] for i, chunk in enumerate(chunks): info = { 'position': i + 1, 'total': len(chunks), 'chunk_précédent': chunks[i-1] if i > 0 else None, 'chunk_suivant': chunks[i+1] if i < len(chunks) - 1 else None, } prompt = f"""Tu décris le contexte de ce fragment de document. Position: fragment {i+1} sur {len(chunks)} Fragment: {chunk} {précédent(f"Fragment précédent: {info['chunk_précédent']}") if info['chunk_précédent'] else ""} {suivant(f"Fragment suivant: {info['chunk_suivant']}") if info['chunk_suivant'] else ""}""" enrichis.append({ "chunk": chunk, "contexte": generer_contexte(prompt), "position": i # Stocker la position pour debug }) return enrichis

Erreur 4 : Latence Élevée en Production

Symptôme : L'enrichissement prend plusieurs secondes par chunk en production.

Cause : Appels API séquentiels au lieu de parallélisation.

<