Практическое руководство GenAI для руководителей продукта и инженерии

GenAI Практическое руководство для руководителей продукта и инженерии

Принимайте лучшие решения о продукте, заглянув под капот LLM-основанных продуктов

Изображение, сгенерированное Bing Image Creator на основе запроса «владелец продукта для прототипа приложения, работающего на основе машинного обучения»

Введение

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

Аналогично, если вы являетесь владельцем продукта, бизнес-лидером или инженером, отвечающим за создание новых продуктов на основе больших языковых моделей (Large Language Model или LLM), или за внедрение LLM или генеративного искусственного интеллекта в существующие продукты, понимание строительных блоков, которые входят в состав продуктов на основе LLM, поможет вам разработать стратегические и тактические вопросы, связанные с технологией, такие как:

  1. Подходит ли наш случай использования для решений, основанных на LLM? Возможно, традиционная аналитика, обучение с учителем или другой подход подходят лучше?
  2. Если LLM – это правильное решение, может ли наш случай использования быть решен с помощью готового продукта (например, ChatGPT Enterprise) сейчас или в ближайшем будущем? Классическое решение “создать или купить”.
  3. Какие строительные блоки входят в состав нашего продукта на основе LLM? Какие из них уже стандартизированы, а какие могут требовать больше времени для разработки и тестирования?
  4. Как мы измеряем производительность нашего решения? Какие рычаги доступны для улучшения качества результатов нашего продукта?
  5. Удовлетворяет ли наша качество данных требованиям случая использования? Мы правильно организовываем наши данные и передаем соответствующие данные в LLM?
  6. Можем ли мы быть уверены в том, что ответы LLM всегда будут фактически точными? То есть, будет ли наше решение «галлюцинировать», создавая ответы иногда?

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

В предыдущей статье я рассмотрел некоторые основные концепции, связанные с созданием продуктов на основе LLM. Но вы не можете научиться водить, просто читая блоги или смотря видео – для этого вам нужно сесть за руль. Благодаря текущей эпохе, у нас есть бесплатные инструменты (которые стоили миллионы долларов) под рукой, чтобы создать свое собственное решение на основе LLM менее чем за час! Поэтому в этой статье я предлагаю нам сделать именно это. Это гораздо более простая задача, чем научиться водить машину 😝.

Создайте чат-бота, который позволяет “общаться” с веб-сайтами

Задача: Создать чат-бота, который отвечает на вопросы на основе информации на предоставленном веб-сайте, чтобы лучше понять строительные блоки популярных решений на основе GenAI сегодня.

Мы создадим чат-бота, отвечающего на вопросы на основе информации в хранилище знаний. Этот шаблон решения, называемый Retrieval Augmented Generation (RAG), стал основным в компаниях. Одна из причин популярности RAG заключается в том, что вместо полного полагания на собственные знания LLM, вы можете предоставить внешнюю информацию LLM автоматически. В реальных реализациях внешняя информация может быть из хранилища знаний организации, содержащего закрытую информацию, чтобы продукт мог отвечать на вопросы о бизнесе, его продуктах, бизнес-процессах и т.д. RAG также снижает “галлюцинации” LLM, так как сгенерированные ответы основываются на предоставленной LLM информации. Согласно недавнему докладу,

«RAG станет стандартным способом использования LLM-моделей предприятиями» – доктор Валид Кадус, главный ученый, AnyScale

В рамках нашего практического упражнения мы позволим пользователю вводить веб-сайт, который наше решение будет “читать” и сохранять в своем хранилище знаний. Затем решение сможет отвечать на вопросы, основанные на информации на веб-сайте. Веб-сайт является заполнителем – на самом деле это можно настроить для использования текста из любого источника данных, таких как PDF-файлы, Excel, другой продукт или внутренняя система и т. д. Этот подход работает и для других медиа, например, изображений, но для них требуются некоторые другие LLM-модели. В настоящее время мы сосредоточимся на тексте с веб-сайтов.

Для нашего примера мы будем использовать веб-страницу с образцом списка книг, созданную для этого блога: «Книги, которые я бы взял – если бы в сутках было больше часов!» Можете использовать другой веб-сайт по вашему выбору.

Вот как будет выглядеть наш результат:

Чатбот на основе LLM для интеллектуального отвечания на вопросы, основанные на информации на веб-сайте. (Изображение от автора)

Вот этапы, которые мы пройдем для создания нашего решения:

0. Подготовка – Google Colaboratory и ключ API OpenAI1. Создание хранилища знаний2. Поиск контекста, связанного с вопросом3. Генерация ответа с помощью LLM4. Добавление возможности «чата» (по желанию)5. Добавление простого предварительно скодированного интерфейса (по желанию)

0.1. Подготовка – Google Colaboratory и ключ API OpenAI

Для создания решения на основе LLM нам нужно место для написания и выполнения кода, а также LLM для генерации ответов на вопросы. Мы будем использовать Google Colab в качестве среды разработки для кода и модель, лежащую в основе ChatGPT, в качестве LLM.

Давайте начнем с настройки Google Colab, бесплатного сервиса от Google, который позволяет запускать код Python в удобном формате без необходимости установки чего-либо на компьютер. Мне удобно добавить Colab в Google Drive, чтобы впоследствии легко найти блокноты Colab.

Для этого перейдите на Google Drive (с помощью браузера) > Создание > Еще > Подключить дополнительные приложения > Поиск «Colaboratory» в Google Marketplace > Установить.

Чтобы начать использовать Colab (“Colab”), выберите Новый > Еще > Google Colaboratory. Это создаст новый блокнот в вашем Google Drive, чтобы вы могли вернуться к нему позже.

Google Colaboratory доступен в Google Drive. (Изображение от автора)

