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

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.

How to Build an Enterprise-Ready RAG Pipeline on AWS with Bedrock, Zilliz Cloud, and LangChain
Build production-ready enterprise RAG with AWS Bedrock, Nova models, Zilliz Cloud, and LangChain. Complete tutorial with deployable code.

AI Agents Are Quietly Transforming E-Commerce — Here’s How
Discover how AI agents transform e-commerce with autonomous decision-making, enhanced product discovery, and vector search capabilities for today's retailers.



