Sdílet prostřednictvím


Přidání nativního kódu jako modulu plug-in

Nejjednodušší způsob, jak poskytnout agenta AI s funkcemi, které nejsou nativně podporovány, je zabalit nativní kód do modulu plug-in. Díky tomu můžete využít své stávající dovednosti jako vývojář aplikací k rozšíření možností agentů AI.

Na pozadí pak sémantické jádro použije popisy, které poskytnete spolu s reflexí, k sémantickému popisu modulu plug-in agentovi AI. To umožňuje agentu umělé inteligence porozumět možnostem modulu plug-in a jak s ním pracovat.

Poskytnutí správných informací pro LLM

Při vytváření modulu plug-in musíte agentovi AI poskytnout správné informace, abyste porozuměli možnostem modulu plug-in a jeho funkcím. Sem patří:

  • Název modulu plug-in
  • Názvy funkcí
  • Popisy funkcí
  • Parametry funkcí
  • Schéma parametrů
  • Schéma návratové hodnoty

Hodnota sémantického jádra spočívá v tom, že může automaticky generovat většinu těchto informací ze samotného kódu. Jako vývojář to znamená, že musíte zadat sémantické popisy funkcí a parametrů, aby je agent umělé inteligence mohl pochopit. Pokud kód správně okomentujete a přidáte k němu poznámky, pravděpodobně už tyto informace máte po ruce.

Níže si projdeme dva různé způsoby poskytování nativního kódu agenta AI a způsobu poskytování těchto sémantických informací.

Definování modulu plug-in pomocí třídy

Nejjednodušší způsob, jak vytvořit nativní modul plug-in, je začít s třídou a pak přidat metody anotované atributem KernelFunction . Doporučuje se také použít poznámku Description k tomu, aby agent umělé inteligence poskytl potřebné informace k pochopení funkce.

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

Tip

Vzhledem k tomu, že LLM jsou převážně vytrénované v kódu Pythonu, doporučujeme použít snake_case pro názvy a parametry funkcí (i když používáte C# nebo Javu). To pomůže agentu AI lépe porozumět funkci a jeho parametrům.

Tip

Vaše funkce mohou jako parametry zadat Kernel, KernelArguments, ILoggerFactory, ILogger, IAIServiceSelector, CultureInfo, IFormatProvider, CancellationToken a nebudou inzerovány do LLM a budou automaticky nastaveny při zavolání funkce. Pokud spoléháte na KernelArguments místo explicitních vstupních argumentů, bude váš kód zodpovědný za provádění převodů typů.

Pokud má vaše funkce komplexní objekt jako vstupní proměnnou, sémantické jádro také vygeneruje schéma pro tento objekt a předá ho agentu AI. Podobně jako u funkcí byste měli zadat Description poznámky pro vlastnosti, které nejsou pro AI zřejmé. Níže je definice třídy LightState a výčtu 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 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;
    }
}

Poznámka:

I když se jedná o "zábavný" příklad, dělá to dobrou úlohu, která ukazuje, jak složité parametry modulu plug-in může být. V tomto jediném případě máme komplexní objekt se čtyřmi různými typy vlastností: celé číslo, řetězec, logická hodnota a výčt. Hodnota sémantického jádra je, že může automaticky generovat schéma pro tento objekt a předat ho agentovi AI a zařadí parametry vygenerované agentem AI do správného objektu.

Jakmile dokončíte vytváření předmětu modulu plug-in, můžete ho přidat do jádra pomocí AddFromType<> metod nebo AddFromObject metod.

Tip

Při vytváření funkce se vždy zeptejte, jak můžu umělé inteligenci poskytnout další pomoc s používáním této funkce? To může zahrnovat použití konkrétních typů zadávání (vyhýbejte se řetězcům, pokud je to možné), zadání popisů a příkladů.

Přidání modulu plug-in pomocí AddFromObject metody

Tato AddFromObject metoda umožňuje přidat instanci třídy plug-in přímo do kolekce modulů plug-in v případě, že chcete přímo řídit, jak je modul plug-in vytvořen.

Například konstruktor LightsPlugin třídy vyžaduje seznam světel. V tomto případě můžete vytvořit instanci třídy modulu plug-in a přidat ji do kolekce modulů plug-in.

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

Přidání modulu plug-in pomocí AddFromType<> metody

Při použití AddFromType<> metody jádro automaticky použije injektáž závislostí k vytvoření instance třídy modulu plug-in a jeho přidání do kolekce modulů plug-in.

To je užitečné, pokud konstruktor vyžaduje, aby se do modulu plug-in vložily služby nebo jiné závislosti. Například naše LightsPlugin třída může vyžadovat protokolovač a světlou službu, aby se do ní vložily místo seznamu světel.

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

Pomocí injektáže závislostí můžete před sestavením jádra přidat požadované služby a moduly plug-in do tvůrce jádra.

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

Definování modulu plug-in pomocí kolekce funkcí

