Compartilhar via


Adicionar código nativo como um plug-in

A maneira mais fácil de fornecer a um agente de IA recursos que não têm suporte nativo é encapsular o código nativo em um plug-in. Isso permite que você aproveite suas habilidades existentes como desenvolvedor de aplicativos para estender os recursos de seus agentes de IA.

Nos bastidores, o Semantic Kernel usará as descrições que você fornecer, juntamente com a reflexão, para descrever semanticamente o plug-in para o agente de IA. Isso permite que o agente de IA entenda os recursos do plug-in e como interagir com ele.

Fornecendo ao LLM as informações corretas

Ao criar um plug-in, você precisa fornecer ao agente de IA as informações corretas para entender os recursos do plug-in e suas funções. Isso inclui:

  • O nome do plugin
  • Os nomes das funções
  • As descrições das funções
  • Os parâmetros das funções
  • O esquema dos parâmetros
  • O esquema do valor retornado

O valor do Semantic Kernel é que ele pode gerar automaticamente a maioria dessas informações a partir do próprio código. Como desenvolvedor, isso significa apenas que você deve fornecer as descrições semânticas das funções e parâmetros para que o agente de IA possa entendê-los. Se você comentar e anotar corretamente seu código, no entanto, provavelmente já tem essas informações em mãos.

Abaixo, veremos as duas maneiras diferentes de fornecer código nativo ao seu agente de IA e como fornecer essas informações semânticas.

Definindo um plug-in usando uma classe

A maneira mais fácil de criar um plug-in nativo é começar com uma classe e, em seguida, adicionar métodos anotados com o KernelFunction atributo. Também é recomendável usar liberalmente a Description anotação para fornecer ao agente de IA as informações necessárias para entender a função.

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

Dica

