Вступление Pythonista в семантическое ядро
Pythonista в семантическое ядро
С момента выпуска ChatGPT большие языковые модели (LLMs) получили огромное количество внимания как в индустрии, так и в СМИ, что привело к беспрецедентному спросу на использование LLMs практически в любом возможном контексте.
Semantic Kernel – это открытый SDK, изначально разработанный Microsoft для поддержки продуктов, таких как Microsoft 365 Copilot и Bing, с целью облегчить интеграцию LLMs в приложения. Он позволяет пользователям использовать LLMs для организации рабочих процессов на основе естественных языковых запросов и команд, связывая эти модели с внешними службами, которые обеспечивают дополнительную функциональность, необходимую модели для выполнения задач.
Поскольку Semantic Kernel был создан с учетом экосистемы Microsoft, многие сложные примеры, доступные в настоящее время, написаны на C#, меньше ресурсов сосредоточено на Python SDK. В этой статье я покажу, как начать работу с Semantic Kernel с использованием Python, представлю основные компоненты и рассмотрю, как они могут использоваться для выполнения различных задач.
В этой статье мы рассмотрим следующие вопросы:
- Неправильна ли реализация метода Nesterov Momentum в PyTorch?
- Разблокирование силы разнообразия в нейронных сетях как адаптивные нейроны превосходят однородность в классификации изображений и нелинейной регрессии
- Мониторинг моделей машинного обучения в производстве почему и как?
- Ядро
- Соединители
- Семантические функции- Создание конфигурации семантической функции- Создание пользовательского соединителя
- Использование службы чата- Создание простого чат-бота
- Память – Использование службы встраивания текста- Интеграция памяти в контекст
- Плагины – Использование готовых плагинов- Создание пользовательских плагинов- Цепочка нескольких плагинов
- Организация рабочих процессов с планировщиком
Отказ от ответственности: Semantic Kernel, как и все, что связано с LLMs, развивается с огромной скоростью. Поэтому интерфейсы могут незначительно изменяться со временем, и я постараюсь обновлять этот пост, где смогу.
Хотя я работаю в Microsoft, мне не было поручено и не компенсируется никаким образом продвижение Semantic Kernel. В отделе Industry Solutions Engineering (ISE) мы гордимся тем, что используем лучшие инструменты для работы в зависимости от ситуации и клиентов, с которыми работаем. В случаях, когда мы выбираем не использовать продукты Microsoft, мы предоставляем подробную обратную связь командам разработчиков по причинам, по которым мы приняли такое решение, и областям, где мы считаем, что что-то отсутствует или может быть улучшено; обычно эта обратная связь приводит к тому, что продукты Microsoft отлично подходят для наших потребностей.
Здесь я решил продвигать Semantic Kernel, потому что, несмотря на некоторые недостатки, я верю, что он обладает большим потенциалом, и мне нравятся выбранные в Semantic Kernel дизайнерские решения по сравнению с некоторыми другими решениями, которые я изучал.
Использованные пакеты на момент написания:
зависимости: - python=3.10.1.0 - pip: - semantic-kernel==0.3.10.dev - timm==0.9.5 - transformers==4.32.0 - sentence-transformers==2.2.2 - curated-transformers==1.1.0
Краткое изложение: Если вам просто нужен рабочий код, который вы можете использовать непосредственно, все код, необходимый для воспроизведения этой статьи, доступен в виде блокнота здесь.
Благодарности
Я хотел бы поблагодарить моего коллегу Карола Зака за сотрудничество в исследовании возможностей Semantic Kernel для наших сценариев использования и предоставление кода, который вдохновил некоторые из примеров в этой статье!

