Udostępnij za pośrednictwem


Dodawanie kodu natywnego jako wtyczki

Najprostszym sposobem zapewnienia agentowi sztucznej inteligencji możliwości, które nie są obsługiwane natywnie, jest zawijanie kodu natywnego do wtyczki. Dzięki temu możesz wykorzystać istniejące umiejętności jako deweloper aplikacji, aby rozszerzyć możliwości agentów sztucznej inteligencji.

W tle semantyczne jądro będzie następnie używać podanych opisów wraz z odbiciem, aby semantycznie opisać wtyczkę do agenta sztucznej inteligencji. Dzięki temu agent sztucznej inteligencji może zrozumieć możliwości wtyczki i jak z nią korzystać.

Dostarczanie funkcji LLM z odpowiednimi informacjami

Podczas tworzenia wtyczki należy podać agentowi sztucznej inteligencji odpowiednie informacje, aby zrozumieć możliwości wtyczki i jej funkcji. Obejmuje to:

  • Nazwa wtyczki
  • Nazwy funkcji
  • Opisy funkcji
  • Parametry funkcji
  • Schemat parametrów

Wartość jądra semantycznego polega na tym, że może automatycznie wygenerować większość tych informacji na podstawie samego kodu. Jako deweloper oznacza to tylko, że musisz podać semantyczne opisy funkcji i parametrów, aby agent sztucznej inteligencji mógł je zrozumieć. Jeśli jednak poprawnie skomentujesz i dodasz adnotację do kodu, prawdopodobnie masz już te informacje.

Poniżej omówimy dwa różne sposoby udostępniania agenta sztucznej inteligencji za pomocą kodu natywnego i sposobu dostarczania tych informacji semantycznych.

Definiowanie wtyczki przy użyciu klasy

Najprostszym sposobem utworzenia wtyczki natywnej jest rozpoczęcie od klasy, a następnie dodanie metod z adnotacjami do atrybutu KernelFunction . Zaleca się również liberalne użycie Description adnotacji w celu udostępnienia agentowi sztucznej inteligencji niezbędnych informacji w celu zrozumienia funkcji.

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

Napiwek

Ponieważ maszyny LLM są głównie trenowane w kodzie języka Python, zaleca się użycie snake_case dla nazw i parametrów funkcji (nawet jeśli używasz języka C# lub Java). Pomoże to agentowi sztucznej inteligencji lepiej zrozumieć funkcję i jego parametry.

Jeśli funkcja ma złożony obiekt jako zmienną wejściową, semantyczne jądro również wygeneruje schemat dla tego obiektu i przekaże go do agenta sztucznej inteligencji. Podobnie jak w przypadku funkcji, należy podać Description adnotacje dla właściwości, które nie są oczywiste dla sztucznej inteligencji. Poniżej znajduje się definicja LightState klasy i wyliczenia 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 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;
    }
}

Uwaga

Chociaż jest to przykład "zabawa", dobrym zadaniem jest pokazanie, jak złożone mogą być parametry wtyczki. W tym pojedynczym przypadku mamy złożony obiekt z czterema różnymi typami właściwości: liczbą całkowitą, ciągiem, wartością logiczną i wyliczeniową. Wartość semantycznego jądra polega na tym, że może automatycznie wygenerować schemat dla tego obiektu i przekazać go do agenta sztucznej inteligencji i przeprowadzić marshaling parametrów wygenerowanych przez agenta sztucznej inteligencji do poprawnego obiektu.

Po zakończeniu tworzenia klasy wtyczki możesz dodać ją do jądra przy użyciu AddFromType<> metod lub AddFromObject .

Napiwek

Podczas tworzenia funkcji zawsze zadaj sobie pytanie "jak mogę udzielić dodatkowej pomocy dotyczącej używania tej funkcji za pomocą sztucznej inteligencji?" Może to obejmować używanie określonych typów danych wejściowych (unikaj ciągów tam, gdzie to możliwe), dostarczanie opisów i przykładów.