Como os LLMs são predominantemente treinados em código Python, é recomendável usar snake_case para nomes e parâmetros de função (mesmo se você estiver usando C# ou Java). Isso ajudará o agente de IA a entender melhor a função e seus parâmetros.

Dica

Suas funções podem especificar Kernel, KernelArguments, ILoggerFactory, ILogger, IAIServiceSelector, CultureInfo, IFormatProvider, CancellationToken como parâmetros e elas não serão anunciadas para a LLM e serão definidas automaticamente quando a função for chamada. Se você depender de KernelArguments em vez de argumentos de entrada explícitos, seu código será responsável por executar conversões de tipo.

Se sua função tiver um objeto complexo como uma variável de entrada, o Semantic Kernel também gerará um esquema para esse objeto e o passará para o agente de IA. Semelhante às funções, você deve fornecer Description anotações para propriedades que não são óbvias para a IA. Abaixo está a definição da LightState classe e da Brightness enumeração.

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

Observação

Embora este seja um exemplo "divertido", ele faz um bom trabalho mostrando o quão complexos os parâmetros de um plug-in podem ser. Neste único caso, temos um objeto complexo com quatro tipos diferentes de propriedades: um inteiro, string, booleano e enum. O valor do Semantic Kernel é que ele pode gerar automaticamente o esquema para esse objeto e passá-lo para o agente de IA e empacotar os parâmetros gerados pelo agente de IA no objeto correto.

Quando terminar de criar sua classe de plugin, você pode adicioná-la ao kernel usando os AddFromType<> métodos or AddFromObject .

Dica

Ao criar uma função, sempre pergunte a si mesmo "como posso dar ajuda adicional à IA para usar essa função?" Isso pode incluir o uso de tipos de entrada específicos (evite cadeias de caracteres sempre que possível), fornecendo descrições e exemplos.

Adicionando um plugin usando o AddFromObject método

O AddFromObject método permite que você adicione uma instância da classe de plug-in diretamente à coleção de plug-ins, caso queira controlar diretamente como o plug-in é construído.

Por exemplo, o LightsPlugin construtor da classe requer a lista de luzes. Nesse caso, você pode criar uma instância da classe de plug-in e adicioná-la à coleção de plug-ins.

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

Adicionando um plugin usando o AddFromType<> método

Ao usar o AddFromType<> método, o kernel usará automaticamente a injeção de dependência para criar uma instância da classe de plug-in e adicioná-la à coleção de plug-ins.

Isso é útil se o construtor exigir que serviços ou outras dependências sejam injetados no plug-in. Por exemplo, nossa LightsPlugin classe pode exigir que um agente e um serviço de luz sejam injetados nele em vez de uma lista de luzes.

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

Com a Injeção de Dependência, você pode adicionar os serviços e plug-ins necessários ao construtor de kernel antes de compilar o kernel.

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

Definindo um plug-in usando uma coleção de funções

Menos comum, mas ainda útil, é definir um plugin usando uma coleção de funções. Isso é particularmente útil se você precisar criar dinamicamente um plug-in a partir de um conjunto de funções em tempo de execução.

O uso desse processo requer que você use a fábrica de funções para criar funções individuais antes de adicioná-las ao plug-in.

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

Estratégias adicionais para adicionar código nativo com Injeção de Dependência

Se você estiver trabalhando com Injeção de Dependência, existem estratégias adicionais que você pode adotar para criar e adicionar plug-ins ao kernel. Abaixo estão alguns exemplos de como você pode adicionar um plug-in usando a injeção de dependência.

Injetar uma coleção de plug-ins

Dica

Recomendamos tornar sua coleção de plug-ins um serviço transitório para que ela seja descartada após cada uso, pois a coleção de plug-ins é mutável. Criar uma nova coleção de plugins para cada uso é barato, portanto, não deve ser uma preocupação de desempenho.

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

Dica

Conforme mencionado no artigo do kernel, o kernel é extremamente leve, portanto, criar um novo kernel para cada uso como um transiente não é uma preocupação de desempenho.

Gere seus plugins como singletons

Os plug-ins não são mutáveis, portanto, normalmente é seguro criá-los como singletons. Isso pode ser feito usando a fábrica de plug-ins e adicionando o plug-in resultante à sua coleção de serviços.

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

Adicionando um plugin usando o add_plugin método

O add_plugin método permite adicionar uma instância de plug-in ao kernel. Abaixo está um exemplo de como você pode construir a LightsPlugin classe e adicioná-la ao kernel.

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

Adicionando um plugin usando o createFromObject método

O createFromObject método permite que você construa um plug-in de kernel a partir de um objeto com métodos anotados.

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

Este plugin pode então ser adicionado a um kernel.

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

Fornecer funções retorna esquema de tipo para LLM

Atualmente, não há um padrão bem definido em todo o setor para fornecer metadados de tipo de retorno de função para modelos de IA. Até que esse padrão seja estabelecido, as técnicas a seguir podem ser consideradas para cenários em que os nomes das propriedades de tipo de retorno são insuficientes para as LLMs raciocinarem sobre seu conteúdo ou em que instruções adicionais de contexto ou tratamento precisam ser associadas ao tipo de retorno para modelar ou aprimorar seus cenários.

Antes de empregar qualquer uma dessas técnicas, é aconselhável fornecer nomes mais descritivos para as propriedades do tipo de retorno, pois essa é a maneira mais simples de melhorar a compreensão da LLM sobre o tipo de retorno e também é econômica em relação ao uso de tokens.

Forneça informações sobre o tipo de retorno na descrição da função

Para aplicar essa técnica, inclua o esquema de tipo de retorno no atributo de descrição da função. O esquema deve detalhar os nomes, descrições e tipos de propriedade, conforme mostrado no exemplo a seguir:

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

Alguns modelos podem ter limitações no tamanho da descrição da função, portanto, é aconselhável manter o esquema conciso e incluir apenas informações essenciais.

Nos casos em que as informações de tipo não são críticas e minimizar o consumo de token é uma prioridade, considere fornecer uma breve descrição do tipo de retorno no atributo de descrição da função em vez do esquema completo.

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

Ambas as abordagens mencionadas acima exigem adicionar manualmente o esquema de tipo de retorno e atualizá-lo sempre que o tipo de retorno for alterado. Para evitar isso, considere a próxima técnica.

Fornecer o esquema do tipo de retorno da função como parte do valor retornado da função.

Essa técnica envolve fornecer o valor retornado da função e seu esquema para a LLM, em vez de apenas o valor retornado. Isso permite que o LLM use o esquema para raciocinar sobre as propriedades do valor retornado.

Para implementar essa técnica, você precisa criar e registrar um filtro de invocação de função automática. Para obter mais detalhes, consulte o artigo Filtro de Invocação de Função Automática. Esse filtro deve encapsular o valor retornado da função em um objeto personalizado que contém o valor de retorno original e seu esquema. Veja abaixo um exemplo:

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

Com o filtro registrado, agora você pode fornecer descrições para o tipo de retorno e suas propriedades, que serão extraídas automaticamente pelo Kernel Semântico:

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

Essa abordagem elimina a necessidade de fornecer e atualizar manualmente o esquema de tipo de retorno sempre que o tipo de retorno for alterado, pois o esquema é extraído automaticamente pelo Kernel Semântico.

Próximas etapas

Agora que você sabe como criar um plug-in, pode aprender a usá-lo com seu agente de IA. Dependendo do tipo de funções que você adicionou aos seus plug-ins, existem diferentes padrões que você deve seguir. Para funções de recuperação, consulte o artigo usando funções de recuperação. Para funções de automação de tarefas, consulte o artigo usando funções de automação de tarefas.