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