Теперь давайте получим доступ к LLM. Существует несколько вариантов открытых и проприетарных LLM доступны. В то время как открытые LLM бесплатны, мощные LLM обычно требуют мощных графических процессоров для обработки входных данных и генерации ответов, и есть некоторые небольшие затраты на использование GPU. В нашем примере, вместо этого мы будем использовать сервис OpenAI для использования LLM, используемого ChatGPT. Для этого вам потребуется ключ API, который представляет собой комбинацию имени пользователя и пароля, используемого OpenAI для определения того, кто пытается получить доступ к LLM. На момент написания этого текста OpenAI предлагает новым пользователям кредит в размере $5, что должно быть достаточно для этого практического учебника. Вот шаги для получения API-ключа:

Перейдите на Страницу OpenAI > Начать > Зарегистрироваться по электронной почте и паролю или войдите с помощью учетной записи Google или Microsoft. Возможно, вам также потребуется номер телефона для подтверждения.

После входа в систему щелкните на значке профиля в верхнем правом углу > Просмотр ключей API > Создать новый секретный ключ. Ключ будет выглядеть примерно следующим образом (поддельный ключ только для информационных целей). Сохраните его для использования позднее.

sk-4f3a9b8e7c4f4c8f8f3a9b8e7c4f4c8f-UsH4C3vE64

Теперь мы готовы создать решение.

0.2. Подготовьте блокнот для создания решения

Нам нужно установить некоторые пакеты в среде Colab для удобства нашего решения. Просто введите следующий код в текстовое поле (называемое “ячейкой”) в Colab и нажмите “Shift + Return (ввод)”. Или просто щелкните кнопку “воспроизведение” слева от ячейки или используйте меню “Run” в верхней части блокнота. Вам может потребоваться использовать меню для вставки новых ячеек с кодом для запуска последующего кода:

# Установите пакеты OpenAI и tiktoken, чтобы использовать модель встраивания и модель завершения чата!pip install openai tiktoken# Установите пакет langchain, чтобы облегчить большую часть функциональности нашего решения, от обработки документов до использования LLM в режиме "чата"!pip install langchain# Установите ChromaDB - пакет базы данных векторов в памяти - для сохранения "знаний", на которые полагается наше решение для ответов на вопросы!pip install chromadb# Установите пакет HTML to text для преобразования содержимого веб-страницы в более удобочитаемый формат!pip install html2text# Установите gradio, чтобы создать базовый интерфейс для нашего решения!pip install gradio

Затем мы должны затянуть код из установленных пакетов, чтобы пакеты можно было использовать в коде, который мы пишем. Вы можете использовать новую ячейку с кодом и снова нажать “Shift + Return” – и продолжайте в этом режиме для каждого последующего блока кода.

# Импортируйте пакеты, необходимые для активации различной функциональности решенияfrom langchain.document_loaders import AsyncHtmlLoader # Для загрузки содержимого веб-сайта в документfrom langchain.text_splitter import MarkdownHeaderTextSplitter # Для разделения документа на более мелкие части по заголовкам главы из langchain.document_transformers import Html2TextTransformer # Для преобразования HTML в Markdown-текстfrom langchain.chat_models import ChatOpenAI # Для использования модели LLM от OpenAIfrom langchain.prompts import PromptTemplate # Для формулирования инструкций / подсказокfrom langchain.chains import RetrievalQA, ConversationalRetrievalChain # Для RAGfrom langchain.memory import ConversationTokenBufferMemory # Для поддержки истории чатаfrom langchain.embeddings.openai import OpenAIEmbeddings # Для преобразования текста в числовое представлениеfrom langchain.vectorstores import Chroma # Для взаимодействия с векторной базой данныхimport pandas as pd, gradio as gr # Для отображения данных в виде таблиц и создания пользовательского интерфейса соответственноimport chromadb, json, textwrap # Векторная база данных, преобразование json в текст и форматированный вывод соответственноfrom chromadb.utils import embedding_functions # Настройка функции встраивания в соответствии с протоколом, требуемым Chroma

Наконец, добавьте ключ API OpenAI в переменную. Обратите внимание, что этот ключ, подобно вашему паролю, не следует раскрывать. Также не делитесь своим блокнотом Colab, не удалив ключ API.

# Добавьте свой ключ API OpenAI в переменную# Сохранение ключа в переменной таким образом является плохой практикой. Он должен быть загружен в переменные среды и загружаться оттуда, но это приемлемо для быстрой демонстрацииOPENAI_API_KEY='sk-4f3a9b8e7c4f4c8f8f3a9b8e7c4f4c8f-UsH4C3vE64' # Поддельный ключ - используйте свой реальный ключ

Теперь мы готовы приступить к созданию решения. Вот общий обзор следующих шагов:

Основные шаги для создания решения RAG (изображение автора)

При написании кода мы будем использовать LangChain, который стал популярным фреймворком для создания решений, подобных этому. Он содержит пакеты, которые облегчают каждый из шагов, начиная от подключения к источникам данных и заканчивая отправкой и получением информации от LLM. Еще одним вариантом для упрощения создания приложений, основанных на LLM, является LlamaIndex. Хотя использование LangChain (или LlamaIndex) не является обязательным, и в некоторых случаях высокоуровневая абстракция может оставить команды в неведении о происходящем под капотом, мы будем использовать LangChain, но часто заглядывать под капот.

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

1. Создание базы знаний

1.1. Определение и чтение документовДавайте получим доступ к списку книг и считаем контент в нашу среду Colab. Содержимое исходно загружается в виде HTML, что удобно для веб-браузеров. Однако мы преобразуем его в более удобочитаемый формат с помощью инструмента для преобразования HTML в текст.

