Explorando tres estrategias clave para crear una generación aumentada por recuperación (RAG) eficiente
Generación Aumentada por Recuperación (RAG) es una técnica útil para usar tus propios datos en un Chatbot impulsado por IA. En esta publicación del blog, te guiaré por tres estrategias clave para sacar el máximo provecho de RAG:
Fragmentación inteligente de texto 📦:
- El primer paso es descomponer tus datos de texto en fragmentos significativos y manejables. Este paso garantiza que tu Base de datos vectorial pueda recuperar la información más relevante de forma rápida y precisa.
Iteración con diferentes modelos de embeddings 🔍:
- Iterar sobre el modelo de embeddings es crucial. El modelo de embeddings determina cómo se representan tus datos como vectores. Los vectores son la lingua franca de la IA, mejorando la forma en que la Base de datos vectorial puede recuperar las piezas de información adecuadas.
Experimentar con diferentes LLMs o modelos generativos 🧪:
- Cada API de Modelo de Lenguaje (LLM) tiene diferentes costos, latencias y precisiones. Probarlas te permite elegir la que mejor funcione para tu carga de trabajo.
¡Profundicemos y exploremos cómo funcionan estas estrategias y cómo puedes identificar las configuraciones de mejor rendimiento para tus aplicaciones RAG del mundo real con evaluaciones! 🚀📚
Fragmentación inteligente de texto
La fragmentación de texto es como cortar una historia larga en piezas más pequeñas y fáciles de manejar para que una computadora pueda encontrar y usar fácilmente las partes más importantes al responder preguntas o ayudar con tareas.
A continuación, explicaré algunas técnicas diferentes. Estas técnicas están explicadas muy bien y en profundidad en este artículo original de Greg Kamradt.
División recursiva de texto por caracteres 🔄:
- Dividir el texto en fragmentos según el número de caracteres garantiza que cada pieza sea manejable y coherente.
División de texto de pequeño a grande 📏:
- Comenzar con fragmentos más grandes y descomponerlos progresivamente en otros más pequeños. Buscar usando pequeños, pero recuperar usando grandes.
División semántica de texto 🧠:
- Dividir el texto según el significado para que cada fragmento represente una idea o tema completo, garantizando que se conserve el contexto.
Estos métodos te ayudarán a organizar y recuperar texto de manera efectiva para diversas aplicaciones. ¡Sumérgete para explorar cómo funciona cada técnica!
División recursiva de texto por caracteres 🔄
Comienza dividiendo el texto en fragmentos de tamaño fijo con solapamiento de tamaño fijo usando RecursiveCharacterTextSplitter de 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')
Imagen del autor: gráfico de Matplotlib de las longitudes de fragmentos de RecursiveCharacterTextSplitter, código completo disponible en GitHub.
División de texto de pequeño a grande 📏
Esta técnica busca usando fragmentos pequeños (hijos), pero recupera usando fragmentos grandes (padres) de texto. Se utilizan dos almacenes de memoria: 1) almacenamiento de documentos y 2) almacenamiento vectorial. El código a continuación usa MultiVectorRetriever de 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
# Create doc storage for the parent documents
store = InMemoryByteStore()
id_key = "doc_id"
# Create vectorstore for vector index and retrieval.
COLLECTION_NAME = "MilvusDocs"
vectorstore = Milvus(
collection_name=COLLECTION_NAME,
embedding_function=embed_model,
connection_args={"uri": "./milvus_demo.db"},
auto_id=True,
# Set to True to drop the existing collection if it exists.
drop_old=True,
)
# The MultiVectorRetriever (empty to start)
retriever = MultiVectorRetriever(
vectorstore=vectorstore,
byte_store=store,
id_key=id_key,
)
PARENT_CHUNK_SIZE = 1586
# The splitter to use to create bigger (parent) chunks
parent_text_splitter = RecursiveCharacterTextSplitter(
chunk_size=PARENT_CHUNK_SIZE,
length_function = len, # use built-in Python len function
# separators=["\n\n"], # split at end of paragraphs
)
# Parent docs directly from raw docs
parent_docs = parent_text_splitter.split_documents(docs)
doc_ids = [str(uuid.uuid4()) for _ in parent_docs]
# Inspect chunk lengths
print(f"{len(docs)} docs split into {len(parent_docs)} parent documents.")
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}")
# 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 parent docs
sub_docs = child_text_splitter.split_documents(parent_docs)
# Inspect chunk lengths
print(f"{len(docs)} docs split into {len(sub_docs)} child documents.")
plot_chunk_lengths(sub_docs, 'Small-to-big')
fragmentación de pequeño a grande.png
Imagen del autor: gráfico de Matplotlib de longitudes de fragmentos de pequeño a grande; el código completo está disponible en github.
División semántica de texto 🧠
Este fragmentador funciona determinando cuándo "separar" las oraciones. Realiza esta tarea calculando distancias coseno entre oraciones adyacentes. Al observar todas estas distancias coseno, busca distancias atípicas que superen algún umbral. Estas distancias atípicas determinan cuándo se dividen los fragmentos.
Hay algunas formas de determinar ese umbral, que se controlan mediante el breakpoint_threshold_type kwarg.
from langchain_experimental.text_splitter import SemanticChunker
semantic_docs = []
for doc in docs:
# Extract and clean document content.
cleaned_content = clean_text(doc.page_content)
# Initialize the SemanticChunker with the embedding model.
text_splitter = SemanticChunker(embed_model)
semantic_list = text_splitter.create_documents([cleaned_content])
# Append the list of semantic chunks to semantic_docs.
semantic_docs.extend(semantic_list)
# Inspect chunk lengths
print(f"Created {len(semantic_docs)} semantic documents from {len(docs)}.")
plot_chunk_lengths(semantic_docs, 'Semantic')
Usaremos la documentación de Milvus como nuestros datos y Ragas como método de evaluación para tu RAG. Lee mi blog sobre cómo usar RAGAS.
El resultado fue:
- Método de fragmentación = Recursive Character Text Splitter con top_k=2 fue el mejor.
Diferentes modelos de Embedding
Fijando el método de fragmentación en Recursive Character Text Splitter con top_k=2, probé dos modelos de Embedding diferentes.
BAAI/bge-large-en-v1.5
Text-embedding-3-small con embedding-dim = 512
Usando la documentación de Milvus y el método de evaluación Ragas, el resultado fue:
- Modelo de Embedding = BAAI/bge-large-en-v1.5 fue el mejor.
Diferentes LLMs
Después de fijar el método de fragmentación como Recursive Character Text Splitter con top_k=2 y el modelo de Embedding como BAAI/bge-large-en-v1.5, probé seis endpoints de API de LLM diferentes.
Usando la documentación de Milvus y el método de evaluación Ragas, el resultado fue:
- LLM = MistralAI mixtral_8x7b_instruct usando Anyscale Endpoints fue el mejor.
Conclusión
La evaluación de un pipeline RAG variará según tus datos y caso de uso particulares. Una conclusión clave a partir de la experiencia personal y la literatura es que las mejoras más significativas a menudo provienen de refinar tus estrategias de recuperación. 🛠️
Usando datos de la documentación de Milvus y la evaluación de Ragas, este blog observó:
35% de mejora al cambiar la estrategia de fragmentación 📦
27% de mejora al cambiar el modelo de Embedding 🔍
6% de mejora al cambiar el modelo LLM 🤖
Iterar sobre estos elementos puede ayudar a optimizar tu pipeline RAG para obtener mejores resultados.
Referencias
Tutorial de Greg Kamradt sobre 5 niveles diferentes de división 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
Cómo usar RAGAS para evaluar tu pipeline RAG: https://medium.com/towards-data-science/rag-evaluation-using-ragas-4645a4c6c477
Sigue leyendo

Introducing Zilliz Cloud Global Cluster: Region-Level Resilience for Mission-Critical AI
Zilliz Cloud Global Cluster delivers multi-region resilience, automatic failover, and fast global AI search with built-in security and compliance.

Introducing Business Critical Plan: Enterprise-Grade Security and Compliance for Mission-Critical AI Applications
Discover Zilliz Cloud’s Business Critical Plan—offering advanced security, compliance, and uptime for mission-critical AI and vector database workloads.

How to Build RAG with Milvus, QwQ-32B and Ollama
Hands-on tutorial on how to create a streamlined, powerful RAG pipeline that balances efficiency, accuracy, and scalability using the QwQ-32B and Milvus.



