Chunking : découper vos documents intelligemment
Objectifs
- Comprendre pourquoi le chunking est critique pour la qualité du RAG
- Maîtriser les différentes stratégies de découpage
- Choisir la bonne taille et le bon overlap
Pourquoi découper ?
Les modèles d’embedding ont une limite de tokens (8 191 pour OpenAI). Mais même en dessous de cette limite, un document entier comme embedding donne de mauvais résultats : le vecteur « moyenne » tout le contenu et perd les détails.
Le chunking découpe un document en morceaux cohérents, chacun capturant une idée ou un paragraphe spécifique.
Stratégie 1 : Découpage par taille fixe
La méthode la plus simple. Utile comme baseline :
def chunker_taille_fixe(
texte: str,
taille: int = 500,
overlap: int = 100
) -> list[str]:
"""Découpe en chunks de taille fixe avec chevauchement."""
mots = texte.split()
chunks = []
for i in range(0, len(mots), taille - overlap):
chunk = " ".join(mots[i:i + taille])
if chunk.strip():
chunks.append(chunk)
return chunks
texte_long = "Un très long document... " * 500
chunks = chunker_taille_fixe(texte_long, taille=300, overlap=50)
print(f"{len(chunks)} chunks générés")
L’importance de l’overlap
Le chevauchement (overlap) entre chunks évite de couper une idée en deux. Un overlap de 10-20 % de la taille du chunk est un bon point de départ.
Stratégie 2 : Découpage par séparateurs
Respecte la structure du texte (paragraphes, sections) :
import re
def chunker_separateurs(
texte: str,
separateurs: list[str] = ["\n\n", "\n", ". ", " "],
taille_max: int = 1000
) -> list[str]:
"""Découpe en respectant les séparateurs naturels."""
chunks = []
chunk_courant = ""
# Découper par le séparateur le plus fort d'abord
segments = [texte]
for sep in separateurs:
nouveaux_segments = []
for segment in segments:
parties = segment.split(sep)
for partie in parties:
if len(partie.split()) > taille_max:
nouveaux_segments.append(partie)
else:
if (chunk_courant and
len((chunk_courant + sep + partie).split()) > taille_max):
chunks.append(chunk_courant.strip())
chunk_courant = partie
else:
chunk_courant = (
chunk_courant + sep + partie
if chunk_courant else partie
)
segments = nouveaux_segments
if chunk_courant.strip():
chunks.append(chunk_courant.strip())
return chunks
Stratégie 3 : Découpage par sections Markdown
Idéale pour la documentation structurée :
def chunker_markdown(texte: str, niveau_max: int = 2) -> list[dict]:
"""Découpe un document Markdown par sections."""
chunks = []
section_courante = {"titre": "", "contenu": "", "niveau": 0}
for ligne in texte.split("\n"):
# Détecter les titres
match = re.match(r"^(#{1,6})\s+(.+)$", ligne)
if match:
niveau = len(match.group(1))
titre = match.group(2)
if niveau <= niveau_max and section_courante["contenu"].strip():
chunks.append({
"titre": section_courante["titre"],
"texte": section_courante["contenu"].strip(),
"niveau": section_courante["niveau"]
})
if niveau <= niveau_max:
section_courante = {
"titre": titre,
"contenu": "",
"niveau": niveau
}
else:
section_courante["contenu"] += ligne + "\n"
else:
section_courante["contenu"] += ligne + "\n"
if section_courante["contenu"].strip():
chunks.append({
"titre": section_courante["titre"],
"texte": section_courante["contenu"].strip(),
"niveau": section_courante["niveau"]
})
return chunks
Stratégie 4 : Chunking sémantique
Découpe le texte là où le sens change, en utilisant les embeddings eux-mêmes :
from openai import OpenAI
import numpy as np
client = OpenAI()
def chunker_semantique(
texte: str,
seuil_similarite: float = 0.75,
taille_phrase_min: int = 20
) -> list[str]:
"""Regroupe les phrases par similarité sémantique."""
# Découper en phrases
phrases = re.split(r"(?<=[.!?])\s+", texte)
phrases = [p for p in phrases if len(p) > taille_phrase_min]
if len(phrases) <= 1:
return [texte]
# Embeddings de chaque phrase
resp = client.embeddings.create(
input=phrases,
model="text-embedding-3-small" # small suffit ici
)
embs = np.array([d.embedding for d in sorted(resp.data, key=lambda x: x.index)])
# Trouver les points de rupture
chunks = []
chunk_phrases = [phrases[0]]
for i in range(1, len(phrases)):
sim = float(np.dot(embs[i], embs[i - 1]))
if sim < seuil_similarite:
# Rupture sémantique détectée
chunks.append(" ".join(chunk_phrases))
chunk_phrases = [phrases[i]]
else:
chunk_phrases.append(phrases[i])
if chunk_phrases:
chunks.append(" ".join(chunk_phrases))
return chunks
Enrichir les chunks avec du contexte
Ajoutez le titre du document ou de la section pour améliorer la pertinence :
def enrichir_chunk(chunk: str, titre_doc: str, section: str = "") -> str:
"""Ajoute du contexte au chunk pour un meilleur embedding."""
prefixe = f"Document : {titre_doc}"
if section:
prefixe += f" | Section : {section}"
return f"{prefixe}\n\n{chunk}"
Quelle taille de chunk choisir ?
| Taille (tokens) | Avantages | Inconvénients |
|---|---|---|
| 100-200 | Très précis, bonne granularité | Perd le contexte |
| 300-500 | Bon compromis | Standard |
| 500-1000 | Contexte riche | Moins précis, coûteux |
| 1000+ | Contexte maximal | Dilue l’information |
La taille idéale dépend de vos documents et de vos requêtes. Testez avec les métriques de la leçon 10.
Résumé
- Le chunking est l’étape la plus impactante du pipeline RAG
- Taille fixe comme baseline, séparateurs pour respecter la structure
- Le chunking sémantique détecte les changements de sujet automatiquement
- L’overlap et l’enrichissement contextuel améliorent la qualité
- Testez différentes tailles et mesurez avec des métriques de recherche