url = "https://ninadsohoni.github.io/booklist/" # Вы можете использовать любой другой веб-сайт, но имейте в виду, что некоторый код может потребоваться изменить, чтобы отображать содержимое правильно# Загрузка HTML с URL и преобразование в более удобочитаемый текстформатdocs = Html2TextTransformer().transform_documents(AsyncHtmlLoader(url).load())# Давайте еще раз посмотрим, чтобы увидеть, что у нас есть сейчасprint("\n\nВключенные метаданные:\n", textwrap.fill(json.dumps(docs[0].metadata), width=100), "\n\n")print("Загруженное содержимое страницы:")print('...', textwrap.fill(docs[0].page_content[2500:3000], width=100, replace_whitespace=False), '...')

Вот что происходит при выполнении этого кода в Google Colab:

Результат выполнения вышеуказанного кода. Содержимое веб-сайта загружается в среду Colab. (Изображение от автора)

1.2. Разделение документов на более мелкие отрывкиЕсть еще один шаг, прежде чем мы загрузим информацию блога в нашу базу знаний (которая фактически является базой данных на ваш выбор). Текст не должен загружаться в базу данных как есть. Сначала его нужно разбить на более мелкие куски. Это делается по нескольким причинам:

  1. Если наш текст слишком длинный, его нельзя отправить в LLM из-за превышения порога длины текста (известного как “размер контекста”).
  2. Более длинный текст может содержать широкую, слабо связанную информацию. Мы будем полагаться на LLM, чтобы выбрать соответствующие части информации, но это не всегда работает ожидаемым образом. С более маленькими кусками мы можем использовать механизмы извлечения информации, чтобы идентифицировать только необходимые части информации для отправки в LLM, как мы увидим позже.
  3. LLM обычно имеют более сильный фокус на начале и конце текста, поэтому более длинные куски могут привести к меньшему вниманию LLM к более позднему контенту (известному как “пропавший посредине”).

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

# Теперь мы разделяем все содержимое веб-сайта на меньшие куски# Каждое рецензируемое книжное предложение будет получать свой собственный кусок, так как мы разделяем по заголовкам# Используемый здесь разделитель MarkdownHeaderTextSplitter также создает набор метаданных из заголовков и связывает его с текстом в каждом кускеheaders_to_split_on = [ ("#", "Заголовок 1"), ("##", "Заголовок 2"),    ("###", "Заголовок 3"), ("####", "Заголовок 4"), ("#####", "Заголовок 5") ]splitter = MarkdownHeaderTextSplitter(headers_to_split_on = headers_to_split_on)chunks = splitter.split_text(docs[0].page_content)print(f"Создано {len(chunks)} меньших кусков из исходного документа")# Давайте посмотрим на один из кусковprint("\nПросмотр примера куска:")print("Включенные метаданные:\n", textwrap.fill(json.dumps(chunks[5].metadata), width=100), "\n\n")print("Загруженное содержимое страницы:")print(textwrap.fill(chunks[5].page_content[:500], width=100, drop_whitespace=False), '...')
Один из множества кусков документа в результате разделения исходного содержимого. (Изображение от автора)

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

1.3. Загрузка экскурсов в хранилище знанийТекстовые сегменты теперь готовы быть загруженными в хранилище знаний. Сначала они проходят через модель встраивания, чтобы преобразовать текст в серию чисел, которые передают смысл текста. Затем фактический текст вместе с числовым представлением (т.е. встраиваниями) будет загружен в векторную базу данных – наше хранилище знаний. Обратите внимание, что встраивания также создаются LLM, только другого типа, чем чат LLM. Если вы хотите узнать больше о встраиваниях, предыдущая статья демонстрирует концепцию с примерами (ссылка).

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

# Мы получим встраивания для каждого сегмента (а затем вопросы) с помощью модели встраиваний из OpenAIopenai_embedding_func = embedding_functions.OpenAIEmbeddingFunction(api_key=OPENAI_API_KEY)# Инициализируем векторную БД и создадим коллекциюpersistent_chroma_client = chromadb.PersistentClient()collection = persistent_chroma_client.get_or_create_collection("my_rag_demo_collection", embedding_function=openai_embedding_func)cur_max_id = collection.count() # Чтобы не перезаписывать существующие данные# Давайте добавим данные в нашу коллекцию в векторную БДcollection.add(    ids=[str(t) for t in range(cur_max_id+1, cur_max_id+len(chunks)+1)],    documents=[t.page_content for t in chunks],    metadatas=[None if len(t.metadata) == 0 else t.metadata for t in chunks]    )print(f"{collection.count()} документов в векторной БД")#  25 документов в векторной БД# Дополнительно: Мы напишем скромную вспомогательную функцию для лучшего отображения данных -#  она ограничивает длину встраиваний, отображаемых на экране (поскольку они состоят более чем из 1000 чисел).#  Также показывает подмножество текста из документов, а также поля метаданныхdef render_vectorDB_content(chromadb_collection):    vectordb_data = pd.DataFrame(chromadb_collection.get(include=["embeddings", "metadatas", "documents"]))    return pd.DataFrame({'IDs': [str(t) if len(str(t)) <= 10 else str(t)[:10] + '...'for t in vectordb_data.ids],                         'Embeddings': [str(t)[:27] + '...' for t in vectordb_data.embeddings],                         'Documents': [str(t) if len(str(t)) <= 300 else str(t)[:300] + '...' for t in vectordb_data.documents],                         'Metadatas': ['' if not t else json.dumps(t) if len(json.dumps(t))  <= 90 else '...' + json.dumps(t)[-90:] for t in vectordb_data.metadatas]                        })# Давайте посмотрим, что есть в векторной БД с помощью нашей вспомогательной функции. Будем смотреть на первые 4 сегментарендерить векторDB_содержания(collection)[:4]
Просмотр первых нескольких текстовых сегментов, загруженных в векторную БД вместе с числовыми представлениями (встраиваниями). (Изображение автора)

