Strategie di chunking in RAG: alternative, compromessi e esempi
Confronto delle strategie di chunking in RAG
Chunking è il parametro iperparametrico più sottovalutato nel Retrieval ‑ Augmented Generation (RAG): determina in silenzio ciò che il tuo LLM “vede”, quanto diventa costosa l’ingestione, e quanto del contesto dell’LLM bruci per ogni risposta.
Questo articolo tratta il chunking come un problema di ottimizzazione ingegneristica: definisci gli obiettivi, scegli una strategia, misura, quindi iterare.
Se sei nuovo dell’architettura RAG, inizia con il principale Tutorial su Retrieval-Augmented Generation (RAG): Architettura, Implementazione e Guida alla Produzione.

TL;DR (Riassunto esecutivo)
I sistemi RAG recuperano chunk, non documenti. Il chunking quindi definisce l’unità di recupero, l’unità di costo di embedding e l’unità di evidenza che puoi mostrare o citare. Nell’originale formulazione RAG, il recupero fornisce passaggi alla generazione; i confini dei passaggi sono effettivamente i confini dei chunk.
Una buona strategia di chunking cerca una frontiera di Pareto tra: qualità del recupero (ricerca/precisione dell’evidenza), coerenza (i chunk devono essere interpretabili) e costo (embedding, archiviazione e latenza delle query). Non esiste una dimensione o metodo di chunking globalmente ottimale, e i sistemi di produzione utilizzano routine strategie miste (ad esempio, chunking strutturale per i PDF + split semantici per la prosa + chunking AST per il codice).
Per la maggior parte delle “domande e risposte sulla documentazione” e dei knowledge base interni, un valore predefinito sicuro è un splitter ricorsivo rispettoso della struttura con un sovrapposizione modesta (per ridurre la perdita ai confini), supportato da un
archivio vettoriale
con filtraggio dei metadati e rirango opzionale. LangChain’s RecursiveCharacterTextSplitter è un’implementazione comune di questa idea di separatore gerarchico; la sovrapposizione esiste specificamente per mitigare la perdita di informazioni quando il contesto rilevante viene tagliato ai confini.
Quando i documenti hanno una struttura forte (PDF con intestazioni, tabelle, elenchi, didascalie), il chunking basato sugli elementi / struttura-aware può superare il taglio basato sul conteggio dei token, producendo meno chunk. Uno studio del 2024 sui documenti SEC ha trovato che il chunking basato sui tipi di elemento ha migliorato i risultati RAG, riducendo anche il numero di chunk (e quindi vettori) di circa la metà rispetto ai metodi non strutturati—tagliando i costi di indicizzazione e potenzialmente migliorando la latenza delle query.
Se puoi permetterti un calcolo iniziale maggiore, il chunking semantico (tagliare ai cambi di argomento usando la similarità degli embedding) può migliorare in modo sostanziale la fedeltà del recupero per il testo narrativo e le pagine con argomenti misti. Algoritmi più vecchi di segmentazione di argomenti come TextTiling mostrano il principio generale: forti cambiamenti lessicali/semantici sono buoni candidati per i confini.
Per materiali molto lunghi, internamente interconnessi (politiche, RFC, standard, manuali di grandi dimensioni), il chunking gerarchico + recupero/merging gerarchico (nodi padre/figlio) può recuperare contesti più lunghi e continui su richiesta. LlamaIndex’s hierarchical node parser produce gerarchie di chunk da grossi a fini, e l’AutoMergingRetriever può unire i nodi foglia in nodi padre al momento del recupero quando vengono recuperati abbastanza figli correlati.
Obiettivi del chunking e trade-off
Il chunking non è solo “suddividi il testo così che si adatti a un modello di embedding”. Controlla diversi comportamenti downstream e operativi.
Granularità del recupero vs rumore del recupero. I chunk più piccoli aumentano la possibilità che la frase esatta contenente una risposta sia recuperabile (maggiore potenziale di richiamo a fissi top-k). Ma producono anche più vettori, aumentando la dimensione dell’indice e a volte mostrando “match vicini” che sono semanticamente simili ma non effettivamente evidenziali (minore precisione). I retriever densi come DPR sono stati costruiti intorno al recupero efficace di passaggi per QA, sottolineando che i confini dei passaggi contano per le prestazioni QA end-to-end.
Coerenza del contesto vs perdita ai confini. I chunk coerenti aiutano l’LLM a ragionare correttamente e ridurre le illusioni riducendo le informazioni complete del contesto locale (definizioni, vincoli, prerequisiti). La sovrapposizione riduce la perdita ai confini ma crea testo duplicato, che può portare a risultati di recupero ridondanti e allungamento del prompt se non si deduplica/merge.
Costo di embedding e indicizzazione. Il costo di embedding è tipicamente proporzionale ai token embedded, e il tempo di ingestione cresce con il numero di chunk (più sovraccarico di scrittura del DB vettoriale). Per gli embedding di OpenAI, le richieste hanno un limite massimo di token per input (8192 token per tutti i modelli di embedding) e un limite massimo di token totali sommati per input per richiesta (300.000 token). Per grandi corpora, l’API Batch può ridurre i costi del ~50% con un turnaround asincrono di 24 ore—utile per backfill e reindicizzazione periodica.
Dimensione dell’indice vettoriale, RAM e latenza. Più chunk significa più vettori e potenzialmente più memoria e query più lente (a seconda del tipo di indice). FAISS presenta esplicitamente il design dell’indice come un insieme di trade-off tra tempo di ricerca, qualità di ricerca e memoria per vettore indicizzato; offre anche implementazioni GPU per la ricerca veloce e approssimativa.
Lunghezza del prompt downstream / utilizzo della finestra di contesto. L’output del recupera diventa budget del prompt. Una strategia di chunking che recupera “esattamente abbastanza” contesto può migliorare la qualità della risposta e ridurre i costi. Al contrario, sovrapposizione e chunk troppo grandi allungano la lunghezza del prompt. Nella pratica, spesso si regola: (dimensione del chunk, sovrapposizione, top-k, rirango/merging) insieme.
Costo di aggiornamento/ingestione e deduplicazione. Il chunking influisce su quanto costoso è aggiornare i dati. I chunk più piccoli rendono gli aggiornamenti parziali più economici (puoi riconvertire solo la sezione modificata) ma rendono anche più difficile la deduplicazione se i chunk sovrapposti o quasi duplicati proliferano.
Dove si trova il chunking nel flusso di lavoro RAG

