Когда я отправился в свое путешествие по изучению GenAI, меня привлекли некоторые проницательные работы Джерома Раджана и Прадипа Ничите. Их сообщения в блогах послужили катализатором моего любопытства, разжигая страсть к более глубокому изучению возможностей этой передовой технологии.

В этом блоге я поделюсь своим опытом о том, как я пытаюсь превратить вдохновение в осязаемые решения, создавая что-то, что могло бы стать практическим решением реальных бизнес-задач, которые я определил из своего опыта в моих предыдущих организациях.

Бизнес вызов

В моем прошлом опыте внедрения технологий в финансовых учреждениях я работал со многими коллегами из различных бэк-офисных отделов, таких как юридический отдел, отдел кадров, комплаенс, ИТ-безопасность и управление. Моя цель состояла в том, чтобы собрать идеи, рекомендации и лучшие практики из этих областей, чтобы мы могли принимать обоснованные решения во время руководящих комитетов проекта или технических стратегических совещаний.

Однако этот традиционный подход требует постоянной связи с МСП из различных ведомств. Помимо затрат времени, любые потенциальные задержки также в значительной степени зависят от таких факторов, как доступность людей, актуальность их знаний и возможность человеческой ошибки.

Разве не было бы намного эффективнее, если бы мы могли сначала обратиться к надежному помощнику по базе знаний (KB), предназначенному для

  • понять ваши запросы
  • оснащены современными внутренними знаниями в этих различных областях
  • быстро давать почти фактические ответы

Высокоуровневое решение

В последующих разделах мы рассмотрим, как можно быстро создать и развернуть такой помощник базы знаний.

На очень высоком уровне это решение можно разбить на 2 части

  1. Индексирование знаний: малые и средние предприятия или создатели знаний будут загружать или удалять материалы из репозитория документов. Всякий раз, когда материалы обновляются, срабатывает индексация, так что наш помощник по базе знаний будет автоматически обучен «понимать» их.
  2. Запросы: пользователи будут общаться с чат-ботом в Интернете, чтобы отправлять свои запросы и получать соответствующие ответы.

Индексация знаний

Идея высокого уровня здесь состоит в том, чтобы сначала обработать загруженные документы, преобразовать текст в векторные вложения, пропустив его через модель встраивания текста Vertex AI, которая обучена переводить семантическое сходство в векторное пространство. При встраивании текста векторы слов и словосочетаний со схожим значением будут располагаться рядом друг с другом.

Затем мы сохраним эти вложения в индекс, чтобы позже можно было выполнять семантический поиск в процессе запроса. Семантический поиск извлекает текст с похожими значениями, находя расстояние между двумя векторами и извлекая ближайший из них. Векторы представляют собой числовые представления слов или фраз.

Далее мы рассмотрим различные шаги, необходимые для создания этого решения.

  1. Создание индекса Pinecone: Pinecone — это сервис базы данных векторов, который мы будем использовать для хранения наших вложений, а затем мы будем использовать клиент Pinecone для поиска сходства в векторных данных.
  • Зарегистрируйте аккаунт в Pinecone. После входа в систему создайте индекс с размерами 768. Это связано с тем, что в этом примере мы используем модель встраивания текста Vertex AI textembedding-gecko, которая принимает максимум 3072 входных токена и выводит 768-мерные векторные вложения.

  • Скопируйте имя индекса, ключ API и информацию о регионе. Они потребуются позже, когда нам понадобится запись в индекс или поиск векторных данных.

2. Создание хранилища знаний. Для этого мы будем полагаться на простую корзину GCS в Google Cloud. Сюда мы будем загружать наши материалы, которые мы намерены использовать для обучения нашего помощника по БЗ.

3. Создание облачной функции Google (GCF). Эта облачная функция будет запускаться всякий раз, когда в корзину загружается новый объект. В этой облачной функции мы стремимся обрабатывать новые документы и записывать сгенерированные вложения в индекс.

  • Создайте GCF и добавьте триггер облачного хранилища. Настройте триггер для прослушивания события google.cloud.storage.object.v1.finalized. Это гарантирует, что этот GCF запускается всякий раз, когда в корзину добавляется новый объект.

  • В конфигурации кода введите следующий код Python и замените параметры PROJECT_NAME, BUCKET_NAME, API_KEY, REGION_INFO и INDEX_NAME:
