Dela via


Lägga till intern kod som ett plugin-program

Det enklaste sättet att tillhandahålla en AI-agent med funktioner som inte stöds internt är att omsluta inbyggd kod i ett plugin-program. På så sätt kan du utnyttja dina befintliga kunskaper som apputvecklare för att utöka funktionerna i dina AI-agenter.

I bakgrunden använder Semantic Kernel sedan de beskrivningar som du tillhandahåller, tillsammans med reflektion, för att semantiskt beskriva plugin-programmet för AI-agenten. På så sätt kan AI-agenten förstå funktionerna i plugin-programmet och hur du interagerar med det.

Tillhandahålla LLM med rätt information

När du skapar ett plugin-program måste du ge AI-agenten rätt information för att förstå funktionerna i plugin-programmet och dess funktioner. Detta omfattar:

  • Namnet på plugin-programmet
  • Namnen på funktionerna
  • Beskrivningarna av funktionerna
  • Funktionernas parametrar
  • Schemat för parametrarna

Värdet för semantisk kernel är att den automatiskt kan generera det mesta av den här informationen från själva koden. Som utvecklare innebär det bara att du måste ange semantiska beskrivningar av funktionerna och parametrarna så att AI-agenten kan förstå dem. Om du kommenterar och kommenterar koden korrekt har du förmodligen redan den här informationen till hands.

Nedan går vi igenom de två olika sätten att tillhandahålla din AI-agent med inbyggd kod och hur du tillhandahåller den här semantiska informationen.

Definiera ett plugin-program med hjälp av en klass

Det enklaste sättet att skapa ett inbyggt plugin-program är att börja med en klass och sedan lägga till metoder som kommenterats med KernelFunction attributet. Vi rekommenderar också att du liberalt använder kommentaren Description för att ge AI-agenten nödvändig information för att förstå funktionen.

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

Dricks

Eftersom LLM:erna främst tränas på Python-kod rekommenderar vi att du använder snake_case för funktionsnamn och parametrar (även om du använder C# eller Java). Detta hjälper AI-agenten att bättre förstå funktionen och dess parametrar.

Om din funktion har ett komplext objekt som en indatavariabel genererar Semantic Kernel också ett schema för objektet och skickar det till AI-agenten. Precis som med funktioner bör du ange Description anteckningar för egenskaper som inte är uppenbara för AI:n. Nedan visas definitionen för LightState klassen och Brightness uppräkningen.

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;
    }
}

Kommentar

Även om detta är ett "roligt" exempel, gör det ett bra jobb som visar hur komplexa parametrarna för ett plugin-program kan vara. I det här fallet har vi ett komplext objekt med fyra olika typer av egenskaper: ett heltal, en sträng, ett booleskt objekt och enum. Semantisk kernels värde är att det automatiskt kan generera schemat för det här objektet och skicka det till AI-agenten och konvertera parametrarna som genereras av AI-agenten till rätt objekt.

När du är klar med redigeringen av plugin-klassen kan du lägga till den i kerneln med hjälp av AddFromType<> metoderna eller AddFromObject .

Dricks

När du skapar en funktion ska du alltid fråga dig själv "hur kan jag ge AI ytterligare hjälp med att använda den här funktionen?" Detta kan omfatta användning av specifika indatatyper (undvik strängar där det är möjligt), tillhandahålla beskrivningar och exempel.

Lägga till ett plugin-program med hjälp av AddFromObject metoden

Med AddFromObject metoden kan du lägga till en instans av plugin-klassen direkt till plugin-samlingen om du vill styra hur plugin-programmet konstrueras direkt.

Konstruktorn LightsPlugin för klassen kräver till exempel en lista över lampor. I det här fallet kan du skapa en instans av plugin-klassen och lägga till den i plugin-samlingen.

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

Lägga till ett plugin-program med hjälp av AddFromType<> metoden

När du använder AddFromType<> metoden använder kerneln automatiskt beroendeinmatning för att skapa en instans av plugin-klassen och lägga till den i plugin-samlingen.

Det här är användbart om konstruktorn kräver att tjänster eller andra beroenden matas in i plugin-programmet. Till exempel kan vår LightsPlugin klass kräva att en logger och en ljustjänst matas in i den i stället för en lista över lampor.

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);
   }
}

Med beroendeinmatning kan du lägga till nödvändiga tjänster och plugin-program i kernelverktyget innan du skapar kerneln.

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

Definiera ett plugin-program med hjälp av en samling funktioner

Mindre vanligt men ändå användbart är att definiera ett plugin-program med hjälp av en samling funktioner. Detta är särskilt användbart om du behöver skapa ett plugin-program dynamiskt från en uppsättning funktioner vid körning.

Med den här processen måste du använda funktionsfabriken för att skapa enskilda funktioner innan du lägger till dem i plugin-programmet.

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"
    )
]);

Ytterligare strategier för att lägga till inbyggd kod med beroendeinmatning

Om du arbetar med beroendeinmatning finns det ytterligare strategier som du kan använda för att skapa och lägga till plugin-program i kerneln. Nedan visas några exempel på hur du kan lägga till ett plugin-program med hjälp av beroendeinmatning.

Mata in en plugin-samling

Dricks

Vi rekommenderar att du gör plugin-samlingen till en tillfällig tjänst så att den tas bort efter varje användning eftersom plugin-samlingen kan ändras. Att skapa en ny plugin-samling för varje användning är billigt, så det bör inte vara ett prestandaproblem.

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);
});

Dricks

Som vi nämnde i kernelartikeln är kerneln extremt lätt, så att skapa en ny kernel för varje användning som tillfälligt är inte ett prestandaproblem.

Generera dina plugin-program som singletons

Plugin-program är inte föränderliga, så det är vanligtvis säkert att skapa dem som singletons. Detta kan göras genom att använda plugin-fabriken och lägga till det resulterande plugin-programmet i din tjänstsamling.

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);
});

Lägga till ett plugin-program med hjälp av add_plugin metoden

Med add_plugin metoden kan du lägga till en plugin-instans i kerneln. Nedan visas ett exempel på hur du kan konstruera LightsPlugin klassen och lägga till den i kerneln.

# 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)

Lägga till ett plugin-program med hjälp av createFromObject metoden

Med createFromObject metoden kan du skapa ett kernel-plugin-program från ett objekt med kommenterade metoder.

// Import the LightsPlugin
KernelPlugin lightPlugin = KernelPluginFactory.createFromObject(new LightsPlugin(),
        "LightsPlugin");

Det här plugin-programmet kan sedan läggas till i en kernel.

// Create a kernel with Azure OpenAI chat completion and plugin
Kernel kernel = Kernel.builder()
        .withAIService(ChatCompletionService.class, chatCompletionService)
        .withPlugin(lightPlugin)
        .build();

Nästa steg

Nu när du vet hur du skapar ett plugin-program kan du nu lära dig hur du använder dem med din AI-agent. Beroende på vilken typ av funktioner du har lagt till i dina plugin-program finns det olika mönster som du bör följa. Information om hämtningsfunktioner finns i artikeln använda hämtningsfunktioner . Information om funktioner för uppgiftsautomatisering finns i artikeln använda funktioner för uppgiftsautomatisering .