Aller au contenu
Développement medium

Chunking : découper vos documents pour le RAG

22 min de lecture

logo python

Vos documents sont trop longs pour être envoyés directement à un LLM ? Le chunking (découpage) est l’étape cruciale qui transforme un document de 50 pages en fragments cohérents que votre système RAG peut rechercher efficacement.

Ce guide vous montre 5 stratégies de chunking — du plus simple au plus intelligent — avec du code Python fonctionnel et des recommandations pour choisir la bonne approche selon votre cas.

Un système RAG fonctionne en 3 étapes :

Pipeline RAG : Document → Chunking → Embeddings → Recherche → LLM

Un mauvais chunking produit des résultats médiocres, même avec un excellent modèle d’embeddings :

ProblèmeConséquence
Chunks trop grands (>1000 tokens)Embeddings dilués, recherche imprécise
Chunks trop petits (<50 tokens)Contexte insuffisant, réponses incomplètes
Coupures au milieu d’une phrasePerte de sens, hallucinations
Séparation phrase-contexte”3.2” sans savoir que c’est une version

Le bon chunking garantit que chaque fragment :

  • Est suffisamment long pour avoir du sens (contexte)
  • Est suffisamment court pour être précis (ciblage)
  • Conserve les frontières naturelles du texte (cohérence)

Voici un comparatif des approches, de la plus simple à la plus sophistiquée :

StratégieComplexitéAvantagesInconvénients
Fixe (caractères)Simple, prévisibleCoupe au milieu des mots
Récursif⭐⭐Respecte les frontièresTailles variables
Par phrases⭐⭐Cohérence sémantiquePhrases longues problématiques
Par sections⭐⭐⭐Structure préservéeDépend du format (Markdown, HTML)
Sémantique⭐⭐⭐⭐Chunks thématiquesPlus lent, modèle requis
Fenêtre de terminal
# Environnement Python 3.10+
python3 -m venv chunking-test
source chunking-test/bin/activate
# Dépendances (optionnelles selon la stratégie)
pip install sentence-transformers nltk

La méthode la plus simple : découper tous les N caractères avec un chevauchement (overlap) pour éviter de perdre le contexte aux frontières.

def chunk_fixed(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
"""Découpage fixe avec overlap.
Args:
text: Texte à découper
chunk_size: Taille maximale de chaque chunk
overlap: Nombre de caractères en commun entre chunks consécutifs
Returns:
Liste de chunks
"""
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end]
if chunk.strip(): # Ignorer les chunks vides
chunks.append(chunk)
# Avancer en tenant compte de l'overlap
start = end - overlap
return chunks

Test avec un texte réel :

texte = """Ansible est un outil d'automatisation IT open source. Il permet de
gérer la configuration des serveurs de manière déclarative.
Les playbooks Ansible sont écrits en YAML. Ils décrivent l'état souhaité
de l'infrastructure plutôt que les étapes pour y arriver.
L'inventaire définit les groupes de serveurs. Un groupe peut contenir
des serveurs de production, staging ou développement.
Les modules Ansible sont les unités d'exécution. Le module apt installe
des paquets, le module copy copie des fichiers."""
chunks = chunk_fixed(texte, chunk_size=200, overlap=30)
for i, chunk in enumerate(chunks):
print(f"[{i+1}] ({len(chunk)} car.): {chunk[:50]}...")

Résultat :

[1] (200 car.): Ansible est un outil d'automatisation IT open so...
[2] (200 car.): re déclarative.
Les playbooks Ansible sont éc...
[3] (200 car.): ure plutôt que les étapes pour y arriver.
L'inv...
[4] (170 car.): r de production, staging ou développement.
Les ...

Quand l’utiliser :

  • Texte non structuré (logs, transcriptions)
  • Prototypage rapide
  • Quand vous avez besoin de tailles exactes

Stratégie 2 : Chunking récursif (LangChain style)

Section intitulée « Stratégie 2 : Chunking récursif (LangChain style) »

Cette méthode utilise une hiérarchie de séparateurs : elle essaie d’abord de couper aux paragraphes, puis aux lignes, puis aux phrases, puis aux mots.