import nltk
import functions_framework
import pinecone
from langchain.document_loaders import GCSDirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import VertexAIEmbeddings
from langchain.vectorstores import Pinecone

#function to load documents from GCS bucket
def load_docs(projectID, bucketName):
    loader = GCSDirectoryLoader(project_name=projectID, bucket=bucketName)
    documents = loader.load()
    return documents

#function to split documents into chunk size
def split_docs(documents,chunk_size=500,chunk_overlap=20):
  text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
  docs = text_splitter.split_documents(documents)
  return docs

#gcf entry point
@functions_framework.cloud_event
def new_object(cloud_event):
  data=cloud_event.data
  print(cloud_event["id"])
  documents = load_docs("PROJECT_NAME", "BUCKET_NAME")
  docs = split_docs(documents)
  embeddings = VertexAIEmbeddings(model_name="textembedding-gecko")
  pinecone.init(
    api_key="API_KEY",  # find at app.pinecone.io
    environment="REGION_INFO"  # next to api key in console
    )
  index_name = "INDEX_NAME"
  index = Pinecone.from_documents(docs, embeddings, index_name=index_name)
  

В этом коде вы можете видеть, что мы довольно сильно нажимаем на LangChain.

LangChain — это фреймворк приложений с открытым исходным кодом. Он предлагает серию модулей, которые являются основными абстракциями в качестве строительных блоков любого приложения на базе LLM. Таким образом, использование LangChain упрощает разработку приложений на базе LLM.

Далее мы рассмотрим, как GCF обрабатывает каждый документ и записывает сгенерированные вложения в индекс Pinecone.

Как только этот GCF запускается, он запускает функцию new_object(cloud_event), которая сначала использует класс GCSDirectoryLoader (предоставленный LangChain) для загрузки всех документов в корзину GCS, которую мы указали. Затем мы используем класс RecursiveCharacterTextSplitter для разделения документов на более мелкие фрагменты. Затем мы конвертируем фрагменты текста в формат, понятный нашей модели Vertex AI. Мы делаем это, нажимая на класс VertexAIEmbeddings LangChain и модель встраивания текста Vertex AI textembedding-gecko (модель, основанная на базовой модели PaLM 2) для создания встраивания текста. Наконец, мы записываем эти вложения текста в наш индекс Pinecone, чтобы мы могли искать его позже в процессе запроса.

Вот и все! При этом новые документы, загруженные в корзину GCS, всегда будут обрабатываться нашим кодом выше, а сгенерированные векторные вложения будут храниться в нашем индексе для последующего семантического поиска.

4. Будущие усовершенствования процесса индексирования:

  • Мы можем добавить еще один GCF для обнаружения удаления файлов в GCS, чтобы нерелевантные векторные вложения можно было удалить из индекса Pinecone.
  • Мы можем еще больше повысить эффективность существующего GCF, улучшив его для обработки только недавно загруженных документов, сравнив метки времени создания документов и метку времени запуска GCF.
  • Мы можем заменить Pinecone на использование VertexAI Matching Engine (крупномасштабная база данных векторов с низкой задержкой) для хранения наших векторных данных.

Запрос

Чтобы легко создать этого чат-бота, мы нажмем Streamlit, платформу приложений, которая предлагает множество функций и компонентов для создания веб-приложений для машинного обучения и обработки данных. Для создания чат-бота я в значительной степени взял код из поста Прадипа, поэтому вы также можете посмотреть его версию здесь.

Ключевым моментом здесь является обеспечение того, чтобы наш пользовательский интерфейс чат-бота мог эффективно обрабатывать наши запросы и давать правильные ответы. Итак, как мы можем это сделать?

  1. Создание веб-приложения: создайте main.py скрипт Python. Позже мы будем использовать Streamlit для запуска этого скрипта Python, чтобы он активировал наше веб-приложение чат-бота.
