Accueil / Articles

Taille des blocs et chevauchement dans RAG : pas, limites de tokens et joints

Comment diviser un livre long pour RAG : choisir la taille et le degré d’overlap, comprendre le pas de parcours, rester en dessous de la limite de tokens d’embedding, stocker les métadonnées et visualiser l’overlap dans une division réelle.

Éditeur : REACTAPP.TOP8 min de lecture1742 mots

Sources: Chunking is the hidden design decision in RAG

Prenons l’autobiographie de Benjamin Franklin disponible sur Project Gutenberg. Coller tout le livre dans une requête est gaspilleur, même si la fenêtre de contexte pouvait le contenir. Une question comme celle de savoir pourquoi Franklin souhaitait que la Grande-Bretagne conserve le Canada trouve probablement sa réponse dans deux ou trois passages ; les pages restantes consommeraient du contexte et masqueraient les preuves. La génération augmentée par la récupération de données (RAG) existe pour ce type de situation : le système localise les passages pertinents pour la question et ne transmet que ceux-ci au modèle de langage. Le livre reste stocké, tandis que la récupération sélectionne les preuves nécessaires.

Embeddings et raison pour laquelle le texte est fragmenté

RAG commence par convertir des extraits de texte en embeddings, c’est-à-dire en représentations numériques du sens. Des passages ayant un sens similaire se trouvent proches les uns des autres dans un espace vectoriel à haute dimension, même s’ils utilisent des formulations différentes. Lorsqu’une question arrive, elle est également convertie en embedding, les vecteurs stockés les plus proches sont identifiés, et les extraits de texte correspondants sont restitués. Le vecteur localise le texte, qui est ensuite lu par le modèle.

Un seul embedding ne peut pas représenter un livre entier. Un vecteur unique décrivant la vie de Franklin mélangerait son enfance à Boston, ses expériences sur l’impression et l’électricité, *Poor Richard* ainsi que ses témoignages devant le Parlement en une description qui ne rendrait bien aucun de ces éléments. Il est donc nécessaire de diviser le texte en segments avant de le vectoriser.

Taille, chevauchement et pas

Un point de départ raisonnable :

DEFAULT_CHUNK_SIZE = 800      # characters
DEFAULT_CHUNK_OVERLAP = 150   # characters

Les gens ont souvent tendance à les considérer comme deux boutons indépendants. En réalité, ils définissent une seule chose : la distance que la fenêtre avance à chaque étape.

stride = chunk_size − chunk_overlap
       = 800 − 150
       = 650 characters

Cette longueur correspond à la quantité de texte nouveau que chaque nouveau morceau ajoute.

Considérez la taille du morceau comme une limite maximale et non comme un objectif. Les outils de segmentation préfèrent effectuer la coupure à un point naturel, que ce soit au niveau d’un paragraphe, d’une ligne, d’une phrase ou d’un mot ; ainsi, en demandant 800 caractères, les morceaux obtenus pourront faire 640, 731 ou bien exactement 800 caractères.

L’overlap est le texte partagé par les morceaux voisins, et il joue un rôle essentiel. Sans overlap, une coupure peut laisser un fragment isolé comme celui-ci :

"…but left school at the age of ten."

Seule, cette ligne ne dit pas grand-chose. Avec une superposition, le fragment suivant conserve suffisamment de contexte précédent pour rester lisible et facile à trouver. Cela est particulièrement important pour les phrases longues et les arguments étroitement liés. La lettre de Franklin sur le Canada contient une longue chaîne de clauses qui pourraient traverser une frontière, laissant un fragment sans sa conclusion et le suivant sans sa prémisse. Une superposition modérée permet de maintenir le lien entre les deux enregistrements.

Le prix à payer est la duplication. Des documents voisins partageant des formulations peuvent être récupérés ensemble, ce qui est normal. Lorsque des doublons évincent des preuves plus fiables, une méthode tenant compte de la diversité, telle que la pertinence marginale maximale (MMR), est utile.

Ce que contient chaque fragment stocké

Chaque fragment conserve quatre champs :

id
document
embedding
metadata

Un enregistrement d’exemple :

id         7c2a1e90-4b11-4d3e-9f08-12a6c0e84b21

document   "His boyhood in Boston was a stern beginning of the habit
            of hard work and rigid economy which marked the man. For
            a year he went to the Latin Grammar School on School
            Street, but left off at the age of ten to help his father
            in making soap and candles."

