Experimentando diferentes estratégias de fragmentação via LangChain para aplicativos de LLM
O chunking está entre os problemas mais desafiadores na criação de aplicações de geração aumentada por recuperação (RAG). Chunking é o processo de dividir frases em partes menores e gerenciáveis para processamento posterior. Embora pareça simples, o diabo está nos detalhes. Uma estratégia de chunking mal escolhida pode levar a resultados irrelevantes ou incompletos, tornando mais difícil para um sistema de IA fornecer respostas precisas. Por exemplo, chunks pequenos demais podem perder contexto, enquanto chunks grandes demais podem retornar informações irrelevantes.
Neste tutorial, exploramos como diferentes estratégias de chunking afetam o desempenho da recuperação para o mesmo conjunto de dados; em particular, vamos nos concentrar em um caso de uso com chunking do Langchain. Ao final deste guia, você entenderá como o tamanho do chunk e a sobreposição influenciam a qualidade da recuperação e obterá insights práticos sobre como escolher os parâmetros certos para seu caso de uso específico. O código desta publicação pode ser encontrado neste repositório do GitHub sobre experimentação com LLM.
Visão geral do LangChain e por que o chunking é importante
LangChain é uma estrutura de orquestração de Large Language Model com ferramentas integradas para carregamento de documentos e divisão de texto. Sua flexibilidade o torna uma escolha popular para criar aplicações RAG, nas quais o chunking desempenha um papel fundamental na determinação da relevância e da completude das informações recuperadas.
O chunking envolve a seleção de dois parâmetros principais: um determinado chunk, tamanho e sobreposição.
Os tamanhos dos chunks definem o número de caracteres ou tokens em cada chunk. Chunks maiores capturam mais contexto, mas correm o risco de retornar informações irrelevantes, enquanto chunks menores garantem precisão, mas podem perder o contexto necessário. A sobreposição determina quanto texto é compartilhado entre chunks consecutivos. Ela ajuda a preservar a continuidade, especialmente para ideias conectadas entre parágrafos, mas a sobreposição excessiva aumenta a sobrecarga de processamento. Otimizar esses parâmetros garante um equilíbrio entre capturar contexto e manter o foco quando você divide texto, ambos essenciais para uma recuperação precisa.
Estratégias de chunking têm amplas aplicações além dos fluxos de trabalho RAG. Elas são cruciais em chatbots sensíveis ao contexto, que dependem de texto dividido em chunks para fornecer respostas concisas, mas abrangentes, às consultas dos usuários. Por exemplo, um bot de suporte pode usar chunking para dividir guias de solução de problemas em etapas acionáveis sem sobrecarregar o usuário. Da mesma forma, na construção de grafos de conhecimento, o chunking extrai entidades e relacionamentos de texto bruto, ajudando a transformar documentos não estruturados em dados estruturados. Esses exemplos demonstram como a estratégia de chunking certa pode aprimorar tarefas posteriores de IA.
Entendendo os desafios de um chunking inadequado
Uma estratégia de chunking mal otimizada pode criar problemas significativos na recuperação. Tamanhos de chunk realmente pequenos muitas vezes carecem de contexto suficiente, produzindo resultados fragmentados ou incompletos. Por exemplo, consultar um manual de produto pode retornar fragmentos isolados como “Etapa 1: Ligue a energia”, sem informações complementares sobre as etapas subsequentes. Por outro lado, quando sua aplicação divide texto em chunks grandes demais, isso pode diluir a especificidade das informações recuperadas. Uma consulta de similaridade semântica que espera instruções concisas de solução de problemas poderia retornar uma seção inteira, tornando mais difícil para os usuários identificar detalhes relevantes.
A sobreposição adiciona outra camada de complexidade. Sem sobreposição, os sistemas podem perder a continuidade do pensamento entre chunks consecutivos, o que é fundamental para narrativas ou processos conectados. No entanto, a sobreposição excessiva pode introduzir redundância, aumentando os custos de armazenamento e processamento. Equilibrar esses trade-offs é vital para garantir que os sistemas de recuperação entreguem respostas precisas e acionáveis.
Importações e configuração de código do LangChain
Esta primeira seção se concentra em imports e outras ferramentas de configuração. Talvez a primeira coisa que você note no código abaixo seja que há UM MONTE de imports. Os mais usados são: os e dotenv, então não vou abordá-los. Eles são simplesmente usados para suas variáveis de ambiente. Vamos percorrer a divisão de texto no LangChain com python e o cliente pymilvus.
No topo, você verá nossos três imports para obter o doc. Primeiro, há NotionDirectoryLoader, que carrega um diretório com docs markdown/Notion. Depois, temos os divisores de texto Markdown Header e Recursive Character. Eles dividem o texto dentro do doc markdown com base em cabeçalhos (o divisor de cabeçalhos), ou em um conjunto de quebras de caracteres pré-selecionadas (o divisor recursivo).
Em seguida, temos os imports do retriever. Milvus é nosso banco de dados vetorial, OpenAIEmbeddings é nosso modelo de embeddings, e OpenAI é nosso LLM. O SelfQueryRetriever é o retriever nativo do LangChain que permite que um banco de dados vetorial “consulte a si mesmo”. Escrevi mais sobre usar o LangChain para consultar um banco de dados vetorial neste artigo.
Nosso último import do LangChain é AttributeInfo, que passa um atributo com informações para o self-query retriever, como se pode imaginar. Por fim, quero falar sobre os imports do pymilvus. Eles são estritamente por motivos utilitários; não precisamos deles para trabalhar com um banco de dados vetorial no LangChain. Uso esses imports para limpar o banco de dados no final.
A última coisa que fazemos antes de escrever a função é carregar nossas variáveis de ambiente e declarar algumas constantes. A variável headers_to_split_on é essencial - ela lista todos os cabeçalhos que esperamos ver e nos quais queremos dividir no markdown. path apenas diz ao LangChain onde encontrar os docs do 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'
Criando uma função de experimentação de chunking
Criar a função de experimentação é a parte mais crítica do tutorial. Como mencionado, esta função recebe alguns parâmetros para ingestão de documentos e experimentação. Precisamos fornecer o caminho para os docs, os cabeçalhos nos quais dividir (splitters), o tamanho do chunk, a sobreposição máxima do tamanho do chunk e se queremos ou não fazer a limpeza descartando as collections no final. O descarte da collection tem true como padrão
Se pudermos evitar, queremos criar e descartar collections o mínimo possível, porque elas têm sobrecargas que podemos prevenir. Você pode ver o script mudar enquanto procuro boas soluções alternativas.
Esta função é muito semelhante à que linkamos acima sobre usar o Notion com o LangChain. A primeira seção carrega o documento a partir do caminho usando o Notion Directory Loader. Observe que pegamos apenas o conteúdo html da primeira página da web (e temos apenas uma página).
Em seguida, pegamos nossos splitters. Primeiro, usamos o splitter de markdown para dividir com base nos cabeçalhos que passamos acima. Depois, usamos nosso método de splitter recursivo e dividimos com base no tamanho do chunk e na sobreposição.
Isso é toda a divisão de que precisamos. Com a divisão concluída, damos um nome à coleção e inicializamos uma instância do LangChain Milvus usando as variáveis de ambiente padrão, embeddings da OpenAI, divisões e o nome da coleção. Também criamos uma lista de campos de metadados por meio do objeto AttributeInfo para informar ao retriever de self-query que temos “seções.”
Com toda essa configuração, obtemos nosso LLM e então o passamos para um retriever de self-query em python. A partir daí, o retriever faz sua mágica quando fazemos uma pergunta sobre nossos docs. Eu o configurei também para nos dizer qual estratégia de chunk estamos testando. Por fim, podemos descartar a coleção se quisermos.
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)
Testes e resultados do LangChain
Certo, agora vem a parte empolgante! Vamos ver os testes e os resultados.
Código para testar chunks do LangChain
Este breve bloco de código abaixo mostra como podemos executar nossa função para experimentação. Adicionei cinco experimentos. Este tutorial testa estratégias de chunk de 32 a 512 de comprimento por potências de 2, com sobreposições indo de 4 a 64 também por potências de 2. Para testar, percorremos a lista de tuplas e chamamos a função que escrevemos acima.
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])
Aqui está como é a saída inteira. Agora vamos dar uma olhada em saídas individuais. Lembre-se de que nossa pergunta de exemplo escolhida é: "O que faz um engenheiro distinto?"
Comprimento 32, sobreposição 4
Certo, então, a partir disso, podemos ver claramente que 32 é curto demais. Esta frase é totalmente inútil. “É um Distinguished Engineer” é o raciocínio circular mais evidente possível.
Comprimento 64, sobreposição 8
64 e 8 não é muito melhor desde o início. Porém, nos dá um exemplo de um distinguished engineer. Werner Vogels, CTO da Amazon.
Comprimento 128, sobreposição 16
Com 128, começamos a ver frases mais completas. Menos palavras e respostas do tipo “engenheiro.”. Isso não é ruim, consegue extrair a parte sobre Werner Vogel e “Alcançou realizações técnicas e profissionais notáveis enquanto trabalhava como engenheiro.” A última entrada, na verdade, vem da seção de principal engineer.
Uma desvantagem aqui é que já vemos exemplos desses caracteres especiais como \xa0 e \n aparecerem. Isso nos diz que talvez estejamos indo longe demais no comprimento dos chunks.
Comprimento 256, sobreposição 32
Acho que este comprimento de chunk é definitivamente longo demais. Ele puxa as entradas necessárias, mas também puxa entradas de “Fellow”, “Principal Engineer” e “Senior Staff Engineer”. A primeira entrada é de Distinguished Engineer, porém, e cobre três pontos sobre ele.
Comprimento do chunk 512, sobreposição 64
Já estabelecemos que 256 provavelmente é longo demais. No entanto, a primeira extração deste 512 é, na verdade, a seção inteira de distinguished engineers. Agora temos um dilema: queremos “linhas” ou “notas” individuais, ou extrair uma seção inteira? Isso depende do seu caso de uso.
Resumo do experimento com diferentes estratégias de chunking
Legal, então vimos cinco estratégias diferentes de text splitter com uma abordagem parametrizada que destaca estratégias de tamanho de chunks e sobreposição de chunks neste tutorial usando langchain chunking python. Um dos dilemas que vimos ao fazer apenas estas 5 estratégias simples de chunks é entre obter pequenos trechos individuais e uma seção inteira de volta dependendo do tamanho do chunk. Vimos que 128 foi muito bom para obter “linhas” ou “notas” individuais sobre distinguished engineers, mas que 512 poderia trazer a seção inteira de volta.
No entanto, 256 não foi tão bom.
Esses três pontos de dados nos dizem algo sobre o text splitter. Não é apenas que encontrar um tamanho ideal de chunks é difícil. Também é um sinal de que você precisa pensar no que deseja das suas respostas ao criar seus tamanhos de chunks também.
Observe que ainda nem chegamos a testar diferentes sobreposições. Depois de aprender e definir uma boa estratégia de chunks, verificar sobreposições é o próximo passo lógico. Talvez abordemos isso em um tutorial futuro, talvez com outra biblioteca. Fique ligado!
Insights da Experimentação
Esses experimentos destacam a relação sutil entre tamanho de chunk e sobreposição. Chunks menores se destacam em tarefas que exigem precisão pontual, enquanto chunks maiores são mais adequados para perguntas que demandam contexto amplo. A sobreposição, por outro lado, desempenha um papel fundamental no equilíbrio desses objetivos.
Vale enfatizar que os trade-offs observados aqui não são uma solução universal. Os parâmetros ideais de chunking dependem muito do seu caso de uso específico. Aplicações como agentes conversacionais ou ferramentas de consulta rápida podem se beneficiar de chunks menores para respostas precisas. Por outro lado, fluxos de trabalho intensivos em pesquisa, como resumir documentos jurídicos complexos, exigem chunks maiores para garantir contexto abrangente.
Em cenários do mundo real, uma abordagem híbrida muitas vezes pode fornecer os melhores resultados. Ao ajustar dinamicamente os tamanhos dos chunks com base na consulta e na intenção do usuário, os desenvolvedores podem encontrar um equilíbrio entre eficiência e qualidade da recuperação. Essas abordagens podem envolver a adição de camadas de roteamento inteligente de consultas ou pipelines de chunking adaptativo adaptados às necessidades do usuário.
Considerações Futuras
Os experimentos conduzidos neste tutorial são apenas o começo. Explorações futuras poderiam examinar estratégias de chunking hierárquico que combinam diferentes tamanhos de chunks para segmentação em vários níveis. Além disso, expandir para a recuperação de dados multimodais, como combinar embeddings de texto e imagem, abre possibilidades para casos de uso mais complexos.
Também há potencial em experimentar com sobreposições de forma sistemática para entender melhor como elas influenciam a recuperação em diversos conjuntos de dados. Integrar outros frameworks junto com LangChain poderia refinar ainda mais as abordagens de chunking e revelar novas técnicas de otimização. Por exemplo, frameworks como Haystack oferecem recursos adicionais de recuperação que podem complementar o fluxo de trabalho do LangChain.
Por fim, incorporar o feedback do usuário ao processo de recuperação poderia levar a sistemas adaptativos que aprendem e otimizam estratégias de chunking ao longo do tempo. Com experimentação mais refinada, é possível estabelecer pipelines RAG robustos e centrados no usuário.
Conclusão
O chunking é um componente indispensável dos fluxos de trabalho de recuperação em aplicações RAG. Este tutorial demonstrou a importância de ajustar cuidadosamente o tamanho dos chunks e a sobreposição, mostrando como diferentes estratégias influenciam os resultados de recuperação.
Ao equilibrar os trade-offs entre contexto e precisão, os desenvolvedores podem otimizar seus sistemas para uma variedade de casos de uso. Embora este tutorial tenha abordado experimentos fundamentais, as possibilidades de exploração adicional são vastas. Fique ligado para tutoriais mais avançados e insights sobre a otimização de sistemas de recuperação impulsionados por IA.
Referências de Chunking
Chunking, ou estratégias de text splitter, continuam a evoluir, então começamos a criar uma coleção dessas diferentes estratégias para examinar e potencialmente implementar em sua aplicação. Aproveite!
Um Guia para Estratégias de Chunking para Retrieval Augmented Generation (RAG). Exploramos várias facetas das estratégias de chunking em sistemas de Retrieval-Augmented Generation (RAG) neste guia.
Um Guia para Iniciantes sobre Chunking e Embedding de Websites para Suas Aplicações RAG. Neste post, explicaremos como extrair conteúdo de um website e usá-lo como contexto para LLMs em uma aplicação RAG. No entanto, antes de fazer isso, precisamos entender os fundamentos de websites.
Explorando Três Estratégias-Chave para Construir Retrieval Augmented Generation (RAG) Eficiente. Retrieval Augmented Generation (RAG) é uma técnica útil para usar seus próprios dados em um Chatbot impulsionado por IA. Este post do blog guiará você por três estratégias-chave para aproveitar ao máximo o RAG.
Pandas DataFrame: Chunking e Vetorização com Milvus. Se armazenarmos todos os dados, incluindo o texto do chunk e o embedding, dentro do Pandas DataFrame, poderemos integrá-los e importá-los facilmente para o banco de dados vetorial Milvus.
Continue lendo

What Is a Vector Lakebase?
A Vector Lakebase is a unified, lake-native data architecture for AI that combines vector-database-grade serving with open lake storage, reusable lake-level indexes, and a shared semantic layer.

Bringing AI to Legal Tech: The Role of Vector Databases in Enhancing LLM Guardrails
Discover how vector databases enhance AI reliability in legal tech, ensuring accurate, compliant, and trustworthy AI-powered legal solutions.

Vector Databases vs. Graph Databases
Use a vector database for AI-powered similarity search; use a graph database for complex relationship-based queries and network analysis.