def chunk_recursive(
text: str,
chunk_size: int = 500,
separators: list[str] = None
) -> list[str]:
"""Chunking récursif avec hiérarchie de séparateurs.
Essaie de découper avec le premier séparateur.
Si les chunks sont trop grands, applique récursivement
avec le séparateur suivant.
"""
if separators is None:
separators = ["\n\n", "\n", ". ", " "]
if not separators:
# Plus de séparateurs : découpage brutal
return [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
sep = separators[0]
parts = text.split(sep)
chunks = []
current_chunk = ""
for part in parts:
# Si ajouter cette partie dépasse la limite
if len(current_chunk) + len(part) + len(sep) > chunk_size:
if current_chunk:
chunks.append(current_chunk.strip())
# Si la partie seule est trop grande, récursion
if len(part) > chunk_size:
chunks.extend(chunk_recursive(part, chunk_size, separators[1:]))
current_chunk = ""
else:
current_chunk = part
else:
current_chunk += (sep if current_chunk else "") + part
if current_chunk.strip():
chunks.append(current_chunk.strip())
return chunks

Test :

chunks = chunk_recursive(texte, chunk_size=300)
for i, chunk in enumerate(chunks):
print(f"[{i+1}] ({len(chunk)} car.):")
print(f" {chunk[:60]}...")

Résultat :

[1] (193 car.):
Ansible est un outil d'automatisation IT open source. Il p...
[2] (187 car.):
Les playbooks Ansible sont écrits en YAML. Ils décrivent l...
[3] (193 car.):
L'inventaire définit les groupes de serveurs. Un groupe pe...
[4] (151 car.):
Les modules Ansible sont les unités d'exécution. Le module...

Quand l’utiliser :

  • Documentation technique
  • Articles de blog
  • Texte avec paragraphes clairs

Cette approche utilise NLTK pour identifier les frontières de phrases et regroupe plusieurs phrases jusqu’à atteindre la taille cible.

import nltk
from nltk.tokenize import sent_tokenize
# Télécharger le modèle de tokenisation (une seule fois)
nltk.download('punkt_tab', quiet=True)
def chunk_by_sentences(
text: str,
max_sentences: int = 3,
max_chars: int = 500
) -> list[str]:
"""Chunking par groupes de phrases.
Regroupe les phrases jusqu'à atteindre max_sentences
ou max_chars, selon ce qui arrive en premier.
"""
sentences = sent_tokenize(text, language='french')
chunks = []
current_chunk = []
current_length = 0
for sentence in sentences:
sentence = sentence.strip()
# Vérifier les limites
if (len(current_chunk) >= max_sentences or
current_length + len(sentence) > max_chars):
if current_chunk:
chunks.append(" ".join(current_chunk))
current_chunk = [sentence]
current_length = len(sentence)
else:
current_chunk.append(sentence)
current_length += len(sentence) + 1
if current_chunk:
chunks.append(" ".join(current_chunk))
return chunks

Test :

chunks = chunk_by_sentences(texte, max_sentences=2, max_chars=300)
for i, chunk in enumerate(chunks):
print(f"[{i+1}] ({len(chunk)} car.): {chunk[:50]}...")

Résultat :

[1] (131 car.): Ansible est un outil d'automatisation IT open so...
[2] (151 car.): Les playbooks Ansible sont écrits en YAML. Ils d...
[3] (156 car.): L'inventaire définit les groupes de serveurs. Un...
[4] (118 car.): Les modules Ansible sont les unités d'exécution....

Quand l’utiliser :

  • Texte narratif
  • FAQ (question-réponse par chunk)
  • Contenu où chaque phrase a une valeur autonome

Stratégie 4 : Chunking par sections (Markdown/HTML)

Section intitulée « Stratégie 4 : Chunking par sections (Markdown/HTML) »

Pour les documents structurés (Markdown, HTML), respecter la structure originale produit des chunks thématiquement cohérents.

import re
def chunk_by_sections(
text: str,
header_pattern: str = r'^#{1,3}\s+.+$',
max_chars: int = 1000
) -> list[str]:
"""Chunking par sections Markdown.
Découpe aux titres (# ## ###) et retient le titre
dans chaque chunk pour le contexte.
"""
lines = text.split('\n')
chunks = []
current_chunk = []
current_header = ""
current_length = 0
for line in lines:
# Détection d'un titre
if re.match(header_pattern, line, re.MULTILINE):
# Sauvegarder le chunk précédent
if current_chunk:
chunk_text = '\n'.join(current_chunk).strip()
if chunk_text:
chunks.append(chunk_text)
current_header = line
current_chunk = [line]
current_length = len(line)
else:
# Vérifier la limite de taille
if current_length + len(line) > max_chars and current_chunk:
chunk_text = '\n'.join(current_chunk).strip()
if chunk_text:
chunks.append(chunk_text)
# Nouveau chunk avec le header pour le contexte
current_chunk = [current_header, line] if current_header else [line]
current_length = len(current_header) + len(line)
else:
current_chunk.append(line)
current_length += len(line) + 1
# Dernier chunk
if current_chunk:
chunk_text = '\n'.join(current_chunk).strip()
if chunk_text:
chunks.append(chunk_text)
return chunks

Test avec un document Markdown :

doc_markdown = """# Guide Ansible
## Introduction
Ansible est un outil d'automatisation IT. Il gère la configuration des serveurs.
## Installation
Installez Ansible avec votre gestionnaire de paquets (apt, dnf, brew).
## Premiers pas
### Créer un inventaire
L'inventaire liste vos serveurs par groupe.
### Écrire un playbook
Un playbook est un fichier YAML qui décrit l'état souhaité."""
chunks = chunk_by_sections(doc_markdown, max_chars=300)
for i, chunk in enumerate(chunks):
print(f"[{i+1}] ({len(chunk)} car.):")
print(chunk[:80] + "..." if len(chunk) > 80 else chunk)
print()

Résultat :

[1] (14 car.):
# Guide Ansible
[2] (100 car.):
## Introduction
Ansible est un outil d'automatisation IT. Il gère la configuration...
[3] (85 car.):
## Installation
Installez Ansible avec votre gestionnaire de paquets (apt, dnf, brew).
[4] (47 car.):
## Premiers pas
### Créer un inventaire...
[5] (70 car.):
### Écrire un playbook
Un playbook est un fichier YAML qui décrit l'état...

Quand l’utiliser :

  • Documentation technique structurée
  • README, wikis, guides
  • Tout document avec des titres clairs

L’approche la plus sophistiquée : utiliser les embeddings pour détecter les changements de thème et couper là où le sens change.

import numpy as np
from sentence_transformers import SentenceTransformer
from nltk.tokenize import sent_tokenize
def chunk_semantic(
text: str,
model_name: str = "intfloat/multilingual-e5-small",
similarity_threshold: float = 0.75,
min_chunk_sentences: int = 2
) -> list[str]:
"""Chunking sémantique basé sur les embeddings.
Regroupe les phrases tant que leur similarité dépasse
le seuil. Coupe quand le thème change.
"""
# Charger le modèle
model = SentenceTransformer(model_name)
# Tokeniser en phrases
sentences = sent_tokenize(text, language='french')
if len(sentences) < 2:
return [text]
# Encoder toutes les phrases (avec préfixe passage: pour E5)
passages = [f"passage: {s}" for s in sentences]
embeddings = model.encode(passages, show_progress_bar=False)
# Calculer la similarité entre phrases consécutives
similarities = []
for i in range(len(embeddings) - 1):
sim = np.dot(embeddings[i], embeddings[i+1])
sim /= (np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[i+1]))
similarities.append(sim)
# Créer les chunks aux points de faible similarité
chunks = []
current_chunk = [sentences[0]]
for i, sim in enumerate(similarities):
if sim < similarity_threshold and len(current_chunk) >= min_chunk_sentences:
# Changement de thème détecté
chunks.append(" ".join(current_chunk))
current_chunk = [sentences[i+1]]
else:
current_chunk.append(sentences[i+1])
if current_chunk:
chunks.append(" ".join(current_chunk))
return chunks

