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
- Het schema van de retourwaarde
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")]
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());
}
}
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.
Tip
Uw functies kunnen Kernel
, KernelArguments
, ILoggerFactory
, ILogger
, IAIServiceSelector
, CultureInfo
, IFormatProvider
, CancellationToken
als parameters opgeven en deze worden niet aangekondigd voor de LLM en worden automatisch ingesteld wanneer de functie wordt aangeroepen.
Als u afhankelijk bent van KernelArguments
in plaats van expliciete invoerargumenten, is uw code verantwoordelijk voor het uitvoeren van typeconversies.
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 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;
}
}
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")]
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);
}
}
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();
Het leveren van een functie retourtypeschema aan LLM
Momenteel is er geen goed gedefinieerde, branchebrede standaard voor het leveren van metagegevens van functie-retourtypen aan AI-modellen. Totdat een dergelijke standaard tot stand is gebracht, kunnen de volgende technieken worden overwogen voor scenario's waarbij de namen van eigenschappen van retourtypen onvoldoende zijn voor LLM's om te redeneren over hun inhoud, of wanneer aanvullende context- of verwerkingsinstructies moeten worden gekoppeld aan het retourtype om uw scenario's te modelleren of te verbeteren.
Voordat u een van deze technieken gebruikt, is het aan te raden om meer beschrijvende namen voor de eigenschappen van het retourtype op te geven, omdat dit de eenvoudigste manier is om het begrip van het retourtype te verbeteren en ook efficiënt is in termen van tokengebruik.
Geef informatie over het retourtype van de functie op in functiebeschrijving
Als u deze techniek wilt toepassen, neemt u het retourtypeschema op in het beschrijvingskenmerk van de functie. Het schema moet de namen, beschrijvingen en typen van eigenschappen beschrijven, zoals wordt weergegeven in het volgende voorbeeld:
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)
{
...
}
}
Sommige modellen hebben mogelijk beperkingen voor de grootte van de functiebeschrijving, dus het is raadzaam om het schema beknopt te houden en alleen essentiële informatie op te nemen.
In gevallen waarin typegegevens niet kritiek zijn en het verbruik van tokens wordt geminimaliseerd, kunt u overwegen een korte beschrijving te geven van het retourtype in het beschrijvingskenmerk van de functie in plaats van het volledige schema.
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)
{
...
}
}
Beide hierboven genoemde benaderingen vereisen handmatig het schema van het retourtype toe te voegen en bij te werken telkens wanneer het retourtype wordt gewijzigd. Om dit te voorkomen, moet u rekening houden met de volgende techniek.
Geef het retourtypeschema van de functie op als onderdeel van de retourwaarde van de functie
Deze techniek omvat het leveren van zowel de retourwaarde van de functie als het bijbehorende schema aan de LLM, in plaats van alleen de retourwaarde. Hierdoor kan de LLM het schema gebruiken om te redeneren over de eigenschappen van de retourwaarde.
Als u deze techniek wilt implementeren, moet u een aanroepfilter voor automatische functies maken en registreren. Zie het artikel Automatische functieaanroepfilter voor meer informatie. Dit filter moet de retourwaarde van de functie verpakken in een aangepast object dat zowel de oorspronkelijke retourwaarde als het bijbehorende schema bevat. Hieronder ziet u een voorbeeld:
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());
Als het filter is geregistreerd, kunt u nu beschrijvingen opgeven voor het retourtype en de bijbehorende eigenschappen, die automatisch worden geëxtraheerd door Semantische kernel:
[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; }
}
Deze aanpak elimineert de noodzaak om het retourtypeschema handmatig op te geven en bij te werken telkens wanneer het retourtype wordt gewijzigd, omdat het schema automatisch wordt geëxtraheerd door de Semantische kernel.
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 .