Что такое подключаемый модуль?
Подключаемые модули являются ключевым компонентом семантического ядра. Если вы уже использовали плагины к ChatGPT или расширения Copilot в Microsoft 365, то вы с ними уже знакомы. С помощью подключаемых модулей можно инкапсулировать существующие API-интерфейсы в коллекцию, которая может использоваться ИИ. Это позволяет предоставить ИИ возможность выполнять действия, которые он не сможет сделать в противном случае.
За кулисами семантическое ядро использует функцию вызова , встроенную возможность большинства последних LLM для осуществления планирования и вызова ваших API. При вызове функции LLMs может запрашивать (т. е. вызывать) определенную функцию. Затем семантическое ядро перенаправляет запрос к соответствующей функции в кодовой базе и возвращает результаты обратно в LLM, чтобы он смог создать окончательный ответ.
семантическое ядро
Не все пакеты SDK для ИИ имеют аналогичную концепцию подключаемых модулей (большинство из них имеют функции или инструменты). Однако в корпоративных сценариях подключаемые модули ценны, так как они инкапсулируют набор функциональных возможностей, которые отражают, как корпоративные разработчики уже разрабатывают службы и API. Подключаемые модули также хорошо работают с внедрением зависимостей. В конструкторе подключаемого модуля можно внедрить службы, необходимые для выполнения работы подключаемого модуля (например, подключения к базам данных, HTTP-клиентам и т. д.). Это трудно сделать с другими SDK, которые не имеют плагинов.
Анатомия подключаемого модуля
На высоком уровне подключаемый модуль — это группа функций , которые могут быть доступны приложениям и службам ИИ. Затем функции в подключаемых модулях можно управлять приложением ИИ для выполнения запросов пользователей. В семантическом ядре эти функции можно вызывать автоматически с помощью вызова функций.
Заметка
В других платформах функции часто называются "инструментами" или "действиями". В семантическом ядре мы используем термин "функции", так как они обычно определяются как собственные функции в базе кода.
Однако просто предоставление функций недостаточно для создания плагина. Чтобы включить автоматическую оркестрацию с вызовом функций, плагины также должны предоставить сведения, которые семантически описывают их поведение. Все от входных, выходных данных и побочных эффектов функции необходимо описать таким образом, чтобы ИИ понимал, в противном случае ИИ не будет правильно вызывать функцию.
Например, подключаемый модуль WriterPlugin
справа содержит функции с семантическими описаниями, которые поясняют, что делает каждая функция. Затем LLM может использовать эти описания, чтобы выбрать лучшие функции для вызова, чтобы выполнить просьбу пользователя.
На рисунке справа LLM, скорее всего, вызовет функции ShortPoem
и StoryGen
, чтобы удовлетворить запрос пользователя благодаря предоставленным семантическим описаниям.
Импорт различных типов подключаемых модулей
Существует два основных способа импорта подключаемых модулей в семантическое ядро: использование
Ниже приведен простой пример импорта и использования нативного подключаемого модуля. Дополнительные сведения об импорте этих различных типов подключаемых модулей см. в следующих статьях:
Совет
При начале работы рекомендуется использовать плагины нативного кода. По мере развития приложения и при работе между кроссплатформенными командами может потребоваться использовать спецификации OpenAPI для совместного использования подключаемых модулей на разных языках и платформах программирования.
Различные типы функций подключаемого модуля
В плагине обычно используются два разных типа функций: те, которые извлекают данные для генерации с расширением (RAG), и те, которые автоматизируют задачи. Хотя каждый тип функционально одинаков, они обычно используются по-разному в приложениях, использующих семантические ядра.
Например, с функциями извлечения может потребоваться использовать стратегии для повышения производительности (например, кэширования и использования более дешевых промежуточных моделей для суммирования). ** При использовании функций автоматизации задач, вы, вероятно, захотите реализовать процессы утверждения с участием человека, чтобы обеспечить правильность выполнения задач.
Дополнительные сведения о различных типах функций подключаемого модуля см. в следующих статьях:
Начало работы с плагинами
Использование плагинов в оболочке Semantic Kernel всегда включает три этапа:
- Определение подключаемого модуля
- Добавьте подключаемый модуль в ваше ядро
- А затем вызовите функции подключаемого модуля в запросе с вызовом функции
Ниже мы приведем общий пример использования плагина в семантическом ядре. Дополнительные сведения о создании и использовании подключаемых модулей см. в приведенных выше ссылках.
1) Определение подключаемого модуля
Проще всего создать подключаемый модуль путем определения класса и аннотирования его методов с помощью атрибута KernelFunction
. Это позволит Semantic Kernel понимать, что это функция, которую может вызывать ИИ или на которую можно ссылаться в запросе.
Вы также можете импортировать подключаемые модули из спецификации OpenAPI.
Ниже мы создадим плагин, который может получить статус освещения и изменить его.
Совет
Так как большинство LLM обучены с использованием Python для вызовов функций, рекомендуется использовать snake_case для имен функций и свойств, даже если вы используете 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_choice_behavior import FunctionChoiceBehavior
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()
execution_settings.function_choice_behavior = FunctionChoiceBehavior.Auto()
# 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" } |
🔴 помощник | Лампа в настоящее время включена |
Совет
Хотя вы можете вызвать функцию подключаемого модуля напрямую, это не рекомендуется, так как именно ИИ должен решать, какие функции следует вызывать. Если вам нужен явный контроль над вызовом функций, рекомендуется использовать стандартные методы в базе кода вместо подключаемых модулей.
Общие рекомендации по разработке плагинов
Учитывая, что каждый сценарий имеет уникальные требования, использует различные макеты подключаемых модулей и может включать несколько LLM, сложно предоставить одноразмерное руководство по проектированию подключаемых модулей. Однако ниже приведены некоторые общие рекомендации и рекомендации по обеспечению того, чтобы подключаемые модули были понятными для искусственного интеллекта и могут быть легко и эффективно использованы LLM.
Импортируйте только нужные подключаемые модули
Импортируйте только те подключаемые модули, в которых содержатся функции, необходимые для конкретного сценария. Этот подход не только уменьшит число потребляемых входных токенов, но и сведёт к минимуму количество вызовов функций, которые не используются в сценарии. В целом эта стратегия должна повысить точность при вызове функций и уменьшить количество ложных срабатываний.
Кроме того, OpenAI рекомендует использовать не более 20 средств в одном вызове API; в идеале не более 10 инструментов. Как говорится в OpenAI: "Рекомендуется использовать не более 20 средств в одном вызове API. Разработчики обычно видят сокращение возможностей модели выбрать правильный инструмент после определения 10-20 инструментов"* Дополнительные сведения см. в документации по руководстве по вызову функций OpenAI.
Сделайте подключаемые модули дружественными к ИИ
Чтобы повысить способность LLM понять и использовать подключаемые модули, рекомендуется выполнить следующие рекомендации.
Использовать описательные и краткие имена функций: убедитесь, что имена функций четко передают их назначение, чтобы помочь модели понять, когда выбрать каждую функцию. Если имя функции неоднозначно, рекомендуется переименовать ее для ясности. Избегайте использования аббревиаций или акронимов, чтобы сократить имена функций. Используйте
DescriptionAttribute
для предоставления дополнительного контекста и инструкций только при необходимости, чтобы минимизировать потребление токенов.Минимизируйте параметры функции: ограничьте количество параметров функции и используйте примитивные типы по возможности. Этот подход снижает потребление токенов и упрощает сигнатуру функции, что способствует более эффективному сопоставлению параметров функции с LLM.
Четко именуйте параметры функции: Давайте описательные имена параметрам функции, чтобы уточнить их назначение. Избегайте использования аббревиаций или акронимов, чтобы сократить имена параметров, так как это поможет LLM в рассуждениях о параметрах и предоставлении точных значений. Как и в случае с именами функций, используйте
DescriptionAttribute
только при необходимости, чтобы свести к минимуму потребление маркеров.
Поиск правильного баланса между количеством функций и их обязанностями
С одной стороны, наличие функций с одной ответственностью является хорошей практикой, которая позволяет поддерживать простые и многократно используемые функции в нескольких сценариях. С другой стороны, каждый вызов функции вызывает затраты с точки зрения задержки кругового пути сети и количества потребляемых входных и выходных маркеров: входные маркеры используются для отправки определения функции и вызова результата в LLM, а выходные маркеры используются при получении вызова функции из модели.
Кроме того, можно реализовать одну функцию с несколькими обязанностями, чтобы сократить количество потребляемых маркеров и снизить нагрузку на сеть, хотя это происходит за счет снижения повторного использования в других сценариях.
Однако объединение многих обязанностей в одну функцию может увеличить количество и сложность параметров функции и его возвращаемого типа. Эта сложность может привести к ситуациям, когда модель может бороться за правильное соответствие параметров функции, что приводит к пропущенным параметрам или значениям неправильного типа. Поэтому важно обеспечить правильный баланс между числом функций, чтобы сократить расходы на сеть и количество обязанностей каждой функции, гарантируя, что модель может точно соответствовать параметрам функции.
Преобразование функций семантического ядра
Используйте техники преобразования функций семантического ядра, как описано в записи блога Преобразование функций семантического ядра, для:
Изменение поведения функции: Существуют сценарии, в которых поведение функции по умолчанию может не соответствовать желаемому результату, и невозможно изменить реализацию исходной функции. В таких случаях можно создать новую функцию, которая упаковывает исходную функцию и изменяет его поведение соответствующим образом.
Укажите сведения о контексте: функции могут требовать параметры, которые LLM невозможно или не следует выводить. Например, если функция должна действовать от имени текущего пользователя или требует сведений о проверке подлинности, этот контекст обычно доступен для ведущего приложения, но не для LLM. В таких случаях можно преобразовать функцию, чтобы вызвать исходную функцию, предоставив необходимые сведения о контексте из хост-приложения вместе с аргументами, предоставляемыми LLM.
Изменение списка параметров, типов и имен: Если исходная функция имеет сложную сигнатуру, которую LLM пытается интерпретировать, можно преобразовать функцию в одну с более простой сигнатурой, которую LLM может легко понять. Это может включать изменение имен параметров, типов, количества параметров и сглаживание или разворачивание сложных параметров, среди прочих корректировок.
Использование локального состояния
При разработке подключаемых модулей, работающих на относительно больших или конфиденциальных наборах данных, таких как документы, статьи или сообщения электронной почты, содержащие конфиденциальную информацию, рекомендуется использовать локальное состояние для хранения исходных данных или промежуточных результатов, которые не нужно отправлять в LLM. Функции для таких сценариев могут принимать и возвращать идентификатор состояния, что позволяет искать и получать доступ к данным локально, а не передавать фактические данные в LLM, только чтобы получить его обратно в качестве аргумента для следующего вызова функции.
Хранение данных локально позволяет обеспечить конфиденциальность и безопасность информации, избегая ненужного использования токенов во время вызовов функций. Этот подход не только повышает конфиденциальность данных, но и повышает общую эффективность обработки больших или конфиденциальных наборов данных.