Module 04 · Leçon 2

Découper les documents

Comparer pour de vrai trois méthodes de découpage sur la documentation de httpx – par longueur fixe, par titre, par titre avec longueur limitée. Et un vrai piège au passage – des commentaires dans des blocs de code pris pour des titres.

  • Environ 35 minutes
  • Niveau : Intermédiaire
  • Testé : 2026-09-14, Python pur, aucun appel d'API

Le code et les sorties des programmes sont reproduits tels qu’ils ont tourné : commentaires et sorties sont donc en chinois.

L'unité de recherche d'un RAG n'est pas le document entier, mais les petits morceaux (chunks) qu'on en découpe. Un document sur les délais d'expiration peut comporter plusieurs sections ; quand l'utilisateur demande « comment désactiver le délai ? », vous ne voulez récupérer que le petit passage sur la désactivation, pas tout le document.

La façon de découper semble un petit détail technique, mais elle influence énormément l'efficacité du RAG. Des morceaux trop grands, et le contenu pertinent est dilué dans beaucoup de contenu non pertinent lors de la recherche ; trop petits, et une idée complète est éclatée, on ne récupère qu'une demi-phrase. Cette leçon compare plusieurs méthodes de découpage sur la vraie documentation de httpx.

Méthode 1 : découper par longueur fixe

Le découpage le plus simple : sans regarder le contenu, un coup de ciseaux tous les 800 caractères. Pour éviter de couper pile au milieu d'une phrase, deux morceaux voisins se chevauchent de 100 caractères.

def split_fixed(text, size=800, overlap=100):
    """办法一:每 size 个字符切一刀,相邻两块重叠 overlap 个字符。"""
    chunks, start = [], 0
    while start < len(text):
        chunks.append(text[start:start + size])
        start += size - overlap
    return chunks

Voyons ce que cela donne sur timeouts.md, qui traite des délais d'expiration. Voici le 2e morceau :

le.com/api/v1/example", timeout=None)
```

## Setting a default timeout on a client

You can set a timeout on a client instance, which results in the given
`timeout` being used as the default for requests made with this client:

```python
client = httpx.Client()              # Use a default 5s timeout everywhere.
client = httpx.Client(timeout=10.0)  # Use a default 10s timeout everywhere.
client = httpx.Client(timeout=None)  # Disable all timeouts by default.
```

## Fine tuning the configuration

HTTPX also allows you to specify the timeout behavior in more fine grained detail.

There are four different types of timeouts that may occur. These are **connect**,
**read**, **write**, and **pool** timeouts.

* The **connect** timeout specifies the maximum amount of time to wait until
a socket 

Il commence par une URL coupée, le.com/api/v1/example", et s'arrête au milieu d'une phrase, « until a socket ». Il chevauche deux sous-sections : une moitié sur le délai par défaut du client, l'autre sur les quatre types de délais. Le sens de ce morceau est mélangé : si l'utilisateur demande « quels sont les quatre types de délais ? », il ne contient que la moitié de la réponse ; s'il demande « comment définir un délai par défaut pour le client ? », il traîne un tas de contenu sans rapport.

L'avantage du découpage par longueur fixe est sa simplicité : il marche sur n'importe quel texte, et les morceaux ont une longueur homogène. Mais il ignore complètement la structure du document.

Méthode 2 : découper par titre

La documentation de httpx est en Markdown, déjà divisée en sections par des titres ## et ###. Chaque section traite d'un sujet : c'est naturellement une bonne unité de découpage. Découpons donc par titre : à chaque titre, on commence un nouveau morceau.

Le plus direct est une expression régulière qui coupe avant chaque ligne commençant par # :

def split_by_heading_naive(text):
    """办法二(有问题的版本):凡是以 # 开头的行都当成标题切开。"""
    parts = re.split(r"(?m)^(?=#{1,3} )", text)
    return [p.strip() for p in parts if p.strip()]

Voyons en quels morceaux elle découpe timeouts.md (seule la première ligne de chaque morceau est affichée) :

有问题的按标题切法,timeouts.md 的各块开头:
  [ 153 字符] HTTPX is careful to enforce timeouts everywhere by default.
  [  93 字符] ## Setting and disabling timeouts
  [  87 字符] # Using the top-level API:
  [ 186 字符] # Using a client instance:
  [  87 字符] # Using the top-level API:
  [ 127 字符] # Using a client instance:
  [ 424 字符] ## Setting a default timeout on a client
  [1386 字符] ## Fine tuning the configuration
  [ 207 字符] # A client with a 60s timeout for connecting, and a 10s timeout elsewhere.

# Using the top-level API: n'est pas un titre, c'est une ligne de commentaire Python dans un bloc de code. Un commentaire Python et un titre Markdown de niveau 1 se ressemblent exactement : # suivi d'une espace. Le bloc de code a donc été coupé en plein milieu, la section « Setting and disabling timeouts » ne garde que 93 caractères d'explication, et les exemples de code sont partis dans d'autres morceaux.

Cette erreur est sournoise. Le programme ne signale rien, et le nombre de morceaux paraît raisonnable ; il faut les regarder un par un pour s'en apercevoir. Sur toute la documentation, cette version fautive produit 236 morceaux, 60 de plus que la version correcte, tous ces morceaux en trop étant du code découpé en miettes.

La correction : mémoriser si l'on est dans un bloc de code (basculer l'état à chaque ```) et ne jamais compter comme titre un # situé dans un bloc de code :

