Создание чат-бота с открытым исходным кодом с использованием LangChain и Milvus менее чем за 5 минут
В моем предыдущем блоге мы разобрали, как начать работу с подключением к Milvus за несколько минут. В этом посте мы используем полностью open-source стек RAG (Retrieval Augmented Generation) с LangChain, чтобы отвечать на вопросы о Milvus, используя веб-страницы документации нашего продукта.
Использование open-source Q&A с retrieval экономит деньги, поскольку почти всегда мы выполняем бесплатные вызовы к нашим данным — retrieval, evaluation и итерации разработки. Мы делаем платный вызов к OpenAI только один раз, на финальном этапе генерации чата.
Для тех, кто хочет глубже погрузиться в технические аспекты, исходный код live ChatBot доступен на нашем GitHub. Полный код этого notebook находится в нашем bootcamp Git Hub.
RAG (Retrieval Augmented Generation) используется для ground текста Generative AI (основания сгенерированного текста на фактических пользовательских данных для уменьшения галлюцинаций). Текст из ваших пользовательских данных, которые вы считаете истинными, например документации продукта, извлекается из vector database, чтобы ответить на вопрос. Затем вы вставляете точные текстовые ответы “context” вместе с “question” в “prompt,” который вы передаете в LLM, например OpenAI’s ChatGPT. LLM генерирует человекоподобный ответ в чате, который является grounded.
Процессы RAG:
Начните с ваших пользовательских данных, которые вы считаете истинными, и embedding model для encoder.
Разбейте данные на фрагменты и сгенерируйте embeddings ваших данных с помощью encoder. Сохраните данные и metadata в vector database.
Пользователь задает вопрос. Сгенерируйте embeddings вопроса, используя тот же encoder из шага 1.
Извлеките ответы на ваш вопрос, выполнив semantic search с использованием vector database.
Поместите фрагменты текста с ответами из ваших пользовательских документов в “Context.” Поместите вопрос и context в prompt. Отправьте prompt в generating LLM.
Получите надежный ответ от generating LLM.
Шаг 1: Загрузить данные
Milvus — это высокопроизводительная vector database, которая упрощает загрузку пользовательских неструктурированных данных и создание embeddings. Milvus оптимизирован для быстрого хранения, индексирования и поиска embeddings (или vectors).
OpenAI — это организация, которая разрабатывает и предоставляет AI models и инструменты. Она известна своими передовыми языковыми моделями, такими как серия GPT (Generative Pre-trained Transformer).
LangChain — это библиотека инструментов и wrappers, которая помогает разработчикам преодолеть разрыв между традиционным программным обеспечением и LLMs.
Данные, которые мы будем использовать, — это веб-страницы документации нашего продукта. ReadTheDocs — это open-source, бесплатная платформа хостинга документации программного обеспечения, где документация пишется с помощью генератора документов Sphinx.
Давайте начнем.
# 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
Приведенный выше код загружает веб-страницы в локальный каталог под названием rtdocs. Далее мы прочитаем документацию в LangChain.
#!pip install langchain
from langchain.document_loaders import ReadTheDocsLoader
loader = ReadTheDocsLoader(
"rtdocs/pymilvus.readthedocs.io/en/latest/",
features="html.parser")
docs = loader.load()
Шаг 2: Разбить данные на фрагменты с использованием HTML-иерархий
Перед созданием эмбеддингов необходимо определиться со стратегией разбиения на чанки, размером чанка и перекрытием чанков. В этой демоверсии я буду использовать:
Стратегия = Использовать иерархии заголовков markdown. Сохранять разделы markdown вместе, если они не слишком длинные.
Размер чанка = Использовать параметр модели эмбеддингов
MAX_SEQ_LENGTHПерекрытие = Эмпирическое правило 10–15%
Функции =
HTMLHeaderTextSplitter из Langchain для разбиения разделов markdown.
RecursiveCharacterTextSplitter из Langchain для рекурсивного разбиения длинных отзывов.
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)
Обратите внимание выше, что каждый чанк привязан к источнику документа. Кроме того, заголовки сохраняются вместе с чанком markdown-текста. Эти заголовки позже можно использовать для извлечения целого раздела заголовка.
Шаг 3: Сгенерировать эмбеддинги
Большинство демоверсий сейчас используют OpenAI Embeddings APIs. Поскольку это ваши собственные пользовательские данные, почему бы не использовать open-source модели эмбеддингов и бесплатный уровень Zilliz Cloud, чтобы искать по своим данным сколько угодно бесплатно?
Open-source модели эмбеддингов/извлечения так же хороши, как OpenAI Embeddings (ada-002), согласно последним результатам бенчмарка MTEB. Ниже мы видим, что самая маленькая модель с высоким рейтингом — bge-large-en-v1.5. Мы будем использовать эту модель в этом блоге.
Источник изображения: https://huggingface.co/spaces/mteb/leaderboard, отсортировано по столбцу Retrieval Average (15 datasets), напечатано 24 ноября 2023 г.
Изображение выше показывает таблицу лидеров моделей эмбеддингов, где первое место занимает voyage-lite-01-instruct (размер 4,2 ГБ), а третье место — bge-base-en-v1.5 (размер 1,5 ГБ). OpenAIEmbedding text-embeddings-ada-002 занимает 22-е место (не показано, так как находится значительно ниже в списке).
Ниже мы инициализируем encoder, используя checkpoint выбранной модели эмбеддингов.
#pip install torch, sentence-transformers
import torch
from sentence_transformers import SentenceTransformer
# Инициализируем настройки torch
DEVICE = torch.device('cuda:3'
if torch.cuda.is_available()
else 'cpu')
# Загружаем модель энкодера из huggingface model hub.
model_name = "BAAI/bge-base-en-v1.5"
encoder = SentenceTransformer(model_name, device=DEVICE)
# Получаем параметры модели и сохраняем на потом.
MAX_SEQ_LENGTH = encoder.get_max_seq_length()
EMBEDDING_LENGTH = encoder.get_sentence_embedding_dimension()
Теперь сгенерируйте эмбеддинги, используя энкодер, который мы инициализировали из checkpoint HuggingFace. Соберите все данные вместе в список словарей.
chunk_list = []
for chunk in chunks:
# Генерируем эмбеддинги, используя энкодер из 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]
# Собираем вектор эмбеддинга, исходный текстовый фрагмент, метаданные.
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)
Шаг 4: Создайте индекс Milvus и вставьте данные
На этом шаге мы запишем квадруплет (vector, text, source, h1, h2) в базу данных для каждого исходного текстового фрагмента.
Давайте запустим наш сервер Milvus и подключимся к нему. Чтобы использовать serverless Milvus, размещённый в облаке, вам понадобится ZILLIZ_API_KEY. В моём предыдущем блоге, Подключение к Milvus, я показал инструкции по подключению к Zilliz.
#pip install pymilvus
from pymilvus import connections
ENDPOINT=”https://xxxx.api.region.zillizcloud.com:443”
connections.connect(
uri=ENDPOINT,
token=TOKEN)
Создайте коллекцию Milvus (представьте её как таблицу базы данных) с названием MilvusDocs. Коллекция принимает схему и индекс. Схема использует длину эмбеддинга модели энкодера.
from pymilvus import (
FieldSchema, DataType,
CollectionSchema, Collection)
# 1. Определяем минимальную расширяемую схему.
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. Создаём коллекцию.
mc = Collection(“MilvusDocs”, schema)
# 3. Индексируем коллекцию.
mc.create_index(
field_name=”vector”,
index_params={
“index_type”: “AUTOINDEX”,
“metric_type”: “COSINE”,}
В отличие от Pinecone, вставка всех данных и заполнение индекса эмбеддингов в Milvus/Zilliz выполняются быстро!
# Вставляем данные в коллекцию Milvus.
insert_result = mc.insert(chunk_list)
# После вставки последней сущности вызовите flush,
# чтобы остановить растущие сегменты, оставшиеся в памяти.
mc.flush()
print(mc.partitions)
Шаг 5: Задавайте вопросы о ваших документах
Теперь мы готовы задавать вопросы о наших пользовательских документах, используя возможности семантического поиска. Семантический поиск использует метод ближайших соседей в векторном пространстве, чтобы найти наиболее подходящие документы, которые отвечают на вопрос пользователя. Семантический поиск стремится понять смысл вопросов и документов, а не просто сопоставлять ключевые слова. Во время извлечения Milvus также может использовать метаданные для улучшения поискового опыта (с помощью булевых выражений в опции Milvus API expr=).
# Определяем пример вопроса о ваших данных.
QUESTION = "what is the default distance metric used in AUTOINDEX?"
QUERY = [question]
# Перед выполнением поиска загрузите данные в память.
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")

Ниже мы быстро посмотрим, что было извлечено. Затем мы помещаем все тексты в поле `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,}

Выше мы видим, что действительно было извлечено 5 фрагментов текста. В частности, первый фрагмент содержит ответ на вопрос о метрике по умолчанию. **Поскольку мы извлекали данные с использованием опции Milvus API `output_fields=`, метаданные источников и цитирования извлекаются вместе с фрагментом.**
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'}
## Шаг 6: Используйте LLM, чтобы сгенерировать ответ в чате на вопрос пользователя с использованием извлеченного контекста
Мы будем использовать открытую, очень маленькую генеративную AI-модель, или LLM, доступную на 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']}")

Ответ оказался не очень полезным! Теперь задайте тот же вопрос, используя извлеченный контекст.
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)

Этот ответ выглядит немного лучше! Мы потренировались выполнять извлечение бесплатно на собственных данных с использованием open-source LLM. **Теперь давайте сделаем платный вызов к OpenAI GPT. Мы ожидаем тот же ответ, что и от простой open-source LLM, но более похожий на человеческий.**
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)

## Резюме
Мы продемонстрировали полный процесс создания RAG-чатбота для поиска и ответов на вопросы по пользовательским документам. Мы увидели, насколько легко выполнять итерации, извлекая данные и отвечая на вопросы с использованием ваших данных бесплатно. Это стало возможным благодаря LangChain, Milvus и LLM с открытым исходным кодом для энкодера и генерации чата. Во время поиска Milvus предоставлял источники и цитаты (просто добавьте эти поля в метаданные при загрузке данных и используйте ‘output_fields=’ в API-вызове поиска). Наконец, мы увидели, что этот подход экономит деньги, поскольку почти всё время мы делаем бесплатные вызовы к нашим данным — поиск, оценка и итерации разработки. Мы выполняем платный вызов к OpenAI только один раз — на финальном этапе генерации чата.
## Дополнительные ресурсы для начала работы с Milvus и Zilliz
- [Серия блогов Vector Database 101](https://zilliz.com/learn/what-is-vector-database)
- [Бенчмаркинг для поиска подходящей векторной базы данных для вашего сценария использования](https://zilliz.com/learn/open-source-vector-database-benchmarking-your-way)
- [Zilliz Integration Hub](https://zilliz.com/product/integrations)
- [Ответы в 100 раз быстрее и снижение затрат с использованием GPTCache](https://zilliz.com/blog/building-llm-apps-100x-faster-responses-drastic-cost-reduction-using-gptcache)
- [Рабочие процессы рекомендаций с использованием Milvus с NVIDIA Merlin](https://zilliz.com/blog/efficient-vector-similarity-search-recommender-workflows-using-milvus-nvidia-merlin)
- [Kafka Connector для ИИ в реальном времени](https://zilliz.com/blog/announce-confluent-kafka-connector-for-Milvus-and-Zilliz-unlock-power-of-real-time-ai)
Читать далее

Zilliz Skills Breakdown: How AI Agents Master Vector Databases
Zilliz's Milvus Skill (pymilvus, 7 files) and Zilliz Cloud Skill (zilliz-cli, 14 modules) bring vector-DB dev and ops into one Claude Code session.

The AWS Outage Was a Wake-Up Call for Vector Database Cross-Region Disaster Recovery
Zilliz Cloud Had the Answer Before the Crisis. Zilliz Cloud is the world's first vector database with native cross-region disaster recovery.

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.