Теперь давайте начнем с центрального компонента библиотеки.
Ядро
Kernel: “Ядро, центр или суть объекта или системы.” – Wiktionary
Одно из ключевых понятий в Semantic Kernel – это само ядро, которое является основным объектом, который мы будем использовать для организации наших рабочих процессов на основе LLM. Изначально ядро имеет очень ограниченную функциональность; все его возможности в значительной степени основаны на внешних компонентах, с которыми мы будем соединяться. Затем ядро действует как обработчик запросов, вызывая соответствующие компоненты для выполнения задачи.
Мы можем создать ядро, как показано ниже:
import semantic_kernel as skkernel = sk.Kernel()
Соединители
Чтобы сделать наш ядро полезным, нам нужно соединить одну или несколько моделей искусственного интеллекта, которые позволят нам использовать наше ядро для понимания и генерации естественного языка; это делается с помощью соединителя. Семантическое ядро предоставляет готовые соединители, которые упрощают добавление моделей искусственного интеллекта из разных источников, таких как OpenAI, Azure OpenAI и Hugging Face. Затем эти модели используются для предоставления сервиса ядру.
На данный момент поддерживаются следующие сервисы:
- сервис автозавершения текста: используется для генерации естественного языка
- сервис чата: используется для создания разговорного опыта
- сервис генерации векторных представлений текста: используется для кодирования естественного языка в векторы представлений
Каждый тип сервиса может поддерживать несколько моделей из разных источников одновременно, что позволяет переключаться между разными моделями в зависимости от задачи и предпочтений пользователя. Если не указан конкретный сервис или модель, ядро будет использовать первый заданный сервис и модель по умолчанию.
Мы можем увидеть все зарегистрированные сервисы с помощью следующих методов:
def print_ai_services(kernel): print(f"Сервисы автозавершения текста: {kernel.all_text_completion_services()}") print(f"Сервисы чата: {kernel.all_chat_services()}") print( f"Сервисы генерации векторных представлений текста: {kernel.all_text_embedding_generation_services()}" )
Как и ожидалось, у нас пока нет подключенных сервисов! Давайте это изменить.
Здесь я начну с доступа к модели GPT3.5-turbo, которую я развернул, используя сервис Azure OpenAI в своей подписке Azure.
Поскольку эту модель можно использовать как для автозавершения текста, так и для чата, я зарегистрирую ее с использованием обоих сервисов.
from semantic_kernel.connectors.ai.open_ai import ( AzureChatCompletion, AzureTextCompletion,)kernel.add_text_completion_service( service_id="azure_gpt35_text_completion", service=AzureTextCompletion( OPENAI_DEPLOYMENT_NAME, OPENAI_ENDPOINT, OPENAI_API_KEY ),)gpt35_chat_service = AzureChatCompletion( deployment_name=OPENAI_DEPLOYMENT_NAME, endpoint=OPENAI_ENDPOINT, api_key=OPENAI_API_KEY,)kernel.add_chat_service("azure_gpt35_chat_completion", gpt35_chat_service)
Теперь мы видим, что сервис чата был зарегистрирован как сервис автозавершения текста и сервис чата.
Для использования API OpenAI, единственное изменение, которое нам нужно сделать, это использовать соединители OpenAITextCompletion
и OpenAIChatCompletion
вместо наших классов Azure. Не беспокойтесь, если у вас нет доступа к моделям OpenAI, мы рассмотрим, как подключиться к моделям с открытым исходным кодом немного позже; выбор модели не повлияет на следующие шаги.
Теперь, когда мы зарегистрировали некоторые сервисы, давайте изучим, как мы можем взаимодействовать с ними!
Семантические функции
Способ взаимодействия с LLM через Семантическое ядро заключается в создании Семантической функции. Семантическая функция ожидает ввода на естественном языке и использует LLM для интерпретации заданного вопроса, а затем действует соответствующим образом, чтобы вернуть соответствующий ответ. Например, семантическая функция может использоваться для задач таких, как генерация текста, суммирование, анализ тональности и вопросно-ответный анализ.
В Семантическом ядре семантическая функция состоит из двух компонентов:
- Шаблон запроса: естественный языковый запрос или команда, которые будут отправлены в LLM
- Объект конфигурации: содержит настройки и параметры для семантической функции, такие как сервис, который она должна использовать, ожидаемые параметры и описание того, что делает функция.
Простейший способ начать – использовать метод create_semantic_function
ядра, который принимает фиксированные аргументы, такие как temperature
и max_tokens
, которые обычно требуются LLM, и использует их для создания конфигурации для нас.
Для наглядности создадим простой приглашение:
prompt = """{{$input}} - столица"""generate_capital_city_text = kernel.create_semantic_function(prompt, max_tokens=100, temperature=0, top_p=0)
Здесь мы использовали синтаксис {{$}}
для представления аргумента, который будет вставлен в наше приглашение. В этом посте мы рассмотрим еще много примеров использования этого синтаксиса, подробное руководство по синтаксису шаблонов можно найти в документации.
Мы можем изучить некоторые детали нашей семантической функции, как показано ниже:
Здесь мы видим, что ему было дано общее описание, так как мы не предоставили его.
Теперь мы можем использовать нашу функцию просто вызвав ее:
response = generate_capital_city_text("Париж")
Кроме того, большинство методов ядра поддерживают асинхронное выполнение с использованием Asyncio. Поскольку многие из наших подключенных служб, скорее всего, будут вызывать внешние API, асинхронный вызов должен обеспечить повышение производительности при использовании семантической функции в приложении, работающем в цикле событий.
Мы можем сделать это следующим образом.
response = await generate_capital_city_text.invoke_async("Париж")
Объект ответа содержит ценную информацию о вызове нашей функции, такую как возникла ли ошибка и какая она была; предоставив, что все работает как ожидалось, мы можем получить доступ к нашему результату, используя response.result
.
Если мы напечатаем наш ответ, результат будет получен за нас.
Здесь мы видим, что наша функция сработала!
Одна вещь, на которую нужно обратить внимание, это скрытые сбои. Способ работы семантического ядра заключается в передаче объекта контекста между функциями, который постоянно обновляется.
Это означает, что если у нас есть одна функция и она завершается с ошибкой, иногда возвращается входное значение. Мы можем продемонстрировать это, неправильно установив параметр.
Здесь мы можем явно проверить ошибку, следующим образом.
Это будет понятно, если мы напечатаем ответ, но без соответствующих проверок это может привести к запутанным результатам при доступе к результатам в приложениях!
Создание конфигурации семантической функции
В то время как create_semantic_function
полезна для начала, она не предоставляет множество опций, которые нам требуются в более сложных случаях, таких как указание модели, которую мы хотим использовать, или пользовательских аргументов, которые нам могут понадобиться.
Для наглядности зарегистрируем еще одну службу завершения текста и создадим конфигурацию, которая позволит нам указать, что мы хотим использовать нашу новую службу. Для нашей второй службы завершения текста давайте использовать модель из библиотеки Hugging Face transformers. Для этого мы используем коннектор HuggingFaceTextCompletion
.
Здесь, поскольку мы будем запускать модель локально, я выбрал GPT2, старшего представителя семейства моделей GPT, который должен быстро работать на большинстве оборудования.
from semantic_kernel.connectors.ai.hugging_face import HuggingFaceTextCompletionhf_model = HuggingFaceTextCompletion("gpt2", task="text-generation")kernel.add_text_completion_service("hf_gpt2_text_completion", hf_model)
Теперь давайте создадим наш объект config. Я нашел самый простой способ сделать это – создать словарь и загрузить config из него; таким образом, мы можем сохранить нашу конфигурацию в JSON-файл при необходимости.
Мы можем сделать это, используя следующий формат:
hf_config_dict = { "schema": 1, # Тип запроса "type": "completion", # Описание того, что делает семантическая функция "description": "Предоставляет информацию о столице, которая предоставляется в качестве входных данных, с использованием модели GPT2", # Указывает, какую модель(и) использовать "default_services": ["hf_gpt2_text_completion"], # Параметры, которые будут переданы в коннектор и модель "completion": { "temperature": 0.01, "top_p": 1, "max_tokens": 256, "number_of_responses": 1, }, # Определяет переменные, которые используются внутри запроса "input": { "parameters": [ { "name": "input", "description": "Название столицы", "defaultValue": "Лондон", } ] },}
Теперь мы можем загрузить наш config непосредственно в объект PromptTemplateConfig
.
from semantic_kernel import PromptTemplateConfigprompt_template_config = PromptTemplateConfig.from_dict(hf_config_dict)
Теперь у нас есть наш конфиг запроса, давайте создадим наш запрос. Раньше мы делали это с помощью строки, но Semantic Kernel предоставляет некоторые классы шаблонов, чтобы обеспечить более структурированное решение.
from semantic_kernel import PromptTemplateprompt_template = sk.PromptTemplate( template="{{$input}} - столица", prompt_config=prompt_template_config, template_engine=kernel.prompt_template_engine,)
Наконец, мы можем создать наш конфиг семантической функции, который объединяет наш запрос и его конфигурацию в одном объекте.
from semantic_kernel import SemanticFunctionConfigfunction_config = SemanticFunctionConfig(prompt_template_config, prompt_template)
Поскольку здесь есть несколько шагов, давайте соберем их в функцию для удобства.
from semantic_kernel import PromptTemplateConfig, SemanticFunctionConfig, PromptTemplatedef create_semantic_function_config(prompt_template, prompt_config_dict, kernel): prompt_template_config = PromptTemplateConfig.from_dict(prompt_config_dict) prompt_template = sk.PromptTemplate( template=prompt_template, prompt_config=prompt_template_config, template_engine=kernel.prompt_template_engine, ) return SemanticFunctionConfig(prompt_template_config, prompt_template)
Теперь мы можем зарегистрировать нашу семантическую функцию, используя нашу определенную конфигурацию, как показано ниже:
gpt2_complete = kernel.register_semantic_function( skill_name="GPT2Complete", function_name="gpt2_complete", function_config=create_semantic_function_config( "{{$input}} - столица", hf_config_dict, kernel ),)
Мы можем вызвать нашу функцию, как раньше.
response = gpt2_complete("Париж")
Ну, генерация, кажется, сработала, но информация неточная и в целом не очень хорошая! Это неудивительно, так как это часто бывает при использовании более старых моделей, таких как GPT2, и показывает, насколько далеко продвинулась эта область с момента ее выпуска.
Создание пользовательского коннектора
Теперь, когда мы увидели, как создать семантическую функцию и указать, какой сервис мы хотим использовать для нашей функции. Однако, до этого момента все сервисы, которые мы использовали, полагались на готовые коннекторы. В некоторых случаях мы можем захотеть использовать модель из другой библиотеки, которая в настоящее время не поддерживается, для этого нам потребуется пользовательский коннектор. Давайте посмотрим, как мы можем это сделать.
В качестве примера давайте использовать модель трансформера из библиотеки curated transformers.
Для создания пользовательского коннектора нам нужно унаследовать TextCompletionClientBase
, который выступает в качестве тонкой обертки вокруг нашей модели. Простой пример, как это сделать, приведен ниже.
from typing import List, Optional, Unionimport torchfrom curated_transformers.generation import ( AutoGenerator, SampleGeneratorConfig,)from semantic_kernel.connectors.ai.ai_exception import AIExceptionfrom semantic_kernel.connectors.ai.complete_request_settings import ( CompleteRequestSettings,)from semantic_kernel.connectors.ai.text_completion_client_base import ( TextCompletionClientBase,)class CuratedTransformersCompletion(TextCompletionClientBase): def __init__( self, model_name: str, device: Optional[int] = -1, ) -> None: """ Используйте модель curated transformer для завершения текста. Аргументы: model_name {str} device_idx {Optional[int]} -- Устройство, на котором будет запущена модель, -1 для CPU, 0+ для GPU. Обратите внимание, что эта модель будет загружена из модельного хаба Hugging Face. """ self.model_name = model_name self.device = ( "cuda:" + str(device) if device >= 0 and torch.cuda.is_available() else "cpu" ) self.generator = AutoGenerator.from_hf_hub( name=model_name, device=torch.device(self.device) ) async def complete_async( self, prompt: str, request_settings: CompleteRequestSettings ) -> Union[str, List[str]]: generator_config = SampleGeneratorConfig( temperature=request_settings.temperature, top_p=request_settings.top_p, ) try: with torch.no_grad(): result = self.generator([prompt], generator_config) return result[0] except Exception as e: raise AIException("CuratedTransformer completion failed", e) async def complete_stream_async( self, prompt: str, request_settings: CompleteRequestSettings ): raise NotImplementedError( "Streaming is not supported for CuratedTransformersCompletion." )
Теперь мы можем зарегистрировать наш коннектор и создать семантическую функцию, как показано ранее. Здесь я использую модель Falcon-7B, которая требует графический процессор для выполнения за разумное время. В данном случае я использовал Nvidia A100 на виртуальной машине Azure, так как запуск модели локально был слишком медленным.
kernel.add_text_completion_service( "falcon-7b_text_completion", CuratedTransformersCompletion(model_name="tiiuae/falcon-7b", device=0),)config_dict = { "schema": 1, # Тип промпта "type": "completion", # Описание того, что делает семантическая функция "description": "Предоставляет информацию о столице, которая задается на входе, с использованием модели Falcon-7B", # Указывает, какие модели сервис(ы) использовать "default_services": ["falcon-7b_text_completion"], # Параметры, которые будут переданы коннектору и модельному сервису "completion": { "temperature": 0.01, "top_p": 1, }, # Определяет переменные, используемые внутри промпта "input": { "parameters": [ { "name": "input", "description": "Название столицы", "defaultValue": "Лондон", } ] },}falcon_complete = kernel.register_semantic_function( skill_name="Falcon7BComplete", function_name="falcon7b_complete", function_config=create_semantic_function_config( "{{$input}} - столица", config_dict, kernel ),)
Еще раз мы видим, что генерация сработала, но быстро переходит к повторению после ответа на наш вопрос.
Вероятной причиной этого является выбранная нами модель. Обычно авторегрессивные трансформерные модели обучаются предсказывать следующее слово на большом корпусе текста, что делает их мощными автодополнителями! Здесь, кажется, она попыталась «завершить» наш вопрос, что привело к генерации текста, который нам не помогает.
Использование чат-сервиса
Некоторые LLM-модели прошли дополнительное обучение, чтобы сделать их более полезными для взаимодействия. Пример этого процесса подробно описан в статье InstructGPT от OpenAI.
На высоком уровне это обычно включает добавление одного или нескольких этапов контролируемого дообучения, где вместо случайного неструктурированного текста модель обучается на отобранных примерах задач, таких как вопросно-ответная система и резюмирование; такие модели обычно называются инструкционно настроенными или чат-моделями.
Учитывая, что мы уже наблюдали, что базовые LLM-модели могут генерировать больше текста, чем нам нужно, давайте исследуем, будет ли чат-модель вести себя иначе. Для использования нашей чат-модели нам нужно обновить нашу конфигурацию, чтобы указать соответствующий сервис и создать новую функцию; в нашем случае мы будем использовать azure_gpt35_chat_completion
.
chat_config_dict = { "schema": 1, # Тип промпта "type": "completion", # Описание того, что делает семантическая функция "description": "Предоставляет информацию о столице, которая задается на входе, с использованием модели GPT3.5", # Указывает, какие модели сервис(ы) использовать "default_services": ["azure_gpt35_chat_completion"], # Параметры, которые будут переданы коннектору и модельному сервису "completion": { "temperature": 0.0, "top_p": 1, "max_tokens": 256, "number_of_responses": 1, "presence_penalty": 0, "frequency_penalty": 0, }, # Определяет переменные, используемые внутри промпта "input": { "parameters": [ { "name": "input", "description": "Название столицы", "defaultValue": "Лондон", } ] },}capital_city_chat = kernel.register_semantic_function( skill_name="CapitalCityChat", function_name="capital_city_chat", function_config=create_semantic_function_config( "{{$input}} - столица", chat_config_dict, kernel ),)
Отлично, мы видим, что модель чата дала нам гораздо более краткий ответ!
Ранее, когда мы использовали модели завершения текста, мы форматировали свой запрос как предложение, чтобы модель его дополнила. Однако, модели, настроенные на инструкции, должны быть способны понимать вопрос, поэтому мы можем изменить наш запрос, чтобы сделать его более гибким. Давайте посмотрим, как мы можем настроить наш запрос с целью взаимодействия с моделью, как если бы это был чат-бот, предназначенный для предоставления нам информации о местах, которые мы могли бы посетить.
Сначала давайте настроим нашу конфигурацию функции, чтобы сделать наш запрос более общим.
chatbot = kernel.register_semantic_function( skill_name="Chatbot", function_name="chatbot", function_config=create_semantic_function_config( "{{$input}}", chat_config_dict, kernel ),)
Здесь мы видим, что мы передаем только входные данные пользователя, поэтому мы должны сформулировать наш ввод в виде вопроса. Давайте попробуем это.
Отлично, кажется, что это сработало. Давайте попробуем задать дополнительный вопрос.
Мы видим, что модель дала очень общий ответ, который не учитывает наш предыдущий вопрос вообще. Это ожидаемо, так как запрос, полученный моделью, был "Что можно интересного сделать там?"
, мы не предоставляли контекста о том, где находится ‘там’!
Давайте посмотрим, как мы можем расширить наш подход, чтобы создать простого чат-бота в следующем разделе.
Создание простого чат-бота
Теперь, когда мы видели, как мы можем использовать сервис чата, давайте исследуем, как мы можем создать простого чат-бота.
Наш чат-бот должен уметь делать три вещи:
- Знать свою цель и информировать нас об этом
- Понимать текущий контекст разговора
- Отвечать на наши вопросы
Давайте настроим наш запрос, чтобы отразить это.
chatbot_prompt = """"Вы - чат-бот, предоставляющий информацию о разных городах и странах. Для других вопросов, не относящихся к местам, вы должны вежливо отказаться от ответа, объяснив свою цель" +++++{{$history}}Пользователь: {{$input}}Чат-бот: """
Обратите внимание, что мы добавили переменную history
, которая будет использоваться для предоставления предыдущего контекста чат-боту. Хотя это довольно наивный подход, так как длинные разговоры быстро могут привести к достижению максимальной длины контекста модели, он должен работать для наших целей.
До сих пор мы использовали только запросы, которые используют одну переменную. Чтобы использовать несколько переменных, нам нужно изменить нашу конфигурацию, как показано ниже.
chat_config_dict = { "schema": 1, # Тип запроса "type": "completion", # Описание того, что делает семантическая функция "description": "Чат-бот, предоставляющий информацию о городах и странах", # Указывает, какие сервисы модели использовать "default_services": ["azure_gpt35_chat_completion"], # Параметры, которые будут переданы коннектору и сервису модели "completion": { "temperature": 0.0, "top_p": 1, "max_tokens": 256, "number_of_responses": 1, "presence_penalty": 0, "frequency_penalty": 0, }, # Определяет переменные, которые используются внутри запроса "input": { "parameters": [ { "name": "input", "description": "Входные данные, предоставленные пользователем", "defaultValue": "", }, { "name": "history", "description": "Предыдущие взаимодействия между пользователем и чат-ботом", "defaultValue": "", }, ] },}
Теперь давайте используем эту обновленную конфигурацию и запрос, чтобы создать наш чат-бот.
function_config = create_semantic_function_config( chatbot_prompt, chat_config_dict, kernel)chatbot = kernel.register_semantic_function( skill_name="SimpleChatbot", function_name="simple_chatbot", function_config=function_config,)
Чтобы передать несколько переменных в нашу семантическую функцию, нам нужно создать объект Context, который будет хранить состояние наших переменных. Мы можем создать его и инициализировать нашу переменную history, как показано ниже:
context = kernel.create_new_context()context["history"] = ""
Так как input является специальной переменной, это будет обрабатываться автоматически. Однако, если мы решили изменить имя этой переменной — например, на {{$user_input}}
– то это также должно быть инициализировано таким же образом.
Теперь давайте создадим простую функцию чата, чтобы обновлять наш контекст после каждого взаимодействия.
async def chat(input_text, context, verbose=True): # Сохраняем новое сообщение в переменных контекста context["input"] = input_text if verbose: # выводим полный запрос перед каждым взаимодействием print("Запрос:") print("-----") # вставляем переменные в наш запрос print(await function_config.prompt_template.render_async(context)) print("-----") # Обрабатываем сообщение пользователя и получаем ответ answer = await chatbot.invoke_async(context=context) # Показываем ответ print(f"ЧатБот: {answer}") # Добавляем новое взаимодействие в историю чата context["history"] += f"\nПользователь: {input_text}\nЧатБот: {answer}\n"
Давайте попробуем!
Здесь мы видим, что это прекрасно соответствует нашим требованиям!
Однако, было несколько мелких деталей, о которых нам нужно помнить, таких как вручную обновлять контекст после каждого взаимодействия.
Использование ChatPromptTemplate
Хотя мы уже видели, как создать простого чатбота, используя стандартный шаблон запроса, Semantic Kernel предоставляет ChatPromptTemplate
, чтобы упростить этот процесс; отслеживая предыдущие взаимодействия за нас.
Давайте используем этот класс для обновления функции, которую мы использовали для создания нашей семантической конфигурации функции.
from semantic_kernel import ( ChatPromptTemplate, SemanticFunctionConfig, PromptTemplateConfig,)def create_semantic_function_chat_config(prompt_template, prompt_config_dict, kernel): chat_system_message = ( prompt_config_dict.pop("system_prompt") if "system_prompt" in prompt_config_dict else None ) prompt_template_config = PromptTemplateConfig.from_dict(prompt_config_dict) prompt_template_config.completion.token_selection_biases = ( {} ) # required for https://github.com/microsoft/semantic-kernel/issues/2564 prompt_template = ChatPromptTemplate( template=prompt_template, prompt_config=prompt_template_config, template_engine=kernel.prompt_template_engine, ) if chat_system_message is not None: prompt_template.add_system_message(chat_system_message) return SemanticFunctionConfig(prompt_template_config, prompt_template)
Как мы видим, ChatPromptTemplate
предоставляет возможность установить системное сообщение в начале нашего взаимодействия, чтобы нам не нужно было включать его в наш запрос. Чтобы все было в одном месте, давайте добавим наше системное сообщение в наш словарь конфигурации; поскольку это не включено в определенную схему, мы должны удалить это перед использованием нашей конфигурации для создания PromptTemplateConfig
.
chat_config_dict = { "schema": 1, # Тип запроса "type": "completion", # Описание семантической функции "description": "Чатбот, предоставляющий информацию о городах и странах", # Указывает, какую модель(и) использовать "default_services": ["azure_gpt35_chat_completion"], # Параметры, которые будут переданы в коннектор и модель "completion": { "temperature": 0.0, "top_p": 1, "max_tokens": 500, "number_of_responses": 1, "presence_penalty": 0, "frequency_penalty": 0, }, # Определяет переменные, используемые в запросе "input": { "parameters": [ { "name": "input", "description": "Входные данные, указанные пользователем", "defaultValue": "", }, ] }, # Не является переменной схемы "system_prompt": "Вы чатбот, предоставляющий информацию о разных городах и странах. По другим вопросам, не связанным с местами, вы должны вежливо отказаться отвечать на вопрос, указав свою цель",}chatbot = kernel.register_semantic_function( skill_name="Чатбот", function_name="чатбот", function_config=create_semantic_function_chat_config( "{{$input}}", chat_config_dict, kernel ),)
Память
При взаимодействии с нашим чат-ботом одним из ключевых аспектов, которые делали опыт полезным, было то, что чат-бот мог сохранять контекст наших предыдущих вопросов. Мы сделали это, предоставив чат-боту доступ к памяти, используя ChatPromptTemplate
, чтобы обработать это для нас.
Хотя это работало достаточно хорошо для нашего простого случая использования, вся наша история разговора хранилась в оперативной памяти нашей системы и нигде не сохранялась; как только мы выключим нашу систему, она исчезнет навсегда. Для более интеллектуальных приложений может быть полезно создавать и сохранять как краткосрочную, так и долгосрочную память, к которой наши модели могут обращаться.
Кроме того, в нашем примере мы передавали все наши предыдущие взаимодействия в наш запрос. Поскольку у моделей обычно есть окно контекста фиксированного размера – которое определяет, насколько длинными могут быть наши запросы – это быстро приведет к сбоям, если начнется длинный разговор. Чтобы избежать этого, мы можем хранить нашу память в виде отдельных “чанков” и загружать только ту информацию, которая, как мы думаем, может быть актуальной для нашего запроса.
Semantic Kernel предлагает некоторые функции, связанные с тем, как мы можем интегрировать память в наши приложения. Давайте рассмотрим, как мы можем использовать их.
Например, давайте расширим наш чат-бот так, чтобы у него был доступ к некоторой информации, которая хранится в памяти.
Сначала нам нужна некоторая информация, которая может быть актуальна для нашего чат-бота. Хотя мы могли бы вручную исследовать и подготовить соответствующую информацию, быстрее будет попросить модель сгенерировать ее для нас! Давайте попросим модель сгенерировать некоторые факты о городе Лондон. Мы можем сделать это следующим образом.
response = chatbot( """Пожалуйста, предоставьте полный обзор того, что можно сделать в Лондоне. Структурируйте ваш ответ на основе 5 параграфов: - обзор - достопримечательности - история - культура - еда Каждый параграф должен содержать 100 токенов, не добавляйте заголовки, такие как `Обзор:` или `Еда:`, к параграфам в вашем ответе. Не подтверждайте вопрос утверждением вроде "Конечно, вот подробный обзор того, что можно сделать в Лондоне". Не делайте заключительного комментария.""")
Теперь, когда у нас есть какой-то текст, чтобы модель могла получить только ту информацию, которая ей нужна, давайте разделим его на чанки. Semantic Kernel предлагает некоторые функции для этого в своем модуле text_chunker
. Мы можем использовать его, как показано ниже:
from semantic_kernel.text import text_chunker as tcchunks = tc.split_plaintext_paragraph([london_info], max_tokens=100)
Мы видим, что текст был разделен на 8 чанков. В зависимости от текста нам придется настроить максимальное количество токенов для каждого чанка.
Использование службы встраивания текста
Теперь, когда мы разбили наши данные на чанки, нам нужно создать представление каждого чанка, которое позволит нам вычислять связь между текстом; мы можем сделать это, представив наш текст в виде встраиваний.
Чтобы сгенерировать встраивания, нам нужно добавить службу встраивания текста в нашу ядро. Аналогично предыдущему, существуют различные коннекторы, которые можно использовать, в зависимости от источника основной модели.
Сначала давайте используем модель text-embedding-ada-002
, развернутую в службе Azure OpenAI. Эта модель была обучена OpenAI, и дополнительную информацию о ней можно найти в их блоге о запуске.
from semantic_kernel.connectors.ai.open_ai import AzureTextEmbeddingkernel.add_text_embedding_generation_service( "azure_openai_embedding", AzureTextEmbedding( deployment_name=OPENAI_EMBEDDING_DEPLOYMENT_NAME, endpoint=OPENAI_ENDPOINT, api_key=OPENAI_API_KEY, ),)
Теперь, когда у нас есть доступ к модели, которая может генерировать встраивания, нам нужно где-то их хранить. Semantic Kernel предоставляет концепцию MemoryStore, которая является интерфейсом для различных поставщиков хранения.
Для производственных систем, нам, вероятно, захочется использовать базу данных для сохранения данных, чтобы упростить наш пример, мы будем использовать хранилище в памяти. Чтобы использовать память, нам нужно зарегистрировать хранилище в памяти в нашем ядре.
memory_store = sk.memory.VolatileMemoryStore()kernel.register_memory_store(memory_store=memory_store)
Хотя мы использовали хранилище в памяти, чтобы упростить наш пример, в более сложных системах мы, вероятно, захотели бы использовать базу данных для сохранения данных. Semantic Kernel предлагает подключения к популярным решениям для хранения данных, таким как CosmosDB, Redis, Postgres и многие другие. Поскольку хранилища в памяти имеют общий интерфейс, единственное изменение, которое потребуется, – это изменение используемого подключения, что облегчает переключение между провайдерами.
Теперь мы можем сохранять информацию в наше хранилище в памяти следующим образом.
for i, chunk in enumerate(chunks): await kernel.memory.save_information_async( collection="Лондон", id="chunk" + str(i), text=chunk )
Здесь мы создали новую коллекцию, чтобы объединить похожие документы.
Теперь мы можем запросить эту коллекцию следующим образом:
results = await kernel.memory.search_async( "Лондон", "что поесть в Лондоне?", limit=2)
Из результатов мы видим, что возвращена соответствующая информация, что отражается высокими оценками релевантности.
Однако это было довольно просто, поскольку у нас есть информация, непосредственно связанная с тем, о чем нас спрашивают, с использованием очень похожего языка. Давайте попробуем более тонкий запрос.
Здесь мы видим, что мы получили точно такие же результаты. Однако, поскольку второй результат явно упоминает “еду со всего мира”, я считаю, что это лучшее совпадение. Это подчеркивает некоторые потенциальные ограничения семантического подхода к поиску.
Использование модели с открытым исходным кодом
Для интереса давайте посмотрим, как модель с открытым исходным кодом сравнивается с нашим сервисом OpenAI в этом контексте. Мы можем зарегистрировать модель Hugging Face sentence transformer для этой цели, как показано ниже:
from semantic_kernel.connectors.ai.hugging_face import HuggingFaceTextEmbeddinghf_embedding_service = HuggingFaceTextEmbedding( "sentence-transformers/all-MiniLM-L6-v2", device=-1)kernel.add_text_embedding_generation_service( "hf_embedding_service", hf_embedding_service,)
Чтобы изменить наше хранилище в памяти, мы можем использовать следующий метод.
kernel.use_memory(storage=memory_store, embeddings_generator=hf_embedding_service)
Метод use_memory
– это метод удобства, эквивалентный вызову kernel.register_memory(SemanticTextMemory(storage, embeddings_generator))
. SemanticTextMemory
содержит логику генерации вложений и управления их хранением.
Теперь мы можем запросить их таким же способом, как раньше.
for i, chunk in enumerate(chunks): await kernel.memory.save_information_async( "hf_Лондон", id="chunk" + str(i), text=chunk )hf_results = await kernel.memory.search_async( "hf_Лондон", "что поесть в Лондоне", limit=2, min_relevance_score=0)
Мы видим, что мы получили те же самые части, но наши оценки релевантности разные. Мы также можем наблюдать разницу в размерах вложений, сгенерированных разными моделями.
Интеграция памяти в контекст
В нашем предыдущем примере мы видели, что хотя мы могли определить общие релевантные сведения на основе поиска вложений, для более тонких запросов мы не получили самый релевантный результат. Давайте исследуем, можем ли мы улучшить это.
Один из способов решить эту проблему состоит в том, чтобы предоставить соответствующую информацию нашему чат-боту, а затем позволить модели решить, какие части являются наиболее значимыми. Еще раз, давайте определим подходящую конфигурацию для этого.
chat_config_dict = { "schema": 1, # Тип подсказки "type": "completion", # Описание семантической функции "description": "Чат-бот, предоставляющий информацию о городах и странах", # Указывает, какие сервисы моделей использовать "default_services": ["azure_gpt35_chat_completion"], # Параметры, которые будут переданы коннектору и сервису модели "completion": { "temperature": 0.0, "top_p": 1, "max_tokens": 500, "number_of_responses": 1, "presence_penalty": 0, "frequency_penalty": 0, }, # Определяет переменные, используемые внутри подсказки "input": { "parameters": [ { "name": "question", "description": "Вопрос, заданный пользователем", "defaultValue": "", }, { "name": "context", "description": "Контекст, содержащий информацию, которая будет использоваться для ответа на вопрос", "defaultValue": "", }, ] }, # Переменная, не относящаяся к схеме "system_prompt": "Вы чат-бот, предоставляющий информацию о различных городах и странах. ",}
Затем создадим подсказку, которая инструктирует модель отвечать на вопрос на основе предоставленного контекста, и зарегистрируем семантическую функцию.
prompt_with_context = """ Используйте следующие фрагменты контекста, чтобы ответить на вопросы пользователей. Это единственная информация, которую вы должны использовать для ответа на вопрос, не обращайтесь к информации вне этого контекста. Если информация, необходимая для ответа на вопрос, не предоставлена в контексте, просто скажите "Я не знаю", не пытайтесь выдумать ответ. ---------------- Контекст: {{$context}} ---------------- Вопрос пользователя: {{$question}} ---------------- Ответ:"""chatbot_with_context = kernel.register_semantic_function( skill_name="ChatbotWithContext", function_name="chatbot_with_context", function_config=create_semantic_function_chat_config( prompt_with_context, chat_config_dict, kernel ),)
Теперь мы можем использовать эту функцию для ответа на более тонкий вопрос. Сначала создаем объект контекста и добавляем вопрос в него.
question = "Где я могу поесть не британскую пищу в Лондоне?"context = kernel.create_new_context()context["question"] = question
Затем мы можем вручную выполнить поиск по нашему вложенному объекту и добавить полученную информацию в контекст.
results = await kernel.memory.search_async("hf_London", question, limit=2)context["context"] = "\n".join([result.text for result in results])
Наконец, мы можем выполнить нашу функцию. Здесь я использую ядро для выполнения функции, а не вызываю ее напрямую; это может быть полезно, когда мы хотим запустить несколько функций последовательно.
answer = await kernel.run_async(chatbot_with_context, input_vars=context.variables)
Плагины
Примечание: В более ранних версиях Semantic Kernel плагины были известны как “навыки”; они были переименованы для согласованности с Bing и OpenAI. Поэтому многие ссылки на код и документацию относятся к “навыкам”.
Плагин в Semantic Kernel – это набор функций, которые можно загрузить в ядро для предоставления их AI-приложениям и сервисам. Функции внутри плагинов могут быть оркестрированы ядром для выполнения задач.
Документация описывает плагины как “строительные блоки” Semantic Kernel, которые могут быть связаны вместе для создания сложных рабочих процессов; так как плагины соответствуют спецификации плагинов OpenAI, плагины, созданные для сервисов OpenAI, Bing и Microsoft 365, могут быть использованы с Semantic Kernel.
Semantic Kernel предоставляет несколько плагинов изначально, включая:
- ConversationSummarySkill: Для суммирования беседы
- HttpSkill: Для вызова API
- TextMemorySkill: Для хранения и извлечения текста из памяти
- TimeSkill: Для получения времени суток и другой временной информации
Давайте начнем с исследования того, как мы можем использовать предопределенный плагин, прежде чем перейти к изучению того, как мы можем создавать пользовательские плагины.
Использование готового плагина
Один из плагинов, включенных в Semantic Kernel, это TextMemorySkill
, который предоставляет функциональность для сохранения и восстановления информации из памяти. Давайте посмотрим, как мы можем использовать это для упрощения нашего предыдущего примера заполнения контекста нашего запроса из памяти.
Сначала мы должны импортировать наш плагин, как показано ниже.
Здесь мы видим, что этот плагин содержит две семантические функции, recall
и save
.
Теперь давайте изменить наш запрос:
prompt_with_context_plugin = """ Используйте следующие контекстные элементы, чтобы ответить на вопросы пользователя. Это единственная информация, которую вы должны использовать для ответа на вопрос, не ссылайтесь на информацию вне этого контекста. Если информация, необходимая для ответа на вопрос, не предоставляется в контексте, просто скажите "Я не знаю", не пытайтесь придумать ответ. ---------------- Context: {{recall $question}} ---------------- Вопрос пользователя: {{$question}} ---------------- Ответ:"""
Мы видим, что для использования функции recall
мы можем ссылаться на нее в нашем запросе. Теперь давайте создадим конфигурацию и зарегистрируем функцию.
chat_config_dict = { "schema": 1, # Тип запроса "type": "completion", # Описание функции "description": "Чат-бот, который предоставляет информацию о городах и странах", # Указывает, какие модели использовать "default_services": ["azure_gpt35_chat_completion"], # Параметры, которые будут переданы коннектору и модели "completion": { "temperature": 0.0, "top_p": 1, "max_tokens": 500, "number_of_responses": 1, "presence_penalty": 0, "frequency_penalty": 0, }, # Определяет переменные, используемые в запросе "input": { "parameters": [ { "name": "question", "description": "Вопрос, заданный пользователем", "defaultValue": "", }, ] }, # Не является переменной запроса "system_prompt": "Вы чат-бот, который предоставляет информацию о разных городах и странах. ",}chatbot_with_context_plugin = kernel.register_semantic_function( skill_name="ChatbotWithContextPlugin", function_name="chatbot_with_context_plugin", function_config=create_semantic_function_chat_config( prompt_with_context_plugin, chat_config_dict, kernel ),)
В нашем ручном примере мы смогли контролировать аспекты, такие как количество возвращаемых результатов и коллекцию для поиска. При использовании TextMemorySkill
мы можем установить их, добавив их в наш контекст. Давайте попробуем нашу функцию.
question = "Где можно поесть не британскую еду в Лондоне?"context = kernel.create_new_context()context["question"] = questioncontext[sk.core_skills.TextMemorySkill.COLLECTION_PARAM] = "hf_London"context[sk.core_skills.TextMemorySkill.RELEVANCE_PARAM] = 0.2context[sk.core_skills.TextMemorySkill.LIMIT_PARAM] = 2answer = await kernel.run_async( chatbot_with_context_plugin, input_vars=context.variables)
Мы видим, что это эквивалентно нашему ручному подходу.
Создание пользовательских плагинов
Теперь, когда мы понимаем, как создавать семантические функции и как использовать плагины, у нас есть все необходимое, чтобы начать создавать собственные плагины!
Плагины могут содержать два типа функций:
- Семантические функции: используют естественный язык для выполнения действий
- Нативные функции: используют код Python для выполнения действий
которые могут быть объединены в одном плагине.
Выбор между использованием семантической или нативной функции зависит от задачи, которую вы выполняете. Для задач, связанных с пониманием или генерацией языка, семантические функции являются очевидным выбором. Однако для более детерминированных задач, таких как выполнение математических операций, загрузка данных или доступ к времени, нативные функции более подходят.
Давайте исследуем, как мы можем создать каждый тип. Сначала создадим папку для хранения наших плагинов.
from pathlib import Pathplugins_path = Path("Plugins")plugins_path.mkdir(exist_ok=True)
Создание плагина генератора стихов
Для нашего примера давайте создадим плагин, который генерирует стихи; для этого использование семантической функции кажется естественным выбором. Мы можем создать папку для этого плагина в нашем каталоге.
poem_gen_plugin_path = plugins_path / "PoemGeneratorPlugin"poem_gen_plugin_path.mkdir(exist_ok=True)
Напомним, что плагины – это всего лишь набор функций, и мы создаем семантическую функцию, следующая часть должна быть достаточно знакомой. Основное отличие в том, что вместо определения нашего запроса и конфигурации встроенным образом мы создадим отдельные файлы для них, чтобы облегчить загрузку.
Давайте создадим папку для нашей семантической функции, которую мы назовем write_poem
.
poem_sc_path = poem_gen_plugin_path / "write_poem"poem_sc_path.mkdir(exist_ok=True)
Затем мы создаем наш запрос и сохраняем его как skprompt.txt
.
Теперь давайте создадим нашу конфигурацию и сохраните ее в json-файле.
Хотя всегда полезно задавать содержательные описания в нашей конфигурации, это становится особенно важным, когда мы определяем плагины; плагины должны предоставлять четкие описания, которые описывают их поведение, входы и выходы, а также их побочные эффекты. Причина в том, что это интерфейс, который представляет наше ядро, и если мы хотим иметь возможность использовать LLM для оркестрации задач, ему нужно понимать функциональность плагина и способ вызова его, чтобы он мог выбирать соответствующие функции.
config_path = poem_sc_path / "config.json"
%%writefile {config_path}{ "schema": 1, "type": "completion", "description": "Генератор стихотворений, который пишет короткое стихотворение на основе входных данных пользователя", "default_services": ["azure_gpt35_chat_completion"], "completion": { "temperature": 0.0, "top_p": 1, "max_tokens": 250, "number_of_responses": 1, "presence_penalty": 0, "frequency_penalty": 0 }, "input": { "parameters": [{ "name": "input", "description": "Тема, о которой должно быть написано стихотворение", "defaultValue": "" }] }}
Обратите внимание, что поскольку мы сохраняем нашу конфигурацию в виде файла JSON, нам нужно удалить комментарии, чтобы сделать его действительным JSON.
Теперь мы можем импортировать наш плагин:
poem_gen_plugin = kernel.import_semantic_skill_from_directory( plugins_path, "PoemGeneratorPlugin")
Изучая наш плагин, мы видим, что он предоставляет нашу семантическую функцию write_poem
.
Мы можем вызвать нашу семантическую функцию напрямую:
result = poem_gen_plugin["write_poem"]("Мюнхен")
или мы можем использовать ее в другой семантической функции:
chat_config_dict = { "schema": 1, # Тип запроса "type": "completion", # Описание того, что делает семантическая функция "description": "Обертывает плагин для написания стихотворения", # Указывает, какие сервисы моделей использовать "default_services": ["azure_gpt35_chat_completion"], # Параметры, которые будут переданы в коннектор и сервис моделей "completion": { "temperature": 0.0, "top_p": 1, "max_tokens": 500, "number_of_responses": 1, "presence_penalty": 0, "frequency_penalty": 0, }, # Определяет переменные, используемые внутри запроса "input": { "parameters": [ { "name": "input", "description": "Входные данные, предоставленные пользователем", "defaultValue": "", }, ] },}prompt = """{{PoemGeneratorPlugin.write_poem $input}}"""write_poem_wrapper = kernel.register_semantic_function( skill_name="PoemWrapper", function_name="poem_wrapper", function_config=create_semantic_function_chat_config( prompt, chat_config_dict, kernel ),)result = write_poem_wrapper("Мюнхен")
Создание плагина для классификации изображений
Теперь, когда мы увидели, как использовать семантическую функцию в плагине, давайте посмотрим, как мы можем использовать встроенную функцию.
Здесь давайте создадим плагин, который принимает URL изображения, затем загружает и классифицирует изображение. Опять же, создадим папку для нашего нового плагина.
image_classifier_plugin_path = plugins_path / "ImageClassifierPlugin"image_classifier_plugin_path.mkdir(exist_ok=True)download_image_sc_path = image_classifier_plugin_path / "download_image.py"download_image_sc_path.mkdir(exist_ok=True)
Теперь мы можем создать наш модуль Python. Внутри модуля мы можем быть достаточно гибкими. Здесь мы создали класс с двумя методами, ключевым шагом является использование декоратора sk_function
для указания, какие методы должны быть доступны в рамках плагина.
В этом примере нашей функции требуется только один входной параметр. Для функций, требующих нескольких входных параметров, может использоваться sk_function_context_parameter
, как показано в документации.
import requestsfrom PIL import Imageimport timmfrom timm.data.imagenet_info import ImageNetInfofrom semantic_kernel.skill_definition import ( sk_function,)from semantic_kernel.orchestration.sk_context import SKContextclass ImageClassifierPlugin: def __init__(self): self.model = timm.create_model("convnext_tiny.in12k_ft_in1k", pretrained=True) self.model.eval() data_config = timm.data.resolve_model_data_config(self.model) self.transforms = timm.data.create_transform(**data_config, is_training=False) self.imagenet_info = ImageNetInfo() @sk_function( description="Принимает URL в качестве входного параметра и классифицирует изображение", name="classify_image", input_description="URL изображения для классификации", ) def classify_image(self, url: str) -> str: image = self.download_image(url) pred = self.model(self.transforms(image)[None]) return self.imagenet_info.index_to_description(pred.argmax()) def download_image(self, url): return Image.open(requests.get(url, stream=True).raw).convert("RGB")
Для этого примера я использовал отличную библиотеку Pytorch Image Models для создания нашего классификатора. Для получения дополнительной информации о том, как работает эта библиотека, ознакомьтесь с этим блог-постом.
Теперь мы можем просто импортировать наш плагин, как показано ниже.
image_classifier = ImageClassifierPlugin()classify_plugin = kernel.import_skill(image_classifier, skill_name="classify_image")
Изучая наш плагин, мы видим, что только наша декорированная функция доступна.
Мы можем проверить работу нашего плагина, используя изображение кошки с Pixabay.
url = "https://cdn.pixabay.com/photo/2016/02/10/16/37/cat-1192026_1280.jpg"response = classify_plugin["classify_image"](url)
При вызове функции вручную мы видим, что наше изображение было классифицировано правильно! Точно так же, как и раньше, мы также можем ссылаться на эту функцию непосредственно из приглашения. Однако, так как мы уже продемонстрировали это, давайте попробуем что-то немного другое в следующем разделе.
Соединение нескольких плагинов
Также возможно соединять несколько плагинов вместе с помощью ядра, как показано ниже.
context = kernel.create_new_context()context["input"] = urlanswer = await kernel.run_async( classify_plugin["classify_image"], poem_gen_plugin["write_poem"], input_context=context,)
Мы видим, что, используя оба плагина последовательно, мы классифицировали изображение и написали о нем стихотворение!
Организация рабочих процессов с помощью планировщика
На данном этапе мы тщательно исследовали семантические функции, понимаем, как функции могут быть сгруппированы и использованы в качестве части плагина, и видели, как мы можем вручную объединять плагины. Теперь давайте исследуем, как мы можем создавать и организовывать рабочие процессы с использованием LLMs. Для этого Semantic Kernel предоставляет объекты Planner, которые могут динамически создавать цепочки функций для попытки достижения цели.
Планировщик – это класс, который принимает пользовательскую подсказку и ядро, и использует сервисы ядра для создания плана выполнения задачи с использованием функций и плагинов, доступных ядру. Поскольку плагины являются основными строительными блоками этих планов, планировщик сильно полагается на предоставляемые описания; если у плагинов и функций нет ясных описаний, планировщик не сможет использовать их правильно. Кроме того, поскольку планировщик может комбинировать функции различными способами, важно убедиться в том, что мы разрешаем использование только тех функций, с которыми мы довольны.
Поскольку планировщик полагается на модель для создания плана, могут возникать ошибки; они обычно возникают, когда планировщик неправильно понимает, как использовать функцию. В этих случаях я обнаружил, что предоставление явных инструкций – таких как описание входов и выходов и указание, являются ли входы обязательными – в описаниях может привести к лучшим результатам. Кроме того, у меня получалось лучше результатов с использованием настроенных моделей, чем с базовыми моделями; базовые модели завышают наличие функций, которых нет, или создают несколько планов. Несмотря на эти ограничения, при правильной работе планировщики могут быть невероятно мощными!
Давайте исследуем, как мы можем сделать это, изучая, можем ли мы создать план для написания стихотворения о изображении на основе его URL-адреса; с использованием ранее созданных плагинов. Поскольку мы определили множество функций, которые нам больше не нужны, давайте создадим новое ядро, чтобы мы могли контролировать, какие функции доступны.
kernel = sk.Kernel()
Чтобы создать наш план, давайте использовать наш сервис чата OpenAI.
kernel.add_chat_service( service_id="azure_gpt35_chat_completion", service=AzureChatCompletion( OPENAI_DEPLOYMENT_NAME, OPENAI_ENDPOINT, OPENAI_API_KEY ),)
Изучив наши зарегистрированные сервисы, мы видим, что наш сервис может использоваться как для завершения текста, так и для завершения чата.
Теперь давайте импортируем наши плагины.
classify_plugin = kernel.import_skill( ImageClassifierPlugin(), skill_name="classify_image")poem_gen_plugin = kernel.import_semantic_skill_from_directory( plugins_path, "PoemGeneratorPlugin")
Мы можем увидеть, к каким функциям имеет доступ наше ядро, как показано ниже.
Теперь давайте импортируем наш объект планировщика.
from semantic_kernel.planning.basic_planner import BasicPlannerplanner = BasicPlanner()
Для использования нашего планировщика нам нужна только подсказка. Часто нам придется настраивать это в зависимости от создаваемых планов. Здесь я постарался быть как можно более ясным относительно требуемого ввода.
ask = f"""Я хотел бы, чтобы вы написали стихотворение о том, что содержится на этом изображении с этим URL-адресом: {url}. Этот URL-адрес должен быть использован в качестве входных данных."""
Затем мы можем использовать наш планировщик, чтобы создать план, как он будет решать задачу.
plan = await planner.create_plan_async(ask, kernel)
Изучив наш план, мы видим, что модель правильно определила наш ввод и правильные функции для использования!
Наконец, все, что остается сделать, это выполнить наш план.
poem = await planner.execute_plan_async(plan, kernel)
Вау, это сработало! Для модели, обученной предсказывать следующее слово, это довольно мощно!
Как предупреждение, я был довольно удачливым, создавая этот пример, и сгенерированный план сработал с первого раза. Однако, запуская это несколько раз с одним и тем же подсказывающим текстом, мы можем увидеть, что это не всегда так, поэтому важно дважды проверить свой план перед его выполнением! Лично для меня, в производственной системе я бы чувствовал себя намного увереннее, создавая рабочий процесс для выполнения вручную, а не оставляя это LLM! По мере улучшения технологии, особенно на текущей скорости, надеюсь, эта рекомендация устареет!
Вывод
Надеюсь, что это предоставило хорошее введение в Semantic Kernel и вдохновило вас исследовать его использование для ваших собственных случаев использования.
Весь необходимый код для воспроизведения этой публикации доступен в виде записной книжки здесь.
Крис Хьюз находится в LinkedIn
Ссылки
- Introducing ChatGPT (openai.com)
- microsoft/semantic-kernel: Интеграция передовой технологии LLM быстро и легко в ваши приложения (github.com)
- Who We Are — Microsoft Solutions Playbook
- kernel — Викисловарь, свободный словарь
- Overview — OpenAI API
- Azure OpenAI Service — Advanced Language Models | Microsoft Azure
- Hugging Face Hub documentation
- Azure OpenAI Service models — Azure OpenAI | Microsoft Learn
- Create Your Azure Free Account Today | Microsoft Azure
- How to use prompt template language in Semantic Kernel | Microsoft Learn
- asyncio — Асинхронный ввод-вывод — Документация Python 3.11.5
- 🤗 Transformers (huggingface.co)
- gpt2 · Hugging Face
- explosion/curated-transformers: 🤖 Библиотека PyTorch с подобранными моделями и их составными частями для использования с трансформерами (github.com)
- tiiuae/falcon-7b · Hugging Face
- ND A100 v4-series — Виртуальные машины Azure | Microsoft Learn
- [2203.02155] Training language models to follow instructions with human feedback (arxiv.org)
- Azure OpenAI Service models — Azure OpenAI | Microsoft Learn
- New and improved embedding model (openai.com)
- Introduction — Azure Cosmos DB | Microsoft Learn
- PostgreSQL: Самая передовая открытая база данных
- sentence-transformers/all-MiniLM-L6-v2 · Hugging Face
- Understanding AI plugins in Semantic Kernel and beyond | Microsoft Learn
- Out-of-the-box plugins available in Semantic Kernel | Microsoft Learn
- How to add native code to your AI apps with Semantic Kernel | Microsoft Learn
- huggingface/pytorch-image-models: Модели изображений PyTorch, скрипты, предобученные веса — ResNet, ResNeXT, EfficientNet, NFNet, Vision Transformer (ViT), MobileNet-V3/V2, RegNet, DPN, CSPNet, Swin Transformer, MaxViT, CoAtNet, ConvNeXt и другие (github.com)
- Getting Started with PyTorch Image Models (timm): Практическое руководство | Chris Hughes | Towards Data Science