def split_by_heading(text):
    """办法二(修正版):同样按标题切,但跳过代码块里以 # 开头的注释行。"""
    chunks, current, in_code = [], [], False
    for line in text.splitlines():
        if line.startswith("```"):
            in_code = not in_code
        if not in_code and re.match(r"#{1,3} ", line) and current:
            chunks.append("\n".join(current).strip())
            current = []
        current.append(line)
    if current:
        chunks.append("\n".join(current).strip())
    return [c for c in chunks if c]

Après correction :

修正后的按标题切法,timeouts.md 的各块开头:
  [ 153 字符] HTTPX is careful to enforce timeouts everywhere by default.
  [ 586 字符] ## Setting and disabling timeouts
  [ 424 字符] ## Setting a default timeout on a client
  [1594 字符] ## Fine tuning the configuration

Quatre morceaux, chacun une section complète, avec l'explication et l'exemple de code ensemble.

Ce piège rappelle une chose : après un découpage, affichez toujours quelques morceaux pour les regarder. Chaque format a ses pièges : le HTML a ses barres de navigation et pieds de page, le PDF ses en-têtes, numéros de page et tableaux coupés, le code ses limites de fonctions.

Méthode 3 : découper par titre, puis limiter la longueur

Le découpage par titre a aussi un défaut : certaines sections sont très longues. Sur toute la documentation de httpx, le plus long morceau obtenu par titre fait 5530 caractères. Un morceau trop long a un contenu hétérogène, et la recherche en pâtit ; de plus, la leçon 5 du module 01 l'a dit, les modèles d'embedding comme bge-small-zh-v1.5 ne lisent que 512 tokens au plus et jettent le reste.