2. Поиск вопроса-релевантного контекста

Мы в конечном итоге хотим, чтобы наше решение выбирало соответствующую информацию из нашего корпуса знаний векторной БД и передавало ее в LLM вместе с вопросом, ответ на который мы хотим получить от LLM. Давайте опробуем поиск векторной БД, задав вопрос “Можете ли вы порекомендовать несколько детективных романов?”

# Мы здесь связываемся с ранее созданным экземпляром базы данных ChromaDB с использованием клиента ChromaDB LangChainvectordb = Chroma(client=persistent_chroma_client, collection_name="my_rag_demo_collection",                   embedding_function=OpenAIEmbeddings(openai_api_key=OPENAI_API_KEY)                  ) # Дополнительно - Мы зададим еще одну скромную вспомогательную функцию для более красивого отображения данныхdef render_source_documents(source_documents):    return pd.DataFrame({'#': range(1, len(source_documents) + 1),                          'Documents': [t.page_content if len(t.page_content) <= 300 else t.page_content[:300] + '...' for t in source_documents],                         'Metadatas': ['' if not t else '...' + json.dumps(t.metadata, indent=2)[-88:] for t in source_documents]                         })# Здесь мы собираем вопросquestion = "Можете ли вы порекомендовать несколько детективных романов?"# Запуск поиска векторной БД на основе нашего вопросаrelevant_chunks = vectordb.similarity_search(question)# Печать результатовprint(f"Лучшие {len(relevant_chunks)} результатов поиска")render_source_documents(relevant_chunks)
Лучшие результаты поиска по вопросу

По умолчанию мы получаем первые 4 результата, если явно не задаем другое значение. В этом примере первый результат поиска, который представляет собой роман о Шерлоке Холмсе, содержит термин «детектив» напрямую. Второй результат (День шакала) не содержит термина «детектив», но упоминает “полицейские агентства” и “разоблачение заговора”, что связано семантически с “детективными романами”. Третий результат (Тайный экономист) упоминает термин “тайный”, хотя он относится к экономике. Я считаю, что последний результат был найден из-за его связи с романами / книгами в целом, а не с “детективными романами” в частности, потому что было запрошено четыре результата.

Кроме того, не строго обязательно использовать векторную базу данных. Вы можете загружать векторы и облегчать поиск в других формах хранения. Для этого можно использовать «нормальные» связные базы данных или даже Excel. Однако вам придется самостоятельно обрабатывать вычисления “сходства”, которые могут быть дот-продуктом при использовании векторных вложений OpenAI, в логике вашего приложения. С другой стороны, векторная база данных делает это за вас.

Обратите внимание, что если мы хотим предварительно отфильтровать некоторые результаты поиска по метаданным, мы можем сделать это. Для нашей демонстрации давайте отфильтруем по жанру, который находится в разделе «Заголовок 2» в метаданных, загруженных из списка книг.

# Попробуем фильтровать и получать только те записи, которые соответствуют определенному фильтру метаданных. Возможно, это можно преобразовать в предварительный фильтр, если требуетсяpd.DataFrame(vectordb.get(where = {'Заголовок 2': 'Финансы'}))
Результаты поиска на основе применения предварительного фильтра метаданных, отображающие только ключевые столбцы. (Изображение автора)

Интересная возможность, предлагаемая LLM, – использовать сам LLM для изучения вопроса пользователя, просмотра доступных метаданных, определения необходимости и возможности предварительного фильтрации на основе метаданных и формулировки запроса фильтрации, который затем может быть использован для фактической предварительной фильтрации данных в векторной базе данных. См. self-query retriever LangChain для получения более подробной информации об этом.

3. Сгенерировать ответ с использованием LLM

Затем мы добавим инструкции к LLM, которые в основном говорят “Я собираюсь дать вам некоторые фрагменты информации и вопрос. Пожалуйста, ответьте на вопрос, используя предоставленные фрагменты информации”. Затем мы объединяем эти инструкции, результаты поиска из векторной базы данных и наш вопрос в пакет и отправляем его LLM для ответа. Все эти шаги облегчаются следующим кодом.

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

# Давайте выберем языковую модель бесплатной версии ChatGPT: GPT-3.5-turbollm = ChatOpenAI(model_name = 'gpt-3.5-turbo', temperature = 0, openai_api_key = OPENAI_API_KEY)# Давайте составим шаблон. Это то, что на самом деле отправляется в ChatGPT LLM,для этого входит контекст из нашей векторной базы данных и вопросtemplate = """Используйте следующие фрагменты контекста для ответа на вопрос в конце. Если вы не знаете ответа, просто скажите, что доступная информация недостаточна для ответа на вопрос. Не пытайтесь придумать ответ. Сохраните ответ максимально кратким, не превышающим пяти предложений.{context}Вопрос: {question}Полезный ответ:"""QA_CHAIN_PROMPT = PromptTemplate.from_template(template)# Определение цепочки вопрос-ответ Retrieval, которая будет брать вопрос, получать соответствующий контекст из векторной базы данных и передавать оба в модель языка в качестве ответаqa_chain = RetrievalQA.from_chain_type(llm,                                        retriever=vectordb.as_retriever(),                                       return_source_documents=True,                                       chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}                                       )

Теперь давайте снова попросим рекомендации детективных романов и посмотрим, что получим в ответ.

