Ajouter du code natif en tant que plug-in
Le moyen le plus simple de fournir à un agent IA des fonctionnalités qui ne sont pas prises en charge en mode natif consiste à encapsuler le code natif dans un plug-in. Cela vous permet de tirer parti de vos compétences existantes en tant que développeur d’applications pour étendre les fonctionnalités de vos agents IA.
En arrière-plan, le noyau sémantique utilisera ensuite les descriptions que vous fournissez, ainsi que la réflexion, pour décrire sémantiquement le plug-in à l’agent IA. Cela permet à l’agent IA de comprendre les fonctionnalités du plug-in et comment interagir avec lui.
Fournir au LLM les informations appropriées
Lors de la création d’un plug-in, vous devez fournir à l’agent IA les informations appropriées pour comprendre les fonctionnalités du plug-in et de ses fonctions. notamment :
- Nom du plug-in
- Noms des fonctions
- Descriptions des fonctions
- Paramètres des fonctions
- Schéma des paramètres
- Schéma de la valeur de retour
La valeur du noyau sémantique est qu’elle peut générer automatiquement la plupart de ces informations à partir du code lui-même. En tant que développeur, cela signifie simplement que vous devez fournir les descriptions sémantiques des fonctions et des paramètres afin que l’agent IA puisse les comprendre. Si vous commentez et annotez correctement votre code, toutefois, vous disposez probablement déjà de ces informations.
Ci-dessous, nous allons parcourir les deux façons différentes de fournir à votre agent IA du code natif et comment fournir ces informations sémantiques.
Définition d’un plug-in à l’aide d’une classe
Le moyen le plus simple de créer un plug-in natif consiste à commencer par une classe, puis à ajouter des méthodes annotées avec l’attribut KernelFunction
. Il est également recommandé d’utiliser Description
l’annotation de manière libérale pour fournir à l’agent IA les informations nécessaires pour comprendre la fonction.
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());
}
}
Conseil
Étant donné que les modules LLM sont principalement formés sur du code Python, il est recommandé d’utiliser snake_case pour les noms de fonctions et les paramètres (même si vous utilisez C# ou Java). Cela aidera l’agent IA à mieux comprendre la fonction et ses paramètres.
Conseil
Vos fonctions peuvent spécifier Kernel
, KernelArguments
, ILoggerFactory
, ILogger
, IAIServiceSelector
, CultureInfo
, IFormatProvider
, CancellationToken
en tant que paramètres et celles-ci ne seront pas publiées sur le LLM et seront automatiquement définies lorsque la fonction est appelée.
Si vous vous appuyez sur KernelArguments
au lieu d’arguments d’entrée explicites, votre code sera responsable de l’exécution des conversions de type.
Si votre fonction a un objet complexe en tant que variable d’entrée, le noyau sémantique génère également un schéma pour cet objet et le transmet à l’agent IA. Comme pour les fonctions, vous devez fournir Description
des annotations pour les propriétés qui ne sont pas évidentes pour l’IA. Voici la définition de la LightState
classe et de l’énumération 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 enum import Enum
from typing import TypedDict
class Brightness(Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class LightModel(TypedDict):
id: int
name: str
is_on: bool | None
brightness: Brightness | None
color: Annotated[str | None, "The color of the light with a hex code (ensure you include the # symbol)"]
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;
}
}
Remarque
Bien qu’il s’agit d’un exemple « amusant », il fait un bon travail montrant à quel point les paramètres d’un plug-in peuvent être complexes. Dans ce cas unique, nous avons un objet complexe avec quatre types de propriétés différents : un entier, une chaîne, une valeur booléenne et une énumération. La valeur du Noyau Sémantique est qu'il peut générer automatiquement le schéma de cet objet, le transmettre à l'agent IA, et convertir les paramètres générés par l'agent IA dans l'objet correct.
Une fois que vous avez terminé de créer votre classe de plugin, vous pouvez l'ajouter au noyau à l'aide des méthodes AddFromType<>
ou AddFromObject
.
Une fois que vous avez terminé de créer votre classe de plug-in, vous pouvez l’ajouter au noyau à l’aide de la méthode add_plugin
.
Une fois que vous avez terminé de créer votre classe de plug-in, vous pouvez l’ajouter au noyau à l’aide des méthodes AddFromType<>
ou AddFromObject
.
Conseil
Lors de la création d’une fonction, demandez-vous toujours « comment puis-je donner à l’IA une aide supplémentaire pour utiliser cette fonction ? » Cela peut inclure l’utilisation de types d’entrée spécifiques (évitez les chaînes si possible), en fournissant des descriptions et des exemples.
Ajout d’un plug-in à l’aide de la AddFromObject
méthode
La AddFromObject
méthode vous permet d’ajouter une instance de la classe de plug-in directement à la collection de plug-ins au cas où vous souhaitez contrôler directement la façon dont le plug-in est construit.
Par exemple, le constructeur de la LightsPlugin
classe nécessite la liste des lumières. Dans ce cas, vous pouvez créer une instance de la classe de plug-in et l’ajouter à la collection de plug-ins.
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));
Ajout d’un plug-in à l’aide de la AddFromType<>
méthode
Lorsque vous utilisez la AddFromType<>
méthode, le noyau utilise automatiquement l’injection de dépendances pour créer une instance de la classe de plug-in et l’ajouter à la collection de plug-ins.
Cela est utile si votre constructeur nécessite des services ou d’autres dépendances à injecter dans le plug-in. Par exemple, notre LightsPlugin
classe peut nécessiter un enregistreur d’événements et un service de lumière à injecter dans celui-ci au lieu d’une liste de lumières.
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);
}
}
Avec l’injection de dépendances, vous pouvez ajouter les services et plug-ins requis au générateur de noyau avant de générer le noyau.
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();
Définition d’un plug-in à l’aide d’une collection de fonctions
Moins courant mais toujours utile, il est utile de définir un plug-in à l’aide d’une collection de fonctions. Cela est particulièrement utile si vous devez créer dynamiquement un plug-in à partir d’un ensemble de fonctions au moment de l’exécution.
L’utilisation de ce processus vous oblige à utiliser la fabrique de fonctions pour créer des fonctions individuelles avant de les ajouter au plug-in.
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"
)
]);
Stratégies supplémentaires pour l’ajout de code source avec l’injection de dépendances
Si vous travaillez avec l'injection de dépendance, il existe des stratégies supplémentaires que vous pouvez adopter pour créer et ajouter des plug-ins au noyau. Voici quelques exemples de la façon dont vous pouvez ajouter un plug-in à l’aide de l’injection de dépendance.
Injecter une collection de plug-ins
Conseil
Nous vous recommandons de rendre votre collection de plug-ins un service temporaire afin qu’il soit supprimé après chaque utilisation, car la collection de plug-ins est mutable. La création d’une collection de plug-ins pour chaque utilisation est bon marché. Il ne doit donc pas s’agir d’un problème de performances.
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);
});
Conseil
Comme mentionné dans l'article du noyau, le noyau est extrêmement léger, si bien que créer un nouveau noyau pour chaque utilisation en tant que temporaire n'affecte pas les performances.
Générer vos plug-ins en tant que singletons
Les plug-ins ne sont pas mutables. Il est donc généralement sûr de les créer en tant que singletons. Pour ce faire, utilisez la fabrique de plug-ins et ajoutez le plug-in résultant à votre collection de services.
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);
});
Ajout d’un plug-in à l’aide de la add_plugin
méthode
La add_plugin
méthode vous permet d’ajouter une instance de plug-in au noyau. Voici un exemple de la façon dont vous pouvez construire la LightsPlugin
classe et l’ajouter au noyau.
# 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)
Ajout d’un plug-in à l’aide de la createFromObject
méthode
La createFromObject
méthode vous permet de générer un plug-in de noyau à partir d’un objet avec des méthodes annotées.
// Import the LightsPlugin
KernelPlugin lightPlugin = KernelPluginFactory.createFromObject(new LightsPlugin(),
"LightsPlugin");
Ce plug-in peut ensuite être ajouté à un noyau.
// Create a kernel with Azure OpenAI chat completion and plugin
Kernel kernel = Kernel.builder()
.withAIService(ChatCompletionService.class, chatCompletionService)
.withPlugin(lightPlugin)
.build();
Fourniture d’un schéma de type de retour de fonctions à LLM
Actuellement, il n’existe pas de norme bien définie à l’échelle du secteur pour fournir des métadonnées de type de retour de fonction aux modèles IA. Tant qu’une telle norme n’est pas établie, les techniques suivantes peuvent être prises en compte pour les scénarios où les noms des propriétés de type de retour ne sont pas suffisants pour que les LMGs raisonnent sur leur contenu, ou où des instructions de contexte ou de gestion supplémentaires doivent être associées au type de retour pour modéliser ou améliorer vos scénarios.
Avant d’utiliser l’une de ces techniques, il est conseillé de fournir des noms plus descriptifs pour les propriétés de type de retour, car il s’agit du moyen le plus simple d’améliorer la compréhension du type de retour de LLM et est également rentable en termes d’utilisation des jetons.
Fournissez des informations sur le type de retour d'une fonction dans la description de celle-ci.
Pour appliquer cette technique, incluez le schéma de type de retour dans l’attribut de description de la fonction. Le schéma doit détailler les noms, descriptions et types de propriétés, comme illustré dans l’exemple suivant :
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)
{
...
}
}
Certains modèles peuvent avoir des limitations sur la taille de la description de la fonction. Il est donc conseillé de conserver le schéma concis et d’inclure uniquement des informations essentielles.
Dans les cas où les informations de type ne sont pas critiques et que la réduction de la consommation de jetons est une priorité, envisagez de fournir une brève description du type de retour dans l’attribut de description de la fonction au lieu du schéma complet.
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)
{
...
}
}
Les deux approches mentionnées ci-dessus nécessitent l’ajout manuel du schéma de type de retour et la mise à jour chaque fois que le type de retour change. Pour éviter cela, envisagez la technique suivante.
Fournir le schéma de type de retour de fonction dans le cadre de la valeur de retour de la fonction
Cette technique implique de fournir à la fois la valeur de retour de la fonction et son schéma au LLM, plutôt que simplement la valeur de retour. Cela permet au LLM d’utiliser le schéma pour raisonner les propriétés de la valeur de retour.
Pour implémenter cette technique, vous devez créer et enregistrer un filtre d’invocation de fonction automatique. Pour plus d'informations, consultez l'article Filtre d'Invocation de Fonction Automatique. Ce filtre doit encapsuler la valeur de retour de la fonction dans un objet personnalisé qui contient à la fois la valeur de retour d’origine et son schéma. Voici un exemple :
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());
Avec le filtre inscrit, vous pouvez maintenant fournir des descriptions pour le type de retour et ses propriétés, qui seront automatiquement extraites par le noyau sémantique :
[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; }
}
Cette approche élimine la nécessité de fournir et de mettre à jour manuellement le schéma de type de retour chaque fois que le type de retour change, car le schéma est automatiquement extrait par le noyau sémantique.
Fournir plus d’informations sur les fonctions
Lors de la création d’un plug-in dans Python, vous pouvez fournir des informations supplémentaires sur les fonctions du décorateur kernel_function
. Ces informations seront utilisées par l’agent IA pour mieux comprendre les fonctions.
from typing import List, Optional, Annotated
class LightsPlugin:
def __init__(self, lights: List[LightModel]):
self._lights = lights
@kernel_function(name="GetLights", description="Gets a list of lights and their current state")
async def get_lights(self) -> List[LightModel]:
"""Gets a list of lights and their current state."""
return self._lights
@kernel_function(name="ChangeState", description="Changes the state of the light")
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
L’exemple ci-dessus montre comment remplacer le nom de la fonction et fournir une description pour la fonction. Par défaut, le nom de la fonction est le nom de la fonction et la description est vide. Si le nom de la fonction est suffisamment descriptif, vous n’aurez pas besoin d’une description, ce qui vous enregistrera des jetons. Toutefois, si le comportement de la fonction n’est pas évident par le nom, vous devez fournir une description à l’IA.
Étant donné que les LLMs sont principalement formés sur le code Python, il est recommandé d’utiliser des noms de fonction qui suivent les conventions de nommage Python , ce qui signifie que vous n'avez quasiment jamais besoin de remplacer les noms de fonction si vous respectez ces conventions dans votre code Python.
Étapes suivantes
Maintenant que vous savez comment créer un plug-in, vous pouvez maintenant apprendre à les utiliser avec votre agent IA. Selon le type de fonctions que vous avez ajoutées à vos plug-ins, vous devez suivre différents modèles. Pour les fonctions de récupération, reportez-vous à l’article sur l’utilisation des fonctions de récupération. Pour les fonctions d’automatisation des tâches, reportez-vous à l’article utilisation des fonctions d’automatisation des tâches .