Explorando Três Estratégias Principais para Criar Geração Aumentada por Recuperação (RAG) Eficiente
Geração Aumentada por Recuperação (RAG) é uma técnica útil para usar seus próprios dados em um Chatbot com tecnologia de IA. Nesta publicação do blog, vou apresentar três estratégias principais para aproveitar ao máximo a RAG:
Fragmentação Inteligente de Texto 📦:
- O primeiro passo é dividir seus dados de texto em fragmentos significativos e gerenciáveis. Esta etapa garante que seu Banco de Dados Vetorial possa recuperar as informações mais relevantes de forma rápida e precisa.
Iteração em Diferentes Modelos de Embedding 🔍:
- Iterar no modelo de embedding é crucial. O modelo de embedding determina como seus dados são representados como vetores. Vetores são a língua franca da IA, melhorando a forma como o Banco de Dados Vetorial pode recuperar as partes certas das informações.
Experimentação com Diferentes LLMs ou Modelos Generativos 🧪:
- Cada API de Modelo de Linguagem (LLM) tem custos, latências e precisões diferentes. Testá-las permite que você escolha aquela que funciona melhor para sua carga de trabalho.
Vamos mergulhar e explorar como essas estratégias funcionam e como você pode identificar as configurações com melhor desempenho para suas aplicações RAG do mundo real com avaliações! 🚀📚
Fragmentação Inteligente de Texto
A fragmentação de texto é como cortar uma longa história em partes menores e fáceis de consumir, para que um computador possa encontrar e usar facilmente as partes mais importantes ao responder perguntas ou ajudar em tarefas.
Abaixo, explicarei algumas técnicas diferentes. Essas técnicas são muito bem explicadas em profundidade neste artigo original de Greg Kamradt.
Divisão Recursiva de Texto por Caracteres 🔄:
- Dividir texto em fragmentos com base na contagem de caracteres garante que cada parte seja gerenciável e coerente.
Divisão de Texto do Pequeno para o Grande 📏:
- Começar com fragmentos maiores e dividi-los progressivamente em menores. Pesquise usando pequeno, mas recupere usando Grande.
Divisão Semântica de Texto 🧠:
- Dividir texto com base no significado, de modo que cada fragmento represente uma ideia ou tópico completo, garantindo que o contexto seja preservado.
Esses métodos ajudarão você a organizar e recuperar texto de forma eficaz para várias aplicações. Mergulhe para explorar como cada técnica funciona!
Divisão Recursiva de Texto por Caracteres 🔄
Comece dividindo o texto em fragmentos de tamanho fixo com sobreposição de tamanho fixo usando o RecursiveCharacterTextSplitter do LangChain.
from langchain.text_splitter import RecursiveCharacterTextSplitter
CHUNK_SIZE = 512
chunk_overlap = np.round(CHUNK_SIZE * 0.10, 0)
print(f"chunk_size: {CHUNK_SIZE}, chunk_overlap: {chunk_overlap}")
# The splitter to use to create smaller (child) chunks.
child_text_splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=chunk_overlap,
length_function = len, # use built-in Python len function
separators = ["\n\n", "\n", " ", ". ", ""], # defaults
)
# Child docs directly from raw docs
sub_docs = child_text_splitter.split_documents(docs)
# Inspect chunk lengths
print(f"{len(docs)} docs split into {len(sub_docs)} child documents.")
plot_chunk_lengths(sub_docs, 'Recursive Character')
Imagem do autor: gráfico Matplotlib dos comprimentos dos fragmentos do RecursiveCharacterTextSplitter, código completo disponível no GitHub.
Divisão de Texto do Pequeno para o Grande 📏
Esta técnica pesquisa usando pequenos blocos (filhos), mas recupera usando grandes blocos (pais) de texto. Dois armazenamentos de memória são usados: 1) armazenamento de documentos e 2) armazenamento vetorial. O código abaixo usa o MultiVectorRetriever do LangChain.
from langchain_milvus import Milvus
from langchain.text_splitter import RecursiveCharacterTextSplitter
import uuid
from langchain.storage import InMemoryByteStore
from langchain.retrievers.multi_vector import MultiVectorRetriever
# Criar armazenamento de documentos para os documentos pais
store = InMemoryByteStore()
id_key = "doc_id"
# Criar vectorstore para índice vetorial e recuperação.
COLLECTION_NAME = "MilvusDocs"
vectorstore = Milvus(
collection_name=COLLECTION_NAME,
embedding_function=embed_model,
connection_args={"uri": "./milvus_demo.db"},
auto_id=True,
# Definir como True para descartar a coleção existente, se ela existir.
drop_old=True,
)
# O MultiVectorRetriever (vazio para começar)
retriever = MultiVectorRetriever(
vectorstore=vectorstore,
byte_store=store,
id_key=id_key,
)
PARENT_CHUNK_SIZE = 1586
# O divisor a ser usado para criar blocos maiores (pais)
parent_text_splitter = RecursiveCharacterTextSplitter(
chunk_size=PARENT_CHUNK_SIZE,
length_function = len, # usar a função len integrada do Python
# separators=["\n\n"], # dividir no fim dos parágrafos
)
# Documentos pais diretamente dos documentos brutos
parent_docs = parent_text_splitter.split_documents(docs)
doc_ids = [str(uuid.uuid4()) for _ in parent_docs]
# Inspecionar comprimentos dos blocos
print(f"{len(docs)} docs divididos em {len(parent_docs)} documentos pais.")
plot_chunk_lengths(parent_docs, 'Parent')
CHUNK_SIZE = 512
chunk_overlap = np.round(CHUNK_SIZE * 0.10, 0)
print(f"chunk_size: {CHUNK_SIZE}, chunk_overlap: {chunk_overlap}")
# O divisor a ser usado para criar blocos menores (filhos).
child_text_splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=chunk_overlap,
length_function = len, # usar a função len integrada do Python
separators = ["\n\n", "\n", " ", ". ", ""], # padrões
)
# Documentos filhos diretamente dos documentos pais
sub_docs = child_text_splitter.split_documents(parent_docs)
# Inspecionar comprimentos dos blocos
print(f"{len(docs)} docs divididos em {len(sub_docs)} documentos filhos.")
plot_chunk_lengths(sub_docs, 'Small-to-big')
small-to-big chunking.png
Imagem do autor: gráfico Matplotlib dos comprimentos de blocos pequeno-para-grande; o código completo está disponível no github.
Divisão Semântica de Texto 🧠
Este divisor de blocos funciona determinando quando "separar" frases. Ele realiza essa tarefa calculando distâncias de cosseno entre frases adjacentes. Observando todas essas distâncias de cosseno, ele procura distâncias discrepantes além de algum limite. Essas distâncias discrepantes determinam quando os blocos são divididos.
Há algumas maneiras de determinar esse limite, que são controladas pelo breakpoint_threshold_type kwarg.
from langchain_experimental.text_splitter import SemanticChunker
semantic_docs = []
for doc in docs:
# Extrair e limpar o conteúdo do documento.
cleaned_content = clean_text(doc.page_content)
# Inicializar o SemanticChunker com o modelo de embedding.
text_splitter = SemanticChunker(embed_model)
semantic_list = text_splitter.create_documents([cleaned_content])
# Acrescentar a lista de blocos semânticos a semantic_docs.
semantic_docs.extend(semantic_list)
# Inspecionar comprimentos dos blocos
print(f"Criados {len(semantic_docs)} documentos semânticos a partir de {len(docs)}.")
plot_chunk_lengths(semantic_docs, 'Semantic')
Usaremos a documentação do Milvus como nossos dados e o Ragas como o método de avaliação para o seu RAG. Leia meu blog sobre como usar o RAGAS.
O resultado foi:
- Método de Chunking = Recursive Character Text Splitter com top_k=2 foi o melhor.
Diferentes Modelos de Embedding
Fixando o método de chunking como Recursive Character Text Splitter com top_k=2, testei dois modelos de Embedding diferentes.
BAAI/bge-large-en-v1.5
Text-embedding-3-small com embedding-dim = 512
Usando a documentação do Milvus e o método de avaliação Ragas, o resultado foi:
- Modelo de embedding = BAAI/bge-large-en-v1.5 foi o melhor.
Diferentes LLMs
Depois de fixar o método de chunking como Recursive Character Text Splitter com top_k=2 e o modelo de Embedding como BAAI/bge-large-en-v1.5, testei seis endpoints de API de LLM diferentes.
Usando a documentação do Milvus e o método de avaliação Ragas, o resultado foi:
- LLM = MistralAI mixtral_8x7b_instruct usando Anyscale Endpoints foi o melhor.
Conclusão
A avaliação do pipeline RAG variará dependendo dos seus dados e caso de uso específicos. Uma conclusão importante, baseada na experiência pessoal e na literatura, é que as melhorias mais significativas frequentemente vêm do refinamento das suas estratégias de recuperação. 🛠️
Usando os dados da documentação do Milvus e a avaliação Ragas, este blog observou:
35% de Melhoria ao Alterar a Estratégia de Chunking 📦
27% de Melhoria ao Alterar o Modelo de Embedding 🔍
6% de Melhoria ao Alterar o Modelo de LLM 🤖
Iterar sobre esses elementos pode ajudar a otimizar seu pipeline RAG para obter melhores resultados!
Referências
Tutorial de Greg Kamradt sobre 5 Níveis diferentes de Divisão de Texto: https://github.com/FullStackRetrieval-com/RetrievalTutorials/blob/main/tutorials/LevelsOfTextSplitting/5_Levels_Of_Text_Splitting.ipynb
LangChain Recursive Character Text Splitter: https://python.langchain.com/v0.2/docs/how_to/recursive_text_splitter/
LangChain Multivector Retriever: https://python.langchain.com/v0.2/docs/how_to/multi_vector/#smaller-chunks
LangChain Semantic Chunker: https://python.langchain.com/v0.2/docs/how_to/semantic-chunker/#standard-deviation
Como usar o RAGAS para avaliar seu pipeline RAG: https://medium.com/towards-data-science/rag-evaluation-using-ragas-4645a4c6c477
Continue lendo

Introducing Customer-Managed Encryption Keys (CMEK) on Zilliz Cloud
We're announcing the general availability of Customer-Managed Encryption Keys (CMEK) on Zilliz Cloud.

My Wife Wanted Dior. I Spent $600 on Claude Code to Vibe-Code a 2M-Line Database Instead.
Write tests, not code reviews. How a test-first workflow with 6 parallel Claude Code sessions turns a 2M-line C++ codebase into a daily shipping pipeline.

Similarity Metrics for Vector Search
Exploring five similarity metrics for vector search: L2 or Euclidean distance, cosine distance, inner product, and hamming distance.