Strategie di chunking e alternative
Di seguito sono le principali famiglie di chunking che incontrerai nei moderni RAG. Nella pratica, spesso mescoli due: chunking primo struttura (rispetta i confini del documento) più imposizione del budget dei token (assicura che i chunk si adattino ai tuoi budget di embedding e prompt).
Chunking a dimensione fissa
Di cosa si tratta. Suddividi il testo in blocchi di dimensioni uguali per caratteri o token.
Perché esiste. È semplice, veloce, prevedibile e facile da parallelizzare. È anche la strategia più semplice per l’ingestione in streaming dove non hai il contesto completo del documento.
Dove fallisce. Ignora i confini (frasi, sezioni, blocchi di codice) e può rompere le definizioni o dividere “coppie domanda/risposta” tra chunk, aumentando l’errore di recupero.
Profilo operativo. Complessità di ingestione più bassa; numero di chunk prevedibile; caching più semplice. Ma di solito hai bisogno di sovrapposizione (sotto) per evitare la perdita ai confini.
Chunking con sovrapposizione
Di cosa si tratta. Qualsiasi strategia in cui i chunk consecutivi condividono una regione di sovrapposizione fissa (ad esempio 10–20% della dimensione del chunk). La sovrapposizione è standard in molti framework perché riduce la perdita di informazioni quando il contesto è diviso.
Perché è importante. La sovrapposizione è effettivamente un “confine morbido”—consente al recupero di catturare un fatto che si estende su un confine.
Costi e insidie. Più token embedded; più testo duplicato nell’indice; maggiore rischio di recuperare più chunk quasi identici a meno che non si deduplica al momento del recupero (ad esempio, unisci per gli offset del fonte o usa MMR).
Chunking basato su frasi e paragrafi
Di cosa si tratta. Suddividi il testo ai confini delle frasi o dei paragrafi, quindi imballa frasi/paragrafi in chunk fino a un budget di token.
Perché gli ingegneri lo apprezzano. Migliora la coerenza per il linguaggio naturale e è robusto per i documenti con punteggiatura e spazi convenzionali.
Strumenti. NLTK’s sent_tokenize() utilizza per default il rilevamento dei confini delle frasi Punkt, e spaCy offre strumenti basati su regole per i confini delle frasi come Sentencizer (utile quando si desiderano suddivisioni di frasi senza un modello completo di dipendenze).
Modalità di fallimento. Punteggiatura non standard (log, trascrizioni di chat), tabelle, codice e elenchi a puntino possono rompere le ipotesi di suddivisione delle frasi.
Chunking a finestra scorrevole
Di cosa si tratta. Crea chunk utilizzando una dimensione fissa della finestra e un passo (spostamento). Questo è la versione “sovrapposizione sistematica” del chunking.
Quando è buono. Testo a serie temporale, trascrizioni, log di chat, minute di riunioni—qualsiasi cosa dove i fatti rilevanti possono apparire in vicinanza locale e si desidera un ricordo robusto.
Quando è cattivo. Amplifica la ridondanza e può essere costoso su larga scala. Tende anche a recuperare finestre ridondanti a meno che non si deduplica.
Chunking ricorsivo / gerarchia dei separatori
Di cosa si tratta. Inizia con grandi “separatori naturali” (ad esempio \n\n per i paragrafi) e suddividi ricorsivamente in unità più piccole (frasi, spazi) solo quando necessario per rimanere sotto un budget di dimensione. LangChain descrive esplicitamente questo comportamento: un splitter ricorsivo cerca di mantenere intatte le unità più grandi e si riduce solo a separatori più piccoli quando un’unità supera comunque la dimensione del chunk.
Perché è un buon default. Rispetta la struttura senza richiedere un parsing complesso del documento. È un punto pragmatico per Markdown, HTML come testo e documentazione.
Manopole principali di regolazione. chunk_size, chunk_overlap, e la length_function (caratteri vs token), più separatori personalizzati per i codici multilingua.
Chunking semantico (a conoscenza degli embedding)
Di cosa si tratta. Rileva i cambiamenti di argomento utilizzando rappresentazioni semantiche (embedding) e suddividi dove la similarità cala. Questo specchia idee classiche di segmentazione come TextTiling, che utilizza cambiamenti nella coesione lessicale per trovare i confini dei sottotemi.
Perché può superare il chunking basato sulla dimensione. Fermi la suddivisione a conteggi di token arbitrari e allineano i chunk ai confini degli argomenti—spesso migliorando la precisione del recupero per documenti multitematici (blog, documenti di progettazione, ticket, report sugli incidenti).
Costi. Potresti aver bisogno di ulteriori embedding durante il chunking (a livello di frase o paragrafo) prima degli embedding finali del chunk. Questo può raddoppiare o triplicare i chiamati agli embedding a meno che non si riutilizzino gli embedding intermedi.
Trucco pratico. “Packing semantico”: calcola una volta gli embedding delle frasi, raggruppa le frasi in segmenti coerenti tematicamente, quindi embedding ciascun segmento finale.
Chunking gerarchico (padre/figlio)
Di cosa si tratta. Costruisci una rappresentazione multigranularità: chunk padri grossolani (ad esempio, dimensione di sezione) con chunk figli più fini (ad esempio, dimensione di paragrafo). LlamaIndex’s hierarchical node parsing produce per default “gerarchie da grosso a fine” (ad esempio, scale di 2048 → 512 → 128 token), e l’AutoMergingRetriever può unire i nodi figlio nei padri al momento del recupero quando vengono recuperati abbastanza figli correlati.
Perché aiuta. Evita di scegliere tra “chunk piccoli per il ricordo” e “chunk grandi per la coerenza” memorizzando entrambi e selezionandoli al momento della query.
Costi. Logica di ingestione e recupero più complessa, più potenzialmente spazio di archiviazione (perché si memorizzano diverse granularità).
Chunking adattivo / basato su LLM
Di cosa si tratta. Utilizza un LLM per decidere i confini dei chunk (e opzionalmente genera riassunti o intestazioni contestuali). Weaviate descrive esplicitamente il chunking basato su LLM come un LLM che crea chunk coerenti semanticamente, invece di affidarsi a regole fisse o similarità di embedding.
Quando è degno di nota. Corpora ad alto valore dove la correttezza domina il costo (legale, conformità, runbook di supporto), e dove i documenti sono disordinati, eterogenei e male segmentati.
Rischi. Costo, latenza e non determinismo. Vorrà caching, decodifica deterministica e test di regressione (vedi sezione valutazione).
Chunking basato su struttura ed elementi (i documenti non sono solo testo)
Di cosa si tratta. Parsa il documento in elementi (titoli, paragrafi, elenchi, tabelle, didascalie) utilizzando uno strato di comprensione del documento, quindi chunka utilizzando quegli elementi. Le funzioni di chunking di Unstructured utilizzano esplicitamente metadati e elementi del documento (prodotto da partitioning) per produrre chunk per RAG. Docling’s HierarchicalChunker crea chunk per ciascun elemento rilevato del documento e aggiunge metadati strutturali come intestazioni/didascalie.
Evidenza da recenti studi. Uno studio del 2024 sui documenti SEC sostiene che il chunking solo su paragrafi ignora la struttura del documento e propone chunking basato su elementi strutturali; segnala miglioramenti dei risultati RAG e meno chunk/vettori rispetto agli approcci non strutturali.
Perché è importante per il multimodale. Tabelle, figure e didascalie spesso contengono la verità oggettiva. “Flattening” di esse in testo puro può distruggere segnali che il recupero altrimenti sfrutterebbe.
Chunking consapevole del codice (AST/struttura)
Di cosa si tratta. Chunka il codice per unità sintattiche (funzioni, classi, moduli), opzionalmente includendo le docstring e i commenti.
Perché è importante. I tagli basati su dimensione fissa tendono a tagliare a metà le funzioni e a separare le docstring dalle implementazioni—cattivo per la ricerca di codice e per i casi d’uso RAG “spiega questa funzione”.
Opzioni di implementazione. Per Python, il modulo ast integrato è spesso sufficiente. Per repository multilingua, i chunker basati su tree-sitter sono comuni.
Dimensioni di valutazione e come confrontare le strategie di chunking
Il chunking deve essere benchmarkato come componente del sistema.
Metriche della qualità del recupero
Utilizza metriche standard di IR per il layer di recupero:
- Recall@k / Precision@k: Il top-k contiene l’evidenza d’oro?
- MRR / nDCG: L’evidenza d’oro ha un alto ranking?
BEIR è un benchmark eterogeneo ampiamente utilizzato per l’valutazione IR su compiti/domini, e sottolinea i trade-off tra approcci sparsi, densi, late-interaction e rirango.
Il chunking influisce su queste metriche perché definisce cosa conta come “elemento recuperato rilevante”.
Metriche della qualità delle risposte RAG end-to-end
Se stai costruendo QA o assistenti, le metriche di recupero sono necessarie ma non sufficienti. Hai anche bisogno di:
- Recall / precisione del contesto: se i contesti recuperati contengono evidenza rilevante e evitano il rumore.
- Fedeltà: se la risposta generata è supportata dal contesto recuperato.
RAGAS fornisce definizioni concrete e implementazioni per “fedeltà” e altre metriche orientate al RAG.
Dimensioni del costo e delle prestazioni del sistema
Il chunking modifica questi levers:
Latenza (p50/p95). La latenza delle query aumenta di solito con più vettori e più post-processing. Il tuo indice vettoriale conta anche: i tipi di indice FAISS scambiano tempo di ricerca, qualità, memoria e tempo di training/aggiunta.[^faiss]
Costo e throughput degli embedding. Gli embedding di OpenAI sono fatturati per token; l’API degli embedding ha limiti espliciti per input e per richiesta.[^openai_embed_create] Per l’ingestione offline, l’API Batch riduce i costi e offre un maggiore quota in cambio di un turnaround non reale tempo.[^openai_batch]
Dimensione dell’indice e memoria. Approssimativamente, archiviare N vettori float32 di dimensione d costa ~4 * N * d byte solo per i vettori grezzi (più metadati + overhead dell’indice). Il chunking influisce su N. La dimensionalità degli embedding influisce su d, e l’API degli embedding di OpenAI permette di controllare la dimensionalità di output tramite il parametro dimensions.[^openai_embed_create]
Budget del prompt LLM. Chunk più grandi e sovrapposizione inflazionano i token del prompt. Questo può aumentare la latenza e il costo, e aumentare i modi di fallimento “persi nel mezzo” dove i modelli prestano meno attenzione a certi contesti. Nella pratica spesso:
- recuperi chunk piccoli,
- unisci/deduplica,
- riassumi opzionalmente,
- invia un insieme compatto di evidenze all’LLM.
Costo di aggiornamento/ingestione. Chunk più piccoli permettono riconversioni parziali ma aumentano la contabilità. Per l’ingestione in streaming, preferisci chunking deterministico, incrementale (fisso o finestra scorrevole) e aggiungi ID stabili (document_id, range di offset, hash).
Progettazione sperimentale: un loop di benchmark pragmatico
Un benchmark riproducibile del chunking tipicamente ha:
- Un snapshot fisso del corpus + un insieme fisso di query con evidenza d’oro (o almeno span di risposta attesi).
- Un modello di embedding fisso e una configurazione fissa dell’indice vettoriale.
- Un “valutazione solo di recupero” (recall@k, nDCG) più una “valutazione RAG” (fedeltà, rilevanza della risposta).
- Telemetria dei costi: #chunk, token embedded, $/mese di archiviazione, latenza p95 delle query, token del prompt.
L’articolo Unstructured SEC-filings è un buon esempio di valutazione delle strategie di chunking con sia metriche orientate al recupero che misure di accuratezza QA.
Linee guida pratiche, matrice decisionale e default consigliati
Default consigliati che funzionano sorprendentemente bene
Se hai bisogno di una strategia “giorno 1” robusta per la QA generale sulla documentazione:
- Parsing leggero: preserva gli intestazioni e i metadati di base (fonte, titolo della sezione, URL/path, timestamp).
- Chunking con un splitter ricorsivo separatore (paragrafo → frase → parola), con sovrapposizione modesta.
- Embedding con un modello di embedding generale forte.
- Indicizzazione con metadati (ID del documento, sezione, ACL) e deduplicazione durante il recupero.
- Aggiungi rirango o merging gerarchico solo se la tua valutazione mostra un gap.
Questo si allinea con come i framework RAG comuni descrivono sovrapposizione del chunking e suddivisione rispettosa della struttura.
Quale metodo di chunking utilizzare - Matrice decisionale