Dodawanie wtyczki przy użyciu AddFromObject metody

Metoda AddFromObject umożliwia dodanie wystąpienia klasy wtyczki bezpośrednio do kolekcji wtyczek, jeśli chcesz bezpośrednio kontrolować sposób konstruowania wtyczki.

Na przykład konstruktor LightsPlugin klasy wymaga listy świateł. W takim przypadku możesz utworzyć wystąpienie klasy wtyczki i dodać je do kolekcji wtyczek.

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

Dodawanie wtyczki przy użyciu AddFromType<> metody

W przypadku użycia AddFromType<> metody jądro automatycznie użyje wstrzykiwania zależności w celu utworzenia wystąpienia klasy wtyczki i dodania jej do kolekcji wtyczek.

Jest to przydatne, jeśli konstruktor wymaga wprowadzenia usług lub innych zależności do wtyczki. Na przykład nasza LightsPlugin klasa może wymagać, aby rejestrator i usługa światła została do niej wstrzyknięta zamiast listy świateł.

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

Za pomocą wstrzykiwania zależności można dodać wymagane usługi i wtyczki do konstruktora jądra przed utworzeniem 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();

Definiowanie wtyczki przy użyciu kolekcji funkcji

Mniej typowe, ale nadal przydatne jest definiowanie wtyczki przy użyciu kolekcji funkcji. Jest to szczególnie przydatne, jeśli musisz dynamicznie utworzyć wtyczkę na podstawie zestawu funkcji w czasie wykonywania.

Użycie tego procesu wymaga użycia fabryki funkcji do utworzenia poszczególnych funkcji przed dodaniem ich do wtyczki.

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

Dodatkowe strategie dodawania kodu natywnego za pomocą wstrzykiwania zależności

Jeśli pracujesz z iniekcją zależności, istnieją dodatkowe strategie, które można wykonać, aby utworzyć i dodać wtyczki do jądra. Poniżej przedstawiono kilka przykładów sposobu dodawania wtyczki przy użyciu wstrzykiwania zależności.

Wstrzykiwanie kolekcji wtyczek

Napiwek

Zalecamy utworzenie kolekcji wtyczek jako usługi przejściowej, tak aby została usunięta po każdym użyciu, ponieważ kolekcja wtyczek jest modyfikowalna. Tworzenie nowej kolekcji wtyczek dla każdego użycia jest tanie, więc nie powinno to być problemem z wydajnością.

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

Napiwek

Jak wspomniano w artykule jądra, jądro jest niezwykle lekkie, więc utworzenie nowego jądra dla każdego użycia jako przejściowego nie jest problemem z wydajnością.

Generowanie wtyczek jako pojedynczych

Wtyczki nie są modyfikowalne, więc ich tworzenie jest zwykle bezpieczne jako pojedynczetony. Można to zrobić za pomocą fabryki wtyczek i dodania wynikowej wtyczki do kolekcji usług.

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

Dodawanie wtyczki przy użyciu add_plugin metody

Metoda add_plugin umożliwia dodanie wystąpienia wtyczki do jądra. Poniżej przedstawiono przykład sposobu konstruowania LightsPlugin klasy i dodawania jej 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)

Dodawanie wtyczki przy użyciu createFromObject metody

Metoda createFromObject umożliwia utworzenie wtyczki jądra z obiektu z metodami z adnotacjami.

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

Tę wtyczkę można następnie dodać do jądra.

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

Następne kroki

Teraz, gdy wiesz, jak utworzyć wtyczkę, możesz teraz dowiedzieć się, jak używać ich z agentem sztucznej inteligencji. W zależności od typu funkcji dodanych do wtyczek należy przestrzegać różnych wzorców. Aby zapoznać się z funkcjami pobierania, zapoznaj się z artykułem using retrieval functions (Korzystanie z funkcji pobierania). W przypadku funkcji automatyzacji zadań zapoznaj się z artykułem using task automation functions (Korzystanie z funkcji automatyzacji zadań).