# Давайте зададим вопрос и запустим цепочку для ответа на негоquestion = "Вы можете порекомендовать несколько детективных романов?"result = qa_chain({"query": question})# Давайте посмотрим на результатresult["result"]
Ответ от решения, рекомендующего детективные романы (изображение автора)

Давайте проверим, просмотрела ли модель все четыре наших предыдущих результатов поиска из векторной базы данных, или она получила только два результата, отмеченных в ответе?

# Давайте посмотрим на исходные документы, использованные в качестве контекста LLM# Мы воспользуемся нашей вспомогательной функцией, чтобы ограничить размер отображаемой информацииrender_source_documents(result["source_documents"])
Что было передано в качестве контекста LLM вместе с вопросом для облегчения ответа. (Изображение автора)

Мы видим, что у LLM все еще есть доступ ко всем четырем результатам поиска, и она выводит, что только первые две книги являются детективными романами.

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

4. Добавление возможности «чата» (необязательно)

Решение теперь обладает необходимой основной функциональностью – оно способно читать информацию с веб-сайта и отвечать на вопросы, основываясь на этой информации. Но в настоящее время оно не предлагает «конверсационного» пользовательского опыта. Благодаря ChatGPT «интерфейс чата» стал доминирующим дизайном: мы теперь ожидаем, что это будет «естественный» способ взаимодействия с генеративным искусственным интеллектом, а особенно с LLM 😅. Первые шаги к созданию интерфейса чата состоят в добавлении «памяти» в решение.

«Память» здесь является иллюзией, поскольку LLM фактически не запоминает беседу до этого момента – ему нужно предоставить полную историю беседы на каждом шаге. Таким образом, если пользователь задает LLM последующий вопрос, решение упаковывает исходный вопрос, первоначальный ответ LLM и последующий вопрос и отправляет это LLM. LLM читает всю беседу и генерирует содержательный ответ для продолжения беседы.

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

  1. Сохранение всех вопросов и ответов (в переменной) как «историю чата»
  2. Когда пользователь задает вопрос, отправляем историю чата и новый вопрос в LLM и просим его сгенерировать автономный вопрос
  3. На этом этапе история чата больше не нужна. Используем автономный вопрос для запуска нового поиска в векторной базе данных
  4. Передаем автономный вопрос и результаты поиска, а также инструкции LLM для получения окончательного ответа. Этот шаг аналогичен предыдущему этапу «Генерация ответа с использованием LLM»

Хотя мы можем отслеживать историю чата в простых переменных, воспользуемся одним из типов памяти LangChain. Особенностью выбранного объекта памяти является автоматическое обрезание более старой истории чата при достижении указанного вами предела размера, обычно размера текста, который может принять выбранный LLM. В нашем случае LLM должна быть способна принять чуть более 4 000 «токенов» (частей слова), что примерно соответствует 3 000 словам или ~ 5 страницам Word-документа. OpenAI также предлагает вариант размером 16k того же LLM ChatGPT, который может принимать в 4 раза больший ввод. Поэтому требуется настройка размера памяти.

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

# Создадим объект памяти для отслеживания истории чата. Здесь используется память на основе "токенов", чтобы ограничить длину истории чата так, чтобы она могла быть передана в выбранный LLM. # Обычно максимальная длина токена настраивается в зависимости от LLM. Предположим, что мы используем версию LLM с размером 4К слотов, # мы установим максимальное значение для токена 3К, чтобы оставить место для предложения вопроса.#  Параметр LLM служит для определения схемы токенизации выбранного LLM. memory = ConversationTokenBufferMemory(memory_key="chat_history", return_messages=True, input_key="question", output_key="answer", max_token_limit=3000, llm=llm)# В то время как LangChain включает в себя стандартное предложение для формирования отдельного вопроса на основе последнего вопроса пользователя и # любого контекста из разговора до этого момента, мы расширим стандартное предложение дополнительными инструкциями.standalone_question_generator_template = """На основе следующего разговора и последующего вопроса переформулируйте последующий вопрос так, чтобы он был самостоятельным вопросом на его первоначальном языке. Сформулируйте самостоятельный вопрос явно и включите все необходимые контекстные данные для его уточнения.Разговор: {chat_history}Последующий вопрос: {question}Самостоятельный вопрос:"""updated_condense_question_prompt = PromptTemplate.from_template(standalone_question_generator_template)# Перестраиваем окончательное предложение (это опционально, так как LangChain использует стандартное предложение, хотя оно может немного отличаться)final_response_synthesizer_template = """Используйте следующие фрагменты контекста для ответа на вопрос вконце. Если вы не знаете ответа, просто скажите, что доступная информация недостаточна для ответа на вопрос. Не пытайтесь выдумать ответ. Ответьте кратко, ограничиваясь пятью предложениями.{context}Вопрос: {question}Полезный ответ:"""custom_final_prompt = PromptTemplate.from_template(final_response_synthesizer_template)qa = ConversationalRetrievalChain.from_llm(    llm=llm,     retriever=vectordb.as_retriever(),     memory=memory,    return_source_documents=True,    return_generated_question=True,    condense_question_prompt= updated_condense_question_prompt,    combine_docs_chain_kwargs={"prompt": custom_final_prompt})# Зададим вопрос, который мы ранее задавали цепочке запросовquery = "Можете ли вы порекомендовать несколько детективных романов?"result = qa({"question": query})print(textwrap.fill(result['answer'], width=100))
Рекомендации по детективным романам из решения. Тот же ответ, который был получен ранее, только с использованием возможности «вопрос-ответ», без «памяти» (Иллюстрация автора)

Зададим последующий вопрос и посмотрим на ответ, чтобы проверить, что решение теперь имеет «память» и способно вести диалог при последующих вопросах:

