3) Stratégie de chunking & enrichissement — RAG
Chunking hiérarchique (H1/H2/H3) + fenêtre glissante (overlap 15–25%), taille 400–800 tokens. Contexte augmenté (mots-clés, ancrages, embeddings de headers, tables→texte). Qualité (dupli, lisibilité, longueur utile). Livrables YAML + ablations.
Hiérarchique Fenêtre glissante Taille 400–800 Contexte augmenté Qualité Livrables
Mise en Œuvre des Outils
Algorithmes, heuristiques par domaine, prise en charge des tableaux & blocs de code, multilingue, YAML, ablations & visualisation.
3) Chunking & Enrichissement (Jours 7–12)
Modes & heuristiques
- Hiérarchique : split H1/H2/H3 (ou équivalents Markdown) → passages cohérents & citables.
- Fenêtre glissante : taille 400–800 tokens, overlap 15–25% (densité lexicale forte → +overlap).
- Hybride : hiérarchie primaire + fenêtres sur sections > 1.5× la taille cible.
Réglages par domaine
| Domaine | Taille | Overlap | Tokeniseur | Notes |
|---|---|---|---|---|
| Tech/Docs | 600–800 | 20% | tiktoken/cl100k | Préserver blocs de code entiers |
| Juridique | 500–700 | 25% | tiktoken/cl100k | Paragraphes denses & références |
| FAQ/Support | 400–600 | 15% | tiktoken/cl100k | Questions courtes multi-langue |
Règle empirique : overlap_ratio ≈ min(0.25, 0.1 + entropie_section) ; augmente si vocabulaire très technique.
Config YAML (chunking.yaml)
mode: hybrid # hierarchical | sliding | hybrid
tokenizer: cl100k # info: pour l'estimation des tokens
hierarchy:
levels: [h1, h2, h3]
keep_titles: true
keep_code_blocks: true # ne pas couper au milieu d'un bloc ```code```
keep_tables: true # tables traitées comme bloc atomique
sliding_window:
size_tokens: 700 # 400–800 selon domaine
overlap_ratio: 0.20 # 0.15–0.25
context:
parent_titles: true # ancrages (H1 › H2 › H3)
section_embedding: true
keywords:
method: keybert # rake | keybert
top_k: 6
entities:
ner: simple # none | simple
keep: ["ORG","PRODUCT","LAW","API","CLASS","FUNC"]
tables:
render: markdown # markdown | csv
summarize: true
quality:
dedup:
method: simhash # simhash | minhash
threshold: 0.90
readability:
metric: flesch_fr
min_score: 40
length:
min_tokens: 300
max_tokens: 1200
outputs:
format: jsonl
fields: [id, text, meta, spans, section_emb, quality]
Enrichissements recommandés
- Mots-clés : RAKE/KeyBERT → meta.keywords.
- Ancrages : titres parents (H1 › H2 › H3) → meta.anchors.
- Embeddings de headers : section_emb utile pour le rerank local.
- Tables→texte : conversion (Markdown/CSV) + résumé (2–3 lignes).
- NER léger : entités ORG/PRODUCT/API/LAW pour améliorer la recherche.
Ne pas mélanger plusieurs sections non adjacentes dans un seul chunk → garder une granulosité citables/traçables.
Pseudo-code d’enrichissement
# enrich.py
def enrich_chunk(text, anchors, cfg):
kws = extract_keywords(text, method=cfg["context"]["keywords"]["method"],
top_k=cfg["context"]["keywords"]["top_k"])
ents = simple_ner(text, keep=cfg["context"]["entities"]["keep"]) # clé:label
sec_emb = embed(" › ".join(anchors)) if cfg["context"]["section_embedding"] else None
return {
"text": text,
"meta": {"anchors": anchors, "keywords": kws, "entities": ents},
"section_emb": sec_emb
}Contrôles & score qualité
- Duplicats : SimHash/MinHash ≥ 0.90 → fusion/suppression.
- Lisibilité : Flesch FR ≥ 40 (technique) ; ≥ 50 (FAQ).
- Longueur utile : 300–1200 tokens (hors borne → re-split / merge).
- Boilerplate : ignorer navigateurs, disclaimers, pieds de page répétitifs.
- Offets : conserver spans.char_start/char_end pour l’explanation & les citations précises.
Score composite (ex.) : Q = 0.4·readability_norm + 0.4·coverage_norm + 0.2·(1 - dup_penalty).
Ablations (taille/overlap → hit@k)
| Taille | Overlap | hit@5 | MRR | nDCG@10 |
|---|---|---|---|---|
| 400 | 15% | 0.80 | 0.58 | 0.74 |
| 600 | 20% | 0.86 | 0.64 | 0.80 |
| 800 | 25% | 0.85 | 0.63 | 0.79 |
Pseudo-code (dupli + lisibilité + spans)
# quality.py
def spans(text, win): # renvoie (char_start, char_end) dans le doc original
# ... selon l'indexation durant le split
return win.begin_char, win.end_char
def is_dupe(a, b, thr=0.90):
return simhash(a).similarity(simhash(b)) >= thr
def flesch_fr(text):
m, p, s = count_words(text), max(1,count_sentences(text)), max(1,count_syllables(text))
return 207 - (1.015 * (m/p)) - (0.736 * (s/m))
def quality_flags(t):
L = len(tokens(t))
return {
"readability": flesch_fr(t),
"length_ok": (300 <= L <= 1200),
"boilerplate": is_boilerplate(t)
}Livrables attendus
- Config YAML (ci-dessus) versionnée (ADR liée).
- Rapport d’ablation (CSV/JSON) + recommandations (size/overlap).
- Échantillon JSONL annoté (spans, anchors, keywords, quality).
Sortie JSONL (exemple)
{"id":"docA#H1:Intro#001","text":"...","meta":{"anchors":["Intro"],"keywords":["vision","objectif"],"entities":{"ORG":["ACME"]},"len_tokens":712},"section_emb":[0.01,...],"spans":{"char_start":120,"char_end":1520},"quality":{"readability":48.2,"length_ok":true}}
{"id":"docA#H2:Usage#002","text":"...","meta":{"anchors":["Intro","Usage"],"keywords":["installation","config"]},"section_emb":[0.08,...],"spans":{"char_start":1521,"char_end":2600},"quality":{"readability":45.6,"length_ok":true}}
Implémentation technique – Chunking
Découpage hiérarchique + fenêtres
# algo_chunk.py
def hierarchical_windows(html, size=700, overlap=0.20, keep_code=True, keep_tables=True):
for sec in split_by_headings(html, levels=("h1","h2","h3"), keep_code=keep_code, keep_tables=keep_tables):
toks = tokens(sec.text) # via le tokeniseur choisi
for w in sliding_windows(toks, size=size, overlap=overlap):
yield {
"text": detokenize(w),
"anchors": get_parent_titles(sec),
"spans": (w.begin_char, w.end_char)
}Pipeline complet
# build_chunks.py
cfg = load_yaml("chunking.yaml")
chunks = []
for ch in hierarchical_windows(doc.html, cfg["sliding_window"]["size_tokens"], cfg["sliding_window"]["overlap_ratio"],
cfg["hierarchy"]["keep_code_blocks"], cfg["hierarchy"]["keep_tables"]):
enriched = enrich_chunk(ch["text"], ch["anchors"], cfg)
q = quality_flags(ch["text"])
if q["length_ok"] and not q["boilerplate"]:
chunks.append({**ch, **enriched, "quality": q})
chunks = dedup(chunks, thr=cfg["quality"]["dedup"]["threshold"])
write_jsonl(chunks, "chunks.jsonl")Règles de segmentation
- Ne jamais couper un header (titre) de sa section.
- Respecter les blocs atomiques : table, code, blockquote.
- Fusionner les paragraphes < 80 tokens avec leur voisin pour éviter les micro-chunks.
- Limiter les chunks > 1200 tokens (split doux).
Tables → texte
- Rendu Markdown simple (en-têtes + n lignes max 30).
- Résumé automatique (2–3 phrases) si > 20 colonnes / 30 lignes.
- Stocker meta.table_digest (hash) pour dupli.
# tables.py
def table_to_md(tbl):
md = to_markdown(tbl)[:4000] # couper très longues tables
return md, sha256(md.encode()).hexdigest()Blocs de code
- Conserver fence```lang et le fichier/chemin si connu.
- Ne pas couper un bloc ; si trop long, fournir un résumé + lien ancre.
- Indexer entities.FUNC/CLASS/API extraites.
# code_blocks.py
def keep_or_summarize(code, max_tokens=600):
if len(tokens(code)) <= max_tokens:
return code
return summarize_code(code, max_lines=40)Langues & tokenisation
- Détection langue au document puis au chunk (champ meta.lang si différent).
- Uniformiser le tokeniseur (cl100k) pour comparer tailles/overlaps.
- Normalisation : NFC Unicode + trims + espaces insécables → espaces simples.
Exemple (compte tokens)
# tokens.py
def count_tokens(text, tokenizer="cl100k"):
# wrapper vers le lib choisi ; fallback sur count_words()*1.3
return approx_tokens(text)Validation rapide
# validate_yaml.py
cfg = load_yaml("chunking.yaml")
assert cfg["mode"] in ("hierarchical","sliding","hybrid")
ov = cfg["sliding_window"]["overlap_ratio"]
sz = cfg["sliding_window"]["size_tokens"]
assert 0.10 <= ov <= 0.30 and 300 <= sz <= 1200
assert cfg["quality"]["readability"]["min_score"] >= 30Schéma de sortie
# fields attendus
id, text, meta.anchors[], meta.keywords[], meta.entities{label:[..]},
section_emb[], spans.char_start, spans.char_end, quality.readability, quality.length_okProtocole
- Golden set (Q→passages pertinents) ≥ 200 items / langue.
- Grille : taille ∈ {400, 600, 800} × overlap ∈ {15, 20, 25}%.
- Indexer → retrieve (hybride) → metrics hit@5, MRR, nDCG@10.
- Reporter coût/latence & taille totale de l’index (nb chunks × dim).
Script (pseudo)
# ablate.py
grid = [(400,0.15),(600,0.20),(800,0.25)]
results = []
for size, ov in grid:
chunks = build_chunks(corpus, size=size, overlap=ov)
index(chunks); m = eval_retrieval(questions, gold)
results.append({"size":size,"ov":ov,"hit5":m.hit5,"mrr":m.mrr,"ndcg10":m.ndcg10})
save_json(results, "ablation.json")