Méně časté, ale stále užitečné je definování modulu plug-in pomocí kolekce funkcí. To je zvlášť užitečné, pokud potřebujete dynamicky vytvořit modul plug-in ze sady funkcí za běhu.

Použití tohoto procesu vyžaduje, abyste před přidáním do modulu plug-in pomocí objektu pro vytváření funkcí vytvořili jednotlivé funkce.

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

Další strategie pro přidání nativního kódu pomocí injektáže závislostí

Pokud pracujete s injektáží závislostí, můžete do jádra vytvořit a přidat moduly plug-in. Níže je několik příkladů, jak přidat modul plug-in pomocí injektáže závislostí.

Vložení kolekce modulů plug-in

Tip

Doporučujeme, aby kolekce modulů plug-in byla přechodná služba, aby se po každém použití odstranila, protože kolekce modulů plug-in je proměnlivá. Vytvoření nové kolekce modulů plug-in pro každé použití je levné, takže by nemělo být problém s výkonem.

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

Tip

Jak je uvedeno v článku o jádru, jádro je extrémně jednoduché, takže vytvoření nového jádra pro každé použití jako přechodné není problém s výkonem.

Generování modulů plug-in jako singletonů

Moduly plug-in nejsou proměnlivé, takže jejich obvykle bezpečné je vytvářet jako singletony. Můžete to provést pomocí továrny modulu plug-in a přidáním výsledného modulu plug-in do kolekce služeb.

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

Přidání modulu plug-in pomocí add_plugin metody

Metoda add_plugin umožňuje přidat instanci modulu plug-in do jádra. Níže je příklad, jak můžete sestavit LightsPlugin třídu a přidat ji do jádra.

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

Přidání modulu plug-in pomocí createFromObject metody

Tato createFromObject metoda umožňuje vytvořit modul plug-in jádra z objektu pomocí anotovaných metod.

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

Tento modul plug-in je pak možné přidat do jádra.

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

Poskytování schématu návratových typů funkcí do LLM

V současné době neexistuje žádný dobře definovaný oborový standard pro poskytování metadat návratového typu funkce k modelům AI. Dokud se takový standard nezavede, je možné zvážit následující techniky pro scénáře, ve kterých názvy vlastností návratového typu nejsou pro LLM dostatečné k tomu, aby mohly porozumět jejich obsahu, nebo pokud je potřeba přidružit další kontext nebo pokyny pro zpracování s návratovým typem, aby modelovaly nebo zlepšily vaše scénáře.

Před použitím některé z těchto technik je vhodné zadat popisnější názvy vlastností návratového typu, protože se jedná o nejjednodušší způsob, jak zlepšit pochopení návratového typu LLM a je také nákladově efektivní z hlediska použití tokenu.

Uveďte informace o návratovém typu funkce v popisu funkce

Pokud chcete tuto techniku použít, zahrňte schéma návratového typu do atributu popisu funkce. Schéma by mělo podrobně popisovat názvy vlastností, popisy a typy, jak je znázorněno v následujícím příkladu:

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)
   {
      ...
   }
}

Některé modely můžou mít omezení velikosti popisu funkce, proto je vhodné udržovat schéma stručné a zahrnout pouze základní informace.

V případech, kdy informace o typu nejsou kritické a minimalizace spotřeby tokenů je prioritou, zvažte poskytnutí stručného popisu návratového typu v atributu popisu funkce místo úplného schématu.

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)
   {
      ...
   }
}

Oba výše uvedené přístupy vyžadují ruční přidání schématu návratového typu a jeho aktualizaci při každé změně návratového typu. Abyste tomu předešli, zvažte další techniku.

Zadejte schéma návratového typu funkce jako součást návratové hodnoty funkce.

Tato technika zahrnuje dodání návratové hodnoty funkce i jeho schématu do LLM, nikoli pouze návratové hodnoty. LLM tak může použít schéma k odůvodnění vlastností návratové hodnoty.

Pokud chcete tuto techniku implementovat, musíte vytvořit a zaregistrovat filtr automatického vyvolání funkce. Další podrobnosti najdete v článku filtr automatického vyvolání funkce. Tento filtr by měl zabalit návratovou hodnotu funkce do vlastního objektu, který obsahuje původní návratovou hodnotu i jeho schéma. Níže je příklad:

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

Když je filtr zaregistrovaný, můžete teď zadat popis návratového typu a jeho vlastností, které se automaticky extrahují pomocí sémantického jádra:

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

Tento přístup eliminuje potřebu ručního poskytování a aktualizace schématu návratového typu pokaždé, když se návratový typ změní, protože schéma se automaticky extrahuje pomocí sémantického jádra.

Další kroky

Teď, když víte, jak vytvořit modul plug-in, se teď můžete naučit, jak je používat s vaším agentem AI. V závislosti na typu funkcí, které jste přidali do modulů plug-in, byste měli postupovat podle různých vzorů. Informace o funkcích načítání najdete v článku o funkcích načítání . Informace o funkcích automatizace úloh najdete v článku o funkcích automatizace úloh.