query = "Расскажите больше о второй книге"result = qa({"question": query})print(textwrap.fill(result['answer'], width=100))
Ответ на последующий вопрос о получении дополнительной информации о «второй книге». Решение отвечает больше информации о той же книге, что и ранее (Иллюстрация автора)

Давайте посмотрим, что происходит внутри, чтобы проверить, что решение действительно проходит через четыре шага, описанные в начале этого раздела. Начнем с проверки истории чата, чтобы убедиться, что решение действительно регистрирует весь разговор:

# Давайте посмотрим на историю чата до этого момента result['chat_history']
История чата после задания второго вопроса. Обратите внимание, что ответ также включен в разговор на этом этапе. (Изображение от автора)

Давайте посмотрим, какие еще данные отслеживает решение помимо истории чата:

# Выведем другие части результатов
print("Вот самостоятельный вопрос, сгенерированный LLM на основе истории чата:")
print(textwrap.fill(result['generated_question'], width=100))
print("\nВот исходные документы, на которые ссылается модель:")
display(render_source_documents(result['source_documents']))
print(textwrap.fill(f"\nСгенерированный ответ: {result['answer']}", width=100, replace_whitespace=False))
Выводы, кроме истории чата, после задания второго вопроса. (Изображение от автора)

Внутри решения используется LLM для преобразования вопроса “Расскажи мне больше о второй книге” в “Какую дополнительную информацию вы можете предоставить о ‘День шакала’ Фредерика Форсайта?”. Имея этот вопрос, решение способно искать векторную базу данных для получения соответствующей информации и сначала извлечь информацию о книге “День шакала”. Но стоит отметить, что в поисковые результаты также включены некоторые неподходящие результаты о других книгах.

Быстрое дополнительное обсуждение возможных проблем

Возможная проблема №1 — Плохая генерация самостоятельного вопроса: В моих тестах решение чата не всегда успешно генерировало хороший самостоятельный вопрос, пока не была изменена подсказка для генератора вопросов. Например, для вопроса, заданного в продолжение, “Расскажи мне о второй книге”, в большинстве случаев сгенерированный вопрос звучал как “Что вы можете мне рассказать о второй книге?”, что само по себе не особо содержательно и приводило к случайным результатам поиска и соответственно к случайно сгенерированному ответу от LLM.

Возможная проблема №2 — Изменение результатов поиска между исходным вопросом и вопросом в продолжение: Следует отметить, что несмотря на то, что второй сгенерированный вопрос специально называет интересующую книгу, возвращенные результаты поиска из векторной базы данных включают в себя результаты поиска других книг, и, что более важно, эти результаты поиска отличаются от результатов для исходного вопроса! В этом примере этот сдвиг в результатах поиска был желательным, так как вопрос изменился с “рекомендации детективных романов” на конкретный роман. Однако, когда пользователь задает вопросы в продолжение, с целью углубиться в тему, вариации в формулировке вопроса или сгенерированный самостоятельный вопрос LLM могут привести к разным результатам поиска или разному ранжированию результатов поиска, что может быть нежелательно.

Эта проблема возможно будет автоматически смягчаться, хотя бы в некоторой степени, путем проведения более обширного исходного поиска в векторной базе данных — возвращения большего количества результатов, а не только 4-5, как в нашем примере — и переранжировки их, чтобы наиболее релевантные результаты всегда всплывали наверх и всегда передавались LLM для генерации конечного ответа (см. “Переранжировка” от Cohere). Кроме того, приложению относительно просто определить, что результаты поиска изменились. Возможно, можно будет применить некоторые эвристики в зависимости от того, насколько изменились результаты поиска (измеряя ранжирование и метрики перекрытия), и насколько изменился вопрос (измеряя метрики расстояния, такие как косинусовая схожесть). По крайней мере в тех случаях, когда возникают неожиданные изменения в результатах поиска между ходами разговора, пользователь может быть оповещен и привлечен для более тщательного рассмотрения, в зависимости от важности сценария использования и подготовки или уровня пользователей.

Еще одна идея для контроля этого поведения — использование LLM для принятия решения, требуется ли повторный обращение к векторной базе данных для ответа на вопрос в продолжение или можно дать осмысленный ответ с использованием ранее полученных результатов. В некоторых случаях может потребоваться сгенерировать два набора результатов поиска и ответов и позволить LLM решить между ними, в других случаях пользователю могут передаваться возможности контроля над контекстом, позволяющие заморозить контекст (в зависимости от сценария использования, подготовки или уровня пользователей и других соображений), а в некоторых случаях изменяющиеся результаты поиска в продолжении вопросов могут быть допустимыми.

Как вы, наверное, уже поняли, довольно легко получить работающее базовое решение, но достичь идеальности – вот трудная часть. Здесь рассматриваются только некоторые проблемы. Хорошо, вернемся к основным упражнениям …

5. Добавьте предварительно закодированный пользовательский интерфейс

В конце концов, функциональность чат-бота готова. Теперь мы можем добавить удобный пользовательский интерфейс, чтобы улучшить пользовательский опыт. Благодаря таким библиотекам Python, как Gradio и Streamlit, это (в некоторой степени) можно достаточно легко сделать, так как они создают виджеты для интерфейса на основе инструкций, написанных на Python. Здесь мы выберем Gradio, чтобы быстро создать пользовательский интерфейс.

С двойной целью: дать возможность догнать тех, кто не смог выполнить код до сих пор, и продемонстрировать некоторые вариации на том же месте, следующие два блока кода являются самодостаточными и могут быть выполнены в полностью новой записной книжке Colab для создания полного чат-бота.

