Что такое подключаемый модуль?

Подключаемые модули являются ключевым компонентом семантического ядра. Если вы уже использовали подключаемые модули из расширений ChatGPT или Copilot в Microsoft 365, вы уже знакомы с ними. С помощью подключаемых модулей можно инкапсулировать существующие API-интерфейсы в коллекцию, которая может использоваться ИИ. Это позволяет предоставить ИИ возможность выполнять действия, которые он не сможет сделать в противном случае.

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

Подключаемый модуль семантического ядра

Не все пакеты SDK для ИИ имеют аналогичную концепцию подключаемых модулей (большинство из них имеют функции или инструменты). Однако в корпоративных сценариях подключаемые модули ценны, так как они инкапсулируют набор функциональных возможностей, которые отражают, как корпоративные разработчики уже разрабатывают службы и API. Подключаемые модули также хорошо играют с внедрением зависимостей. В конструкторе подключаемого модуля можно внедрить службы, необходимые для выполнения работы подключаемого модуля (например, подключения к базе данных, HTTP-клиентов и т. д.). Это трудно сделать с другими пакетами SDK, которые не имеют подключаемых модулей.

Анатомия подключаемого модуля

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

Примечание.

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

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

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

На рисунке справа llM, скорее всего, вызовет ShortPoem и StoryGen функции для удовлетворения пользователей, запрашивая благодаря предоставленным семантические описания.

Семантическое описание в подключаемом модуле WriterPlugin

Импорт различных типов подключаемых модулей

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

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

Совет

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

Различные типы функций подключаемого модуля

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

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

Дополнительные сведения о различных типах функций подключаемого модуля см. в следующих статьях:

Начало работы с подключаемыми модулями

Использование подключаемых модулей в семантическом ядре всегда является трехэтапным процессом:

  1. Определение подключаемого модуля
  2. Добавление подключаемого модуля в ядро
  3. А затем вызовите функции подключаемого модуля в запросе с вызовом функции.

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

1) Определение подключаемого модуля

Проще всего создать подключаемый модуль путем определения класса и аннотирования его методов атрибутом KernelFunction . Это позволит семантике ядра знать, что это функция, которая может вызываться ИИ или ссылаться на запрос.

Вы также можете импортировать подключаемые модули из спецификации OpenAPI.

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

Совет

Так как большинство LLM были обучены с помощью Python для вызова функций, рекомендуется использовать змеиный вариант для имен функций и имен свойств, даже если вы используете пакет SDK для C# или Java.

using System.ComponentModel;
using Microsoft.SemanticKernel;

public class LightsPlugin
{
   // Mock data for the lights
   private readonly List<LightModel> lights = new()
   {
      new LightModel { Id = 1, Name = "Table Lamp", IsOn = false, Brightness = 100, Hex = "FF0000" },
      new LightModel { Id = 2, Name = "Porch light", IsOn = false, Brightness = 50, Hex = "00FF00" },
      new LightModel { Id = 3, Name = "Chandelier", IsOn = true, Brightness = 75, Hex = "0000FF" }
   };

   [KernelFunction("get_lights")]
   [Description("Gets a list of lights and their current state")]
   [return: Description("An array of lights")]
   public async Task<List<LightModel>> GetLightsAsync()
   {
      return lights
   }

   [KernelFunction("get_state")]
   [Description("Gets the state of a particular light")]
   [return: Description("The state of the light")]
   public async Task<LightModel?> GetStateAsync([Description("The ID of the light")] int id)
   {
      // Get the state of the light with the specified ID
      return lights.FirstOrDefault(light => light.Id == id);
   }

   [KernelFunction("change_state")]
   [Description("Changes the state of the light")]
   [return: Description("The updated state of the light; will return null if the light does not exist")]
   public async Task<LightModel?> ChangeStateAsync(int id, LightModel LightModel)
   {
      var light = lights.FirstOrDefault(light => light.Id == id);

      if (light == null)
      {
         return null;
      }

      // Update the light with the new state
      light.IsOn = LightModel.IsOn;
      light.Brightness = LightModel.Brightness;
      light.Hex = LightModel.Hex;

      return light;
   }
}

