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 où 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 :
| Question | RAG Standard | Contextual Retrieval | ||
|---|---|---|---|---|
| Chunk trouvé | Score | Chunk 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
| Provider | Modèle | Prix (USD/MToken) | Latence Moyenne |
|---|---|---|---|
| HolySheep AI | DeepSeek V3.2 | $0.42 | < 50ms |
| OpenAI | GPT-4.1 | $8.00 | ~200ms |
| Anthropic | Claude Sonnet 4.5 | $15.00 | ~300ms |
| Gemini 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
- Chunk size optimal : Visez 300-500 caractères par chunk pour maintenir la cohérence contextuelle tout en permettant des recherches précises.
- Overlap stratégique : Laissez 10-15% de chevauchement entre chunks consécutifs pour capturer les références transitoires.
- Résumé du document : Fournissez toujours un résumé au module d'enrichissement pour améliorer la qualité du contexte généré.
- Chunks voisins : L'inclusion du chunk précédent et suivant améliore significativement la cohérence du contexte.
- Mise en cache : Une fois enrichis, stockez vos chunks contextuels en base pour éviter de regenerer le contexte à chaque démarrage.
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.
<