Aller au contenu principal

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)AvantagesInconvénients
100-200Très précis, bonne granularitéPerd le contexte
300-500Bon compromisStandard
500-1000Contexte richeMoins précis, coûteux
1000+Contexte maximalDilue 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