embedding  384 floats — [-0.0412, 0.0187, 0.0621, -0.0094, 0.0330, …]
           norm = 1.0

metadata   {
             book: "Franklin's Autobiography",
             source: "https://www.gutenberg.org/cache/epub/36151/pg36151-images.html",
             gutenberg_id: 36151,
             page_label: "5",
             chunk_index: 2
           }

L’incorporation sert à la recherche, tandis que le texte original sert à la lecture, aux citations et aux références. Les métadonnées méritent également une attention particulière. Si l’édition affiche des étiquettes de page telles que [Pg 5], conservez-les afin que les citations renvoient à des pages que les lecteurs peuvent trouver dans leur exemplaire.

Supprimez les éléments à faible valeur avant d’indexer : pages de titre, textes génériques, texte de licence, en-têtes répétés et listes de notes de bas de page. Ceux-ci génèrent des embeddings de faible qualité et affaiblissent les résultats de recherche.

Vecteurs normalisés

Les vecteurs d’incorporation sont généralement normalisés selon la norme L2, ce qui les met tous à une longueur de 1 :

‖v‖ = √(v₁² + v₂² + … + vₙ²)

La similarité cosinus compare deux vecteurs de la manière suivante :

cos θ = (a · b) / (‖a‖ ‖b‖)

Lorsque les deux ont une longueur unitaire, cela se réduit à un produit scalaire simple :

If ‖a‖ = ‖b‖ = 1:
cos θ = a · b

Le classement reflète donc dans quelle mesure deux significations indiquent la même direction, sans que l’amplitude vectorielle brute ne joue aucun rôle, ce qui permet une recherche rapide et pertinente.

Tokens, pas caractères

Les paramètres exprimés en caractères peuvent cacher une limite stricte, car les modèles d’incorporation comptent les tokens. Le modèle all-MiniLM-L6-v2 accepte au maximum 256 tokens. Un bloc plus long peut être tronqué sans aucun avertissement. La base de données conserve toujours le texte complet, mais son embedding ne couvre que la partie initiale, et l’extrémité coupée devient difficile ou impossible à récupérer. Une estimation prudente :

maximum safe chunk size in characters
≈ model token limit × 4

Un modèle de 256 tokens correspond à environ 1 000 caractères. En utilisant quatre caractères par token, on laisse de la marge, car le dialogue, les abréviations, la ponctuation et les textes formatés peuvent être tokenisés plus densément que la prose.

Les modèles plus récents acceptent des entrées bien plus longues, mais le découpage en blocs reste important. Une fenêtre plus grande ne résout pas le problème de la récupération d’informations, car des blocs trop larges mélangent des sujets non liés en un seul vecteur flou.

Découvrir le processus

Tout d’abord, découpez les pages chargées en blocs :

from rag_qa.gutenberg import load_pages
from rag_qa.chunk import chunk_documents
from rag_qa.config import load_settings

s = load_settings()
print(s.summary())
# {
#   'chunk_size_chars': 800,
#   'chunk_overlap_chars': 150,
#   'stride_chars': 650,            # size − overlap; this sets chunk count
#   'model': 'all-MiniLM-L6-v2',
#   'model_max_tokens': 256,        # hard cap; overflow is silent
#   'embedding_dims': 384,
#   'max_safe_chunk_chars': 1024,   # 256 × 4
#   'book': "Franklin's Autobiography",
#   'gutenberg_id': 36151,
# }
pages = load_pages()
chunks = chunk_documents(pages, s.chunk_size, s.chunk_overlap)
print(len(pages), len(chunks), s.stride)

Deuxièmement, vérifiez chaque partie par rapport à la limite de tokens du modèle :

from sentence_transformers import SentenceTransformer
from rag_qa.tokens import check_chunk

m = SentenceTransformer("all-MiniLM-L6-v2")
print(m.max_seq_length)                    # 256
for c in chunks:
    r = check_chunk(c)
    if r.truncated:
        print("silent truncate:", r.n_chars, "chars /", r.n_tokens, "tokens")

Le script affiche la longueur maximale du modèle, puis ne liste que les fragments qui seraient supprimés en silence. Une exécution normale affiche uniquement cette limite :

256

Lorsque tout tient, c’est tout ce que vous voyez. Si certaines parties dépassent cette limite, des lignes supplémentaires apparaissent :

