Créer un chatbot open source avec LangChain et Milvus en moins de 5 minutes
Dans mon blog précédent, nous avons expliqué comment démarrer avec une connexion Milvus en quelques minutes. Cet article utilisera une pile RAG (Retrieval Augmented Generation) entièrement open source avec LangChain pour répondre aux questions sur Milvus à l’aide des pages web de notre documentation produit.
L’utilisation d’un Q&A open source avec récupération permet d’économiser de l’argent, car nous effectuons des appels gratuits à nos données presque tout le temps : récupération, évaluation et itérations de développement. Nous n’effectuons qu’un seul appel payant à OpenAI pour l’étape finale de génération du chat.
Pour ceux qui souhaitent approfondir les aspects techniques, le code source d’un ChatBot en direct est disponible sur notre GitHub. Le code complet de ce notebook se trouve dans notre bootcamp Git Hub.
RAG (Retrieval Augmented Generation) est utilisé pour ancrer le texte de l’IA générative (en fondant le texte généré sur des données factuelles et personnalisées afin de réduire les hallucinations). Le texte issu de vos données personnalisées, que vous considérez comme vrai, comme la documentation produit, est récupéré depuis une base de données vectorielle pour répondre à une question. Ensuite, vous insérez les réponses textuelles exactes en tant que « contexte » avec la « question » dans le « prompt », que vous transmettez à un LLM tel que ChatGPT d’OpenAI. Le LLM génère une réponse de chat semblable à celle d’un humain qui est ancrée.
Processus RAG :
Commencez par vos données personnalisées, que vous considérez comme vraies, et un modèle d’embedding pour l’encodeur.
Découpez et générez des embeddings de vos données à l’aide de l’encodeur. Enregistrez les données et les métadonnées dans une base de données vectorielle.
L’utilisateur pose une question. Générez des embeddings de la question à l’aide du même encodeur qu’à l’étape 1.
Récupérez les réponses à votre question en effectuant une recherche sémantique à l’aide de la base de données vectorielle.
Insérez les morceaux de texte de réponse issus de vos documents personnalisés dans un « Contexte ». Insérez la question et le contexte dans un prompt. Envoyez le prompt à un LLM génératif.
Récupérez une réponse fiable du LLM génératif.
Étape 1 : Ingérer les données
Milvus est une base de données vectorielle haute performance qui simplifie l’ingestion de données non structurées personnalisées et la création d’embeddings. Milvus est optimisé pour le stockage, l’indexation et la recherche rapides d’embeddings (ou vecteurs).
OpenAI est une organisation qui développe et fournit des modèles et des outils d’IA. Elle est connue pour ses modèles de langage de pointe comme la série GPT (Generative Pre-trained Transformer).
LangChain est une bibliothèque d’outils et de wrappers qui aide les développeurs à combler le fossé entre les logiciels traditionnels et les LLM.
Les données que nous utiliserons sont les pages web de notre documentation produit. ReadTheDocs est une plateforme open source et gratuite d’hébergement de documentation logicielle, où la documentation est écrite avec le générateur de documents Sphinx.
Commençons.
# 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
Le code ci-dessus télécharge les pages web dans un répertoire local appelé rtdocs. Ensuite, nous lirons la documentation dans LangChain.
#!pip install langchain
from langchain.document_loaders import ReadTheDocsLoader
loader = ReadTheDocsLoader(
"rtdocs/pymilvus.readthedocs.io/en/latest/",
features="html.parser")
docs = loader.load()
Étape 2 : Découper les données à l’aide des hiérarchies HTML
Avant l’intégration, il est nécessaire de décider de votre stratégie de découpage, de la taille des chunks et du chevauchement des chunks. Dans cette démo, j’utiliserai :
Stratégie = Utiliser les hiérarchies d’en-têtes markdown. Conserver les sections markdown ensemble, sauf si elles sont trop longues.
Taille des chunks = Utiliser le paramètre du modèle d’embedding
MAX_SEQ_LENGTHChevauchement = Règle empirique de 10-15%
Fonctions =
HTMLHeaderTextSplitter de Langchain pour diviser les sections markdown.
RecursiveCharacterTextSplitter de Langchain pour diviser récursivement les longues critiques.
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)
Remarquez ci-dessus que chaque chunk est ancré avec la source du document. De plus, les titres d’en-tête sont conservés avec le chunk de texte markdown. Ces en-têtes peuvent ensuite être utilisés pour récupérer une section d’en-tête complète.
Étape 3 : Générer des embeddings
La plupart des démos utilisent maintenant les API OpenAI Embeddings. Comme il s’agit de vos propres données personnalisées, pourquoi ne pas utiliser des modèles d’embedding open-source et le niveau gratuit Zilliz Cloud pour rechercher dans vos propres données autant que vous le souhaitez gratuitement ?
Les modèles d’embedding/récupération open-source sont tout aussi performants que OpenAI Embeddings (ada-002), selon les derniers résultats du benchmark MTEB. Ci-dessous, nous pouvons voir que le plus petit modèle le mieux classé est bge-large-en-v1.5. Nous utiliserons ce modèle dans ce blog.
Source de l’image : https://huggingface.co/spaces/mteb/leaderboard, triée par colonne, Retrieval Average (15 datasets), imprimée le 24 nov. 2023.
L’image ci-dessus montre le classement des modèles d’embedding, avec au premier rang voyage-lite-01-instruct (taille 4,2 Go, et au troisième rang bge-base-en-v1.5 (taille 1,5 Go). OpenAIEmbedding text-embeddings-ada-002 est classé 22e (non affiché, plus bas dans la liste).
Ci-dessous, nous initialisons un encodeur à l’aide du checkpoint du modèle d’embedding choisi.
#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()
Maintenant, générez des embeddings à l’aide de l’encodeur que nous avons initialisé à partir d’un checkpoint HuggingFace. Assemblez toutes les données dans une liste de dictionnaires.
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)
Étape 4 : Créer un index Milvus et insérer les données
Dans cette étape, nous allons écrire le quadruplet (vector, text, source, h1, h2) dans la base de données pour chaque fragment de texte original.
Démarrons notre serveur Milvus et connectons-nous-y. Pour utiliser Milvus serverless hébergé dans le cloud, vous aurez besoin d’une ZILLIZ_API_KEY. Dans mon précédent article de blog, Connexion à Milvus, j’ai montré les instructions pour se connecter à Zilliz.
#pip install pymilvus
from pymilvus import connections
ENDPOINT=”https://xxxx.api.region.zillizcloud.com:443”
connections.connect(
uri=ENDPOINT,
token=TOKEN)
Créez une collection Milvus (considérez-la comme une table de base de données) appelée MilvusDocs. La collection prend un schéma et un index. Le schéma utilise la longueur d’embedding du modèle encodeur.
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”,}
Contrairement à Pinecone, l’insertion de toutes les données et le remplissage d’un index d’embeddings dans Milvus/Zilliz sont rapides !
# 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)
Étape 5 : Poser des questions sur vos documents
Nous sommes maintenant prêts à poser des questions sur nos documents personnalisés grâce à la puissance de la recherche sémantique. La recherche sémantique utilise une technique de plus proches voisins dans l’espace vectoriel pour trouver les documents correspondants les plus proches qui répondent à la question de l’utilisateur. La recherche sémantique vise à comprendre le sens derrière les questions et les documents plutôt qu’à simplement faire correspondre des mots-clés. Lors de la récupération, Milvus peut également utiliser les métadonnées pour améliorer l’expérience de recherche (à l’aide d’expressions booléennes dans l’option de l’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()
# Intégrer la question en utilisant le même encodeur.
embedded_question = torch.tensor(encoder.encode([QUESTION]))
# Normaliser les embeddings à une longueur unitaire.
embedded_question = F.normalize(embedded_question, p=2, dim=1)
# Convertir les embeddings en liste de listes de np.float32.
embedded_question = list(map(np.float32, embedded_question))
# Renvoyer les k meilleurs résultats avec AUTOINDEX.
TOP_K = 5
# Exécuter une recherche vectorielle sémantique en utilisant votre requête et la base de données vectorielle.
start_time = time.time()
results = mc.search(
data=embedded_question,
anns_field="vector",
# Aucun paramètre pour AUTOINDEX
param={},
# Expression booléenne le cas échéant
expr="",
output_fields=["h1", "h2", "text", "source"],
limit=TOP_K,
consistency_level="Eventually")
elapsed_time = time.time() - start_time
print(f"Temps de recherche Milvus : {elapsed_time} sec")
Ci-dessous, nous jetons un rapide coup d’œil à ce qui a été récupéré. Ensuite, nous plaçons tous les textes dans un champ 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,}
Ci-dessus, nous pouvons voir qu’en effet, 5 morceaux de texte ont été récupérés. En particulier, le premier morceau contient la réponse à la question sur la métrique par défaut. Parce que nous avons effectué la récupération en utilisant l’option de l’API Milvus output_fields=, les métadonnées des sources et des citations sont récupérées avec le morceau.
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'}
Étape 6 : Utiliser un LLM pour générer une réponse de chat à la question de l’utilisateur en utilisant le contexte récupéré
Nous utiliserons un modèle d’IA générative ouvert et très petit, ou LLM, disponible sur 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 réponse n’a pas été très utile ! Maintenant, posez la même question en utilisant le contexte récupéré.
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)
Cette réponse semble un peu meilleure ! Nous nous sommes exercés à la récupération gratuitement sur nos propres données en utilisant des LLM open source. Maintenant, faisons un appel payant à OpenAI GPT. Nous nous attendons à la même réponse que celle du simple LLM open source, mais plus humaine.
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)
Résumé
Nous avons démontré un chatbot RAG de récupération et de questions-réponses de bout en bout sur des documents personnalisés. Nous avons vu à quel point il est facile d’itérer en récupérant et en répondant à des questions à l’aide de vos données gratuitement. Cela a été possible avec LangChain, Milvus et des LLM open source pour l’encodeur et la génération de chat. Lors de la récupération, Milvus a fourni des sources et des citations (il suffit d’ajouter ces champs dans les métadonnées lors du chargement des données et d’utiliser « output_fields= » dans l’appel de récupération de l’API). Enfin, nous avons vu que cette approche permet d’économiser de l’argent puisque nous effectuons des appels gratuits à nos données presque tout le temps : récupération, évaluation et itérations de développement. Nous n’effectuons un appel payant à OpenAI qu’une seule fois pour l’étape finale de génération de chat.
Plus de ressources pour démarrer avec Milvus et Zilliz
Continuer à lire

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.

Vector Lakebase: End the AI Data Silo
Learn how Vector Lakebase unifies vector search, data lakes, and AI data operations so teams can serve RAG and agents without copy-and-sync pipelines.

Data Deduplication at Trillion Scale: How to Solve the Biggest Bottleneck of LLM Training
Explore how MinHash LSH and Milvus handle data deduplication at the trillion-scale level, solving key bottlenecks in LLM training for improved AI model performance.



