Aller au contenu principal

Chunking : découper vos documents intelligemment

Mis à jour le 29 juillet 2026

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 chez OpenAI. Cette limite est la première raison de découper, mais ce n’est pas la plus importante. Même un document qui tient largement en dessous du plafond donne de mauvais résultats quand on l’encode d’un bloc, parce que le vecteur produit moyenne tout le contenu et perd les détails. Prenez un manuel RH de quarante pages qui couvre les congés, le télétravail, les notes de frais et la mutuelle : son embedding unique ne ressemble vraiment à aucune de ces quatre questions, et la recherche « combien de jours de congés » ira le classer derrière un document bien plus court mais bien plus ciblé.

Le chunking résout ce problème en découpant le document en morceaux cohérents, chacun capturant une idée ou un paragraphe spécifique. C’est l’étape sur laquelle un pipeline RAG gagne ou perd le plus de qualité, avant même le choix du modèle d’embedding.

Stratégie 1 : découpage par taille fixe

La méthode la plus simple compte les mots et coupe à intervalle régulier. Elle ignore complètement la structure du texte, ce qui la rend brutale, mais elle est en trois lignes et sert de point de comparaison indispensable : toute stratégie plus sophistiquée doit prouver qu’elle fait mieux que cette 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")

Le paramètre overlap mérite qu’on s’y arrête. Sans chevauchement, une coupure tombe tôt ou tard au milieu d’une explication : la phrase « le préavis est de trois mois » se retrouve dans un chunk, et « sauf pendant la période d’essai » dans le suivant. Aucun des deux ne répond correctement à la question, et le second est même trompeur. En faisant se recouvrir les chunks de 10 à 20 % de leur taille, vous garantissez que chaque idée existe au moins une fois entière quelque part dans l’index. C’est un bon point de départ, à ajuster ensuite selon la densité de vos documents.

Stratégie 2 : découpage par séparateurs

Plutôt que de couper au mot près, on peut respecter les frontières naturelles du texte. L’idée est de tenter d’abord le séparateur le plus fort — la double ligne vide qui sépare deux paragraphes —, puis de descendre vers des séparateurs de plus en plus faibles tant que le morceau obtenu dépasse la taille maximale. Un article de blog sera ainsi découpé paragraphe par paragraphe, et seul un paragraphe monstrueux sera fragmenté par phrases.

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

Quand votre corpus est de la documentation structurée, les titres font déjà le travail à votre place : l’auteur a lui-même délimité les unités de sens. Découper au niveau des titres # et ## produit des chunks qui correspondent exactement aux sections, et la fonction ci-dessous conserve le titre à côté du texte, ce qui servira juste après pour l’enrichissement.

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

Sur un texte sans structure explicite — un compte rendu de réunion, une transcription d’appel —, ni la taille ni les séparateurs ne savent où le sujet change. Les embeddings, eux, le savent. On encode chaque phrase, on compare chaque phrase à la précédente, et on coupe là où la similarité s’effondre : c’est exactement l’endroit où le locuteur est passé du budget au recrutement. Notez que text-embedding-3-small suffit ici, puisqu’on ne cherche pas une précision absolue mais une rupture relative.

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

Un chunk isolé perd souvent l’information qui le rendait interprétable. « La durée est de trois ans » ne veut rien dire si le lecteur — ou le modèle d’embedding — ignore qu’il s’agit du chapitre sur la garantie constructeur. Préfixer chaque chunk avec le titre du document et celui de la section rétablit ce contexte dans le vecteur lui-même, et la recherche « durée de garantie » retrouve alors le passage.

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 ?

Le choix se joue sur un arbitrage entre précision et contexte : un chunk court cible finement mais amputé, un chunk long comprend tout mais dilue.

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

Il n’existe pas de valeur universelle : la taille idéale dépend de vos documents et de vos requêtes. La seule méthode fiable consiste à indexer le même corpus avec deux ou trois tailles, puis à mesurer avec les métriques de la leçon 10 sur un jeu de questions réelles. Une base de connaissances de support, faite de réponses courtes, tolère 200 tokens ; un corpus juridique en réclamera 800.

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