Creare un chatbot open source usando LangChain e Milvus in meno di 5 minuti
Nel mio blog precedente, abbiamo visto come iniziare con una connessione Milvus in pochi minuti. Questo post utilizzerà uno stack RAG (Retrieval Augmented Generation) completamente open-source con LangChain per rispondere a domande su Milvus usando le pagine web della documentazione del nostro prodotto.
Usare Q&A open-source con retrieval fa risparmiare denaro, poiché effettuiamo chiamate gratuite ai nostri dati quasi sempre: retrieval, valutazione e iterazioni di sviluppo. Effettuiamo una chiamata a pagamento a OpenAI solo una volta, per il passaggio finale di generazione della chat.
Per chi è interessato ad approfondire gli aspetti tecnici, il codice sorgente di un ChatBot live è disponibile sul nostro GitHub. Il codice completo di questo notebook si trova nel nostro bootcamp Git Hub.
RAG (Retrieval Augmented Generation) viene usata per ancorare il testo dell’IA generativa (basando il testo generato su dati fattuali e personalizzati per ridurre le allucinazioni). Il testo proveniente dai tuoi dati personalizzati, che ritieni veritieri, come la documentazione di prodotto, viene recuperato da un database vettoriale per rispondere a una domanda. Quindi, inserisci le risposte testuali accurate come “context” insieme alla “question” nel “prompt,” che fornisci a un LLM come ChatGPT di OpenAI. L’LLM genera una risposta in chat simile a quella umana e ancorata ai fatti.
Processi RAG:
Inizia con i tuoi dati personalizzati, che ritieni veritieri, e un modello di embedding per l’encoder.
Suddividi in chunk e genera gli embedding dei tuoi dati usando l’encoder. Salva i dati e i metadati in un database vettoriale.
L’utente pone una domanda. Genera gli embedding della domanda usando lo stesso encoder del passaggio 1.
Recupera le risposte alla tua domanda eseguendo una ricerca semantica usando il database vettoriale.
Inserisci i chunk di testo delle risposte dai tuoi documenti personalizzati in un “Context.” Inserisci la domanda e il contesto in un prompt. Invia il prompt a un LLM generativo.
Ricevi una risposta affidabile dall’LLM generativo.
Passaggio 1: Ingerire i dati
Milvus è un database vettoriale ad alte prestazioni che semplifica l’ingestione di dati non strutturati personalizzati e la creazione di embedding. Milvus è ottimizzato per l’archiviazione, l’indicizzazione e la ricerca rapida di embedding (o vettori).
OpenAI è un’organizzazione che sviluppa e fornisce modelli e strumenti di IA. È nota per i suoi modelli linguistici all’avanguardia, come la serie GPT (Generative Pre-trained Transformer).
LangChain è una libreria di strumenti e wrapper che aiuta gli sviluppatori a colmare il divario tra il software tradizionale e gli LLM.
I dati che useremo sono le pagine web della documentazione del nostro prodotto. ReadTheDocs è una piattaforma open-source e gratuita per l’hosting di documentazione software, dove la documentazione viene scritta con il generatore di documenti Sphinx.
Iniziamo.
# 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
Il codice sopra scarica le pagine web in una directory locale chiamata rtdocs. Successivamente, leggeremo i documenti in LangChain.
#!pip install langchain
from langchain.document_loaders import ReadTheDocsLoader
loader = ReadTheDocsLoader(
"rtdocs/pymilvus.readthedocs.io/en/latest/",
features="html.parser")
docs = loader.load()
Passaggio 2: Suddividere i dati usando gerarchie HTML
Prima dell'embedding, è necessario decidere la strategia di chunking, la dimensione dei chunk e l'overlap dei chunk. In questa demo, userò:
Strategia = Usare le gerarchie degli header markdown. Mantenere insieme le sezioni markdown a meno che non siano troppo lunghe.
Dimensione del chunk = Usare il parametro del modello di embedding
MAX_SEQ_LENGTHOverlap = Regola empirica 10-15%
Funzioni =
HTMLHeaderTextSplitter di Langchain per suddividere le sezioni markdown.
RecursiveCharacterTextSplitter di Langchain per suddividere ricorsivamente le recensioni lunghe.
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)
Nota sopra che ogni chunk è collegato alla fonte del documento. Inoltre, i titoli degli header vengono mantenuti insieme al chunk di testo markdown. Questi header possono essere utilizzati in seguito per recuperare un'intera sezione di header.
Passo 3: Generare embeddings
La maggior parte delle demo ora utilizza le API OpenAI Embeddings. Poiché si tratta dei tuoi dati personalizzati, perché non usare modelli di embedding open-source e il free-tier Zilliz Cloud per cercare nei tuoi dati quanto vuoi gratuitamente?
I modelli di embedding/retrieval open-source sono validi quanto OpenAI Embeddings (ada-002), secondo gli ultimi risultati del benchmark MTEB. Sotto, possiamo vedere che il modello più piccolo tra quelli con il ranking più alto è bge-large-en-v1.5. Useremo quel modello in questo blog.
Fonte immagine: https://huggingface.co/spaces/mteb/leaderboard, ordinata per colonna, Retrieval Average (15 datasets), stampata il 24 nov 2023.
L'immagine sopra mostra la classifica dei modelli di embedding, con al primo posto voyage-lite-01-instruct (dimensione 4,2 GB, e al terzo posto bge-base-en-v1.5 (dimensione 1,5 GB). OpenAIEmbedding text-embeddings-ada-002 è classificato al 22° posto (non mostrato perché troppo in basso nell'elenco).
Di seguito, inizializziamo un encoder usando il checkpoint del modello di embedding scelto.
#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()
Ora, genera gli embedding usando l’encoder che abbiamo inizializzato da un checkpoint HuggingFace. Assembla tutti i dati insieme in un elenco di dizionari.
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)
Passaggio 4: Crea l’indice Milvus e inserisci i dati
In questo passaggio, scriveremo la quadrupla (vector, text, source, h1, h2) nel database per ogni chunk di testo originale.
Avviamo il nostro server Milvus e connettiamoci. Per usare Milvus serverless ospitato nel cloud, avrai bisogno di una ZILLIZ_API_KEY. Nel mio blog precedente, Connecting to Milvus, ho mostrato le istruzioni per connettersi a Zilliz.
#pip install pymilvus
from pymilvus import connections
ENDPOINT=”https://xxxx.api.region.zillizcloud.com:443”
connections.connect(
uri=ENDPOINT,
token=TOKEN)
Crea una collection Milvus (pensala come una tabella di database) chiamata MilvusDocs. La collection accetta uno schema e un indice. Lo schema usa la lunghezza degli embedding del modello 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”,}
A differenza di Pinecone, inserire tutti i dati e popolare un indice di embedding in Milvus/Zilliz è veloce!
# 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)
Passaggio 5: Fai domande sui tuoi documenti
Ora siamo pronti a fare domande sui nostri documenti personalizzati usando la potenza della ricerca semantica. La ricerca semantica utilizza una tecnica dei vicini più prossimi nello spazio vettoriale per trovare i documenti più pertinenti che rispondono alla domanda dell’utente. La ricerca semantica mira a comprendere il significato dietro domande e documenti, invece di limitarsi a confrontare parole chiave. Durante il recupero, Milvus può anche utilizzare i metadati per migliorare l’esperienza di ricerca (usando espressioni booleane nell’opzione dell’API Milvus expr=).
# 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()
# Embed the question using the same encoder.
embedded_question = torch.tensor(encoder.encode([QUESTION]))
# Normalize embeddings to unit length.
embedded_question = F.normalize(embedded_question, p=2, dim=1)
# Convert the embeddings to list of list of np.float32.
embedded_question = list(map(np.float32, embedded_question))
# Return top k results with AUTOINDEX.
TOP_K = 5
# Run semantic vector search using your query and the vector database.
start_time = time.time()
results = mc.search(
data=embedded_question,
anns_field="vector",
# No params for AUTOINDEX
param={},
# Boolean expression if any
expr="",
output_fields=["h1", "h2", "text", "source"],
limit=TOP_K,
consistency_level="Eventually")
elapsed_time = time.time() - start_time
print(f"Milvus search time: {elapsed_time} sec")
Di seguito, diamo una rapida occhiata a ciò che è stato recuperato. Poi inseriamo tutti i testi in un campo context.
for n, hits in enumerate(results):
print(f"{n}th query result")
for hit in hits:
print(hit)
# Assemble the context as a stuffed string.
context = ""
for r in results[0]:
text = r.entity.text
context += f"{text} "
# Also save the context metadata to retrieve along with the answer.
context_metadata = {
"h1": results[0][0].entity.h1,
"h2": results[0][0].entity.h2,
"source": results[0][0].entity.source,}
Sopra, possiamo vedere che effettivamente sono stati recuperati 5 frammenti di testo. In particolare, il primo frammento contiene la risposta alla domanda sulla metrica predefinita. Poiché abbiamo effettuato il recupero usando l’opzione dell’API Milvus output_fields=, i metadati di fonti e citazioni vengono recuperati insieme al frammento.
id: 445766022949255988, distance: 0.708217978477478, entity: {
'chunk': "...# Optional, default MetricType.L2 } timeout (float) –
An optional duration of time in seconds to allow for the
RPC. …",
'source': 'https://pymilvus.readthedocs.io/en/latest/api.html',
'h1': 'API reference',
'h2': 'Client'}
Passaggio 6: Usa un LLM per generare una risposta di chat alla domanda dell’utente usando il contesto recuperato
Useremo un modello di IA generativa aperto e molto piccolo, o LLM, disponibile su HuggingFace.
#pip install transformers
from transformers import AutoTokenizer, pipeline
tiny_llm = "deepset/tinyroberta-squad2"
tokenizer = AutoTokenizer.from_pretrained(tiny_llm)
# context cannot be empty so just put random text in it.
QA_input = {
'question': question,
'context': 'The quick brown fox jumped over the lazy dog'}
nlp = pipeline('question-answering',
model=tiny_llm,
tokenizer=tokenizer)
result = nlp(QA_input)
print(f"Question: {question}")
print(f"Answer: {result['answer']}")
La risposta non è stata molto utile! Ora, poni la stessa domanda usando il contesto recuperato.
QA_input = {
'question': question,
'context': context,}
nlp = pipeline('question-answering',
model=tiny_llm,
tokenizer=tokenizer)
result = nlp(QA_input)
# Print the question, answer, grounding sources and citations.
Answer = assemble_grounding_sources(result[‘answer’], context_metadata)
print(f"Question: {question}")
print(answer)
Quella risposta sembra un po’ migliore! Abbiamo fatto pratica con il retrieval gratuitamente sui nostri dati usando LLM open-source. Ora facciamo una chiamata a pagamento a OpenAI GPT. Ci aspettiamo la stessa risposta del semplice LLM open-source, ma più naturale.
def prepare_response(response):
return response["choices"][-1]["message"]["content"]
def generate_response(
llm,
temperature=0.0, #0 for reproducible experiments
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)
Riepilogo
Abbiamo dimostrato un chatbot RAG di recupero e risposta alle domande end-to-end su documenti personalizzati. Abbiamo visto quanto sia facile iterare recuperando informazioni e rispondendo alle domande utilizzando i tuoi dati gratuitamente. Questo è stato possibile con LangChain, Milvus e LLM open-source per l’encoder e la generazione della chat. Durante il recupero, Milvus ha fornito fonti e citazioni (basta aggiungere questi campi nei metadati durante il caricamento dei dati e usare ‘output_fields=’ nella chiamata API di recupero). Infine, abbiamo visto che questo approccio consente di risparmiare denaro poiché effettuiamo chiamate gratuite ai nostri dati quasi sempre: recupero, valutazione e iterazioni di sviluppo. Effettuiamo una chiamata a pagamento a OpenAI solo una volta per il passaggio finale di generazione della chat.
Altre risorse per iniziare con Milvus e Zilliz
Continua a leggere

Build Multimodal Search for 3D Assets with Tripo and Zilliz Cloud
Generate 3D assets with Tripo, then search them by text, image, and metadata with multimodal embeddings and Zilliz Cloud.

How to Install and Run OpenClaw (Previously Clawdbot/Moltbot) on Mac
Turn your Mac into an AI gateway for WhatsApp, Telegram, Discord, iMessage, and more — in under 5 minutes.

Will Amazon S3 Vectors Kill Vector Databases—or Save Them?
AWS S3 Vectors aims for 90% cost savings for vector storage. But will it kill vectordbs like Milvus? A deep dive into costs, limits, and the future of tiered storage.