import streamlit as st
from streamlit_chat import message
from utils import *
from langchain.chat_models import ChatVertexAI
from langchain.chains import ConversationChain
from langchain.chains.conversation.memory import ConversationBufferWindowMemory
from langchain.prompts import (
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
    ChatPromptTemplate,
    MessagesPlaceholder
)

#Page Config
st.set_page_config(
     layout="wide",
     page_title="JS Lab",
     page_icon="https://api.dicebear.com/5.x/bottts-neutral/svg?seed=gptLAb"
)

#Sidebar
st.sidebar.header("About")
st.sidebar.markdown(
    "A place for me to experiment different LLM use cases, models, application frameworks and etc."
)

#Main Page and Chatbot components
st.title("Internal Knowledge Base Chatbot")

if 'responses' not in st.session_state:
    st.session_state['responses'] = ["How can I help you today?"]

if 'requests' not in st.session_state:
    st.session_state['requests'] = []

llm = ChatVertexAI(model_name="chat-bison")
if 'buffer_memory' not in st.session_state:
            st.session_state.buffer_memory=ConversationBufferWindowMemory(k=3,return_messages=True)

system_msg_template = SystemMessagePromptTemplate.from_template(template="""Answer the question as truthfully as possible using the provided context, 
and if the answer is not contained within the text below, say 'Sorry! I don't know'""")
human_msg_template = HumanMessagePromptTemplate.from_template(template="{input}")
prompt_template = ChatPromptTemplate.from_messages([system_msg_template, MessagesPlaceholder(variable_name="history"), human_msg_template])
conversation = ConversationChain(memory=st.session_state.buffer_memory, prompt=prompt_template, llm=llm, verbose=True)

response_container = st.container()
textcontainer = st.container()

with textcontainer:
    query = st.text_input("Query: ", key="input")
    if query:
        with st.spinner("typing..."):
            conversation_history = get_conversation_history()
            refined_query = query_refiner(conversation_history, query)
            st.subheader("Refined Query:")
            st.write(refined_query)
            context = find_match(refined_query)
            response = conversation.predict(input=f"Context:\n {context} \n\n Query:\n{query}")
        st.session_state.requests.append(query)
        st.session_state.responses.append(response) 

with response_container:
    if st.session_state['responses']:

        for i in range(len(st.session_state['responses'])):
            message(st.session_state['responses'][i],key=str(i))
            if i < len(st.session_state['requests']):
                message(st.session_state["requests"][i], is_user=True,key=str(i)+ '_user')

Помимо частей пользовательского интерфейса, которые мы можем легко определить с помощью Streamlit, основная суть приведенного выше кода сосредоточена на двух частях:

  1. Как мы сначала инициализируем модель большого языка и настраиваем цепочку диалогов
llm = ChatVertexAI(model_name="chat-bison")
...
conversation = ConversationChain(memory=st.session_state.buffer_memory, prompt=prompt_template, llm=llm, verbose=True)
  • Мы создаем экземпляр ChatVertexAI, который использует модель chat-bison-001 из Vertex AI.
  • Мы настроили цепочку диалогов, чтобы интегрировать пользовательский ввод, шаблоны подсказок и LLM, чтобы организовать поток того, как чат-бот должен вести интерактивный разговор.

2. Как мы обрабатываем каждый запрос и возвращаем правильный ответ

Прежде чем мы углубимся в понимание того, как генерируется ответ на запрос, мы сначала создадим отдельный скрипт utils.py для хранения некоторых наших вспомогательных функций.

import pinecone
import vertexai
import streamlit as st
from vertexai.language_models import TextGenerationModel
from vertexai.language_models import TextEmbeddingModel

model= TextEmbeddingModel.from_pretrained("textembedding-gecko@001")
pinecone.init(api_key=API_KEY, environment=REGION_NAME)
index = pinecone.Index(INDEX_NAME)

def text_embedding(input) -> list:
    embeddings = model.get_embeddings([input])
    for embedding in embeddings:
        vector = embedding.values
        print(f"Length of Embedding Vector: {len(vector)}")
    return vector