Test :

texte_mixte = """Ansible est un outil d'automatisation pour gérer des serveurs Linux.
Il utilise SSH pour se connecter aux machines distantes.
Les playbooks sont écrits en YAML avec une syntaxe déclarative.
Docker permet de créer des conteneurs isolés pour les applications.
Les images Docker sont construites à partir de Dockerfiles.
Kubernetes orchestre les conteneurs à grande échelle dans un cluster."""
chunks = chunk_semantic(texte_mixte, similarity_threshold=0.80)
for i, chunk in enumerate(chunks):
print(f"[{i+1}] Thème {i+1}:")
print(f" {chunk}")
print()

Résultat :

[1] Thème 1:
Ansible est un outil d'automatisation pour gérer des serveurs Linux.
Il utilise SSH pour se connecter aux machines distantes.
Les playbooks sont écrits en YAML avec une syntaxe déclarative.
[2] Thème 2:
Docker permet de créer des conteneurs isolés pour les applications.
Les images Docker sont construites à partir de Dockerfiles.
[3] Thème 3:
Kubernetes orchestre les conteneurs à grande échelle dans un cluster.

Quand l’utiliser :

  • Texte sans structure claire (transcriptions, HTML converti)
  • Documents qui mélangent plusieurs sujets
  • Quand la qualité prime sur la vitesse

