Systeemeigen code toevoegen als invoegtoepassing
De eenvoudigste manier om een AI-agent mogelijkheden te bieden die niet systeemeigen worden ondersteund, is door systeemeigen code in een invoegtoepassing te verpakken. Hierdoor kunt u uw bestaande vaardigheden als app-ontwikkelaar gebruiken om de mogelijkheden van uw AI-agents uit te breiden.
Achter de schermen gebruikt Semantische kernel vervolgens de beschrijvingen die u opgeeft, samen met reflectie, om de invoegtoepassing semantisch te beschrijven aan de AI-agent. Hierdoor kan de AI-agent de mogelijkheden van de invoegtoepassing begrijpen en ermee communiceren.
De LLM de juiste informatie verstrekken
Bij het ontwerpen van een invoegtoepassing moet u de AI-agent de juiste informatie verstrekken om inzicht te hebben in de mogelijkheden van de invoegtoepassing en de bijbehorende functies. Dit zijn onder andere de nieuwe mogelijkheden:
- De naam van de invoegtoepassing
- De namen van de functies
- De beschrijvingen van de functies
- De parameters van de functies
- Het schema van de parameters
De waarde van Semantische kernel is dat de meeste van deze informatie automatisch kan worden gegenereerd op basis van de code zelf. Als ontwikkelaar betekent dit alleen dat u de semantische beschrijvingen van de functies en parameters moet opgeven, zodat de AI-agent deze kan begrijpen. Als u uw code op de juiste manier commentaar geeft en er aantekeningen op plaatst, hebt u deze informatie waarschijnlijk al bij de hand.
Hieronder doorlopen we de twee verschillende manieren om uw AI-agent systeemeigen code te bieden en hoe u deze semantische informatie kunt opgeven.
Een invoegtoepassing definiëren met behulp van een klasse
De eenvoudigste manier om een systeemeigen invoegtoepassing te maken, is door te beginnen met een klasse en vervolgens methoden toe te voegen die zijn voorzien van het KernelFunction
kenmerk. Het wordt ook aanbevolen om de Description
aantekening vrijelijk te gebruiken om de AI-agent de benodigde informatie te bieden om de functie te begrijpen.
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());
}
}
Tip
Omdat de LLM's voornamelijk worden getraind in Python-code, is het raadzaam om snake_case te gebruiken voor functienamen en -parameters (zelfs als u C# of Java gebruikt). Hierdoor krijgt de AI-agent meer inzicht in de functie en de bijbehorende parameters.
Als uw functie een complex object heeft als invoervariabele, genereert Semantische kernel ook een schema voor dat object en geeft deze door aan de AI-agent. Net als bij functies moet u aantekeningen opgeven Description
voor eigenschappen die niet duidelijk zijn voor de AI. Hieronder ziet u de definitie voor de LightState
klasse en de Brightness
opsomming.
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;
}
}
Notitie
Hoewel dit een 'leuk' voorbeeld is, is het handig om te laten zien hoe complex de parameters van een invoegtoepassing kunnen zijn. In dit ene geval hebben we een complex object met vier verschillende typen eigenschappen: een geheel getal, tekenreeks, booleaanse waarde en opsomming. De waarde van Semantic Kernel is dat het automatisch het schema voor dit object kan genereren en doorgeeft aan de AI-agent en de parameters die door de AI-agent zijn gegenereerd, in het juiste object kunnen doorgeven.
Zodra u klaar bent met het ontwerpen van uw invoegtoepassingsklasse, kunt u deze aan de kernel toevoegen met behulp van de AddFromType<>
of AddFromObject
methoden.
Tip
Wanneer u een functie maakt, vraagt u zich altijd af hoe kan ik de AI aanvullende hulp geven bij het gebruik van deze functie? Dit kan omvatten het gebruik van specifieke invoertypen (vermijd waar mogelijk tekenreeksen), het verstrekken van beschrijvingen en voorbeelden.
Een invoegtoepassing toevoegen met behulp van de AddFromObject
methode
Met AddFromObject
de methode kunt u rechtstreeks een exemplaar van de invoegtoepassingsklasse toevoegen aan de invoegtoepassingverzameling voor het geval u rechtstreeks wilt bepalen hoe de invoegtoepassing wordt samengesteld.
De constructor van de LightsPlugin
klasse vereist bijvoorbeeld de lijst met lichten. In dit geval kunt u een exemplaar van de invoegtoepassingsklasse maken en deze toevoegen aan de verzameling invoegtoepassingen.
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));
Een invoegtoepassing toevoegen met behulp van de AddFromType<>
methode
Wanneer de AddFromType<>
methode wordt gebruikt, gebruikt de kernel automatisch afhankelijkheidsinjectie om een exemplaar van de invoegtoepassingsklasse te maken en toe te voegen aan de invoegtoepassingverzameling.
Dit is handig als voor uw constructor services of andere afhankelijkheden moeten worden geïnjecteerd in de invoegtoepassing. Onze klasse kan bijvoorbeeld LightsPlugin
vereisen dat een logger en een lichte service erin worden geïnjecteerd in plaats van een lijst met lichten.
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);
}
}
Met afhankelijkheidsinjectie kunt u de vereiste services en invoegtoepassingen toevoegen aan de kernelbouwer voordat u de kernel bouwt.
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();
Een invoegtoepassing definiëren met behulp van een verzameling functies
Minder gangbaar maar nog steeds nuttig is het definiëren van een invoegtoepassing met behulp van een verzameling functies. Dit is met name handig als u dynamisch een invoegtoepassing moet maken op basis van een set functies tijdens runtime.
Voor het gebruik van dit proces moet u de functiefactory gebruiken om afzonderlijke functies te maken voordat u ze toevoegt aan de invoegtoepassing.
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"
)
]);
Aanvullende strategieën voor het toevoegen van systeemeigen code met afhankelijkheidsinjectie
Als u met afhankelijkheidsinjectie werkt, zijn er extra strategieën die u kunt nemen om plug-ins te maken en toe te voegen aan de kernel. Hieronder vindt u enkele voorbeelden van hoe u een invoegtoepassing kunt toevoegen met behulp van afhankelijkheidsinjectie.
Een invoegtoepassingsverzameling injecteren
Tip
Het is raadzaam om uw invoegtoepassingverzameling een tijdelijke service te maken, zodat deze na elk gebruik wordt verwijderd omdat de invoegtoepassingverzameling veranderlijk is. Het maken van een nieuwe invoegtoepassingverzameling voor elk gebruik is goedkoop, dus het mag geen prestatieprobleem zijn.
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
Zoals vermeld in het kernelartikel, is de kernel uiterst lichtgewicht, dus het maken van een nieuwe kernel voor elk gebruik als een tijdelijk probleem is geen prestatieprobleem.
Uw invoegtoepassingen genereren als singletons
Invoegtoepassingen zijn niet veranderlijk, dus het is meestal veilig om ze als singletons te maken. Dit kan worden gedaan met behulp van de plugin factory en het toevoegen van de resulterende invoegtoepassing aan uw serviceverzameling.
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);
});
Een invoegtoepassing toevoegen met behulp van de add_plugin
methode
Met de add_plugin
methode kunt u een invoegtoepassingexemplaren toevoegen aan de kernel. Hieronder ziet u een voorbeeld van hoe u de LightsPlugin
klasse kunt maken en deze aan de kernel kunt toevoegen.
# 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)
Een invoegtoepassing toevoegen met behulp van de createFromObject
methode
Met de createFromObject
methode kunt u een kernelinvoegtoepassing bouwen vanuit een object met aantekeningen.
// Import the LightsPlugin
KernelPlugin lightPlugin = KernelPluginFactory.createFromObject(new LightsPlugin(),
"LightsPlugin");
Deze invoegtoepassing kan vervolgens worden toegevoegd aan een kernel.
// Create a kernel with Azure OpenAI chat completion and plugin
Kernel kernel = Kernel.builder()
.withAIService(ChatCompletionService.class, chatCompletionService)
.withPlugin(lightPlugin)
.build();
Volgende stappen
Nu u weet hoe u een invoegtoepassing maakt, kunt u nu leren hoe u deze kunt gebruiken met uw AI-agent. Afhankelijk van het type functies dat u aan uw invoegtoepassingen hebt toegevoegd, zijn er verschillende patronen die u moet volgen. Raadpleeg het artikel over het ophalen van functies voor het ophalen van functies . Raadpleeg het artikel over het gebruik van taakautomatiseringsfuncties voor taakautomatiseringsfuncties .