将本机代码添加为插件

为 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")]
   [return: Description("An array of lights")]
   public async Task<List<LightModel>> GetLightsAsync()
   {
      return _lights;
   }

   [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(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) -> Annotated[List[LightModel], "An array of lights"]:
        """Gets a list of lights and their current state."""
        return self._lights

    @kernel_function
    async def change_state(
        self,
        change_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"] == 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());
    }
}

提示

由于 LLM 以 Python 代码为主,因此建议对函数名称和参数使用snake_case(即使使用的是 C# 或 Java)。 这将有助于 AI 代理更好地了解函数及其参数。

如果函数具有复杂对象作为输入变量,语义内核还将为该对象生成架构并将其传递给 AI 代理。 与函数类似,应为 AI 不明显的属性提供 Description 注释。 下面是类和Brightness枚举的定义LightState

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 enum? 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 typing import TypedDict

class LightModel(TypedDict):
    id: int
    name: str
    is_on: bool | None
    brightness: int | None
    hex: str | None
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 代理生成的参数封送到正确的对象中。

完成插件类的创作后,可以使用或AddFromObject方法将其添加到内核AddFromType<>

提示

创建函数时,请始终问自己“如何向 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")]
   [return: Description("An array of lights")]
   public async Task<List<LightModel>> GetLightsAsync()
   {
      _logger.LogInformation("Getting lights");
      return lightService.GetLights();
   }

   [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(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();

后续步骤

了解如何创建插件后,现在可以了解如何将其与 AI 代理配合使用。 根据添加到插件的函数类型,应遵循不同的模式。 有关检索函数,请参阅 using 检索函数 一文。 有关任务自动化函数,请参阅 使用任务自动化函数 一文。