Voici un tableau récapitulatif pour vous aider à choisir :

CritèreFixeRécursifPhrasesSectionsSémantique
Vitesse⚡⚡⚡⚡⚡⚡⚡⚡⚡
DépendancesAucuneAucuneNLTKAucunesentence-transformers
CohérenceFaibleMoyenneBonneExcellenteExcellente
PrévisibilitéExacteVariableVariableVariableVariable
Documents structurés✓✓✓✓✓
Texte libre✓✓✓✓✓

Documentation Markdown

Stratégie : Sections (priorité) + Récursif (fallback)

Les docs techniques ont des titres — exploitez-les !

Articles de blog

Stratégie : Récursif

Les paragraphes sont des unités naturelles de sens.

FAQ / Q&A

Stratégie : Phrases (2-3 par chunk)

Question + réponse = 1 chunk cohérent.

Transcriptions

Stratégie : Sémantique

Pas de structure → laissez le modèle trouver les thèmes.

L’overlap : récupérer le contexte aux frontières

Section intitulée « L’overlap : récupérer le contexte aux frontières »

Quelle que soit la stratégie, l’overlap (chevauchement) aide à conserver le contexte entre chunks.

def add_overlap(chunks: list[str], overlap_sentences: int = 1) -> list[str]:
"""Ajoute un overlap au début de chaque chunk.
Répète la dernière phrase du chunk précédent
au début du chunk suivant.
"""
if len(chunks) < 2:
return chunks
result = [chunks[0]]
for i in range(1, len(chunks)):
# Extraire les dernières phrases du chunk précédent
prev_sentences = sent_tokenize(chunks[i-1], language='french')
overlap = " ".join(prev_sentences[-overlap_sentences:])
# Ajouter au début du chunk actuel
result.append(f"{overlap} {chunks[i]}")
return result

Une fois vos chunks créés, l’étape suivante est de les transformer en embeddings (vecteurs numériques) pour la recherche sémantique.

from sentence_transformers import SentenceTransformer
import chromadb
# 1. Chunker le document
chunks = chunk_by_sections(document_markdown)
# 2. Créer les embeddings
model = SentenceTransformer("intfloat/multilingual-e5-small")
embeddings = model.encode([f"passage: {c}" for c in chunks])
# 3. Stocker dans ChromaDB
client = chromadb.PersistentClient(path="./ma_base")
collection = client.get_or_create_collection("documentation")
collection.add(
embeddings=embeddings.tolist(),
documents=chunks,
ids=[f"chunk_{i}" for i in range(len(chunks))]
)

Le chunking est une étape fondamentale qui impacte directement la qualité de votre RAG :

Point cléRecommandation
Taille cible200-500 tokens (800-2000 caractères)
Stratégie par défautRécursif pour texte général
Documents structurésSections (Markdown/HTML)
Texte non structuréSémantique si qualité critique
Overlap10-20% pour conserver le contexte

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.