public class LightModel
{
   [JsonPropertyName("id")]
   public int Id { get; set; }

   [JsonPropertyName("name")]
   public string Name { get; set; }

   [JsonPropertyName("is_on")]
   public bool? IsOn { get; set; }

   [JsonPropertyName("brightness")]
   public byte? Brightness { get; set; }

   [JsonPropertyName("hex")]
   public string? Hex { get; set; }
}
from typing import TypedDict, Annotated

class LightModel(TypedDict):
   id: int
   name: str
   is_on: bool | None
   brightness: int | None
   hex: str | None

class LightsPlugin:
   lights: list[LightModel] = [
      {"id": 1, "name": "Table Lamp", "is_on": False, "brightness": 100, "hex": "FF0000"},
      {"id": 2, "name": "Porch light", "is_on": False, "brightness": 50, "hex": "00FF00"},
      {"id": 3, "name": "Chandelier", "is_on": True, "brightness": 75, "hex": "0000FF"},
   ]

   @kernel_function
   async def get_lights(self) -> Annotated[list[LightModel], "An array of lights"]:
      """Gets a list of lights and their current state."""
      return self.lights

   @kernel_function
   async def get_state(
      self,
      id: Annotated[int, "The ID of the light"]
   ) -> Annotated[LightModel | None], "The state of the light"]:
      """Gets the state of a particular light."""
      for light in self.lights:
         if light["id"] == id:
               return light
      return None

   @kernel_function
   async def change_state(
      self,
      id: Annotated[int, "The ID of the light"],
      new_state: LightModel
   ) -> Annotated[Optional[LightModel], "The updated state of the light; will return null if the light does not exist"]:
      """Changes the state of the light."""
      for light in self.lights:
         if light["id"] == id:
               light["is_on"] = new_state.get("is_on", light["is_on"])
               light["brightness"] = new_state.get("brightness", light["brightness"])
               light["hex"] = new_state.get("hex", light["hex"])
               return light
      return None
public class LightsPlugin {

  // Mock data for the lights
  private final Map<Integer, LightModel> lights = new HashMap<>();

  public LightsPlugin() {
    lights.put(1, new LightModel(1, "Table Lamp", false));
    lights.put(2, new LightModel(2, "Porch light", false));
    lights.put(3, new LightModel(3, "Chandelier", true));
  }

  @DefineKernelFunction(name = "get_lights", description = "Gets a list of lights and their current state")
  public List<LightModel> getLights() {
    System.out.println("Getting lights");
    return new ArrayList<>(lights.values());
  }

  @DefineKernelFunction(name = "change_state", description = "Changes the state of the light")
  public LightModel changeState(
      @KernelFunctionParameter(name = "id", description = "The ID of the light to change") int id,
      @KernelFunctionParameter(name = "isOn", description = "The new state of the light") boolean isOn) {
    System.out.println("Changing light " + id + " " + isOn);
    if (!lights.containsKey(id)) {
      throw new IllegalArgumentException("Light not found");
    }

    lights.get(id).setIsOn(isOn);

    return lights.get(id);
  }
}

Обратите внимание, что мы предоставляем описания для функции, возвращаемого значения и параметров. Это важно для ИИ, чтобы понять, что делает функция и как его использовать.

Совет

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

2) Добавление подключаемого модуля в ядро

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

В этом примере демонстрируется самый простой способ добавления класса в качестве подключаемого модуля с методом AddFromType . Дополнительные сведения о других способах добавления подключаемых модулей см. в статье о добавлении собственных подключаемых модулей.

var builder = new KernelBuilder();
builder.Plugins.AddFromType<LightsPlugin>("Lights")
Kernel kernel = builder.Build();
kernel = Kernel()
kernel.add_plugin(
   LightsPlugin(),
   plugin_name="Lights",
)
// Import the LightsPlugin
KernelPlugin lightPlugin = KernelPluginFactory.createFromObject(new LightsPlugin(),
    "LightsPlugin");
