Генерация с дополнением к поиску (RAG) с помощью Milvus и BentoML
Введение
В этом руководстве показано, как использовать открытую модель встраивания и крупноязычную модель на BentoCloud с векторной базой данных Milvus для создания приложения RAG (Retrieval Augmented Generation). BentoCloud - это платформа AI Inference Platform для быстро развивающихся команд ИИ, предлагающая полностью управляемую инфраструктуру, предназначенную для вывода моделей. Она работает в сочетании с BentoML, фреймворком для обслуживания моделей с открытым исходным кодом, чтобы облегчить создание и развертывание высокопроизводительных модельных сервисов. В этой демонстрации мы используем Milvus Lite в качестве базы данных векторов - это облегченная версия Milvus, которая может быть встроена в ваше приложение на Python.
Прежде чем начать
Milvus Lite доступен на PyPI. Вы можете установить его через pip для Python 3.8+:
$ pip install -U pymilvus bentoml
Если вы используете Google Colab, для включения только что установленных зависимостей вам может потребоваться перезапустить среду выполнения (нажмите на меню "Runtime" в верхней части экрана и выберите "Restart session" из выпадающего меню).
После входа в BentoCloud мы можем взаимодействовать с развернутыми сервисами BentoCloud Services в Deployments, а соответствующий END_POINT и API находятся в Playground -> Python. Данные о городе можно скачать здесь.
Обслуживание вкраплений с помощью BentoML/BentoCloud
Чтобы использовать эту конечную точку, импортируйте bentoml
и настройте HTTP-клиент, используя SyncHTTPClient
, указав конечную точку и, опционально, токен (если вы включите Endpoint Authorization
в BentoCloud). В качестве альтернативы можно использовать ту же модель, обслуживаемую через BentoML, используя его репозиторий Sentence Transformers Embeddings.
import bentoml
BENTO_EMBEDDING_MODEL_END_POINT = "BENTO_EMBEDDING_MODEL_END_POINT"
BENTO_API_TOKEN = "BENTO_API_TOKEN"
embedding_client = bentoml.SyncHTTPClient(
BENTO_EMBEDDING_MODEL_END_POINT, token=BENTO_API_TOKEN
)
После подключения к embedding_client нам нужно обработать наши данные. Мы предоставили несколько функций для выполнения разбиения и встраивания данных.
Чтение файлов и предварительная обработка текста в список строк.
# naively chunk on newlines
def chunk_text(filename: str) -> list:
with open(filename, "r") as f:
text = f.read()
sentences = text.split("\n")
return sentences
Сначала нам нужно загрузить данные о городе.
import os
import requests
import urllib.request
# set up the data source
repo = "ytang07/bento_octo_milvus_RAG"
directory = "data"
save_dir = "./city_data"
api_url = f"https://api.github.com/repos/{repo}/contents/{directory}"
response = requests.get(api_url)
data = response.json()
if not os.path.exists(save_dir):
os.makedirs(save_dir)
for item in data:
if item["type"] == "file":
file_url = item["download_url"]
file_path = os.path.join(save_dir, item["name"])
urllib.request.urlretrieve(file_url, file_path)
Затем мы обработаем каждый из имеющихся файлов.
# please upload your data directory under this file's folder
cities = os.listdir("city_data")
# store chunked text for each of the cities in a list of dicts
city_chunks = []
for city in cities:
chunked = chunk_text(f"city_data/{city}")
cleaned = []
for chunk in chunked:
if len(chunk) > 7:
cleaned.append(chunk)
mapped = {"city_name": city.split(".")[0], "chunks": cleaned}
city_chunks.append(mapped)
Разбивает список строк на список вкраплений, в каждом из которых сгруппировано 25 текстовых строк.
def get_embeddings(texts: list) -> list:
if len(texts) > 25:
splits = [texts[x : x + 25] for x in range(0, len(texts), 25)]
embeddings = []
for split in splits:
embedding_split = embedding_client.encode(sentences=split)
embeddings += embedding_split
return embeddings
return embedding_client.encode(
sentences=texts,
)
Теперь нам нужно сопоставить вкрапления и текстовые фрагменты. Поскольку список вкраплений и список предложений должны совпадать по индексу, мы можем пройтись по enumerate
по любому из этих списков, чтобы сопоставить их.
entries = []
for city_dict in city_chunks:
# No need for the embeddings list if get_embeddings already returns a list of lists
embedding_list = get_embeddings(city_dict["chunks"]) # returns a list of lists
# Now match texts with embeddings and city name
for i, embedding in enumerate(embedding_list):
entry = {
"embedding": embedding,
"sentence": city_dict["chunks"][
i
], # Assume "chunks" has the corresponding texts for the embeddings
"city": city_dict["city_name"],
}
entries.append(entry)
print(entries)
Вставка данных в векторную базу данных для извлечения
Подготовив вкрапления и данные, мы можем вставить векторы вместе с метаданными в Milvus Lite для последующего поиска векторов. Первый шаг в этом разделе - запуск клиента для подключения к Milvus Lite. Мы просто импортируем модуль MilvusClient
и инициализируем клиент Milvus Lite, который подключается к вашей векторной базе данных Milvus Lite. Размерность определяется размером модели встраивания, например, модель Sentence Transformer all-MiniLM-L6-v2
создает векторы размером 384.
from pymilvus import MilvusClient
COLLECTION_NAME = "Bento_Milvus_RAG" # random name for your collection
DIMENSION = 384
# Initialize a Milvus Lite client
milvus_client = MilvusClient("milvus_demo.db")
Что касается аргумента MilvusClient
:
- Установка
uri
в качестве локального файла, например,./milvus.db
, является наиболее удобным методом, так как он автоматически использует Milvus Lite для хранения всех данных в этом файле. - Если у вас большой объем данных, вы можете настроить более производительный сервер Milvus на docker или kubernetes. В этом случае используйте ури сервера, например
http://localhost:19530
, в качествеuri
. - Если вы хотите использовать Zilliz Cloud, полностью управляемый облачный сервис для Milvus, измените
uri
иtoken
, которые соответствуют публичной конечной точке и ключу Api в Zilliz Cloud.
Или с помощью старого API connections.connect (не рекомендуется):
from pymilvus import connections
connections.connect(uri="milvus_demo.db")
Создание коллекции Milvus Lite
Создание коллекции с помощью Milvus Lite включает в себя два этапа: во-первых, определение схемы, а во-вторых, определение индекса. Для этого раздела нам понадобится один модуль: DataType, который указывает нам, какой тип данных будет находиться в поле. Также нам понадобятся две функции для создания схемы и добавления полей. create_schema(): создает схему коллекции, add_field(): добавляет поле в схему коллекции.
from pymilvus import MilvusClient, DataType, Collection
# Create schema
schema = MilvusClient.create_schema(
auto_id=True,
enable_dynamic_field=True,
)
# 3.2. Add fields to schema
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True)
schema.add_field(field_name="embedding", datatype=DataType.FLOAT_VECTOR, dim=DIMENSION)
Теперь, когда мы создали схему и успешно определили поле данных, нам нужно определить индекс. С точки зрения поиска, "индекс" определяет, как мы собираемся отображать наши данные для поиска. Для этого проекта мы используем стандартный вариант AUTOINDEX для индексации данных.
Далее мы создаем коллекцию с заданными ранее именем, схемой и индексом. Наконец, мы вставляем обработанные ранее данные.
# prepare index parameters
index_params = milvus_client.prepare_index_params()
# add index
index_params.add_index(
field_name="embedding",
index_type="AUTOINDEX", # use autoindex instead of other complex indexing method
metric_type="COSINE", # L2, COSINE, or IP
)
# create collection
if milvus_client.has_collection(collection_name=COLLECTION_NAME):
milvus_client.drop_collection(collection_name=COLLECTION_NAME)
milvus_client.create_collection(
collection_name=COLLECTION_NAME, schema=schema, index_params=index_params
)
# Outside the loop, now you upsert all the entries at once
milvus_client.insert(collection_name=COLLECTION_NAME, data=entries)
Настройка LLM для RAG
Чтобы создать приложение RAG, нам нужно развернуть LLM на BentoCloud. Давайте воспользуемся последней версией Llama3 LLM. Как только он будет запущен, просто скопируйте конечную точку и токен этого модельного сервиса и настройте для него клиента.
BENTO_LLM_END_POINT = "BENTO_LLM_END_POINT"
llm_client = bentoml.SyncHTTPClient(BENTO_LLM_END_POINT, token=BENTO_API_TOKEN)
Инструкции LLM
Теперь мы настроим инструкции LLM с подсказкой, контекстом и вопросом. Вот функция, которая ведет себя как LLM и возвращает вывод от клиента в строковом формате.
def dorag(question: str, context: str):
prompt = (
f"You are a helpful assistant. The user has a question. Answer the user question based only on the context: {context}. \n"
f"The user question is {question}"
)
results = llm_client.generate(
max_tokens=1024,
prompt=prompt,
)
res = ""
for result in results:
res += result
return res
Пример RAG
Теперь мы готовы задать вопрос. Эта функция просто принимает вопрос, а затем выполняет RAG, чтобы сгенерировать соответствующий контекст из фоновой информации. Затем мы передаем контекст и вопрос в dorag() и получаем результат.
question = "What state is Cambridge in?"
def ask_a_question(question):
embeddings = get_embeddings([question])
res = milvus_client.search(
collection_name=COLLECTION_NAME,
data=embeddings, # search for the one (1) embedding returned as a list of lists
anns_field="embedding", # Search across embeddings
limit=5, # get me the top 5 results
output_fields=["sentence"], # get the sentence/chunk and city
)
sentences = []
for hits in res:
for hit in hits:
print(hit)
sentences.append(hit["entity"]["sentence"])
context = ". ".join(sentences)
return context
context = ask_a_question(question=question)
print(context)
Реализация RAG
print(dorag(question=question, context=context))
Для примера с вопросом о том, в каком штате находится Кембридж, мы можем распечатать весь ответ из BentoML. Однако если мы потратим время на его разбор, он будет выглядеть более симпатично и сообщит нам, что Кембридж находится в штате Массачусетс.