Criando um chatbot de código aberto usando LangChain e Milvus em menos de 5 minutos
No meu blog anterior, percorremos como começar com uma conexão Milvus em poucos minutos. Este post usará uma stack RAG (Retrieval Augmented Generation) totalmente open-source com LangChain para responder a perguntas sobre o Milvus usando nossas páginas web de documentação do produto.
Usar Q&A open-source com recuperação economiza dinheiro, já que fazemos chamadas gratuitas aos nossos dados quase o tempo todo - recuperação, avaliação e iterações de desenvolvimento. Só fazemos uma chamada paga à OpenAI uma vez, na etapa final de geração do chat.
Para aqueles interessados em se aprofundar nos aspectos técnicos, o código-fonte de um ChatBot ao vivo está disponível no nosso GitHub. O código completo para este notebook está no nosso bootcamp Git Hub.
RAG (Retrieval Augmented Generation) é usado para ground texto de IA generativa (baseando o texto gerado em dados factuais e personalizados para reduzir alucinações). O texto dos seus dados personalizados, que você acredita serem verdadeiros, como documentação de produto, é recuperado de um banco de dados vetorial para responder a uma pergunta. Então, você insere as respostas de texto precisas como “contexto” juntamente com a “pergunta” no “prompt”, que você fornece a um LLM, como o ChatGPT da OpenAI. O LLM gera uma resposta de chat semelhante à humana que é fundamentada.
Processos de RAG:
Comece com seus dados personalizados, que você acredita serem verdadeiros, e um modelo de embeddings para o codificador.
Divida em chunks e gere embeddings dos seus dados usando o codificador. Salve os dados e metadados em um banco de dados vetorial.
O usuário faz uma pergunta. Gere embeddings da pergunta usando o mesmo codificador da etapa 1.
Recupere respostas para sua pergunta realizando uma busca semântica usando o banco de dados vetorial.
Coloque os chunks de texto de resposta dos seus documentos personalizados em um “Contexto”. Coloque a pergunta e o contexto em um prompt. Envie o prompt para um LLM gerador.
Receba de volta uma resposta confiável do LLM gerador.
Etapa 1: Ingerir os dados
Milvus é um banco de dados vetorial de alto desempenho que simplifica a ingestão de dados não estruturados personalizados e a criação de embeddings. O Milvus é otimizado para armazenamento, indexação e busca rápidos de embeddings (ou vetores).
OpenAI é uma organização que desenvolve e fornece modelos e ferramentas de IA. É conhecida por seus modelos de linguagem de ponta, como a série GPT (Generative Pre-trained Transformer).
LangChain é uma biblioteca de ferramentas e wrappers que ajuda desenvolvedores a preencher a lacuna entre software tradicional e LLMs.
Os dados que usaremos são nossas páginas web de documentação do produto. ReadTheDocs é uma plataforma open-source e gratuita de hospedagem de documentação de software onde a documentação é escrita com o gerador de documentos Sphinx.
Vamos começar.
# Download readthedocs pages locally.
DOCS_PAGE="https://pymilvus.readthedocs.io/en/latest/"
wget -r -A.html -P rtdocs --header="Accept-Charset: UTF-8" $DOCS_PAGE
O código acima baixa as páginas web para um diretório local chamado rtdocs. Em seguida, leremos a documentação no LangChain.
#!pip install langchain
from langchain.document_loaders import ReadTheDocsLoader
loader = ReadTheDocsLoader(
"rtdocs/pymilvus.readthedocs.io/en/latest/",
features="html.parser")
docs = loader.load()
Etapa 2: Dividir os dados em chunks usando hierarquias HTML
Antes da incorporação, é necessário decidir sua estratégia de chunks, o tamanho do chunk e a sobreposição de chunks. Nesta demo, vou usar:
Estratégia = Usar hierarquias de cabeçalhos markdown. Manter as seções markdown juntas, a menos que sejam longas demais.
Tamanho do chunk = Usar o parâmetro do modelo de incorporação
MAX_SEQ_LENGTHSobreposição = Regra geral de 10-15%
Funções =
HTMLHeaderTextSplitter do Langchain para dividir seções markdown.
RecursiveCharacterTextSplitter do Langchain para dividir reviews longas recursivamente.
from langchain.text_splitter import HTMLHeaderTextSplitter, RecursiveCharacterTextSplitter
# Define the headers to split on for the HTMLHeaderTextSplitter
headers_to_split_on = [
("h1", "Header 1"),
("h2", "Header 2"),]
# Create an instance of the HTMLHeaderTextSplitter
html_splitter = HTMLHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
# Use the embedding model parameters.
chunk_size = MAX_SEQ_LENGTH - HF_EOS_TOKEN_LENGTH
chunk_overlap = np.round(chunk_size * 0.10, 0)
# Create an instance of the RecursiveCharacterTextSplitter
child_splitter = RecursiveCharacterTextSplitter(
chunk_size = chunk_size,
chunk_overlap = chunk_overlap,
length_function = len,)
# Split the HTML text using the HTMLHeaderTextSplitter.
html_header_splits = []
for doc in docs:
splits = html_splitter.split_text(doc.page_content)
for split in splits:
# Add the source URL and header values to the metadata
metadata = {}
new_text = split.page_content
for header_name, metadata_header_name in headers_to_split_on:
header_value = new_text.split("¶ ")[0].strip()
metadata[header_name] = header_value
try:
new_text = new_text.split("¶ ")[1].strip()
except:
break
split.metadata = {
**metadata,
"source": doc.metadata["source"]}
# Add the header to the text
split.page_content = split.page_content
html_header_splits.extend(splits)
# Split the documents further into smaller, recursive chunks.
chunks = child_splitter.split_documents(html_header_splits)
end_time = time.time()
print(f"chunking time: {end_time - start_time}")
print(f"docs: {len(docs)}, split into: {len(html_header_splits)}")
print(f"split into chunks: {len(chunks)}, type: list of {type(chunks[0])}")
# Inspect a chunk.
print()
print("Looking at a sample chunk...")
print(chunks[1].page_content[:100])
print(chunks[1].metadata)
Observe acima que cada chunk é fundamentado com a fonte do documento. Além disso, os títulos dos cabeçalhos são mantidos junto com o chunk de texto markdown. Esses cabeçalhos podem ser usados posteriormente para recuperar uma seção inteira de cabeçalho.
Etapa 3: Gerar incorporações
A maioria das demos agora usa as APIs de OpenAI Embeddings. Como estes são seus próprios dados personalizados, por que não usar modelos de incorporação open-source e a camada gratuita do Zilliz Cloud para pesquisar seus próprios dados o quanto quiser gratuitamente?
Modelos open-source de incorporação/recuperação são tão bons quanto OpenAI Embeddings (ada-002), de acordo com os resultados mais recentes do benchmark MTEB. Abaixo, podemos ver que o menor modelo com classificação mais alta é bge-large-en-v1.5. Usaremos esse modelo neste blog.
Fonte da imagem: https://huggingface.co/spaces/mteb/leaderboard, ordenado por coluna, Retrieval Average (15 datasets), impresso em 24 de nov. de 2023.
A imagem acima mostra o ranking dos modelos de incorporação, com a primeira posição voyage-lite-01-instruct (tamanho 4,2 GB, e a terceira posição bge-base-en-v1.5 (tamanho 1,5 GB). OpenAIEmbedding text-embeddings-ada-002 está classificado em 22º (não mostrado, muito mais abaixo na lista).
Abaixo, inicializamos um codificador usando o checkpoint do modelo de incorporação escolhido.
#pip install torch, sentence-transformers
import torch
from sentence_transformers import SentenceTransformer
# Initialize torch settings
DEVICE = torch.device('cuda:3'
if torch.cuda.is_available()
else 'cpu')
# Load the encoder model from huggingface model hub.
model_name = "BAAI/bge-base-en-v1.5"
encoder = SentenceTransformer(model_name, device=DEVICE)
# Get the model parameters and save for later.
MAX_SEQ_LENGTH = encoder.get_max_seq_length()
EMBEDDING_LENGTH = encoder.get_sentence_embedding_dimension()
Agora, gere embeddings usando o encoder que inicializamos a partir de um checkpoint do HuggingFace. Reúna todos os dados em uma lista de dicionários.
chunk_list = []
for chunk in chunks:
# Generate embeddings using encoder from HuggingFace.
embeddings = torch.tensor(encoder.encode([chunk.page_content]))
embeddings = F.normalize(embeddings, p=2, dim=1)
converted_values = list(map(np.float32, embeddings))[0]
# Assemble embedding vector, original text chunk, metadata.
chunk_dict = {
'vector': converted_values,
'text': chunk.page_content,
'source': chunk.metadata['source'],
'h1': chunk.metadata['h1'][:50],
'h2': chunk.metadata['h1'][:50],}
chunk_list.append(chunk_dict)
Etapa 4: Criar índice Milvus e inserir dados
Nesta etapa, escreveremos o quádruplo (vector, text, source, h1, h2) no banco de dados para cada fragmento de texto original.
Vamos iniciar nosso servidor Milvus e conectar a ele. Para usar o Milvus serverless hospedado na nuvem, você precisará de uma ZILLIZ_API_KEY. No meu blog anterior, Conectando ao Milvus, mostrei instruções sobre como se conectar ao Zilliz.
#pip install pymilvus
from pymilvus import connections
ENDPOINT=”https://xxxx.api.region.zillizcloud.com:443”
connections.connect(
uri=ENDPOINT,
token=TOKEN)
Crie uma coleção Milvus (pense nela como uma tabela de banco de dados) chamada MilvusDocs. A coleção recebe um esquema e um índice. O esquema usa o comprimento de embedding do modelo encoder.
from pymilvus import (
FieldSchema, DataType,
CollectionSchema, Collection)
# 1. Define a minimum expandable schema.
fields = [
FieldSchema(“pk”, DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(“vector”, DataType.FLOAT_VECTOR, dim=768),]
schema = CollectionSchema(
fields,
enable_dynamic_field=True,)
# 2. Create the collection.
mc = Collection(“MilvusDocs”, schema)
# 3. Index the collection.
mc.create_index(
field_name=”vector”,
index_params={
“index_type”: “AUTOINDEX”,
“metric_type”: “COSINE”,}
Ao contrário do Pinecone, inserir todos os dados e popular um índice de embeddings no Milvus/Zilliz é rápido!
# Insert data into the Milvus collection.
insert_result = mc.insert(chunk_list)
# After final entity is inserted, call flush
# to stop growing segments left in memory.
mc.flush()
print(mc.partitions)
Etapa 5: Faça perguntas sobre seus documentos
Agora estamos prontos para fazer perguntas sobre nossos documentos personalizados usando o poder da busca semântica. A busca semântica usa uma técnica de vizinhos mais próximos no espaço vetorial para encontrar os documentos correspondentes mais próximos que respondem à pergunta do usuário. A busca semântica visa entender o significado por trás das perguntas e dos documentos, em vez de apenas corresponder palavras-chave. Durante a recuperação, o Milvus também pode utilizar metadados para aprimorar a experiência de busca (usando expressões booleanas na opção expr= da API do Milvus).
# Define a sample question about your data.
QUESTION = "what is the default distance metric used in AUTOINDEX?"
QUERY = [question]
# Before conducting a search, load the data into memory.
mc.load()
# Incorpore a pergunta usando o mesmo codificador.
embedded_question = torch.tensor(encoder.encode([QUESTION]))
# Normalize as incorporações para comprimento unitário.
embedded_question = F.normalize(embedded_question, p=2, dim=1)
# Converta as incorporações para lista de lista de np.float32.
embedded_question = list(map(np.float32, embedded_question))
# Retorne os top k resultados com AUTOINDEX.
TOP_K = 5
# Execute a busca vetorial semântica usando sua consulta e o banco de dados vetorial.
start_time = time.time()
results = mc.search(
data=embedded_question,
anns_field="vector",
# Sem params para AUTOINDEX
param={},
# Expressão booleana, se houver
expr="",
output_fields=["h1", "h2", "text", "source"],
limit=TOP_K,
consistency_level="Eventually")
elapsed_time = time.time() - start_time
print(f"Tempo de busca do Milvus: {elapsed_time} sec")
Abaixo, damos uma olhada rápida no que foi recuperado. Em seguida, colocamos todos os textos em um campo context.
for n, hits in enumerate(results):
print(f"{n}º resultado da consulta")
for hit in hits:
print(hit)
# Monte o contexto como uma string preenchida.
context = ""
for r in results[0]:
text = r.entity.text
context += f"{text} "
# Também salve os metadados de contexto para recuperar junto com a resposta.
context_metadata = {
"h1": results[0][0].entity.h1,
"h2": results[0][0].entity.h2,
"source": results[0][0].entity.source,}
Acima, podemos ver que, de fato, 5 trechos de texto foram recuperados. Em particular, o primeiro trecho contém a resposta para a pergunta sobre a métrica padrão. Como recuperamos usando a opção da API do Milvus output_fields=, os metadados de fontes e citações são recuperados junto com o trecho.
id: 445766022949255988, distance: 0.708217978477478, entity: {
'chunk': "...# Opcional, padrão MetricType.L2 } timeout (float) –
Uma duração opcional de tempo em segundos para permitir para a
RPC. …",
'source': 'https://pymilvus.readthedocs.io/en/latest/api.html',
'h1': 'Referência da API',
'h2': 'Cliente'}
Etapa 6: Use um LLM para gerar uma resposta de chat à pergunta do usuário usando o contexto recuperado
Usaremos um modelo aberto e muito pequeno de IA generativa, ou LLM, disponível no HuggingFace.
#pip install transformers
from transformers import AutoTokenizer, pipeline
tiny_llm = "deepset/tinyroberta-squad2"
tokenizer = AutoTokenizer.from_pretrained(tiny_llm)
# context não pode estar vazio, então basta colocar texto aleatório nele.
QA_input = {
'question': question,
'context': 'A rápida raposa marrom pulou sobre o cachorro preguiçoso'}
nlp = pipeline('question-answering',
model=tiny_llm,
tokenizer=tokenizer)
result = nlp(QA_input)
print(f"Pergunta: {question}")
print(f"Resposta: {result['answer']}")
A resposta não foi muito útil! Agora, faça a mesma pergunta usando o contexto recuperado.
QA_input = {
'question': question,
'context': context,}
nlp = pipeline('question-answering',
model=tiny_llm,
tokenizer=tokenizer)
result = nlp(QA_input)
# Imprima a pergunta, a resposta, as fontes de fundamentação e as citações.
Answer = assemble_grounding_sources(result[‘answer’], context_metadata)
print(f"Pergunta: {question}")
print(answer)
Essa resposta parece um pouco melhor! Praticamos recuperação gratuitamente em nossos próprios dados usando LLMs de código aberto. Agora vamos fazer uma chamada paga para o OpenAI GPT. Esperamos a mesma resposta que o LLM simples de código aberto, mas mais humana.
def prepare_response(response):
return response["choices"][-1]["message"]["content"]
def generate_response(
llm,
temperature=0.0, #0 para experimentos reproduzíveis
grounding_sources=None,
system_content="", assistant_content="", user_content=""):
response = openai.ChatCompletion.create(
model=llm,
temperature=temperature,
api_key=openai.api_key,
messages=[
{"role": "system", "content": system_content},
{"role": "assistant", "content": assistant_content},
{"role": "user", "content": user_content}, ])
answer = prepare_response(response=response)
# Add the grounding sources and citations.
answer = assemble_grounding_sources(answer, grounding_sources)
return answer
# Generate response
response = generate_response(
llm="gpt-3.5-turbo-1106",
temperature=0.0,
grounding_sources=context_metadata,
system_content="Answer the question using the context provided. Be succinct.",
user_content=f"question: {QUESTION}, context: {context}")
# Print the question, answer, grounding sources and citations.
print(f"Question: {QUESTION}")
print(response)
Resumo
Demonstramos um chatbot RAG de recuperação e perguntas e respostas, do início ao fim, em documentos personalizados. Vimos como é fácil iterar recuperando e respondendo a perguntas usando seus dados gratuitamente. Isso foi possível com LangChain, Milvus e LLMs open-source para o codificador e a geração de chat. Durante a recuperação, o Milvus forneceu fontes e citações (basta adicionar esses campos nos metadados durante o carregamento dos dados e usar ‘output_fields=’ na chamada de recuperação da API). Por fim, vimos que essa abordagem economiza dinheiro, pois fazemos chamadas gratuitas aos nossos dados quase o tempo todo — recuperação, avaliação e iterações de desenvolvimento. Fazemos uma chamada paga para a OpenAI apenas uma vez, na etapa final de geração do chat.
Mais recursos para começar com Milvus e Zilliz
Continue lendo

Migrating from S3 Vectors to Zilliz Cloud: Unlocking the Power of Tiered Storage
Learn how Zilliz Cloud bridges cost and performance with tiered storage and enterprise-grade features, and how to migrate data from AWS S3 Vectors to Zilliz Cloud.

Expanding Our Global Reach: Zilliz Cloud Launches in Azure Central India
Zilliz Cloud expands to Azure Central India. This new region helps customers meet compliance, reduce latency, and optimize cloud costs when building AI applications.

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.