// Create a kernel with Azure OpenAI chat completion and plugin
Kernel kernel = Kernel.builder()
    .withAIService(ChatCompletionService.class, chatCompletionService)
    .withPlugin(lightPlugin)
    .build();

3) Вызов функций подключаемого модуля

Наконец, вы можете вызвать функции подключаемого модуля СИ с помощью вызова функции. Ниже приведен пример, демонстрирующий, как принудить get_lights ИИ вызвать функцию из Lights подключаемого модуля перед вызовом change_state функции, чтобы включить свет.

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;

// Create a kernel with Azure OpenAI chat completion
var builder = Kernel.CreateBuilder().AddAzureOpenAIChatCompletion(modelId, endpoint, apiKey);

// Build the kernel
Kernel kernel = builder.Build();
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();

// Add a plugin (the LightsPlugin class is defined below)
kernel.Plugins.AddFromType<LightsPlugin>("Lights");

// Enable planning
OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new() 
{
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};

// Create a history store the conversation
var history = new ChatHistory();
history.AddUserMessage("Please turn on the lamp");

// Get the response from the AI
var result = await chatCompletionService.GetChatMessageContentAsync(
   history,
   executionSettings: openAIPromptExecutionSettings,
   kernel: kernel);

// Print the results
Console.WriteLine("Assistant > " + result);

// Add the message from the agent to the chat history
history.AddAssistantMessage(result);
import asyncio

from semantic_kernel import Kernel
from semantic_kernel.functions import kernel_function
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from semantic_kernel.connectors.ai.function_call_behavior import FunctionCallBehavior
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.functions.kernel_arguments import KernelArguments

from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import (
    AzureChatPromptExecutionSettings,
)

async def main():
   # Initialize the kernel
   kernel = Kernel()

   # Add Azure OpenAI chat completion
   chat_completion = AzureChatCompletion(
      deployment_name="your_models_deployment_name",
      api_key="your_api_key",
      base_url="your_base_url",
   )
   kernel.add_service(chat_completion)

   # Add a plugin (the LightsPlugin class is defined below)
   kernel.add_plugin(
      LightsPlugin(),
      plugin_name="Lights",
   )

   # Enable planning
   execution_settings = AzureChatPromptExecutionSettings(tool_choice="auto")
   execution_settings.function_call_behavior = FunctionCallBehavior.EnableFunctions(auto_invoke=True, filters={})

   # Create a history of the conversation
   history = ChatHistory()
   history.add_message("Please turn on the lamp")

   # Get the response from the AI
   result = await chat_completion.get_chat_message_content(
      chat_history=history,
      settings=execution_settings,
      kernel=kernel,
   )

   # Print the results
   print("Assistant > " + str(result))

   # Add the message from the agent to the chat history
   history.add_message(result)

# Run the main function
if __name__ == "__main__":
    asyncio.run(main())
// Enable planning
InvocationContext invocationContext = new InvocationContext.Builder()
    .withReturnMode(InvocationReturnMode.LAST_MESSAGE_ONLY)
    .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true))
    .build();

// Create a history to store the conversation
ChatHistory history = new ChatHistory();
history.addUserMessage("Turn on light 2");

List<ChatMessageContent<?>> results = chatCompletionService
    .getChatMessageContentsAsync(history, kernel, invocationContext)
    .block();

System.out.println("Assistant > " + results.get(0));

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

Роль Сообщение
🔵Пользователь Включите лампу
🔴Помощник (вызов функции) Lights.get_lights()
🟢Инструмент [{ "id": 1, "name": "Table Lamp", "isOn": false, "brightness": 100, "hex": "FF0000" }, { "id": 2, "name": "Porch light", "isOn": false, "brightness": 50, "hex": "00FF00" }, { "id": 3, "name": "Chandelier", "isOn": true, "brightness": 75, "hex": "0000FF" }]
🔴Помощник (вызов функции) Lights.change_state(1, { "isOn": true })
🟢Инструмент { "id": 1, "name": "Table Lamp", "isOn": true, "brightness": 100, "hex": "FF0000" }
🔴Помощник Лампа в настоящее время включена

Совет

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