# Начальная настройка - установка необходимого программного обеспечения в кодовой среде!pip install openai tiktoken langchain chromadb html2text gradio   # Раскомментируйте, удалив '#' в начале, если вы начинаете здесь и еще ничего не установили# Импортирование пакетов, необходимых для включения различных функций для решенияfrom langchain.document_loaders import AsyncHtmlLoader # Загрузка содержимого веб-сайта в документfrom langchain.text_splitter import MarkdownHeaderTextSplitter # Разбиение документа на мелкие части по заголовкам документов from langchain.document_transformers import Html2TextTransformer # Преобразование HTML в текст формата Markdownfrom langchain.chat_models import ChatOpenAI # Использование LLM от OpenAIfrom langchain.prompts import PromptTemplate # Формулирование инструкций / подсказокfrom langchain.chains import RetrievalQA, ConversationalRetrievalChain # Для RAGfrom langchain.memory import ConversationTokenBufferMemory # Поддержание истории перепискиfrom langchain.embeddings.openai import OpenAIEmbeddings # Преобразование текста в числовое представлениеfrom langchain.vectorstores import Chroma # Взаимодействие с векторной базой данныхimport pandas as pd, gradio as gr # Отображение данных в виде таблиц и создание пользовательского интерфейса соответственноimport chromadb, json, textwrap # Векторная база данных, преобразование json в текст и красивая печать соответственноfrom chromadb.utils import embedding_functions # Настройка функции вложения в соответствии с требованиями Chroma# Добавление ключа API OpenAI в переменную# Если сохранение ключа в переменной - это плохая практика. Он должен быть загружен в переменные среды и загружен оттуда, но для быстрого демо это подойдетOPENAI_API_KEY = 'sk-4f3a9b8e7c4f4c8f8f3a9b8e7c4f4c8f-UsH4C3vE64' # Поддельный ключ - используйте свой настоящий ключ

Перед запуском следующего набора кода для отображения пользовательского интерфейса чат-бота, обратите внимание, что при отображении через Colab приложение становится общедоступным на 3 дня для всех, у кого есть ссылка (ссылка предоставляется в выводе ячейки записной книжки Colab). Теоретически, приложение можно оставить приватным, изменив последнюю строку в коде на demo.launch(share=False), но я не смог сделать работающее приложение в таком случае. Вместо этого я предпочитаю запускать его в режиме “отладка” в Colab, чтобы ячейка Colab оставалась “в работе” до остановки, что приводит к завершению работы чат-бота. В качестве альтернативы вы можете запустить показанный ниже код в другой ячейке Colab, чтобы завершить работу чат-бота и удалить содержимое, загруженное в базу данных Chroma.

# Запуск в конце для завершения демонстрационного чат-ботадемо.close() # Завершение сеанса чата и завершение общей демонстрации# Получение и удаление коллекции векторной базы данных, созданной для чат-ботавекторная_бд = Chroma(client=persistent_chroma_client, collection_name="my_rag_demo_collection", embedding_function=openai_embedding_func_for_langchain)векторная_бд.delete_collection()

Ниже приведен код для запуска чат-бота в виде приложения. Большая часть этого кода повторяет код до этой части статьи, поэтому должна быть знакома. Обратите внимание, что код ниже отличается от предыдущего кода, в том числе отсутствием управления памятью с использованием объекта памяти ‘token’ LangChain, который мы использовали ранее. Это означает, что при продолжении разговора на протяжении некоторого времени история станет слишком длинной для передачи в контекст модели языка, и приложению потребуется перезапуск.

# Инициализация функций вложения OpenAI. Их два, потому что протокол функции отличается, когда функция передается непосредственно в Chroma DB по сравнению с использованием ее с Chroma DB через LangChainopenai_embedding_func_for_langchain = OpenAIEmbeddings(openai_api_key=OPENAI_API_KEY)openai_embedding_func_for_chroma = embedding_functions.OpenAIEmbeddingFunction(api_key=OPENAI_API_KEY)# Инициализация объекта модели чата LangChain с использованием модели GPT 3.5 турбоМodel_m = ChatOpenAI(model_name='gpt-3.5-turbo', temperature=0, openai_api_key=OPENAI_API_KEY)# Инициализация векторной базы данных и создание коллекцииpersistent_chroma_client = chromadb.PersistentClient()collection = persistent_chroma_client.get_or_create_collection("my_rag_demo_collection", embedding_function=openai_embedding_func_for_chroma)# Функция загрузки содержимого веб-сайта в векторную базу данныхdef load_content_from_url(url):  # Загрузка HTML с URL и пре

Вы можете поиграть с приложением, предоставив ему другой URL для загрузки контента. Очевидно, что это не приложение производственного класса, и его создание было лишь демонстрацией основных элементов решений, основанных на RAG-технологиях GenAI. Лучше всего это можно описать как ранний прототип, и если бы его превратили в обычный продукт, большая часть работы была бы впереди в области программной инженерии.

Повторное ознакомление с вопросами из введения

