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
  • Schemat wartości zwracanej

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

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.

Napiwek

Funkcje mogą określać Kernel, KernelArguments, ILoggerFactory, ILogger, IAIServiceSelector, CultureInfo, IFormatProvider, CancellationToken jako parametry, i nie będą one anonsowane do LLM oraz zostaną automatycznie ustawione po wywołaniu funkcji. Jeśli polegasz na KernelArguments zamiast jawnych argumentów wejściowych, kod będzie odpowiedzialny za wykonywanie konwersji typów.

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

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

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

Określanie schematu typu zwracania funkcji dla LLM

Obecnie nie ma dobrze zdefiniowanego, branżowego standardu dostarczania metadanych dotyczących typu zwracanego funkcji do modeli sztucznej inteligencji. Do czasu ustanowienia takiego standardu można rozważyć następujące techniki w scenariuszach, w których nazwy właściwości typu zwracanego są niewystarczające, aby LLM-y mogły rozumieć ich treść, lub gdy dodatkowe instrukcje kontekstowe lub instrukcje obsługi muszą być powiązane z typem zwracanym, aby modelować lub ulepszyć scenariusze.

Przed zastosowaniem dowolnej z tych technik zaleca się podanie bardziej opisowych nazw właściwości typu zwracanego, ponieważ jest to najprostszy sposób na poprawę zrozumienia tego typu przez LLM, a także jest opłacalny pod względem wykorzystania tokenów.

Podaj informacje o typie zwracanym przez funkcję w opisie funkcji

Aby zastosować tę technikę, dołącz schemat zwracanego typu w atrybucie description funkcji. Schemat powinien zawierać szczegółowe nazwy właściwości, opisy i typy, jak pokazano w poniższym przykładzie:

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

Niektóre modele mogą mieć ograniczenia dotyczące rozmiaru opisu funkcji, dlatego zaleca się zachowanie zwięzłości schematu i uwzględnianie tylko podstawowych informacji.

W przypadkach, gdy informacje o typie nie są krytyczne i minimalizacja użycia tokenu jest priorytetem, rozważ podanie krótkiego opisu typu zwracanego w atrybucie opisu funkcji zamiast pełnego schematu.

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 wymienione powyżej podejścia wymagają ręcznego dodania schematu typu zwracanego i zaktualizowania go za każdym razem, gdy zwracany typ ulegnie zmianie. Aby tego uniknąć, rozważ następną technikę.

Podaj schemat zwracanego typu funkcji w ramach wartości zwracanej funkcji

Ta technika obejmuje podanie zarówno wartości zwracanej funkcji, jak i jej schematu do modelu LLM, a nie tylko wartości zwracanej. Dzięki temu usługa LLM może używać schematu do wnioskowania o właściwościach wartości zwracanej.

Aby zaimplementować tę technikę, należy utworzyć i zarejestrować filtr wywołania funkcji automatycznej. Aby uzyskać więcej informacji, zobacz artykuł Auto Function Invocation Filter (Filtr wywołań funkcji automatycznych). Ten filtr powinien opakowować wartość zwracaną funkcji w obiekcie niestandardowym, który zawiera zarówno oryginalną wartość zwracaną, jak i jej schemat. Poniżej przedstawiono przykład:

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

Po zarejestrowaniu filtru można teraz podać opisy typu zwracanego i jego właściwości, które zostaną automatycznie wyodrębnione przez jądro semantyczne:

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

Takie podejście eliminuje konieczność ręcznego podawania i aktualizowania schematu typu zwracanego za każdym razem, gdy zwracany typ zmienia się, ponieważ schemat jest automatycznie wyodrębniany przez jądro semantyczne.

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ń).