Aller au contenu principal

Module 3 — Découpage : taille, chevauchement, respect de la structure

Un document brut de dix pages n'est pas cherchable : il est à la fois trop long pour tenir dans une consigne et trop hétérogène pour qu'un plongement le représente fidèlement. Il faut le découper en passages — parfois appelés morceaux, ou chunks — de taille utile. Ce module explique comment choisir cette taille, où poser les coupures, et pourquoi le mauvais découpage explique la moitié des échecs de recherche que nous mesurerons au module 8.

Ce qu'on cherche à obtenir

Un bon passage a trois propriétés simultanées, souvent en tension :

  1. Il est assez court pour qu'un plongement de dimension modeste le représente sans dilution.
  2. Il est assez long pour contenir une idée complète, donc pour qu'un modèle puisse y répondre sans autre contexte.
  3. Il n'est pas coupé au milieu d'une unité sémantique — phrase, tableau, cadre normatif.

La taille se mesure en jetons, pas en caractères ni en mots. Un jeton fait à peu près trois caractères en français ; les bornes standard sont 200 à 600 jetons par passage, avec un chevauchement de 10 à 20 %. Ces chiffres ne sortent pas d'une théorie mais d'expériences : nous les vérifierons sur le fil rouge à la fin du module.

Le découpage fixe est le pire

C'est le plus tentant à coder, et le premier qu'on abandonne :

def decouper_naif(texte, taille=400, chevauchement=50):
morceaux = []
debut = 0
while debut < len(texte):
morceaux.append(texte[debut : debut + taille])
debut += taille - chevauchement
return morceaux

Le problème est visible dès la première question posée à l'assistant du fil rouge : « Que dit la procédure sur les stagiaires ? ». Le passage retrouvé commence par « ...ge d'essai de trois mois. Article 4 — Stagiaires. La convention prévoit... » et se termine par « ...pendant six semaines. Article 5 — Fin de contrat... ». La coupure a séparé la définition du sujet du contenu de l'article. Le modèle répond « je ne trouve pas la règle sur les stagiaires » alors qu'elle est là.

Découper par phrase, puis regrouper

Un premier progrès consiste à couper aux frontières de phrase, puis à agréger les phrases jusqu'à approcher la taille cible. nltk ou spacy fournissent un segmenteur français correct.

import tiktoken
import nltk

nltk.download("punkt_tab", quiet=True)
encodeur = tiktoken.get_encoding("cl100k_base")

def compter_jetons(texte):
return len(encodeur.encode(texte))

def decouper_par_phrase(texte, cible=400, chevauchement=60):
phrases = nltk.sent_tokenize(texte, language="french")
morceaux, tampon, taille = [], [], 0
for phrase in phrases:
t = compter_jetons(phrase)
if taille + t > cible and tampon:
morceaux.append(" ".join(tampon))
# chevauchement : on garde les dernieres phrases
recouvrement, r = [], 0
for p in reversed(tampon):
r += compter_jetons(p)
if r >= chevauchement:
break
recouvrement.insert(0, p)
tampon, taille = recouvrement, sum(compter_jetons(p) for p in recouvrement)
tampon.append(phrase)
taille += t
if tampon:
morceaux.append(" ".join(tampon))
return morceaux

Ce découpage évite les coupures au milieu d'une phrase et rend chaque passage lisible. Il reste aveugle à la structure : un titre de section reste séparé de son contenu, et un tableau est cassé par les points-virgules internes qui font office de fins de phrase.

Respecter la structure : découpage hiérarchique

Le module 2 a conservé les titres de section et sérialisé les tableaux à part. On peut donc décomposer d'abord par section, puis par phrase à l'intérieur d'une section trop longue.

def decouper_hierarchique(sections, cible=400, chevauchement=60):
morceaux = []
for section in sections:
entete = section["titre"]
if compter_jetons(section["texte"]) <= cible:
morceaux.append({"titre": entete, "texte": section["texte"]})
else:
for sous in decouper_par_phrase(section["texte"], cible, chevauchement):
morceaux.append({"titre": entete, "texte": sous})
return morceaux

L'entête est ajoutée en tête de chaque passage au moment de l'indexation, ou conservée comme métadonnée et accolée à la question posée au moment de la génération. Le premier choix améliore la recherche, le second économise des jetons ; le module 7 tranche selon le cas.

Le contexte parent

Une variante utile : indexer des passages courts (150 à 250 jetons) pour la recherche, mais renvoyer au modèle la section entière qui les contient. La recherche est précise, la génération dispose du contexte. On garde pour chaque passage court un identifiant vers son parent.

def indexer_avec_parent(document_id, sections):
petits, grands = [], {}
for section in sections:
grands[section["id"]] = section["texte"]
for morceau in decouper_par_phrase(section["texte"], cible=200, chevauchement=30):
petits.append({
"document_id": document_id,
"parent_id": section["id"],
"texte": morceau,
})
return petits, grands

Cette technique coûte peu et améliore mesurablement la fidélité sur les questions qui ont besoin d'un peu plus que la phrase exacte pour être répondues correctement.

Le découpage qui coupe un tableau

Un tableau du règlement intérieur, sérialisé ligne par ligne, ressemble à une suite de phrases séparées par des points-virgules. Le segmenteur peut couper au milieu et rendre invisibles les lignes qui suivent. Le remède est de traiter les tableaux à part, avant tout autre découpage : chaque tableau devient un ou plusieurs passages indépendants dont la première ligne rappelle les en-têtes de colonne. C'est l'un des sept thèmes explicites de la banque d'examen de ce cours.

Choisir taille et chevauchement en pratique

La méthode qui donne un vrai résultat n'est pas la lecture d'un billet de blog mais une mesure sur le corpus. On construit un petit jeu de 50 questions annotées (module 8), on décline trois découpages — 200/30, 400/60, 800/120 — et l'on mesure le rappel à cinq passages. Sur le fil rouge, l'expérience typique donne le tableau ci-dessous :

Taille / chevauchementRappel @ 5Coût moyen par question
200 / 300,711,0 (référence)
400 / 600,841,3
800 / 1200,822,3
1200 / 1800,793,4

La taille moyenne (400) l'emporte : les passages courts manquent le contexte, les passages longs diluent le plongement et laissent moins de place à d'autres passages dans la consigne finale.

Un chiffre à retenir sur le chevauchement

Un chevauchement de 15 % suffit à faire retrouver 95 % des faits qui tombent près d'une coupure, sans coût réel. Au-delà de 25 %, on paie deux fois pour la même information sans gain mesurable. Il ne sert à rien de mettre du chevauchement quand on découpe déjà à la section : le titre suffit à rattacher les morceaux au contexte.

En résumé

  • La taille se mesure en jetons ; la plage utile est 200 à 600 avec 10 à 20 % de chevauchement, à confirmer par mesure sur son propre corpus.
  • Le découpage fixe est le pire ; le découpage par phrase est la référence minimale ; le découpage hiérarchique avec entête de section fait mieux dès qu'on l'essaie.
  • La technique du contexte parent — indexer petit, servir grand — améliore la fidélité pour un coût négligeable.
  • Les tableaux se traitent avant le découpage général, sinon les points-virgules internes cassent l'information et l'assistant répond que la règle n'existe pas.

Module suivant : transformer ces passages en vecteurs et les ranger dans un index qui saura les retrouver rapidement.