def find_match(input):
    input_em = text_embedding(input)
    result = index.query(input_em, top_k=2, includeMetadata=True)
    return result['matches'][0]['metadata']['text']+"\n"+result['matches'][1]['metadata']['text']


def query_refiner(conversation, query):
    model = TextGenerationModel.from_pretrained("text-bison@001")
    response = model.predict(
    prompt=f"Given the following user query and conversation log, formulate a question that would be the most relevant to provide the user with an answer from a knowledge base.\n\nCONVERSATION LOG: \n{conversation}\n\nQuery: {query}\n\nRefined Query:",
    temperature=0.2,
    max_output_tokens=256,
    top_p=0.5,
    top_k=20
    )
    return response.text


def get_conversation_history():
    conversation_string = ""
    for i in range(len(st.session_state['responses'])-1):
        
        conversation_string += "Human: "+st.session_state['requests'][i] + "\n"
        conversation_string += "Bot: "+ st.session_state['responses'][i+1] + "\n"
    return conversation_string

Далее мы рассмотрим, как наш код использует каждую вспомогательную функцию для уточнения нашего запроса, поиска соответствия в индексе Pinecone и, в конечном итоге, форматирования ответа обратно пользователю.

Ранее в нашем сценарии main.py мы вызывали следующие функции для обработки нашего запроса.

1. conversation_history = get_conversation_history()
2. refined_query = query_refiner(conversation_history, query)
3. context = find_match(refined_query)
4. response = conversation.predict(input=f"Context:\n {context} \n\n Query:\n{query}")
  1. get_conversation_string() сначала извлекает нашу текущую историю разговоров (включая запросы пользователя и ответы чат-бота)
  2. query_refiner(conversation_history, query) затем передает нашу историю разговоров в качестве предыдущего контекста в модель текстового бизона Vertex AI, чтобы уточнить наш запрос и создать новый запрос.
  3. Затем find_match(refined_query) принимает уточненный запрос и выполняет семантический поиск в индексе Pinecone.
  4. conversation.predict() затем передает запрос, и ответ от Pinecone сопоставляется с моделью чат-бизона Vertex AI, чтобы проанализировать ответ обратно пользователю.

2. Развертывание веб-приложения в Cloud Run. Последним шагом для нас будет развертывание этого скрипта в качестве веб-приложения в Google Cloud Run.

  1. Сначала мы создадим Dockerfile, чтобы перечислить все необходимые команды для создания образа.
FROM python:3.9-slim

WORKDIR /app

RUN apt-get update && apt-get install -y \
    build-essential \
    curl \
    software-properties-common \
    git \
    && rm -rf /var/lib/apt/lists/*

RUN git clone YOUR_GIT_HUB_REPO .

RUN pip3 install -r requirements.txt

EXPOSE 8501

HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health

ENTRYPOINT ["streamlit", "run", "main.py", "--server.port=8501", "--server.address=0.0.0.0"]

2. Далее мы создадим репозиторий Google Cloud Artifact Repository и используем его для создания нашего образа докера.

gcloud builds submit --region=asia-southeast1 --tag asia-southeast1-docker.pkg.dev/serious-hall-371508/vertex-ai-repo/chatbot:tag1

3. Наконец, мы можем развернуть этот образ в Google Cloud Run.

gcloud run deploy chatbot --image asia-southeast1-docker.pkg.dev/serious-hall-371508/vertex-ai-repo/chatbot

И с этим мы сможем запустить нашего чат-бота!

В этом примере, который я показываю ниже, он обучается с использованием одностраничной брошюры Google Cloud TAM, которую я загрузил в свою корзину GCS. Поэтому ожидается, что он не знает, кто такой Лионель Месси, но должен очень хорошо знать объем и обязанности Google Cloud TAM.

Заключение

Как отмечалось ранее, существует множество возможных улучшений, которые мы можем дополнительно изучить, чтобы улучшить индексирование и запросы этого решения. Несмотря на потенциал роста, процесс создания этого решения был приятным и полезным. Я призываю вас тоже отправиться в это приключение и изучить потенциал этих технологий!