| Caso d’uso | Default consigliato di chunking | Parametri chiave da regolare | Modalità di fallimento comune | Percorso di upgrade |
|---|---|---|---|---|
| QA su documenti brevi (FAQ, wiki interno) | Chunking ricorsivo/separatore + sovrapposizione | chunk_size, sovrapposizione, top-k |
Evidenza mancante tra frasi ai confini | Aggiungi chunking semantico o riranger |
| QA su documenti lunghi (politiche, standard, manuali) | Chunking gerarchico + retriever di merging | dimensioni padre/figlio, soglia di merging | Recupera frammenti piccoli; LLM non ha contesto completo | Merging automatico/recupero gerarchico |
| Riassunto (per documento / per sezione) | Chunking strutturale (sezioni) | rilevamento sezione, token massimi | I riassunti mancano i collegamenti tra sezioni | Riassunto gerarchico + grafo sezione |
| Ricerca di codice & “spiega questa funzione” | Chunking a livello di AST/funzione | include docstring/commenti, token massimi | Funzione tagliata; perde firma/uso | Gerarchia a conoscenza del repository (modulo→classe→funzione) |
| PDF multimodali (tabelle/figure) | Chunking basato sugli elementi (intestazione/tabella/didascalia) | serializzazione tabella, merging didascalia | Contenuto tabella perso o danneggiato | Utilizza Docling/Unstructured + serializzatori strutturati |
| Ingestione in streaming (log, chat, ticket) | Finestra scorrevole o token fissi | finestra, stride, dedup | Recupero ridondante di finestre | Aggiungi rilevamento dei confini semantici sui batch |
chunking - Confronto qualitativo delle prestazioni
Trattalo come “direzione attesa del cambiamento” (valida con i tuoi dati).
| Strategia | Potenziale di accuratezza del recupero | Coerenza del contesto recuperato | Complessità di ingestione | Conteggio vettori / dimensione indice | Costo di embedding | Impatto sulla latenza delle query | Migliore per |
|---|---|---|---|---|---|---|---|
| Fisso (nessuna sovrapposizione) | Medio | Basso | Basso | Medio | Basso | Medio | prototipi veloci, testo omogeneo |
| Fisso + sovrapposizione | Medio–Alto | Medio | Basso | Alto | Medio–Alto | Medio–Alto | QA dove la perdita ai confini danneggia |
| Packing frase/paragrafo | Alto (prosa) | Alto | Medio | Medio | Medio | Medio | documenti, articoli, prosa pulita |
| Finestra scorrevole | Alto richiamo | Medio | Medio | Molto alto | Molto alto | Alto | trascrizioni, log, chat |
| Ricorsivo/separatore | Alto | Alto | Medio | Medio | Medio | Medio | “default” RAG sui documenti |
| Chunking semantico | Alto–Molto alto | Alto | Alto | Medio | Alto | Medio | pagine multitematiche, testo narrativo |
| Gerarchico (padre/figlio) | Molto alto | Molto alto | Alto | Alto | Alto | Medio | manuali lunghi / standard |
| Basato su LLM/adattivo | Molto alto | Molto alto | Molto alto | Medio | Molto alto | Medio–Alto | corpora ad alto rischio |
| Basato su elementi/struttura | Alto–Molto alto | Alto | Alto | Basso–Medio | Medio | Medio | PDF, report, tabelle, layout misti |
| Consapevole del codice (AST) | Alto (codice) | Alto | Medio | Medio | Medio | Medio | ricerca di codice, assistenti di repository |
Note DevOps e hardware (spesso trascurate)
Le scelte di chunking influiscono su quanta infrastruttura ti serve:
- Chunk più piccoli → più vettori → indici più grandi e più RAM/disk. Per FAISS autohosted, questo può costringere a shard o indici su disco.
- Se embeddi localmente, il throughput degli embedding diventa un problema di scheduling GPU; se embeddi tramite API, il volume di token diventa un problema FinOps (l’API Batch è tuo amico per i backfill).
- Alcuni motori (FAISS) offrono ricerca accelerata da GPU; questo può spostare il costo da CPU limitata da RAM a memoria GPU e throughput PCIe.
- Il parsing strutturale (layout PDF, OCR, estrazione tabelle) è spesso limitato da CPU e può superare il costo degli embedding per documenti scansionati; pianifica separatamente.
Chunking - Implementazioni di riferimento in Python
Tutti gli esempi sono progettati per essere leggibili e eseguibili. Se necessiti di una chiave API o di un database in esecuzione, è chiaro dal codice.
Utilità condivise: conteggio dei token e ID di chunk stabili
from __future__ import annotations
import hashlib
from dataclasses import dataclass
from typing import Any, Iterable, Optional
from transformers import AutoTokenizer # pip install transformers
@dataclass(frozen=True)
class Chunk:
text: str
meta: dict[str, Any]
def sha1_id(*parts: str) -> str:
h = hashlib.sha1()
for p in parts:
h.update(p.encode("utf-8"))
h.update(b"\x1e")
return h.hexdigest()
# Usa qualsiasi tokenizer che si adatti approssimativamente alla tokenizzazione del tuo LLM/embedding.
TOKENIZER = AutoTokenizer.from_pretrained("bert-base-uncased")
def token_len(text: str) -> int:
return len(TOKENIZER.encode(text, add_special_tokens=False))
Chunking a dimensione fissa in token
def chunk_fixed_tokens(
text: str,
*,
chunk_size: int = 512,
) -> list[Chunk]:
token_ids = TOKENIZER.encode(text, add_special_tokens=False)
out: list[Chunk] = []
for i in range(0, len(token_ids), chunk_size):
window = token_ids[i : i + chunk_size]
chunk_text = TOKENIZER.decode(window)
out.append(
Chunk(
text=chunk_text,
meta={"strategy": "fixed_tokens", "start_token": i, "end_token": i + len(window)},
)
)
return out
Chunking a dimensione fissa con sovrapposizione + finestra scorrevole
def chunk_sliding_window(
text: str,
*,
window_tokens: int = 512,
stride_tokens: int = 384, # stride minore = sovrapposizione maggiore
) -> list[Chunk]:
assert 1 <= stride_tokens <= window_tokens, "il stride deve essere compreso tra (0, finestra]"
token_ids = TOKENIZER.encode(text, add_special_tokens=False)
out: list[Chunk] = []
start = 0
while start < len(token_ids):
end = min(start + window_tokens, len(token_ids))
window = token_ids[start:end]
out.append(
Chunk(
text=TOKENIZER.decode(window),
meta={"strategy": "sliding_window", "start_token": start, "end_token": end},
)
)
if end == len(token_ids):
break
start += stride_tokens
return out
Chunking basato su frasi (NLTK) con imballaggio a budget di token
# pip install nltk
import nltk
from nltk.tokenize import sent_tokenize
nltk.download("punkt", quiet=True)
def chunk_by_sentences_nltk(
text: str,
*,
max_tokens: int = 512,
overlap_sentences: int = 1,
) -> list[Chunk]:
sents = [s.strip() for s in sent_tokenize(text) if s.strip()]
out: list[Chunk] = []
buf: list[str] = []
buf_tokens = 0
def flush():
nonlocal buf, buf_tokens
if not buf:
return
chunk_text = " ".join(buf).strip()
out.append(
Chunk(
text=chunk_text,
meta={"strategy": "sentences_nltk", "sent_count": len(buf)},
)
)
# Sovrapposizione mantenendo le ultime N frasi
if overlap_sentences > 0:
buf = buf[-overlap_sentences:]
buf_tokens = token_len(" ".join(buf))
else:
buf, buf_tokens = [], 0
for s in sents:
s_tokens = token_len(s)
# Se una singola frase supera il budget, passa al chunking a dimensione fissa su quel frammento
if s_tokens > max_tokens:
flush()
out.extend(chunk_fixed_tokens(s, chunk_size=max_tokens))
continue
if buf_tokens + s_tokens > max_tokens and buf:
flush()
buf.append(s)
buf_tokens += s_tokens
flush()
return out
Chunking basato su frasi (spaCy) quando si desidera un SBD basato su regole o su modello
# pip install spacy
# python -m spacy download en_core_web_sm
import spacy
def chunk_by_sentences_spacy(
text: str,
*,
max_tokens: int = 512,
) -> list[Chunk]:
# Per divisioni leggere basate su regole (nessun analisi delle dipendenze), usa il sentencizer.
nlp = spacy.blank("en")
nlp.add_pipe("sentencizer") # rilevamento dei confini delle frasi basato su regole
doc = nlp(text)
sents = [sent.text.strip() for sent in doc.sents if sent.text.strip()]
return chunk_by_sentences_nltk(" ".join(sents), max_tokens=max_tokens, overlap_sentences=1)
Chunking ricorsivo con separatori (LangChain)
# pip install langchain-text-splitters
from langchain_text_splitters import RecursiveCharacterTextSplitter
def chunk_recursive_langchain(
text: str,
*,
chunk_size: int = 1200,
chunk_overlap: int = 150,
) -> list[Chunk]:
splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=token_len, # budgeting a conoscenza dei token
separators=["\n\n", "\n", ". ", " ", ""], # personalizza per il tuo contenuto (es. codice)
)
pieces = splitter.split_text(text)
return [
Chunk(text=p, meta={"strategy": "recursive_langchain", "chunk_size": chunk_size, "overlap": chunk_overlap})
for p in pieces
]
Chunking semantico con similarità degli embedding (embedding OpenAI)
Questo approccio calcola gli embedding per unità candidate (frasi/paragrafi), quindi trova i “punti di interruzione” semantici.
# pip install openai numpy
import os
import numpy as np
from openai import OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY", "YOUR_OPENAI_API_KEY"))
def embed_texts_openai(texts: list[str], *, model: str = "text-embedding-3-small") -> np.ndarray:
# NOTA: per grandi lotti, rispetta i limiti di token richiesta e di dimensione del lotto.
resp = client.embeddings.create(model=model, input=texts)
embs = np.array([d.embedding for d in resp.data], dtype=np.float32)
return embs
def cosine_sim(a: np.ndarray, b: np.ndarray) -> float:
denom = (np.linalg.norm(a) * np.linalg.norm(b)) + 1e-8
return float(np.dot(a, b) / denom)
def chunk_semantic(
text: str,
*,
max_tokens: int = 800,
breakpoint_threshold: float = 0.70,
) -> list[Chunk]:
# 1) Parti da candidati frase
sents = [s.strip() for s in sent_tokenize(text) if s.strip()]
if len(sents) <= 1:
return [Chunk(text=text, meta={"strategy": "semantic", "note": "no_split"})]
# 2) Embed ogni frase
embs = embed_texts_openai(sents)
# 3) Calcola le cadute di similarità
sims = [cosine_sim(embs[i], embs[i + 1]) for i in range(len(sents) - 1)]
# 4) Crea segmenti nei punti di interruzione
out: list[Chunk] = []
buf: list[str] = []
buf_tokens = 0
for i, s in enumerate(sents):
# Aggiungi la frase
s_tok = token_len(s)
if buf and buf_tokens + s_tok > max_tokens:
out.append(Chunk(text=" ".join(buf), meta={"strategy": "semantic", "reason": "max_tokens"}))
buf, buf_tokens = [], 0
buf.append(s)
buf_tokens += s_tok
# Decidi il punto di interruzione dopo la frase i (in base alla similarità con la successiva)
if i < len(sims) and sims[i] < breakpoint_threshold:
out.append(
Chunk(
text=" ".join(buf),
meta={"strategy": "semantic", "reason": "sim_drop", "sim_to_next": sims[i]},
)
)
buf, buf_tokens = [], 0
if buf:
out.append(Chunk(text=" ".join(buf), meta={"strategy": "semantic", "reason": "final"}))
return out
Chunking gerarchico + fusione di recupero (LlamaIndex)
# pip install llama-index llama-index-llms-openai
from llama_index.core import Document, StorageContext, VectorStoreIndex
from llama_index.core.node_parser import HierarchicalNodeParser, get_leaf_nodes
from llama_index.core.retrievers import AutoMergingRetriever
from llama_index.core.storage.docstore import SimpleDocumentStore
def build_hierarchical_index(text: str):
docs = [Document(text=text)]
node_parser = HierarchicalNodeParser.from_defaults() # predefinito a dimensioni da grossolano a fine
nodes = node_parser.get_nodes_from_documents(docs)
docstore = SimpleDocumentStore()
docstore.add_documents(nodes)
storage_context = StorageContext.from_defaults(docstore=docstore)
leaf_nodes = get_leaf_nodes(nodes)
base_index = VectorStoreIndex(leaf_nodes, storage_context=storage_context)
base_retriever = base_index.as_retriever(similarity_top_k=6)
retriever = AutoMergingRetriever(base_retriever, storage_context, verbose=True)
return retriever
Chunking basato su elementi per PDF (Docling)
# pip install docling
# NOTA: la qualità del parsing dei PDF dipende dall'ambiente (font, OCR, ecc.).
from docling.document_converter import DocumentConverter
from docling.transforms.chunker.hierarchical_chunker import HierarchicalChunker
def chunk_pdf_docling(pdf_path: str) -> list[Chunk]:
converter = DocumentConverter()
doc = converter.convert(pdf_path).document # DoclingDocument
chunker = HierarchicalChunker()
doc_chunks = list(chunker.chunk(doc))
out: list[Chunk] = []
for c in doc_chunks:
# c.text contiene il contenuto serializzato del chunk; c.meta porta informazioni strutturali
out.append(Chunk(text=c.text, meta={"strategy": "docling_hierarchical", **dict(c.meta)}))
return out
Chunking a conoscenza del codice per Python (AST)
import ast
def chunk_python_by_ast(code: str, *, filepath: str = "<memory>") -> list[Chunk]:
tree = ast.parse(code)
out: list[Chunk] = []
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
start = getattr(node, "lineno", None)
end = getattr(node, "end_lineno", None)
if start is None or end is None:
continue
lines = code.splitlines()
snippet = "\n".join(lines[start - 1 : end])
kind = "class" if isinstance(node, ast.ClassDef) else "function"
name = getattr(node, "name", "<anon>")
out.append(
Chunk(
text=snippet,
meta={
"strategy": "python_ast",
"kind": kind,
"name": name,
"filepath": filepath,
"start_line": start,
"end_line": end,
},
)
)
return out
Esempi di indicizzazione
FAISS (locale) — base minima, veloce
# pip install faiss-cpu numpy
import numpy as np
import faiss
def build_faiss_index(vectors: np.ndarray) -> faiss.Index:
# vectors: forma (N, d), tipo float32
d = vectors.shape[1]
index = faiss.IndexFlatIP(d) # prodotto interno; similitudine coseno se i vettori sono normalizzati
faiss.normalize_L2(vectors)
index.add(vectors)
return index
def faiss_search(index: faiss.Index, query_vec: np.ndarray, k: int = 5):
q = query_vec.astype(np.float32).reshape(1, -1)
faiss.normalize_L2(q)
scores, ids = index.search(q, k)
return ids[0].tolist(), scores[0].tolist()
Chroma (locale) — persistenza semplice per prototipazione RAG
# pip install chromadb
import chromadb
def build_chroma_collection(chunks: list[Chunk], embeddings: np.ndarray, *, path: str = "./chroma_store"):
client = chromadb.PersistentClient(path=path)
col = client.get_or_create_collection(name="docs")
ids = [sha1_id(c.meta.get("strategy", "chunk"), str(i), c.text[:50]) for i, c in enumerate(chunks)]
col.upsert(
ids=ids,
documents=[c.text for c in chunks],
metadatas=[c.meta for c in chunks],
embeddings=embeddings.tolist(),
)
return col
Weaviate (self-hosted / cloud) — vettori forniti da te
# pip install weaviate-client
import weaviate
from weaviate.classes.config import Configure
def weaviate_upsert_self_provided(
chunks: list[Chunk],
embeddings: np.ndarray,
):
client = weaviate.connect_to_local() # o connect_to_weaviate_cloud(...)
try:
collection = client.collections.create(
name="Chunk",
vector_config=Configure.Vectors.self_provided(), # tu fornisci i vettori
)
with collection.batch.dynamic() as batch:
for c, v in zip(chunks, embeddings):
batch.add_object(
properties={"text": c.text, **{f"m_{k}": str(vv) for k, vv in c.meta.items()}},
vector=v.tolist(),
)
# Query vicino al vettore
q = embeddings[0]
res = collection.query.near_vector(near_vector=q.tolist(), limit=5)
for obj in res.objects:
print(obj.properties.get("text")[:200])
finally:
client.close()
Alcuni Documenti di Origine
- Patrick Lewis et al., “Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks”, NeurIPS 2020; arXiv:2005.11401.- Vladimir Karpukhin et al., “Dense Passage Retrieval for Open-Domain Question Answering”, EMNLP 2020; arXiv:2004.04906.
- OpenAI API Reference: Create embeddings (
/v1/embeddings) — limiti di token (8192 per input; 300k totali per richiesta) e parametrodimensions. - Guida OpenAI Batch API + pagina OpenAI API di prezzo (Batch risparmia ~50% con turnaround di 24 ore).
- Documentazione LangChain: RecursiveCharacterTextSplitter e guida all’integrazione dei splitter (dimensione chunk/sovrapposizione, gerarchia ricorsiva dei separatori).
- Documentazione LlamaIndex: HierarchicalNodeParser e AutoMergingRetriever (nodi da grossolano a fine; fusione durante il recupero).
- Blog Weaviate: “Chunking Strategies to Improve LLM RAG Pipeline Performance” (descrizione del chunking basato su LLM e trade-off).
- Documentazione Docling: HierarchicalChunker crea chunk dalla struttura degli elementi del documento e attacca i metadati degli header/caption.
- Jimeno Yepes et al., “Financial Report Chunking for Effective Retrieval Augmented Generation” (arXiv:2402.05131v3, 2024). Marti A. Hearst, “TextTiling: Segmenting Text into Multi-Paragraph Subtopic Passages”, Computational Linguistics, 1997 (ACL Anthology: J97-1003).
- Documentazione FAISS / repository GitHub: trade-off tra tempo di ricerca, qualità, memoria; supporto opzionale GPU.
- Nandan Thakur et al., “BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models”, NeurIPS 2021; arXiv:2104.08663.
- Documentazione RAGAS: “Faithfulness” metrica e metriche correlate di valutazione RAG.
- Documentazione NLTK:
nltk.tokenize.sent_tokenize- tokenizer di frasi basato su Punkt raccomandato. - Documentazione API spaCy:
Sentencizer- rilevamento dei confini delle frasi basato su regole senza analisi delle dipendenze.
Altri link utili
- Advanced RAG: LongRAG, Self-RAG e GraphRAG Explained
- Vector Stores for RAG Comparison
- Reranking with embedding models
- Reranking text documents with Ollama and Qwen3 Embedding model - in Go
- Reranking text documents with Ollama and Qwen3 Reranker model - in Go
- Self-Hosting Cognee (LLM Memory): LLM Performance Tests
- LLM Hosting: Local, Self-Hosted & Cloud Infrastructure Compared
- LLM Performance: Benchmarks, Bottlenecks & Optimization
- Retrieval-Augmented Generation (RAG) Tutorial: Architecture, Implementation, and Production Guide