Dans un pipeline de RAG (Retrieval-Augmented Generation), la première étape est presque toujours la même: prendre un texte volumineux et le découper en morceaux avant de le vectoriser. Les morceaux ne peuvent pas être trop grands, car le modèle a une limite de contexte, ni trop petits, car l’encodage perdrait sa sémantique. Et il faut qu’il y ait un chevauchement entre les voisins, sinon une réponse qui tombe juste sur la frontière serait comprimée entre deux morceaux et le récupérateur échouerait.
Le chunker doit:
- Découper le texte en fenêtres d’au plus N runes.
- Prévoir un chevauchement configurable.
- Ne pas couper un mot en son milieu.
- Être consommable avec une boucle
for-range, sans charger la liste entière en mémoire.
La dernière condition est triviale à partir de Go 1.23 avec le paquet iter. La fonction renvoie une iter.Seq[string] et le consommateur itère comme n’importe quoi :
for chunk := range Chunker(text, 60, 15) {
// encoder, indexer, envoyer au LLM
}
La fenêtre évolue en runes, pas en octets.
Texte comportant des caractères multioctets, des émoticônes au milieu d’un paragraphe: compter les octets donne une taille erronée et il y a aussi le risque de tronquer un caractère en plein milieu.
On effectue la conversion []rune(text) une fois et on travaille ensuite avec l’indice de rune.
La passe entre les fenêtres est size - overlap. Si cela donne <= 0 la configuration devient absurde (un overlap égal ou supérieur à la taille) et l’itérateur se termine sans émettre quoi que ce soit :
func Chunker(text string, size, overlap int) iter.Seq[string] {
return func(yield func(string) bool) {
runes := []rune(text)
n := len(runes)
step := size - overlap
if n == 0 || size <= 0 || step <= 0 {
return
}
// ...
}
}
Pour ne pas couper un mot, il suffit de ramener la fin de la fenêtre jusqu’à la dernière frontière de mot (n’importe quel espace). Si le mot est si long qu’il occupe seul tout le chunk, alors on coupe à la limite.
C’est la seule façon d’aller de l’avant :
cut := end
for cut > i && !unicode.IsSpace(runes[cut]) {
cut--
}
if cut == i {
cut = end
}
Émet le chunk et respecte le protocole de iter.Seq: si le yield retourne false, le consommateur est sorti de la boucle (break, return) et on sort aussi :
if !yield(strings.TrimSpace(string(runes[i:cut]))) {
return
}
La prochaine fenêtre commence à partir de cut - overlap. Cet indice peut se situer au milieu d’un mot, il faut donc avancer jusqu’au mot partiel et ensuite sauter les espaces pour recommencer proprement :
i = max(cut-overlap, 0)
if i > 0 && !unicode.IsSpace(runes[i-1]) {
for i < n && !unicode.IsSpace(runes[i]) {
i++
}
}
for i < n && unicode.IsSpace(runes[i]) {
i++
}
Lorsque la fin de la fenêtre dépasse la fin du texte, on livre le reste et on termine :
if end >= n {
if last := strings.TrimSpace(string(runes[i:n])); last != "" {
yield(last)
}
return
}
En exécutant avec size=60 et overlap=15 :
document : 174 runes | size=60 overlap=15
------------------------------------------------------------
[01] "The Go programming language favors standard libraries and"
[02] "libraries and simple tools. The use of iterators, introduced"
[03] "introduced in recent versions, simplifies the construction"
[04] "construction of complex pipelines."
Chaque chunk commence en répétant la fin du précédent.
C’est cette répétition qui donne au récupérateur une chance de faire coïncider une requête dont la réponse tombe exactement à la frontière de deux chunks.
Le coût est d’avoir plus de vecteurs dans l’index et la probabilité que la même portion apparaisse deux fois dans le top-k. Pour le RAG de production, cela vaut le coup. LangChain et LlamaIndex par défaut visent quelque chose dans cette plage: une size comprise entre 500 et 1000 tokens, un overlap entre 10% et 20%.
Ce chunker est volontairement maladroit. Il ne connaît ni les phrases, ni les paragraphes, ni la sémantique. Pour un pipeline sérieux, vous voudrez probablement découper à la frontière de la phrase, ou bien opter pour un chunker sémantique qui exploite l’embedding des phrases et qui coupe là où la similarité chute.
code source complet




