什么是插件?
插件是语义内核的关键组件。 如果已在 Microsoft 365 中使用了 ChatGPT 或 Copilot 扩展中的插件,则已熟悉它们。 使用插件,可以将现有 API 封装到 AI 可以使用的集合中。 这使你可以赋予你的 AI 执行其本来无法完成的操作的能力。
在后台,语义内核利用 函数调用,这是大多数最新的 LLM 的原生功能,允许 LLM 执行 规划 并调用您的 API。 使用函数调用,大型语言模型 (LLM) 可以请求某个特定函数,即进行调用。 然后,语义内核将请求封送至代码库中的相应函数,并将结果返回给 LLM,以便 LLM 可以生成最终响应。
并非所有 AI SDK 都有类似于插件的概念(大多数只有函数或工具)。 但是,在企业方案中,插件很有价值,因为它们封装了一组功能,反映了企业开发人员已经开发服务和 API 的方式。 插件也很好地与依赖注入协作。 在插件的构造函数中,可以注入执行插件工作所需的服务(例如数据库连接、HTTP 客户端等)。 这很难通过缺少插件的其他 SDK 来实现。
插件剖析
在高级别上,插件是一组 函数,可以向 AI 应用和服务公开。 然后,AI 应用程序可以协调插件内的函数来完成用户请求。 在 Semantic Kernel 中,可以通过函数调用来自动调用这些函数。
注意
在其他平台上,函数通常称为“工具”或“操作”。 在语义内核中,我们使用术语“functions”,因为它们通常定义为代码库中的本机函数。
但是,只是提供函数不足以生成插件。 若要通过函数调用为自动业务流程提供支持,插件还需要提供以语义方式描述其行为的详细信息。 需要用 AI 可以理解的方式描述函数的输入、输出和副作用的所有内容,否则 AI 将无法正确调用函数。
例如,右侧的示例 WriterPlugin
插件具有用于描述每个函数用途的语义说明的函数。 然后,LLM 可以使用这些说明选择要调用以满足用户请求的最佳函数。
在右侧的图片中,LLM 可能会调用 ShortPoem
和 StoryGen
函数来满足用户的要求,这要归功于提供的语义说明。
WriterPlugin 插件中的
导入不同类型的插件
将插件导入语义内核有两种主要方法:使用 本机代码 或使用 OpenAPI 规范。 前者允许你在现有的代码库中创作插件,这些插件可以利用你已有的依赖项和服务。 后者允许从 OpenAPI 规范导入插件,该规范可以跨不同的编程语言和平台共享。
下面提供了一个简单的导入和使用本机插件的示例。 若要详细了解如何导入这些不同类型的插件,请参阅以下文章:
提示
入门时,建议使用本机代码插件。 随着您的应用程序逐渐成熟,并且在跨平台团队中工作时,您可能需要考虑使用 OpenAPI 规范在不同的编程语言和平台之间共享插件。
不同类型的插件函数
在插件中,通常有两种不同类型的函数:用于检索以增强生成(RAG)的数据的函数,以及用于自动化执行任务的函数。 虽然每种类型在功能上相同,但它们通常在使用语义内核的应用程序中使用不同。
例如,使用检索函数时,你可能想要使用策略来提高性能(例如缓存和使用更便宜的中间模型进行汇总)。 而使用任务自动化函数时,你可能希望实现人工循环审批流程,以确保任务正确完成。
若要详细了解不同类型的插件函数,请参阅以下文章:
插件入门
在语义内核中使用插件始终是三个步骤:
下面我们将提供有关如何在语义内核中使用插件的高级示例。 有关如何创建和使用插件的更多详细信息,请参阅上面的链接。
1) 定义插件
创建插件的最简单方法是使用 KernelFunction
属性定义类并批注其方法。 这让语义内核知道,这是可由 AI 调用或在提示中引用的函数。
还可以从 OpenAPI 规范导入插件。
下面,我们将创建一个插件,该插件可以检索灯的状态并更改其状态。
提示
由于大多数 LLM 已使用 Python 进行函数调用的训练,因此建议,即使使用 C# 或 Java SDK,也要对函数名称和属性名称使用蛇形命名法。
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);
}
}
请注意,我们提供了函数、返回值和参数的说明。 对于 AI 来说,了解函数的作用以及如何使用它,这一点非常重要。
提示
如果 AI 在调用函数时遇到问题,请不要害怕为函数提供详细说明。 使用少量示例、关于何时使用(或不使用)该功能的建议,以及获取所需参数的指导都是很有帮助的。
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) 调用插件函数
最后,可以使用函数调用让 AI 调用插件的函数。 在下面的示例中,演示如何通过引导 AI 在调用 change_state
函数以打开灯之前,先从 Lights
插件调用 get_lights
函数。
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_call_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" } |
🔴 助手 | 灯现已打开 |
提示
尽管可以直接调用插件函数,但不建议这样做,因为应该由 AI 来决定调用哪些函数。 如果需要显式控制调用哪些函数,请考虑在代码库中使用标准方法,而不是插件。
创作插件的一般建议
考虑到每个场景都有独特的要求,利用不同的插件设计,并可能采用多个 LLM,因此很难提供一套适用于所有插件设计的统一指南。 但是,下面是一些常规建议和指南,以确保插件对 AI 友好且可由 LLM 轻松高效地使用。
仅导入必要的插件
仅导入包含特定方案所需的函数的插件。 此方法不仅会减少消耗的输入令牌数,还能最大程度地减少对方案中未使用的函数的函数错误调用次数。 总体而言,此策略应增强函数调用准确性,并减少误报数。
此外,OpenAI 建议在单个 API 调用中使用不超过 20 个工具;理想情况下,不超过 10 个工具。 如 OpenAI 所述:
使插件适合 AI
为了增强 LLM 了解和使用插件的能力,建议遵循以下准则:
使用描述性和简洁的函数名称: 确保函数名称清楚地传达其用途,以帮助模型了解何时选择每个函数。 如果函数名称不明确,请考虑重命名它,以便清楚起见。 避免使用缩写或首字母缩略词来缩短函数名称。 仅在必要时利用
DescriptionAttribute
提供其他上下文和说明,最大程度地减少令牌消耗。最小化函数参数: 限制函数参数的数量,并尽可能使用基元类型。 此方法可减少令牌消耗并简化函数签名,从而使 LLM 能够有效地匹配函数参数。
清楚地命名函数参数: 为函数参数分配描述性名称以阐明其用途。 避免使用缩写或首字母缩略词来缩短参数名称,因为这将有助于 LLM 推理参数并提供准确的值。 与函数名称一样,仅在必要时使用
DescriptionAttribute
来最大程度地减少令牌消耗。
在函数数量与其职责之间找到适当的平衡
一方面,具有单个责任的函数是一种良好做法,使函数在多个方案中保持简单且可重用。 另一方面,每次函数调用都会带来网络往返延迟和输入输出令牌数量方面的额外负担:输入令牌用于将函数定义和调用结果发送到LLM,而从模型接收函数调用时会消耗输出令牌。
或者,可以实现具有多重职责的单个函数,以减少消耗的令牌数量和降低网络开销,虽然这样做会导致在其他场景中可重用性下降。
但是,将许多责任合并到单个函数中可能会增加函数参数的数量和复杂性及其返回类型。 这种复杂性可能导致模型难以正确匹配函数参数的情况,从而导致错误类型的参数或值缺失。 因此,必须平衡函数数量,以减少网络开销和每个函数所承担的责任数,确保模型能够准确匹配函数参数。
转换语义核心函数
按照 转换语义内核函数 博客文章中所述,利用语义内核函数的转换技术:
更改函数行为: 在某些情况下,函数的默认行为可能与所需结果不一致,并且无法修改原始函数的实现。 在这种情况下,可以创建一个新函数来包装原始函数并相应地修改其行为。
提供上下文信息: 函数可能需要 LLM 无法或不应推断的参数。 例如,如果函数需要代表当前用户执行操作或需要身份验证信息,则此上下文通常可用于主机应用程序,但不适用于 LLM。 在这种情况下,可以转换函数以调用原始函数,同时从宿主应用程序提供必要的上下文信息以及 LLM 提供的参数。
更改参数列表、类型和名称: 如果原始函数具有 LLM 难以解释的复杂签名,则可以将函数转换为具有 LLM 更易于理解的更简单签名的函数。 这可能涉及更改参数名称、类型、参数数量,以及将复杂参数展平或恢复其复杂性等其他调整。
本地状态利用率
在设计对相对大型或机密数据集(如包含敏感信息的文档、文章或电子邮件)进行操作的插件时,请考虑使用本地状态来存储不需要发送到 LLM 的原始数据或中间结果。 此类方案的函数可以接受并返回状态 ID,使你能够在本地查找和访问数据,而不是将实际数据传递给 LLM,而只能将其作为下一个函数调用的参数接收回。
通过在本地存储数据,可以在函数调用期间避免不必要的令牌消耗,从而保持信息私密和安全。 此方法不仅增强了数据隐私,而且提高了处理大型或敏感数据集的整体效率。