On ajoute donc une étape : un morceau qui dépasse la limite est redécoupé aux lignes vides (c'est-à-dire aux paragraphes), et chaque petit morceau reçoit en tête le titre de sa section, pour que chacun sache de quoi il parle.

def split_by_heading_capped(text, max_size=1500):
    """办法三:先按标题切;太长的块再按空行(段落)切开,并在每一小块前面补上所属的标题。"""
    chunks = []
    for section in split_by_heading(text):
        if len(section) <= max_size:
            chunks.append(section)
            continue
        title = section.splitlines()[0] if section.startswith("#") else ""
        current = ""
        for para in section.split("\n\n"):
            if current and len(current) + len(para) > max_size:
                chunks.append(current.strip())
                current = title + "\n\n" if title else ""
            current += para + "\n\n"
        if current.strip():
            chunks.append(current.strip())
    return chunks

Comparaison

Statistiques des quatre découpages sur toute la documentation de httpx (code complet dans code/04-rag/chunking.py ; aucun appel d'API, vous devriez obtenir exactement les mêmes chiffres) :

固定长度:179 块,平均 739 字符,最短 12,最长 800
按标题(有问题):236 块,平均 493 字符,最短 8,最长 5530
按标题(修正):176 块,平均 662 字符,最短 8,最长 5530
按标题+限长:196 块,平均 596 字符,最短 8,最长 2193

Avec la limite, le plus long morceau passe de 5530 à 2193 caractères. Il dépasse encore la limite de 1500, car il contient un long bloc de code sans ligne vide, et mon code ne coupe qu'aux lignes vides, jamais au milieu d'un bloc de code. C'est un compromis volontaire : mieux vaut un morceau un peu long qu'un code coupé en deux.

Le plus court morceau ne fait que 8 caractères : ce sont des sections qui n'ont qu'un titre, sans contenu. Elles n'ont presque aucune valeur pour la recherche ; dans un vrai projet, on peut les fusionner avec le morceau suivant, ou les jeter.

Les leçons suivantes utilisent toutes le découpage « par titre + longueur limitée ».

Comment fixer la taille des morceaux

Il n'y a pas de réponse type, mais quelques repères :

  • Ne pas dépasser la limite du modèle d'embedding. bge-small-zh et multilingual-e5-small sont tous deux limités à 512 tokens. En anglais, un token correspond à environ 4 caractères ; 1500 caractères font environ 400 tokens, sous la limite.
  • Un morceau ne devrait traiter que d'un sujet. Découper selon la structure du document y parvient bien plus facilement que découper par longueur.
  • Penser à l'usage après la recherche. Si l'on récupère 5 morceaux de 600 caractères pour le prompt, cela fait 3000 caractères, environ 750 tokens, pas beaucoup. À 5000 caractères par morceau, 5 morceaux feraient plus de dix mille tokens.

La taille définitive se décide par l'évaluation de la leçon 6 : faire passer le jeu d'évaluation avec plusieurs tailles et voir laquelle donne la meilleure recherche.

Faut-il un chevauchement ?

Pour un découpage par longueur fixe, le chevauchement évite qu'une phrase coupée en deux soit incomplète des deux côtés. Pour un découpage selon la structure, les frontières tombent déjà sur des paragraphes ou des titres, et un chevauchement est généralement inutile.

Le chevauchement a aussi un coût : le même contenu apparaît dans deux morceaux, qui risquent d'être récupérés ensemble lors de la recherche et d'occuper des places précieuses.

Exercices

  1. Passez le size de split_fixed à 300 et à 2000, et regardez comment timeouts.md est découpé. Quelle taille vous semble la plus adaptée ?
  2. Modifiez split_by_heading_capped pour fusionner les morceaux qui n'ont qu'un titre sans contenu (par exemple de moins de 50 caractères) avec le morceau qui suit.
  3. Prenez un de vos propres documents (le README d'un projet, un export du wiki de votre entreprise, un PDF converti en texte), découpez-le avec les trois méthodes et regardez chaque morceau pour repérer les découpages ratés.

Auto-test

1. Quel est le plus gros problème du découpage par longueur fixe ?

Il ignore la structure du document et coupe souvent au milieu d'une phrase, d'un code ou d'une URL ; un morceau peut même mélanger deux sous-sections sans rapport. De tels morceaux ont un sens incomplet et hétérogène, ce qui dégrade la recherche comme la réponse.

2. Pourquoi faut-il traiter spécialement les blocs de code quand on découpe selon les titres Markdown ?

Les commentaires Python dans un bloc de code commencent par # suivi d'une espace, exactement comme un titre Markdown. Sans traitement, un commentaire est pris pour un titre, le bloc de code est coupé en plein milieu, et l'explication et l'exemple de code se retrouvent dans des morceaux différents.

3. Pourquoi, en limitant la longueur des morceaux, ajouter en tête de chaque petit morceau le titre de sa section ?

Quand une longue section est découpée en plusieurs petits morceaux, les suivants n'ont parfois que du texte, sans qu'on voie de quoi ils parlent. Avec le titre, chaque petit morceau porte l'information de son sujet : il est plus facilement retrouvé par la recherche, et une fois dans le prompt, le modèle connaît le contexte de ce passage.

Questions et discussion

Bloqué sur cette leçon ? Posez votre question ici. Et si vous pouvez répondre à quelqu'un, n'hésitez pas.

Une question rapporte 3 points, une réponse 6. Les messages paraissent après vérification.

Chargement de la discussion…