将本机代码添加为插件
为 AI 代理提供不受本机支持的功能的最简单方法是将本机代码包装到插件中。 这样,就可以利用作为应用开发人员的现有技能来扩展 AI 代理的功能。
在幕后,语义内核将使用您提供的说明以及反射机制,以语义方式描述 AI 代理的插件。 这使 AI 代理能够了解插件的功能以及如何与之交互。
为 LLM 提供正确的信息
创作插件时,需要向 AI 代理提供正确的信息来了解插件及其功能。 这包括:
- 插件的名称
- 函数的名称
- 函数的描述
- 函数的参数
- 参数的架构
- 返回值的架构
语义内核的值是,它可以从代码本身自动生成大部分此信息。 作为开发人员,这只意味着必须提供函数和参数的语义说明,以便 AI 代理能够理解它们。 如果正确注释代码,那么您可能已经手头有这些信息。
下面,我们将演练为 AI 代理提供本机代码的两种不同的方法,以及如何提供此语义信息。
使用类定义插件
创建本机插件的最简单方法是从类开始,然后添加使用 KernelFunction
特性批注的方法。 此外,建议自由使用 Description
批注来向 AI 代理提供必要的信息来了解函数。
public class LightsPlugin
{
private readonly List<LightModel> _lights;
public LightsPlugin(LoggerFactory loggerFactory, List<LightModel> lights)
{
_lights = lights;
}
[KernelFunction("get_lights")]
[Description("Gets a list of lights and their current state")]
public async Task<List<LightModel>> GetLightsAsync()
{
return _lights;
}
[KernelFunction("change_state")]
[Description("Changes the state of the light")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
// Find the light to change
var light = _lights.FirstOrDefault(l => l.Id == changeState.Id);
// If the light does not exist, return null
if (light == null)
{
return null;
}
// Update the light state
light.IsOn = changeState.IsOn;
light.Brightness = changeState.Brightness;
light.Color = changeState.Color;
return light;
}
}
from typing import List, Optional, Annotated
class LightsPlugin:
def __init__(self, lights: List[LightModel]):
self._lights = lights
@kernel_function
async def get_lights(self) -> List[LightModel]:
"""Gets a list of lights and their current state."""
return self._lights
@kernel_function
async def change_state(
self,
change_state: LightModel
) -> Optional[LightModel]:
"""Changes the state of the light."""
for light in self._lights:
if light["id"] == change_state["id"]:
light["is_on"] = change_state.get("is_on", light["is_on"])
light["brightness"] = change_state.get("brightness", light["brightness"])
light["hex"] = change_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, LightModel.Brightness.MEDIUM, "#FFFFFF"));
lights.put(2, new LightModel(2, "Porch light", false, LightModel.Brightness.HIGH, "#FF0000"));
lights.put(3, new LightModel(3, "Chandelier", true, LightModel.Brightness.LOW, "#FFFF00"));
}
@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 = "model",
description = "The new state of the model to set. Example model: " +
"{\"id\":99,\"name\":\"Head Lamp\",\"isOn\":false,\"brightness\":\"MEDIUM\",\"color\":\"#FFFFFF\"}",
type = LightModel.class) LightModel model
) {
System.out.println("Changing light " + model.getId() + " " + model.getIsOn());
if (!lights.containsKey(model.getId())) {
throw new IllegalArgumentException("Light not found");
}
lights.put(model.getId(), model);
return lights.get(model.getId());
}
}
提示
由于 LLMs 主要基于 Python 代码进行训练,因此建议使用蛇形命名法(snake_case)来命名函数名称和参数(即使使用的是 C# 或 Java)。 这将有助于 AI 代理更好地了解函数及其参数。
提示
函数可以指定 Kernel
、KernelArguments
、ILoggerFactory
、ILogger
、IAIServiceSelector
、CultureInfo
、IFormatProvider
、CancellationToken
作为参数,这些参数不会展示给 LLM,并会在调用函数时自动设置。
如果依赖于 KernelArguments
而不是显式输入参数,则代码将负责执行类型转换。
如果函数具有复杂对象作为输入变量,语义内核还将为该对象生成架构并将其传递给 AI 代理。 与函数类似,您应该为对 AI 来说不明显的属性提供 Description
注释。 下面是LightState
类和Brightness
枚举的定义。
using System.Text.Json.Serialization;
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 Brightness? Brightness { get; set; }
[JsonPropertyName("color")]
[Description("The color of the light with a hex code (ensure you include the # symbol)")]
public string? Color { get; set; }
}
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Brightness
{
Low,
Medium,
High
}
from enum import Enum
from typing import TypedDict
class Brightness(Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class LightModel(TypedDict):
id: int
name: str
is_on: bool | None
brightness: Brightness | None
color: Annotated[str | None, "The color of the light with a hex code (ensure you include the # symbol)"]
public class LightModel {
private int id;
private String name;
private Boolean isOn;
private Brightness brightness;
private String color;
public enum Brightness {
LOW,
MEDIUM,
HIGH
}
public LightModel(int id, String name, Boolean isOn, Brightness brightness, String color) {
this.id = id;
this.name = name;
this.isOn = isOn;
this.brightness = brightness;
this.color = color;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Boolean getIsOn() {
return isOn;
}
public void setIsOn(Boolean isOn) {
this.isOn = isOn;
}
public Brightness getBrightness() {
return brightness;
}
public void setBrightness(Brightness brightness) {
this.brightness = brightness;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
备注
虽然这是一个“有趣的”示例,但它做了一个很好的工作,显示插件的参数有多复杂。 在此单例中,我们有一个具有 四 种不同类型的属性的复杂对象:整数、字符串、布尔值和枚举。 语义内核的值是,它可以自动生成此对象的架构,并将其传递给 AI 代理,并将 AI 代理生成的参数封送到正确的对象中。
完成编写插件类后,可以使用
完成插件类的创作后,可以使用 add_plugin
方法将其添加到内核。
完成编写插件类后,可以使用AddFromType<>
或AddFromObject
方法将其添加到内核中。
提示
创建函数时,请始终问自己“如何向 AI 提供额外的帮助来使用此函数?”这可以包括使用特定的输入类型(尽可能避免字符串)、提供说明和示例。
使用 AddFromObject
方法添加插件
此方法 AddFromObject
允许将插件类的实例直接添加到插件集合,以防你希望直接控制插件的构造方式。
例如,LightsPlugin
类的构造器需要灯列表。 在这种情况下,可以创建插件类的实例并将其添加到插件集合。
List<LightModel> lights = new()
{
new LightModel { Id = 1, Name = "Table Lamp", IsOn = false, Brightness = Brightness.Medium, Color = "#FFFFFF" },
new LightModel { Id = 2, Name = "Porch light", IsOn = false, Brightness = Brightness.High, Color = "#FF0000" },
new LightModel { Id = 3, Name = "Chandelier", IsOn = true, Brightness = Brightness.Low, Color = "#FFFF00" }
};
kernel.Plugins.AddFromObject(new LightsPlugin(lights));
使用 AddFromType<>
方法添加插件
使用 AddFromType<>
该方法时,内核将自动使用依赖项注入来创建插件类的实例并将其添加到插件集合。
如果构造函数需要将服务或其他依赖项注入到插件中,这非常有用。 例如,我们的 LightsPlugin
类可能需要将记录器和光服务注入其中,而不是灯列表。
public class LightsPlugin
{
private readonly Logger _logger;
private readonly LightService _lightService;
public LightsPlugin(LoggerFactory loggerFactory, LightService lightService)
{
_logger = loggerFactory.CreateLogger<LightsPlugin>();
_lightService = lightService;
}
[KernelFunction("get_lights")]
[Description("Gets a list of lights and their current state")]
public async Task<List<LightModel>> GetLightsAsync()
{
_logger.LogInformation("Getting lights");
return lightService.GetLights();
}
[KernelFunction("change_state")]
[Description("Changes the state of the light")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
_logger.LogInformation("Changing light state");
return lightService.ChangeState(changeState);
}
}
使用依赖关系注入,可以在生成内核之前将所需的服务和插件添加到内核生成器。
var builder = Kernel.CreateBuilder();
// Add dependencies for the plugin
builder.Services.AddLogging(loggingBuilder => loggingBuilder.AddConsole().SetMinimumLevel(LogLevel.Trace));
builder.Services.AddSingleton<LightService>();
// Add the plugin to the kernel
builder.Plugins.AddFromType<LightsPlugin>("Lights");
// Build the kernel
Kernel kernel = builder.Build();
使用函数集合定义插件
不太常见,但仍很有用的是使用函数集合定义插件。 如果需要在运行时从一组函数动态创建插件,这将特别有用。
使用此过程要求您使用函数工厂来创建单个函数,然后将其添加到插件中。
kernel.Plugins.AddFromFunctions("time_plugin",
[
KernelFunctionFactory.CreateFromMethod(
method: () => DateTime.Now,
functionName: "get_time",
description: "Get the current time"
),
KernelFunctionFactory.CreateFromMethod(
method: (DateTime start, DateTime end) => (end - start).TotalSeconds,
functionName: "diff_time",
description: "Get the difference between two times in seconds"
)
]);
使用依赖注入添加原生代码的其他策略
如果使用依赖关系注入,则可以采取其他策略来创建插件并将其添加到内核。 下面是有关如何使用依赖关系注入添加插件的一些示例。
注入插件集合
提示
建议将插件集合设置为暂时性服务,以便在每次使用后将其释放,因为插件集合是可变的。 为每个用途创建新的插件集合很便宜,因此它不应该是性能问题。
var builder = Host.CreateApplicationBuilder(args);
// Create native plugin collection
builder.Services.AddTransient((serviceProvider)=>{
KernelPluginCollection pluginCollection = [];
pluginCollection.AddFromType<LightsPlugin>("Lights");
return pluginCollection;
});
// Create the kernel service
builder.Services.AddTransient<Kernel>((serviceProvider)=> {
KernelPluginCollection pluginCollection = serviceProvider.GetRequiredService<KernelPluginCollection>();
return new Kernel(serviceProvider, pluginCollection);
});
提示
如内核文章中所述,由于内核非常轻量,因此在每次使用时创建一个新的临时性内核不会成为性能问题。
将插件生成为单一实例
插件不可变,因此通常可以安全地将其创建为单例。 这可以通过使用插件工厂并将生成的插件添加到服务集合来完成。
var builder = Host.CreateApplicationBuilder(args);
// Create singletons of your plugin
builder.Services.AddKeyedSingleton("LightPlugin", (serviceProvider, key) => {
return KernelPluginFactory.CreateFromType<LightsPlugin>();
});
// Create a kernel service with singleton plugin
builder.Services.AddTransient((serviceProvider)=> {
KernelPluginCollection pluginCollection = [
serviceProvider.GetRequiredKeyedService<KernelPlugin>("LightPlugin")
];
return new Kernel(serviceProvider, pluginCollection);
});
使用 add_plugin
方法添加插件
此方法 add_plugin
允许向内核添加插件实例。 下面是如何构造 LightsPlugin
类并将其添加到内核的示例。
# Create the kernel
kernel = Kernel()
# Create dependencies for the plugin
lights = [
{"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"},
]
# Create the plugin
lights_plugin = LightsPlugin(lights)
# Add the plugin to the kernel
kernel.add_plugin(lights_plugin)
使用 createFromObject
方法添加插件
该方法 createFromObject
允许使用带批注的方法从对象生成内核插件。
// 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();
向 LLM 提供函数返回类型架构
目前,没有明确的行业范围标准,用于向 AI 模型提供函数返回类型元数据。 在建立此类标准之前,如果返回类型属性的名称不足以让大语言模型(LLM)推理其内容,或者需要附加上下文或处理说明以与返回类型相关联,以建模或增强您的方案,可以考虑以下技术。
在采用上述任何技术之前,建议为返回类型属性提供更具描述性的名称,因为这是提高 LLM 对返回类型的理解的最简单方法,在令牌使用方面也是经济高效的方法。
在函数说明中提供函数返回类型信息
若要应用此方法,请在函数的说明属性中包含返回类型架构。 架构应详细说明属性名称、说明和类型,如以下示例所示:
public class LightsPlugin
{
[KernelFunction("change_state")]
[Description("""Changes the state of the light and returns:
{
"type": "object",
"properties": {
"id": { "type": "integer", "description": "Light ID" },
"name": { "type": "string", "description": "Light name" },
"is_on": { "type": "boolean", "description": "Is light on" },
"brightness": { "type": "string", "enum": ["Low", "Medium", "High"], "description": "Brightness level" },
"color": { "type": "string", "description": "Hex color code" }
},
"required": ["id", "name"]
}
""")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
...
}
}
某些模型可能对函数说明的大小有限制,因此建议保持架构简洁,并且仅包含重要信息。
如果类型信息不关键且将令牌消耗降至最低是一个优先级,请考虑在函数的描述属性中提供返回类型的简要说明,而不是完整架构。
public class LightsPlugin
{
[KernelFunction("change_state")]
[Description("""Changes the state of the light and returns:
id: light ID,
name: light name,
is_on: is light on,
brightness: brightness level (Low, Medium, High),
color: Hex color code.
""")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
...
}
}
上述两种方法都需要手动添加返回类型架构,并在每次返回类型更改时更新它。 若要避免这种情况,请考虑下一种方法。
提供函数返回值类型模式作为函数返回值的一部分
此方法涉及向 LLM 提供函数的返回值及其架构,而不仅仅是返回值。 这样,LLM 就可以使用架构来推断返回值的属性。
若要实现此方法,需要创建并注册自动函数调用筛选器。 有关详细信息,请参阅 自动函数调用筛选器 一文。 此筛选器应在包含原始返回值及其架构的自定义对象中包装函数的返回值。 下面是一个示例:
private sealed class AddReturnTypeSchemaFilter : IAutoFunctionInvocationFilter
{
public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func<AutoFunctionInvocationContext, Task> next)
{
await next(context); // Invoke the original function
// Crete the result with the schema
FunctionResultWithSchema resultWithSchema = new()
{
Value = context.Result.GetValue<object>(), // Get the original result
Schema = context.Function.Metadata.ReturnParameter?.Schema // Get the function return type schema
};
// Return the result with the schema instead of the original one
context.Result = new FunctionResult(context.Result, resultWithSchema);
}
private sealed class FunctionResultWithSchema
{
public object? Value { get; set; }
public KernelJsonSchema? Schema { get; set; }
}
}
// Register the filter
Kernel kernel = new Kernel();
kernel.AutoFunctionInvocationFilters.Add(new AddReturnTypeSchemaFilter());
注册筛选器后,现在可以提供返回类型及其属性的说明,语义内核会自动提取这些属性:
[Description("The state of the light")] // Equivalent to annotating the function with the [return: Description("The state of the light")] attribute
public class LightModel
{
[JsonPropertyName("id")]
[Description("The ID of the light")]
public int Id { get; set; }
[JsonPropertyName("name")]
[Description("The name of the light")]
public string? Name { get; set; }
[JsonPropertyName("is_on")]
[Description("Indicates whether the light is on")]
public bool? IsOn { get; set; }
[JsonPropertyName("brightness")]
[Description("The brightness level of the light")]
public Brightness? Brightness { get; set; }
[JsonPropertyName("color")]
[Description("The color of the light with a hex code (ensure you include the # symbol)")]
public string? Color { get; set; }
}
此方法无需在每次返回类型更改时手动提供和更新返回类型架构,因为该架构由语义内核自动提取。
提供有关功能的更多详细信息
在 Python 中创建插件时,可以提供有关 kernel_function
修饰器中函数的其他信息。 AI 代理将使用此信息来更好地了解函数。
from typing import List, Optional, Annotated
class LightsPlugin:
def __init__(self, lights: List[LightModel]):
self._lights = lights
@kernel_function(name="GetLights", description="Gets a list of lights and their current state")
async def get_lights(self) -> List[LightModel]:
"""Gets a list of lights and their current state."""
return self._lights
@kernel_function(name="ChangeState", description="Changes the state of the light")
async def change_state(
self,
change_state: LightModel
) -> Optional[LightModel]:
"""Changes the state of the light."""
for light in self._lights:
if light["id"] == change_state["id"]:
light["is_on"] = change_state.get("is_on", light["is_on"])
light["brightness"] = change_state.get("brightness", light["brightness"])
light["hex"] = change_state.get("hex", light["hex"])
return light
return None
上面的示例演示如何覆盖函数名称并为函数提供描述。 默认情况下,函数名称是函数的名称,说明为空。 如果函数名称具有足够的描述性,则不需要说明,这将节省令牌。 但是,如果函数行为从名称中不明显,则应为 AI 提供说明。
由于 LLM 主要是在 Python 代码上训练的,因此建议使用遵循 Python 命名约定的函数名称,这意味着,如果在 Python 代码中遵循约定,则很少需要重写函数名称。
后续步骤
了解如何创建插件后,现在可以了解如何将其与 AI 代理配合使用。 根据添加到插件的函数类型,应遵循不同的模式。 有关检索函数的使用,请参阅使用检索函数一文。 有关任务自动化函数,请参阅 使用任务自动化函数 一文。