Einen Open-Source-Chatbot mit LangChain und Milvus in unter 5 Minuten erstellen
In meinem vorherigen Blog sind wir durchgegangen, wie man in wenigen Minuten mit einer Milvus-Verbindung startet. Dieser Beitrag verwendet einen vollständig Open-Source-RAG (Retrieval Augmented Generation)-Stack mit LangChain, um Fragen zu Milvus mithilfe der Webseiten unserer Produktdokumentation zu beantworten.
Die Verwendung von Open-Source-Q&A mit Retrieval spart Geld, da wir fast immer kostenlose Aufrufe an unsere Daten durchführen – Retrieval, Evaluierung und Entwicklungsiterationen. Wir führen nur einmal einen kostenpflichtigen Aufruf an OpenAI für den finalen Chat-Generierungsschritt durch.
Für diejenigen, die daran interessiert sind, tiefer in die technischen Aspekte einzutauchen, ist der Quellcode für einen Live-ChatBot auf unserem GitHub verfügbar. Der vollständige Code für dieses Notebook befindet sich in unserem Bootcamp GitHub.
RAG (Retrieval Augmented Generation) wird verwendet, um generativen KI-Text zu grounden (den generierten Text auf faktische, benutzerdefinierte Daten zu stützen, um Halluzinationen zu reduzieren). Text aus Ihren benutzerdefinierten Daten, die Sie für wahr halten, wie z. B. Produktdokumentation, wird aus einer Vektordatenbank abgerufen, um eine Frage zu beantworten. Dann fügen Sie die genauen Textantworten als „Kontext“ zusammen mit der „Frage“ in den „Prompt“ ein, den Sie in ein LLM wie OpenAIs ChatGPT einspeisen. Das LLM generiert eine menschenähnliche Chat-Antwort, die fundiert ist.
RAG-Prozesse:
Beginnen Sie mit Ihren benutzerdefinierten Daten, die Sie für wahr halten, und einem Embedding-Modell für den Encoder.
Teilen Sie Ihre Daten in Chunks auf und generieren Sie Embeddings Ihrer Daten mithilfe des Encoders. Speichern Sie die Daten und Metadaten in einer Vektordatenbank.
Der Benutzer stellt eine Frage. Generieren Sie Embeddings der Frage mit demselben Encoder aus Schritt 1.
Rufen Sie Antworten auf Ihre Frage ab, indem Sie eine semantische Suche mit der Vektordatenbank durchführen.
Füllen Sie die Antwort-Textchunks aus Ihren benutzerdefinierten Dokumenten in einen „Kontext“ ein. Fügen Sie die Frage und den Kontext in einen Prompt ein. Senden Sie den Prompt an ein generierendes LLM.
Erhalten Sie eine zuverlässige Antwort vom generierenden LLM zurück.
Schritt 1: Daten aufnehmen
Milvus ist eine leistungsstarke Vektordatenbank, die die Aufnahme benutzerdefinierter unstrukturierter Daten und die Erstellung von Embeddings vereinfacht. Milvus ist für die schnelle Speicherung, Indexierung und Suche von Embeddings (oder Vektoren) optimiert.
OpenAI ist eine Organisation, die KI-Modelle und -Tools entwickelt und bereitstellt. Sie ist bekannt für ihre hochmodernen Sprachmodelle wie die GPT-(Generative Pre-trained Transformer)-Serie.
LangChain ist eine Bibliothek von Tools und Wrappern, die Entwicklern hilft, die Lücke zwischen traditioneller Software und LLMs zu überbrücken.
Die Daten, die wir verwenden werden, sind unsere Produktdokumentations-Webseiten. ReadTheDocs ist eine Open-Source-, kostenlose Software-Dokumentations-Hosting-Plattform, auf der Dokumentation mit dem Sphinx-Dokumentgenerator geschrieben wird.
Legen wir los.
# 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
Der obige Code lädt die Webseiten in ein lokales Verzeichnis namens rtdocs herunter. Als Nächstes lesen wir die Dokumentation in LangChain ein.
#!pip install langchain
from langchain.document_loaders import ReadTheDocsLoader
loader = ReadTheDocsLoader(
"rtdocs/pymilvus.readthedocs.io/en/latest/",
features="html.parser")
docs = loader.load()
Schritt 2: Daten mithilfe von HTML-Hierarchien in Chunks aufteilen
Vor dem Einbetten ist es notwendig, Ihre Chunk-Strategie, Chunk-Größe und Chunk-Überlappung festzulegen. In dieser Demo werde ich Folgendes verwenden:
Strategie = Markdown-Header-Hierarchien verwenden. Markdown-Abschnitte zusammenhalten, sofern sie nicht zu lang sind.
Chunk-Größe = Den Parameter
MAX_SEQ_LENGTHdes Embedding-Modells verwendenÜberlappung = Faustregel 10-15%
Funktionen =
Langchains HTMLHeaderTextSplitter, um Markdown-Abschnitte aufzuteilen.
Langchains RecursiveCharacterTextSplitter, um lange Rezensionen rekursiv aufzuteilen.
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)
Beachten Sie oben, dass jeder Chunk mit der Dokumentquelle verankert ist. Zusätzlich werden Header-Titel zusammen mit dem Chunk des Markdown-Texts beibehalten. Diese Header können später verwendet werden, um einen ganzen Header-Abschnitt abzurufen.
Schritt 3: Embeddings generieren
Die meisten Demos verwenden jetzt OpenAI Embeddings APIs. Da es sich um Ihre eigenen benutzerdefinierten Daten handelt, warum nicht Open-Source-Embedding-Modelle und die kostenlose Stufe von Zilliz Cloud verwenden, um Ihre eigenen Daten so oft Sie möchten kostenlos zu durchsuchen?
Open-Source-Embedding-/Retrieval-Modelle sind laut den neuesten MTEB-Benchmark-Ergebnissen genauso gut wie OpenAI Embeddings (ada-002). Unten sehen wir, dass das kleinste hochrangige Modell bge-large-en-v1.5 ist. Wir werden dieses Modell in diesem Blog verwenden.
Bildquelle: https://huggingface.co/spaces/mteb/leaderboard, sortiert nach Spalte, Retrieval Average (15 datasets), gedruckt am 24. Nov. 2023.
Das obige Bild zeigt die Rangliste der Embedding-Modelle, mit dem Spitzenrang voyage-lite-01-instruct (Größe 4,2 GB, und dem dritten Rang bge-base-en-v1.5 (Größe 1,5 GB). OpenAIEmbedding text-embeddings-ada-002 ist auf Rang 22 (nicht angezeigt, da zu weit unten in der Liste).
Unten initialisieren wir einen Encoder mit dem ausgewählten Embedding-Modell-Checkpoint.
#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()
Generieren Sie nun Embeddings mit dem Encoder, den wir aus einem HuggingFace-Checkpoint initialisiert haben. Fassen Sie alle Daten in einer Liste von Wörterbüchern zusammen.
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)
Schritt 4: Milvus-Index erstellen und Daten einfügen
In diesem Schritt schreiben wir das Vierer-Tupel (vector, text, source, h1, h2) für jeden ursprünglichen Text-Chunk in die Datenbank.
Starten wir unseren Milvus-Server und verbinden wir uns damit. Um serverloses, in der Cloud gehostetes Milvus zu verwenden, benötigen Sie einen ZILLIZ_API_KEY. In meinem vorherigen Blog Verbindung zu Milvus herstellen habe ich Anweisungen zum Verbinden mit Zilliz gezeigt.
#pip install pymilvus
from pymilvus import connections
ENDPOINT=”https://xxxx.api.region.zillizcloud.com:443”
connections.connect(
uri=ENDPOINT,
token=TOKEN)
Erstellen Sie eine Milvus-Collection (stellen Sie sie sich wie eine Datenbanktabelle vor) namens MilvusDocs. Die Collection benötigt ein Schema und einen Index. Das Schema verwendet die Embedding-Länge des Encoder-Modells.
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”,}
Im Gegensatz zu Pinecone ist das Einfügen aller Daten und das Befüllen eines Index mit Embeddings in Milvus/Zilliz schnell!
# 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)
Schritt 5: Stellen Sie Fragen zu Ihren Dokumenten
Jetzt sind wir bereit, mithilfe der Leistungsfähigkeit der Semantischen Suche. Fragen zu unseren benutzerdefinierten Dokumenten zu stellen. Semantische Suche verwendet eine Nearest-Neighbors-Technik im Vektorraum, um die am besten passenden Dokumente zu finden, die die Frage des Benutzers beantworten. Semantische Suche zielt darauf ab, die Bedeutung hinter Fragen und Dokumenten zu verstehen, anstatt nur Schlüsselwörter abzugleichen. Während des Abrufs kann Milvus außerdem Metadaten nutzen, um das Sucherlebnis zu verbessern (mithilfe boolescher Ausdrücke in der Milvus-API-Option 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")
Unten werfen wir einen kurzen Blick darauf, was abgerufen wurde. Dann packen wir alle Texte in ein context-Feld.
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,}
Oben können wir sehen, dass tatsächlich 5 Text-Chunks abgerufen wurden. Insbesondere enthält der erste Chunk die Antwort auf die Frage nach der Standardmetrik. Da wir mit der Milvus-API-Option output_fields= abgerufen haben, werden Quellen- und Zitierungsmetadaten zusammen mit dem Chunk abgerufen.
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'}
Schritt 6: Verwenden Sie ein LLM, um mithilfe des abgerufenen Kontexts eine Chat-Antwort auf die Frage des Benutzers zu generieren
Wir verwenden ein offenes, sehr kleines generatives KI-Modell, oder LLM, das auf HuggingFace verfügbar ist.
#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']}")
Die Antwort war nicht sehr hilfreich! Stellen Sie nun dieselbe Frage mit dem abgerufenen Kontext.
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)
Diese Antwort sieht etwas besser aus! Wir haben Retrieval kostenlos auf unseren eigenen Daten mit Open-Source-LLMs geübt. Jetzt führen wir einen kostenpflichtigen Aufruf an OpenAI GPT durch. Wir erwarten dieselbe Antwort wie beim einfachen Open-Source-LLM, aber menschlicher formuliert.
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)
Zusammenfassung
Wir haben einen vollständigen RAG-Retrieval- und Frage-Antwort-Chatbot für benutzerdefinierte Dokumente demonstriert. Wir haben gesehen, wie einfach es ist, durch Retrieval und die Beantwortung von Fragen mithilfe Ihrer Daten kostenlos zu iterieren. Dies war mit LangChain, Milvus und Open-Source-LLMs für den Encoder und die Chat-Generierung möglich. Während des Retrievals lieferte Milvus Quellen und Zitationen (fügen Sie diese Felder einfach während des Datenladens in die Metadaten ein und verwenden Sie ‘output_fields=’ im API-Retrieval-Aufruf). Schließlich haben wir gesehen, dass dieser Ansatz Geld spart, da wir fast immer kostenlose Aufrufe an unsere Daten durchführen – Retrieval, Evaluierung und Entwicklungsiterationen. Wir tätigen nur einmal einen kostenpflichtigen Aufruf an OpenAI für den abschließenden Schritt der Chat-Generierung.
Weitere Ressourcen für den Einstieg in Milvus und Zilliz
Weiterlesen

How to Improve Retrieval Quality for Japanese Text with Sudachi, Milvus/Zilliz, and AWS Bedrock
Learn how Sudachi normalization and Milvus/Zilliz hybrid search improve Japanese RAG accuracy with BM25 + vector fusion, AWS Bedrock embeddings, and practical code examples.

Build for the Boom: Why AI Agent Startups Should Build Scalable Infrastructure Early
Explore strategies for developing AI agents that can handle rapid growth. Don't let inadequate systems undermine your success during critical breakthrough moments.

Vector Databases vs. Object-Relational Databases
Use a vector database for AI-powered similarity search; use an object-relational database for complex data modeling with both relational integrity and object-oriented features.