С учетом контекста и знаний о созданном нами чат-боте, давайте повернемся к некоторым вопросам, поставленным во введении, и немного углубимся.

  1. Подходят ли решения, основанные на LLM, для нашего случая использования? Возможно, традиционные аналитические методы, обучение с учителем или другой подход подходят лучше? LLM-ы хороши в "понимании" языковых задач и следовании инструкциям. Таким образом, ранний пример использования LLM-ов - ответы на вопросы, резюмирование, генерация (текст в данном случае), обеспечение лучшего поиска на основе значения, анализ настроений, кодирование и т. д. LLM-ы также получили возможность решать проблемы и рассуждать. Например, LLM-ы могут выступать в качестве автоматических оценщиков заданий студентов, если вы предоставите им ключ к ответам, а иногда даже без него. С другой стороны, предсказания или классификации на основе большого количества данных, эксперименты с многорукими бандитами для оптимизации маркетинга, системы рекомендаций, системы обучения с подкреплением (Roomba, термостат Nest, оптимизация потребления энергии или уровней запасов и т. д.) - это сильные стороны других типов аналитических или машинного обучения... по крайней мере, на данный момент. Также следует рассмотреть гибридный подход, при котором традиционные модели машинного обучения обеспечивают информацию для LLM и наоборот, как голистическое решение для основной бизнес-проблемы.
  2. Если LLM-ы - это правильный путь, может ли наш случай использования быть решен готовым продуктом (например, ChatGPT Enterprise) сейчас или в ближайшем будущем? Классическое решение собирать или покупать. Услуги и продукты, предлагаемые OpenAI, AWS и другими, будут становиться все более широкими, лучшими и, возможно, дешевле. Например, ChatGPT позволяет пользователям загружать свои файлы для анализа, Bing Chat и Bard от Google позволяют указывать внешние веб-сайты для ответов на вопросы, AWS Kendra вносит семантический поиск в информацию о предприятии, Microsoft Copilot позволяет использовать LLM в Word, Powerpoint, Excel и т. д. По той же причине, по которой компании не создают свои собственные операционные системы или свои собственные базы данных, компании должны задуматься, нужно ли им создавать ИИ-решения, которые могут устареть от существующих и будущих готовых продуктов. С другой стороны, если применение компании является специфичным или ограниченным в каком-то смысле, например, компания не может отправлять свои конфиденциальные данные любому поставщику из-за конфиденциальности или регулирующих рекомендаций, то, возможно, потребуется разработка генеративных ИИ-продуктов внутри компании для решения задач. Продукты, использующие возможности рассуждения LLM, но выполняющие задачи или создающие результаты, слишком отличающиеся от предлагаемых решений, могут потребовать разработки внутри компании. Например, система, которая отслеживает состояние производственного цеха, процессы производства или уровни запасов и т. д., может потребовать специализированной разработки, особенно если нет хороших предложений продуктов для конкретной области. Кроме того, если приложение требует специализированного предметного знания, то LLM, настроенный на данные, специфичные для этой области, вполне вероятно, будет превосходить общепринятый LLM от OpenAI, и можно будет рассмотреть разработку внутри компании.
  3. Какие различные строительные блоки нашего продукта, основанного на LLM? Какие из них скоммутированы, а какие потребуют больше времени для построения и тестирования? На высоком уровне строительные блоки решения RAG, такого, как мы создали, включают в себя конвейер данных, векторную базу данных, поиск, генерацию и, конечно же, LLM. Есть много отличных выборов LLM и векторных баз данных. Для оптимизации для определенного случая использования понадобятся эксперименты в области научных данных для конвейера данных, поиска и инструкций для генерации. Когда начальное решение на месте, процесс внедрения в продукцию потребует множество работ, что верно

    Заключение

    Если бы вы все это знали 11 месяцев назад, это бы оправдало демонстрацию перед руководителем вашей компании. Возможно даже доклад TED перед более широкой аудиторией. Сегодня это стало частью основы грамотности в области искусственного интеллекта, особенно если вы занимаетесь разработкой генеративных продуктов на основе ИИ. Надеюсь, что благодаря этому упражнению вы вряд ли отстаете от событий! 👍

    Несколько завершающих мыслей,

    • В технологии есть серьезный потенциал — какие другие технологии могут "мыслить" на таком уровне и использоваться в качестве "подсказывающих движков" (по словам доктора Эндрю Нга).
    • В то время как передовые модели (в настоящее время, GPT-4) будут продолжать развиваться, открытые модели и их специфические для области и задачи модификации будут конкурентоспособны во многих задачах и найдут множество применений.
    • К лучшему или к худшему, эта передовая технология, которая потребовала миллионы (или сотни миллионов?) долларов для разработки, доступна бесплатно — вы можете заполнить форму и загрузить мощную модель Llama2 от Meta с очень гибкой лицензией. На модельном хабе HuggingFace уже находится почти 300 000 базовых LLM или их модифицированных вариантов. Аппаратное обеспечение также стандартизировано.
    • Модели OpenAI теперь способны распознавать и использовать "инструменты" (функции, API и т. д.), позволяя решениям взаимодействовать не только с людьми и базами данных, но и с другими программами. LangChain и другие пакеты уже показали использование LLM в качестве "мозга" для автономных агентов, которые могут принимать ввод, принимать решение о выборе действия и продолжать действовать, повторяя эти шаги, пока агент не достигнет своей цели. Наш простой чат-бот использовал два вызова LLM в детерминированной последовательности: генерация отдельного вопроса и синтез результатов поиска в связанном естественном языке. Представьте, чего можно достичь сотнями вызовов к быстро развивающимся LLM с агентной автономией!
    • Эти быстрые прогрессии являются результатом огромного момента вокруг GenAI, и они проникнут в предприятия и повседневную жизнь через наши устройства. Сначала в более простых способах, а позже в все более сложных приложениях, которые используют способность к принятию решений и рассуждениям технологии, смешивая ее с традиционным искусственным интеллектом.
    • И, наконец, сейчас отличное время для вовлечения, так как условия игры довольно равны, по крайней мере для применения этой технологии — все узнают об этом примерно в одно и то же время, начиная с взрыва ChatGPT в декабре 2022 года. Конечно, ситуация отличается на стороне исследований и разработок, где Большие технологические компании, потратившие годы и миллиарды долларов на разработку этой технологии, имеют свои отличительные особенности. В любом случае, чтобы создавать более сложные решения в будущем, сейчас идеальное время начать!

    Дополнительные ресурсы