256
silent truncate: 1820 chars / 412 tokens
silent truncate: 960 chars / 301 tokens

Comment lire ces lignes :

Troisièmement, regardez ce qui est finalement stocké :

from rag_qa.store import ingest, open_store

store = open_store()
ingest(chunks, store)
row = store.get(limit=1)
print(row["ids"][0])
print(row["documents"][0][:200])
print(len(row["embeddings"][0]), row["metadatas"][0])
# 384 floats, norm 1.0, metadata.page_label == printed [Pg N]

Cette partie du code montre un seul morceau après l’ingestion et affiche trois lignes :

doc-12-p3-c0
She left school at the age of ten. The next sentence continues on the same page…
384 {'page_label': '3', 'source': 'notes.pdf'}

Quatrièmement, interrogez-le :

from rag_qa.retrieve import search, search_mmr

q = "why did Franklin want Britain to keep Canada"

for hit in search(q, k=5):
    print(f"{hit['score']:.3f}  p.{hit['metadata']['page_label']}  {hit['document'][:120]}")
# overlap makes near-duplicate hits; MMR trades a little score for diversity

for hit in search_mmr(q, k=5):
    print(hit["metadata"]["page_label"], hit["score"])

Un échantillon des résultats :

0.812  p.7  I have long been of opinion that the foundations of the future grandeur and stability of the British empire lie in America
0.781  p.7  they are, nevertheless, broad and strong enough to support the greatest political structure that human wisdom ever yet
0.744  p.7  I am, therefore, by no means for restoring Canada. If we keep it all the country from the St. Lawrence to the Mississippi
0.691  p.8  I left England about the end of August, 1762, in company with ten sail of merchant ships
0.640  p.6  In this Autobiography Franklin tells of his own life to the year 1757, when he went to England

Les quelques meilleurs résultats proviennent souvent de la même page. C’est là l’effet du chevauchement, puisque les segments adjacents d’une même phrase correspondent tous.

Chevauchement montré sur un petit échantillon

Prenons un court passage d’environ 190 caractères tiré de l’introduction du livre, décrivant l’enfance de l’auteur à Boston : ses premiers habitudes de travail acharné et d’économie, une année passée à l’école de grammaire latine de School Street, et son départ à l’âge de dix ans. Divisons-le en segments de seulement 60 caractères chacun.

Sans chevauchement, le séparateur coupe exactement à la limite :

[0] 59  His boyhood in Boston was a stern beginning of the habit of
[1] 55  hard work and rigid economy which marked the man. For a
[2] 58  year he went to the Latin Grammar School on School Street,
[3] 31  but left off at the age of ten.

Le bloc 0 se termine par « the habit of » et le bloc 1 commence par « hard work », sans aucun élément les reliant. L’expression « habit of hard work » n’apparaît jamais complète dans aucun des blocs. Si la coupure a lieu au milieu d’une phrase, chaque vecteur encode seulement une moitié de l’idée, et une requête nécessitant la phrase complète ne correspond pas bien à ces deux moitiés.

Avec une superposition de 15 caractères :

[0] 59  His boyhood in Boston was a stern beginning of the habit of
[1] 57  ⟦the habit of⟧ hard work and rigid economy which marked the
[2] 55  ⟦marked the⟧ man. For a year he went to the Latin Grammar
[3] 58  ⟦Latin Grammar⟧ School on School Street, but left off at the
[4] 22  ⟦off at the⟧ age of ten.

Le marqueur ci-dessous indique quel texte se répète à partir du bloc précédent :

⟦…⟧ = text repeated from the previous chunk

Aucun morceau n’atteint 60 caractères, ce qui montre une fois de plus que cette taille n’est qu’une limite maximale. L’omission des chevauchements donne un index plus petit et élimine les résultats dupliqués, mais tout ce que le séparateur a par hasard coupé est perdu. Les chevauchements permettent de conserver ces liaisons et ne constituent pas un contexte supplémentaire pour son propre compte.

Les quatre tâches et les outils associés

RAG comprend quatre étapes : charger le texte, le découper, l’incorporer, puis stocker et rechercher les vecteurs. Aucune bibliothèque unique ne constitue le « RAG ». Les outils pour le découpage et l’orchestration :

Modèles d’incorporation :

Les bases de données vectorielles populaires complètent l’ensemble nécessaire.