Sperimentare diverse strategie di chunking tramite LangChain per app LLM
Il chunking è tra i problemi più complessi nella creazione di applicazioni retrieval-augmented generation (RAG). Il chunking è il processo di suddivisione delle frasi in parti più piccole e gestibili per l'elaborazione a valle. Anche se sembra semplice, il diavolo si nasconde nei dettagli. Una strategia di chunking scelta male può portare a risultati irrilevanti o incompleti, rendendo più difficile per un sistema di IA fornire risposte accurate. Ad esempio, chunk troppo piccoli possono perdere il contesto, mentre chunk troppo grandi possono restituire informazioni irrilevanti.
In questo tutorial, esploriamo come diverse strategie di chunking influenzano le prestazioni del recupero per lo stesso dataset; in particolare, ci concentreremo su un caso d'uso con il chunking di Langchain. Alla fine di questa guida, capirai come la dimensione del chunk e la sovrapposizione influenzano la qualità del recupero e otterrai indicazioni pratiche per scegliere i parametri giusti per il tuo specifico caso d'uso. Il codice di questo post può essere trovato in questo GitHub Repo on LLM Experimentation.
Panoramica di LangChain e perché il chunking è importante
LangChain è un framework di orchestrazione per Large Language Model con strumenti integrati per il caricamento dei documenti e la suddivisione del testo. La sua flessibilità lo rende una scelta popolare per la creazione di applicazioni RAG, in cui il chunking svolge un ruolo critico nel determinare la rilevanza e la completezza delle informazioni recuperate.
Il chunking implica la selezione di due parametri principali: un dato chunk, dimensione e sovrapposizione.
Le dimensioni dei chunk definiscono il numero di caratteri o token in ciascun chunk. Chunk più grandi catturano più contesto ma rischiano di restituire informazioni irrilevanti, mentre chunk più piccoli garantiscono precisione ma possono perdere il contesto necessario. La sovrapposizione determina quanto testo è condiviso tra chunk consecutivi. Aiuta a preservare la continuità, soprattutto per idee collegate tra paragrafi, ma una sovrapposizione eccessiva aumenta il carico di elaborazione. L'ottimizzazione di questi parametri assicura un equilibrio tra catturare il contesto e mantenere il focus quando si suddivide il testo, entrambi fondamentali per un recupero accurato.
Le strategie di chunking hanno ampie applicazioni oltre ai flussi di lavoro RAG. Sono cruciali nei chatbot consapevoli del contesto, che si basano sul testo suddiviso in chunk per fornire risposte concise ma complete alle query degli utenti. Per esempio, un bot di supporto può usare il chunking per suddividere le guide alla risoluzione dei problemi in passaggi azionabili senza sopraffare l'utente. Analogamente, nella costruzione di knowledge graph, il chunking estrae entità e relazioni dal testo grezzo, aiutando a trasformare documenti non strutturati in dati strutturati. Questi esempi dimostrano come la giusta strategia di chunking possa migliorare le attività di IA a valle.
Comprendere le sfide di un chunking inadeguato
Una strategia di chunking ottimizzata male può creare problemi significativi nel recupero. Dimensioni dei chunk davvero piccole spesso mancano di contesto sufficiente, producendo risultati frammentati o incompleti. Ad esempio, interrogare un manuale di prodotto potrebbe restituire frammenti isolati come “Passaggio 1: Accendi l'alimentazione,” senza informazioni di accompagnamento sui passaggi successivi. Al contrario, quando la tua applicazione suddivide il testo in chunk troppo grandi, può diluire la specificità delle informazioni recuperate. Una query di similarità semantica che si aspetta istruzioni concise per la risoluzione dei problemi potrebbe restituire un'intera sezione, rendendo più difficile per gli utenti individuare i dettagli rilevanti.
La sovrapposizione aggiunge un ulteriore livello di complessità. Senza sovrapposizione, i sistemi possono perdere la continuità del pensiero tra chunk consecutivi, che è fondamentale per narrazioni o processi collegati. Tuttavia, una sovrapposizione eccessiva può introdurre ridondanza, aumentando i costi di archiviazione ed elaborazione. Bilanciare questi compromessi è essenziale per garantire che i sistemi di recupero forniscano risposte accurate e azionabili.
Importazioni di codice e configurazione di LangChain
Questa prima sezione si concentra sugli import e su altri strumenti di configurazione. Forse la prima cosa che noti del codice qui sotto è che ci sono UN SACCO di import. Quelli più usati sono: os e dotenv, quindi non li tratterò. Sono semplicemente usati per le tue variabili d’ambiente. Esaminiamo passo dopo passo la suddivisione del testo in LangChain con Python e il client pymilvus.
In alto vedrai i nostri tre import per caricare il documento. Per prima cosa c’è NotionDirectoryLoader, che carica una directory con documenti markdown/Notion. Poi abbiamo gli splitter di testo Markdown Header e Recursive Character. Questi suddividono il testo all’interno del documento markdown in base alle intestazioni (lo splitter delle intestazioni), oppure a un insieme di interruzioni di caratteri preselezionate (lo splitter ricorsivo).
Poi abbiamo gli import del retriever. Milvus è il nostro database vettoriale, OpenAIEmbeddings è il nostro modello di embedding e OpenAI è il nostro LLM. Il SelfQueryRetriever è il retriever nativo di LangChain che consente a un database vettoriale di “interrogare se stesso”. Ho scritto di più su come usare LangChain per interrogare un database vettoriale in questo articolo.
Il nostro ultimo import di LangChain è AttributeInfo, che passa un attributo con informazioni al self-query retriever, come si può intuire. Infine, voglio soffermarmi sugli import di pymilvus. Questi servono esclusivamente per motivi di utilità; non ne abbiamo bisogno per lavorare con un database vettoriale in LangChain. Uso questi import per ripulire il database alla fine.
L’ultima cosa che facciamo prima di scrivere la funzione è caricare le nostre variabili d’ambiente e dichiarare alcune costanti. La variabile headers_to_split_on è essenziale: elenca tutte le intestazioni che ci aspettiamo di vedere e su cui vogliamo suddividere nel markdown. path indica semplicemente a LangChain dove trovare i documenti Notion.
import os
from langchain.document_loaders import NotionDirectoryLoader
from langchain.text_splitter import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
from langchain.vectorstores import Milvus
from langchain.embeddings import OpenAIEmbeddings
from langchain.llms import OpenAI
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
from pymilvus import connections, utility
from dotenv import load_dotenv
load_dotenv()
zilliz_uri = os.getenv("ZILLIZ_CLUSTER_01_URI")
zilliz_token = os.getenv("ZILLIZ_CLUSTER_01_TOKEN")
headers_to_split_on = [
("##", "Section"),
]
path='./notion_docs'
Costruire una funzione di sperimentazione del chunking
Costruire la funzione di sperimentazione è la parte più critica del tutorial. Come accennato, questa funzione accetta alcuni parametri per l’ingestione dei documenti e la sperimentazione. Dobbiamo fornire il percorso ai documenti, le intestazioni su cui suddividere (splitters), la dimensione del chunk, la sovrapposizione massima della dimensione del chunk e se vogliamo o meno ripulire eliminando le collection alla fine. L’eliminazione della collection è impostata su true per impostazione predefinita
Se possiamo evitarlo, vogliamo creare ed eliminare le collection il più raramente possibile perché comportano overhead che possiamo prevenire. Potresti vedere lo script cambiare mentre cerco buone soluzioni alternative.
Questa funzione è molto simile a quella che abbiamo linkato sopra sull’uso di Notion con LangChain. La prima sezione carica il documento dal percorso usando Notion Directory Loader. Nota che prendiamo solo il contenuto html della prima pagina web (e abbiamo solo una pagina).
Successivamente, prendiamo i nostri splitter. Per prima cosa, usiamo lo splitter markdown per suddividere in base agli header che abbiamo passato sopra. Poi, usiamo il nostro metodo di splitter ricorsivo e suddividiamo in base alla dimensione del chunk e alla sovrapposizione.
È tutto lo splitting di cui abbiamo bisogno. Una volta completato lo splitting, assegniamo un nome alla collection e inizializziamo un’istanza di LangChain Milvus usando le variabili d’ambiente predefinite, gli embedding OpenAI, gli split e il nome della collection. Creiamo anche un elenco di campi di metadata tramite l’oggetto AttributeInfo per indicare al self-query retriever che abbiamo delle “sections.”
Con tutta questa configurazione, otteniamo il nostro LLM e poi lo passiamo a un self-query retriever in python. Da lì, il retriever fa la sua magia quando gli poniamo una domanda sui nostri docs. L’ho configurato anche per dirci quale chunk strategy stiamo testando. Infine, possiamo eliminare la collection se lo desideriamo.
def test_langchain_chunking(docs_path, splitters, chunk_size, chunk_overlap, drop_collection=True):
path=docs_path
loader = NotionDirectoryLoader(path)
docs = loader.load()
md_file=docs[0].page_content
# Let's create groups based on the section headers in our page
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=splitters)
md_header_splits = markdown_splitter.split_text(md_file)
# Define our text splitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
all_splits = text_splitter.split_documents(md_header_splits)
test_collection_name = f"EngineeringNotionDoc_{chunk_size}_{chunk_overlap}"
vectordb = Milvus.from_documents(documents=all_splits,
embedding=OpenAIEmbeddings(),
connection_args={"uri": zilliz_uri,
"token": zilliz_token},
collection_name=test_collection_name)
metadata_fields_info = [
AttributeInfo(
name="Section",
description="Part of the document that the text comes from",
type="string or list[string]"
),
]
document_content_description = "Major sections of the document"
llm = OpenAI(temperature=0)
retriever = SelfQueryRetriever.from_llm(llm, vectordb, document_content_description, metadata_fields_info, verbose=True)
res = retriever.get_relevant_documents("What makes a distinguished engineer?")
print(f"""Responses from chunking strategy:
{chunk_size}, {chunk_overlap}""")
for doc in res:
print(doc)
# this is just for rough cleanup, we can improve this
# lots of user considerations to understand for real experimentation use cases though
if drop_collection:
connections.connect(uri=zilliz_uri, token=zilliz_token)
utility.drop_collection(test_collection_name)
Test e risultati di LangChain
Bene, ora arriva la parte entusiasmante! Diamo un’occhiata ai test e ai risultati.
Codice per testare i chunk di LangChain
Questo breve blocco di codice qui sotto mostra come possiamo eseguire la nostra funzione per la sperimentazione. Ho aggiunto cinque esperimenti. Questo tutorial testa le chunk strategies da 32 a 512 in lunghezza per potenze di 2, con sovrapposizioni che vanno da 4 a 64, anch’esse per potenze di 2. Per testare, iteriamo attraverso l’elenco di tuple e chiamiamo la funzione che abbiamo scritto sopra.
chunking_tests = [(32, 4), (64, 8), (128, 16), (256, 32), (512, 64)]
for test in chunking_tests:
test_langchain_chunking(path, headers_to_split_on, test[0], test[1])
Ecco come appare l’intero output. Ora diamo un’occhiata ai singoli output. Ricorda che la nostra domanda di esempio scelta è: "What makes a distinguished engineer?"
Lunghezza 32, sovrapposizione 4
Ok, da questo possiamo vedere chiaramente che 32 è troppo corto. Questa frase è completamente inutile. “È un Distinguished Engineer” è il ragionamento circolare più evidente possibile.
Lunghezza 64, sovrapposizione 8
64 e 8 non sono molto meglio fin dall’inizio. Ci dà però un esempio di distinguished engineer. Werner Vogels, CTO di Amazon.
Lunghezza 128, sovrapposizione 16
A 128, iniziamo a vedere frasi più complete. Meno parole e risposte del tipo “ingegnere.”. Non è male, riesce a estrarre la parte su Werner Vogel e “Ha raggiunto notevoli risultati tecnici e professionali lavorando come ingegnere.” L’ultima voce in realtà proviene dalla sezione principal engineer.
Uno svantaggio qui è che vediamo già comparire esempi di questi caratteri speciali come \xa0 e \n. Questo ci dice che forse stiamo andando troppo oltre con la lunghezza del chunking.
Lunghezza 256, sovrapposizione 32
Penso che questa lunghezza di chunking sia decisamente troppo lunga. Recupera le voci richieste, ma recupera anche voci da “Fellow”, “Principal Engineer” e “Senior Staff Engineer”. La prima voce però proviene da Distinguished Engineer, e copre tre punti al riguardo.
Lunghezza del chunk 512, sovrapposizione 64
Abbiamo già stabilito che 256 è probabilmente troppo lungo. Tuttavia, il primo risultato di questo 512 è in realtà l’intera sezione per i distinguished engineers. Ora abbiamo un dilemma: vogliamo singole “righe” o “note” oppure recuperare un’intera sezione? Dipende dal tuo caso d’uso.
Riepilogo dell’esperimento con diverse strategie di chunking
Bene, quindi abbiamo visto cinque diverse strategie di text splitter con un approccio parametrizzato che evidenzia le strategie di dimensione dei chunk e di sovrapposizione dei chunk in questo tutorial usando langchain chunking python. Uno dei dilemmi che abbiamo visto semplicemente applicando queste 5 strategie di chunking è tra ottenere singole informazioni e recuperare un’intera sezione a seconda della dimensione del chunk. Abbiamo visto che 128 era piuttosto buono per ottenere singole “righe” o “note” sui distinguished engineers, ma che 512 poteva recuperare l’intera sezione.
Tuttavia, 256 non era così buono.
Questi tre dati ci dicono qualcosa sul text splitter. Non è solo che trovare una dimensione ideale dei chunk è difficile. È anche un segnale che devi pensare a cosa vuoi dalle tue risposte quando progetti le dimensioni dei chunk.
Nota che non siamo nemmeno arrivati a testare diverse sovrapposizioni. Dopo aver imparato e definito una buona strategia di chunking, controllare le sovrapposizioni è il passo logico successivo. Forse lo tratteremo in un futuro tutorial, magari con un’altra libreria. Restate sintonizzati!
Insight dalla sperimentazione
Questi esperimenti evidenziano la relazione sfumata tra dimensione del chunk e sovrapposizione. I chunk più piccoli eccellono nei compiti che richiedono accuratezza puntuale, mentre i chunk più grandi sono più adatti a domande che richiedono un contesto esteso. La sovrapposizione, d’altra parte, svolge un ruolo fondamentale nel bilanciare questi obiettivi.
Vale la pena sottolineare che i compromessi osservati qui non sono validi per tutti. I parametri ideali di chunking dipendono fortemente dal tuo caso d’uso specifico. Applicazioni come agenti conversazionali o strumenti di ricerca rapida possono beneficiare di chunk più piccoli per risposte precise. D’altra parte, flussi di lavoro intensivi di ricerca, come la sintesi di documenti legali complessi, richiedono chunk più grandi per garantire un contesto completo.
Negli scenari reali, un approccio ibrido può spesso fornire i risultati migliori. Regolando dinamicamente le dimensioni dei chunk in base alla query e all'intento dell'utente, gli sviluppatori possono trovare un equilibrio tra efficienza e qualità del recupero. Tali approcci possono comportare l'aggiunta di livelli di routing intelligente delle query o pipeline di chunking adattive su misura per le esigenze degli utenti.
Considerazioni future
Gli esperimenti condotti in questo tutorial sono solo l'inizio. Esplorazioni future potrebbero esaminare strategie di chunking gerarchico che combinano diverse dimensioni dei chunk per una segmentazione multilivello. Inoltre, l'espansione al recupero di dati multimodali, come la combinazione di embedding di testo e immagini, apre possibilità per casi d'uso più complessi.
C'è anche del potenziale nello sperimentare sistematicamente con le sovrapposizioni per comprendere meglio come influenzino il recupero su dataset diversi. Integrare altri framework insieme a LangChain potrebbe affinare ulteriormente gli approcci di chunking e scoprire nuove tecniche di ottimizzazione. Ad esempio, framework come Haystack offrono capacità di recupero aggiuntive che possono integrare il workflow di LangChain.
Infine, incorporare il feedback degli utenti nel processo di recupero potrebbe portare a sistemi adattivi che apprendono e ottimizzano le strategie di chunking nel tempo. Con una sperimentazione più accurata, è possibile stabilire pipeline RAG robuste e incentrate sull'utente.
Conclusione
Il chunking è un componente indispensabile dei workflow di recupero nelle applicazioni RAG. Questo tutorial ha dimostrato l'importanza di regolare attentamente la dimensione dei chunk e la sovrapposizione, mostrando come diverse strategie influenzino i risultati del recupero.
Bilanciando i compromessi tra contesto e precisione, gli sviluppatori possono ottimizzare i propri sistemi per una varietà di casi d'uso. Sebbene questo tutorial abbia trattato esperimenti fondamentali, le possibilità di ulteriore esplorazione sono vaste. Restate sintonizzati per tutorial più avanzati e approfondimenti sull'ottimizzazione dei sistemi di recupero basati sull'AI.
Riferimenti sul chunking
Il chunking, o le strategie di text splitter, continuano a evolversi, quindi abbiamo iniziato a creare una raccolta di queste diverse strategie da esaminare e potenzialmente implementare nella vostra applicazione. Buona lettura!
Una guida alle strategie di chunking per la Retrieval Augmented Generation (RAG). In questa guida abbiamo esplorato vari aspetti delle strategie di chunking nei sistemi Retrieval-Augmented Generation (RAG).
Una guida per principianti al chunking e all'embedding dei siti web per le vostre applicazioni RAG. In questo post, spiegheremo come estrarre contenuti da un sito web e usarli come contesto per gli LLM in un'applicazione RAG. Tuttavia, prima di farlo, dobbiamo comprendere i fondamenti dei siti web.
Esplorare tre strategie chiave per creare una Retrieval Augmented Generation (RAG) efficiente. La Retrieval Augmented Generation (RAG) è una tecnica utile per utilizzare i propri dati in un Chatbot basato sull'AI. Questo post del blog vi guiderà attraverso tre strategie chiave per ottenere il massimo da RAG.
Pandas DataFrame: Chunking e vettorizzazione con Milvus. Se archiviamo tutti i dati, incluso il testo del chunk e l'embedding, all'interno di Pandas DataFrame, possiamo integrarli e importarli facilmente nel database vettoriale Milvus.
Continua a leggere

Zilliz Cloud Update: Tiered Storage, Business Critical Plan, Cross-Region Backup, and Pricing Changes
This release offers a rebuilt tiered storage with lower costs, a new Business Critical plan for enhanced security, and pricing updates, among other features.

Zilliz Cloud Launches in AWS Australia, Expanding Global Reach to Australia and Neighboring Markets
We're thrilled to announce that Zilliz Cloud is now available in the AWS Sydney, Australia region (ap-southeast-2).

Announcing VDBBench 1.0: Open-Source VectorDB Benchmarking with Your Real-World Production Workloads
Discover VDBBench 1.0, an open-source tool for benchmarking vector databases with real-world production data, streaming ingestion, and concurrent workloads.



