Appel de fonction avec saisie semi-automatique de conversation
La fonctionnalité la plus puissante de la saisie semi-automatique de conversation est la possibilité d’appeler des fonctions à partir du modèle. Cela vous permet de créer un bot de conversation qui peut interagir avec votre code existant, ce qui vous permet d’automatiser les processus métier, de créer des extraits de code, etc.
Avec le noyau sémantique, nous simplifions le processus d’utilisation de l’appel de fonction en décrivant automatiquement vos fonctions et leurs paramètres sur le modèle, puis en gérant la communication back-and-forth entre le modèle et votre code.
Toutefois, lorsque vous utilisez l’appel de fonction, il est judicieux de comprendre ce qui se passe réellement en arrière-plan afin que vous puissiez optimiser votre code et tirer le meilleur parti de cette fonctionnalité.
Fonctionnement de l’appel de fonction
Lorsque vous effectuez une demande à un modèle avec l’appel de fonction activé, le noyau sémantique effectue les étapes suivantes :
Étape | Description | |
---|---|---|
1 | Sérialiser des fonctions | Toutes les fonctions disponibles (et ses paramètres d’entrée) dans le noyau sont sérialisées à l’aide du schéma JSON. |
2 | Envoyer les messages et fonctions au modèle | Les fonctions sérialisées (et l’historique de conversation actuel) sont envoyées au modèle dans le cadre de l’entrée. |
3 | Le modèle traite l’entrée | Le modèle traite l’entrée et génère une réponse. La réponse peut être un message de conversation ou un appel de fonction |
4 | Gérer la réponse | Si la réponse est un message de conversation, elle est retournée au développeur pour imprimer la réponse à l’écran. Si la réponse est un appel de fonction, toutefois, le noyau sémantique extrait le nom de la fonction et ses paramètres. |
5 | Appeler la fonction | Le nom et les paramètres de fonction extraits sont utilisés pour appeler la fonction dans le noyau. |
6 | Retourner le résultat de la fonction | Le résultat de la fonction est ensuite renvoyé au modèle dans le cadre de l’historique des conversations. Les étapes 2 à 6 sont ensuite répétées jusqu’à ce que le modèle envoie un signal d’arrêt |
Le diagramme suivant illustre le processus d’appel de fonction :
La section suivante utilise un exemple concret pour illustrer le fonctionnement de l’appel de fonction dans la pratique.
Exemple : Commande d’une pizza
Supposons que vous disposez d’un plug-in qui permet à un utilisateur de commander une pizza. Le plug-in a les fonctions suivantes :
get_pizza_menu
: retourne une liste de pizzas disponiblesadd_pizza_to_cart
: ajoute une pizza au panier de l’utilisateurremove_pizza_from_cart
: supprime une pizza du panier de l’utilisateurget_pizza_from_cart
: retourne les détails spécifiques d’une pizza dans le panier de l’utilisateurget_cart
: retourne le panier actuel de l’utilisateurcheckout
: Extrait le panier de l’utilisateur
En C#, le plug-in peut ressembler à ceci :
public class OrderPizzaPlugin(
IPizzaService pizzaService,
IUserContext userContext,
IPaymentService paymentService)
{
[KernelFunction("get_pizza_menu")]
public async Task<Menu> GetPizzaMenuAsync()
{
return await pizzaService.GetMenu();
}
[KernelFunction("add_pizza_to_cart")]
[Description("Add a pizza to the user's cart; returns the new item and updated cart")]
public async Task<CartDelta> AddPizzaToCart(
PizzaSize size,
List<PizzaToppings> toppings,
int quantity = 1,
string specialInstructions = ""
)
{
Guid cartId = userContext.GetCartId();
return await pizzaService.AddPizzaToCart(
cartId: cartId,
size: size,
toppings: toppings,
quantity: quantity,
specialInstructions: specialInstructions);
}
[KernelFunction("remove_pizza_from_cart")]
public async Task<RemovePizzaResponse> RemovePizzaFromCart(int pizzaId)
{
Guid cartId = userContext.GetCartId();
return await pizzaService.RemovePizzaFromCart(cartId, pizzaId);
}
[KernelFunction("get_pizza_from_cart")]
[Description("Returns the specific details of a pizza in the user's cart; use this instead of relying on previous messages since the cart may have changed since then.")]
public async Task<Pizza> GetPizzaFromCart(int pizzaId)
{
Guid cartId = await userContext.GetCartIdAsync();
return await pizzaService.GetPizzaFromCart(cartId, pizzaId);
}
[KernelFunction("get_cart")]
[Description("Returns the user's current cart, including the total price and items in the cart.")]
public async Task<Cart> GetCart()
{
Guid cartId = await userContext.GetCartIdAsync();
return await pizzaService.GetCart(cartId);
}
[KernelFunction("checkout")]
[Description("Checkouts the user's cart; this function will retrieve the payment from the user and complete the order.")]
public async Task<CheckoutResponse> Checkout()
{
Guid cartId = await userContext.GetCartIdAsync();
Guid paymentId = await paymentService.RequestPaymentFromUserAsync(cartId);
return await pizzaService.Checkout(cartId, paymentId);
}
}
Vous devez ensuite ajouter ce plug-in au noyau comme suit :
IKernelBuilder kernelBuilder = new KernelBuilder();
kernelBuilder..AddAzureOpenAIChatCompletion(
deploymentName: "NAME_OF_YOUR_DEPLOYMENT",
apiKey: "YOUR_API_KEY",
endpoint: "YOUR_AZURE_ENDPOINT"
);
kernelBuilder.Plugins.AddFromType<OrderPizzaPlugin>("OrderPizza");
Kernel kernel = kernelBuilder.Build();
Dans Python, le plug-in peut ressembler à ceci :
from semantic_kernel.functions import kernel_function
class OrderPizzaPlugin:
def __init__(self, pizza_service, user_context, payment_service):
self.pizza_service = pizza_service
self.user_context = user_context
self.payment_service = payment_service
@kernel_function
async def get_pizza_menu(self):
return await self.pizza_service.get_menu()
@kernel_function(
description="Add a pizza to the user's cart; returns the new item and updated cart"
)
async def add_pizza_to_cart(self, size: PizzaSize, toppings: List[PizzaToppings], quantity: int = 1, special_instructions: str = ""):
cart_id = await self.user_context.get_cart_id()
return await self.pizza_service.add_pizza_to_cart(cart_id, size, toppings, quantity, special_instructions)
@kernel_function(
description="Remove a pizza from the user's cart; returns the updated cart"
)
async def remove_pizza_from_cart(self, pizza_id: int):
cart_id = await self.user_context.get_cart_id()
return await self.pizza_service.remove_pizza_from_cart(cart_id, pizza_id)
@kernel_function(
description="Returns the specific details of a pizza in the user's cart; use this instead of relying on previous messages since the cart may have changed since then."
)
async def get_pizza_from_cart(self, pizza_id: int):
cart_id = await self.user_context.get_cart_id()
return await self.pizza_service.get_pizza_from_cart(cart_id, pizza_id)
@kernel_function(
description="Returns the user's current cart, including the total price and items in the cart."
)
async def get_cart(self):
cart_id = await self.user_context.get_cart_id()
return await self.pizza_service.get_cart(cart_id)
@kernel_function(
description="Checkouts the user's cart; this function will retrieve the payment from the user and complete the order."
)
async def checkout(self):
cart_id = await self.user_context.get_cart_id()
payment_id = await self.payment_service.request_payment_from_user(cart_id)
return await self.pizza_service.checkout(cart_id, payment_id)
Vous devez ensuite ajouter ce plug-in au noyau comme suit :
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
kernel = Kernel()
kernel.add_service(AzureChatCompletion(model_id, endpoint, api_key))
# Create the services needed for the plugin: pizza_service, user_context, and payment_service
# ...
# Add the plugin to the kernel
kernel.add_plugin(OrderPizzaPlugin(pizza_service, user_context, payment_service), plugin_name="OrderPizza")
En Java, le plug-in peut ressembler à ceci :
public class OrderPizzaPlugin {
private final PizzaService pizzaService;
private final HttpSession userContext;
private final PaymentService paymentService;
public OrderPizzaPlugin(
PizzaService pizzaService,
UserContext userContext,
PaymentService paymentService)
{
this.pizzaService = pizzaService;
this.userContext = userContext;
this.paymentService = paymentService;
}
@DefineKernelFunction(name = "get_pizza_menu", description = "Get the pizza menu.", returnType = "com.pizzashop.Menu")
public Mono<Menu> getPizzaMenuAsync()
{
return pizzaService.getMenu();
}
@DefineKernelFunction(
name = "add_pizza_to_cart",
description = "Add a pizza to the user's cart",
returnDescription = "Returns the new item and updated cart",
returnType = "com.pizzashop.CartDelta")
public Mono<CartDelta> addPizzaToCart(
@KernelFunctionParameter(name = "size", description = "The size of the pizza", type = com.pizzashopo.PizzaSize.class, required = true)
PizzaSize size,
@KernelFunctionParameter(name = "toppings", description = "The toppings to add to the the pizza", type = com.pizzashopo.PizzaToppings.class)
List<PizzaToppings> toppings,
@KernelFunctionParameter(name = "quantity", description = "How many of this pizza to order", type = Integer.class, defaultValue = "1")
int quantity,
@KernelFunctionParameter(name = "specialInstructions", description = "Special instructions for the order",)
String specialInstructions
)
{
UUID cartId = userContext.getCartId();
return pizzaService.addPizzaToCart(
cartId,
size,
toppings,
quantity,
specialInstructions);
}
@DefineKernelFunction(name = "remove_pizza_from_cart", description = "Remove a pizza from the cart.", returnType = "com.pizzashop.RemovePizzaResponse")
public Mono<RemovePizzaResponse> removePizzaFromCart(
@KernelFunctionParameter(name = "pizzaId", description = "Id of the pizza to remove from the cart", type = Integer.class, required = true)
int pizzaId)
{
UUID cartId = userContext.getCartId();
return pizzaService.removePizzaFromCart(cartId, pizzaId);
}
@DefineKernelFunction(
name = "get_pizza_from_cart",
description = "Returns the specific details of a pizza in the user's cart; use this instead of relying on previous messages since the cart may have changed since then.",
returnType = "com.pizzashop.Pizza")
public Mono<Pizza> getPizzaFromCart(
@KernelFunctionParameter(name = "pizzaId", description = "Id of the pizza to get from the cart", type = Integer.class, required = true)
int pizzaId)
{
UUID cartId = userContext.getCartId();
return pizzaService.getPizzaFromCart(cartId, pizzaId);
}
@DefineKernelFunction(
name = "get_cart",
description = "Returns the user's current cart, including the total price and items in the cart.",
returnType = "com.pizzashop.Cart")
public Mono<Cart> getCart()
{
UUID cartId = userContext.getCartId();
return pizzaService.getCart(cartId);
}
@DefineKernelFunction(
name = "checkout",
description = "Checkouts the user's cart; this function will retrieve the payment from the user and complete the order.",
returnType = "com.pizzashop.CheckoutResponse")
public Mono<CheckoutResponse> Checkout()
{
UUID cartId = userContext.getCartId();
return paymentService.requestPaymentFromUser(cartId)
.flatMap(paymentId -> pizzaService.checkout(cartId, paymentId));
}
}
Vous devez ensuite ajouter ce plug-in au noyau comme suit :
OpenAIAsyncClient client = new OpenAIClientBuilder()
.credential(openAIClientCredentials)
.buildAsyncClient();
ChatCompletionService chat = OpenAIChatCompletion.builder()
.withModelId(modelId)
.withOpenAIAsyncClient(client)
.build();
KernelPlugin plugin = KernelPluginFactory.createFromObject(
new OrderPizzaPlugin(pizzaService, userContext, paymentService),
"OrderPizzaPlugin"
);
Kernel kernel = Kernel.builder()
.withAIService(ChatCompletionService.class, chat)
.withPlugin(plugin)
.build();
1) Sérialisation des fonctions
Lorsque vous créez un noyau avec le OrderPizzaPlugin
noyau, le noyau sérialise automatiquement les fonctions et leurs paramètres. Cela est nécessaire pour que le modèle puisse comprendre les fonctions et leurs entrées.
Pour le plug-in ci-dessus, les fonctions sérialisées ressemblent à ceci :
[
{
"type": "function",
"function": {
"name": "OrderPizza-get_pizza_menu",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "OrderPizza-add_pizza_to_cart",
"description": "Add a pizza to the user's cart; returns the new item and updated cart",
"parameters": {
"type": "object",
"properties": {
"size": {
"type": "string",
"enum": ["Small", "Medium", "Large"]
},
"toppings": {
"type": "array",
"items": {
"type": "string",
"enum": ["Cheese", "Pepperoni", "Mushrooms"]
}
},
"quantity": {
"type": "integer",
"default": 1,
"description": "Quantity of pizzas"
},
"specialInstructions": {
"type": "string",
"default": "",
"description": "Special instructions for the pizza"
}
},
"required": ["size", "toppings"]
}
}
},
{
"type": "function",
"function": {
"name": "OrderPizza-remove_pizza_from_cart",
"parameters": {
"type": "object",
"properties": {
"pizzaId": {
"type": "integer"
}
},
"required": ["pizzaId"]
}
}
},
{
"type": "function",
"function": {
"name": "OrderPizza-get_pizza_from_cart",
"description": "Returns the specific details of a pizza in the user's cart; use this instead of relying on previous messages since the cart may have changed since then.",
"parameters": {
"type": "object",
"properties": {
"pizzaId": {
"type": "integer"
}
},
"required": ["pizzaId"]
}
}
},
{
"type": "function",
"function": {
"name": "OrderPizza-get_cart",
"description": "Returns the user's current cart, including the total price and items in the cart.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "OrderPizza-checkout",
"description": "Checkouts the user's cart; this function will retrieve the payment from the user and complete the order.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
]
Voici quelques points à noter qui peuvent avoir un impact sur les performances et la qualité de l’achèvement de la conversation :
Détail du schéma de fonction : la sérialisation des fonctions pour le modèle à utiliser n’est pas disponible gratuitement. Plus le schéma est détaillé, plus le modèle doit traiter les jetons, ce qui peut ralentir le temps de réponse et augmenter les coûts.
Conseil
Gardez vos fonctions aussi simples que possible. Dans l’exemple ci-dessus, vous remarquerez que toutes les fonctions n’ont pas de descriptions où le nom de la fonction est explicite. Cela est intentionnel de réduire le nombre de jetons. Les paramètres sont également simples ; Tout ce que le modèle ne doit pas avoir besoin de savoir (comme le
cartId
oupaymentId
) sont conservés masqués. Ces informations sont plutôt fournies par les services internes.Remarque
La seule chose que vous n’avez pas à vous soucier est la complexité des types de retour. Vous remarquerez que les types de retour ne sont pas sérialisés dans le schéma. Cela est dû au fait que le modèle n’a pas besoin de connaître le type de retour pour générer une réponse. À l’étape 6, toutefois, nous verrons comment les types de retour trop détaillés peuvent avoir un impact sur la qualité de l’achèvement de la conversation.
Types de paramètres : avec le schéma, vous pouvez spécifier le type de chaque paramètre. Il est important pour le modèle de comprendre l’entrée attendue. Dans l’exemple ci-dessus, le
size
paramètre est une énumération et letoppings
paramètre est un tableau d’énumérations. Cela permet au modèle de générer des réponses plus précises.Conseil
Évitez, dans la mesure du possible, l’utilisation
string
comme type de paramètre. Le modèle ne peut pas déduire le type de chaîne, ce qui peut entraîner des réponses ambiguës. Utilisez plutôt des énumérations ou d’autres types (par exemple,int
desfloat
types complexes et des types complexes) si possible.Paramètres obligatoires : vous pouvez également spécifier les paramètres requis. Il est important pour le modèle de comprendre quels paramètres sont réellement nécessaires pour que la fonction fonctionne. Plus loin à l’étape 3, le modèle utilisera ces informations pour fournir les informations minimales nécessaires pour appeler la fonction.
Conseil
Marquez uniquement les paramètres comme requis s’ils sont réellement requis. Cela aide les fonctions d’appel de modèle plus rapidement et plus précisément.
Descriptions de fonction : les descriptions de fonction sont facultatives, mais peuvent aider le modèle à générer des réponses plus précises. En particulier, les descriptions peuvent indiquer au modèle ce qu’il faut attendre de la réponse, car le type de retour n’est pas sérialisé dans le schéma. Si le modèle utilise des fonctions incorrectement, vous pouvez également ajouter des descriptions pour fournir des exemples et des conseils.
Par exemple, dans la fonction, la
get_pizza_from_cart
description indique à l’utilisateur d’utiliser cette fonction au lieu de s’appuyer sur des messages précédents. Cela est important, car le panier a peut-être changé depuis le dernier message.Conseil
Avant d’ajouter une description, demandez-vous si le modèle a besoin de ces informations pour générer une réponse. Si ce n’est pas le cas, envisagez de le laisser pour réduire la détail. Vous pouvez toujours ajouter des descriptions ultérieurement si le modèle a du mal à utiliser la fonction correctement.
Nom du plug-in : comme vous pouvez le voir dans les fonctions sérialisées, chaque fonction a une
name
propriété. Le noyau sémantique utilise le nom du plug-in pour espace de noms les fonctions. Cela est important, car il vous permet d’avoir plusieurs plug-ins avec des fonctions du même nom. Par exemple, vous pouvez avoir des plug-ins pour plusieurs services de recherche, chacun avec leur propresearch
fonction. En nommant les fonctions, vous pouvez éviter les conflits et faciliter la compréhension de la fonction à appeler.Sachant cela, vous devez choisir un nom de plug-in unique et descriptif. Dans l’exemple ci-dessus, le nom du plug-in est
OrderPizza
. Cela rend clair que les fonctions sont liées à la commande de pizza.Conseil
Lorsque vous choisissez un nom de plug-in, nous vous recommandons de supprimer des mots superflus tels que « plug-in » ou « service ». Cela permet de réduire la détail et de faciliter la compréhension du nom du plug-in pour le modèle.
2) Envoi des messages et fonctions au modèle
Une fois les fonctions sérialisées, elles sont envoyées au modèle avec l’historique de conversation actuel. Cela permet au modèle de comprendre le contexte de la conversation et les fonctions disponibles.
Dans ce scénario, nous pouvons imaginer que l’utilisateur demandant à l’assistant d’ajouter une pizza à son panier :
ChatHistory chatHistory = [];
chatHistory.AddUserMessage("I'd like to order a pizza!");
chat_history = ChatHistory()
chat_history.add_user_message("I'd like to order a pizza!")
ChatHistory chatHistory = new ChatHistory();
chatHistory.addUserMessage("I'd like to order a pizza!");
Nous pouvons ensuite envoyer cet historique de conversation et les fonctions sérialisées au modèle. Le modèle utilisera ces informations pour déterminer la meilleure façon de répondre.
IChatCompletionService chatCompletion = kernel.GetRequiredService<IChatCompletionService>();
OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
ChatResponse response = await chatCompletion.GetChatMessageContentAsync(
chatHistory,
executionSettings: openAIPromptExecutionSettings,
kernel: kernel)
chat_completion = kernel.get_service(type=ChatCompletionClientBase)
execution_settings = AzureChatPromptExecutionSettings()
execution_settings.function_choice_behavior = FunctionChoiceBehavior.Auto()
response = (await chat_completion.get_chat_message_contents(
chat_history=history,
settings=execution_settings,
kernel=kernel,
arguments=KernelArguments(),
))[0]
ChatCompletionService chatCompletion = kernel.getService(I)ChatCompletionService.class);
InvocationContext invocationContext = InvocationContext.builder()
.withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(false));
List<ChatResponse> responses = chatCompletion.getChatMessageContentsAsync(
chatHistory,
kernel,
invocationContext).block();
Remarque
Cet exemple utilise le FunctionChoiceBehavior.Auto()
comportement, l’un des rares disponibles. Pour plus d’informations sur les autres comportements de choix de fonction, consultez l’article sur les comportements de choix de fonction.
3) Le modèle traite l’entrée
Avec l’historique des conversations et les fonctions sérialisées, le modèle peut déterminer la meilleure façon de répondre. Dans ce cas, le modèle reconnaît que l’utilisateur souhaite commander une pizza. Le modèle souhaite probablement appeler la add_pizza_to_cart
fonction, mais parce que nous avons spécifié la taille et les garnitures en tant que paramètres requis, le modèle demande à l’utilisateur de fournir ces informations :
Console.WriteLine(response);
chatHistory.AddAssistantMessage(response);
// "Before I can add a pizza to your cart, I need to
// know the size and toppings. What size pizza would
// you like? Small, medium, or large?"
print(response)
chat_history.add_assistant_message(response)
# "Before I can add a pizza to your cart, I need to
# know the size and toppings. What size pizza would
# you like? Small, medium, or large?"
responses.forEach(response -> System.out.printlin(response.getContent());
chatHistory.addAll(responses);
// "Before I can add a pizza to your cart, I need to
// know the size and toppings. What size pizza would
// you like? Small, medium, or large?"
Étant donné que le modèle souhaite que l’utilisateur réponde ensuite, un signal d’arrêt est envoyé au noyau sémantique pour arrêter l’appel automatique des fonctions jusqu’à ce que l’utilisateur réponde.
À ce stade, l’utilisateur peut répondre avec la taille et les garnitures de la pizza qu’il souhaite commander :
chatHistory.AddUserMessage("I'd like a medium pizza with cheese and pepperoni, please.");
response = await chatCompletion.GetChatMessageContentAsync(
chatHistory,
kernel: kernel)
chat_history.add_user_message("I'd like a medium pizza with cheese and pepperoni, please.")
response = (await chat_completion.get_chat_message_contents(
chat_history=history,
settings=execution_settings,
kernel=kernel,
arguments=KernelArguments(),
))[0]
chatHistory.addUserMessage("I'd like a medium pizza with cheese and pepperoni, please.");
responses = chatCompletion.GetChatMessageContentAsync(
chatHistory,
kernel,
null).block();
Maintenant que le modèle a les informations nécessaires, il peut maintenant appeler la add_pizza_to_cart
fonction avec l’entrée de l’utilisateur. En arrière-plan, il ajoute un nouveau message à l’historique des conversations qui ressemble à ceci :
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "OrderPizzaPlugin-add_pizza_to_cart",
"arguments": "{\n\"size\": \"Medium\",\n\"toppings\": [\"Cheese\", \"Pepperoni\"]\n}"
}
}
]
Conseil
Il est judicieux de se rappeler que chaque argument dont vous avez besoin doit être généré par le modèle. Cela signifie que les jetons de dépense pour générer la réponse. Évitez les arguments qui nécessitent de nombreux jetons (comme un GUID). Par exemple, notez que nous utilisons un int
pour le pizzaId
. Demander au modèle d’envoyer un numéro à deux chiffres est beaucoup plus facile que de demander un GUID.
Important
Cette étape est ce qui rend l’appel de fonction si puissant. Auparavant, les développeurs d’applications IA devaient créer des processus distincts pour extraire les fonctions de remplissage d’intention et d’emplacement. Avec l’appel de fonction, le modèle peut décider quand appeler une fonction et quelles informations fournir.
4) Gérer la réponse
Lorsque le noyau sémantique reçoit la réponse du modèle, il vérifie si la réponse est un appel de fonction. Si c’est le cas, le noyau sémantique extrait le nom de la fonction et ses paramètres. Dans ce cas, le nom de la fonction est OrderPizzaPlugin-add_pizza_to_cart
, et les arguments sont la taille et les garnitures de la pizza.
Avec ces informations, le noyau sémantique peut marshaler les entrées dans les types appropriés et les transmettre à la add_pizza_to_cart
fonction dans le OrderPizzaPlugin
. Dans cet exemple, les arguments proviennent d’une chaîne JSON, mais sont désérialisés par le noyau sémantique dans une PizzaSize
énumération et un List<PizzaToppings>
.
Remarque
Le marshaling des entrées dans les types corrects est l’un des principaux avantages de l’utilisation du noyau sémantique. Tout ce qui provient du modèle est un objet JSON, mais le noyau sémantique peut désérialiser automatiquement ces objets dans les types appropriés pour vos fonctions.
Après avoir marshalé les entrées, le noyau sémantique peut également ajouter l’appel de fonction à l’historique des conversations :
chatHistory.Add(
new() {
Role = AuthorRole.Assistant,
Items = [
new FunctionCallContent(
functionName: "add_pizza_to_cart",
pluginName: "OrderPizza",
id: "call_abc123",
arguments: new () { {"size", "Medium"}, {"toppings", ["Cheese", "Pepperoni"]} }
)
]
}
);
from semantic_kernel.contents import ChatMessageContent, FunctionCallContent
from semantic_kernel.contents.utils.author_role import AuthorRole
chat_history.add_message(
ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[
FunctionCallContent(
name="OrderPizza-add_pizza_to_cart",
id="call_abc123",
arguments=str({"size": "Medium", "toppings": ["Cheese", "Pepperoni"]})
)
]
)
)
Le noyau sémantique pour Java gère l’appel de fonction différemment de C# et Python lorsque le comportement d’appel de l’outil d’appel automatique est false. Vous n’ajoutez pas de contenu d’appel de fonction à l’historique des conversations ; au lieu de cela, l’application est chargée d’appeler les appels de fonction. Passez à la section suivante , « Appeler la fonction », pour obtenir un exemple de gestion des appels de fonction en Java lorsque l’appel automatique est false.
5) Appeler la fonction
Une fois que le noyau sémantique a les types corrects, il peut enfin appeler la add_pizza_to_cart
fonction. Étant donné que le plug-in utilise l’injection de dépendances, la fonction peut interagir avec des services externes comme pizzaService
et userContext
ajouter la pizza au panier de l’utilisateur.
Toutefois, toutes les fonctions ne réussissent pas. Si la fonction échoue, le noyau sémantique peut gérer l’erreur et fournir une réponse par défaut au modèle. Cela permet au modèle de comprendre ce qui s’est passé et de générer une réponse à l’utilisateur.
Conseil
Pour vous assurer qu’un modèle peut se corriger automatiquement, il est important de fournir des messages d’erreur qui communiquent clairement ce qui s’est passé et comment le corriger. Cela peut aider le modèle à réessayer l’appel de fonction avec les informations appropriées.
Remarque
Le noyau sémantique appelle automatiquement les fonctions par défaut. Toutefois, si vous préférez gérer manuellement l’appel de fonction, vous pouvez activer le mode d’appel de fonction manuel. Pour plus d’informations sur la procédure à suivre, reportez-vous à l’article d’appel de fonction.
6) Retourner le résultat de la fonction
Une fois la fonction appelée, le résultat de la fonction est renvoyé au modèle dans le cadre de l’historique des conversations. Cela permet au modèle de comprendre le contexte de la conversation et de générer une réponse ultérieure.
En arrière-plan, le noyau sémantique ajoute un nouveau message à l’historique des conversations à partir du rôle d’outil qui ressemble à ceci :
chatHistory.Add(
new() {
Role = AuthorRole.Tool,
Items = [
new FunctionResultContent(
functionName: "add_pizza_to_cart",
pluginName: "OrderPizza",
id: "0001",
result: "{ \"new_items\": [ { \"id\": 1, \"size\": \"Medium\", \"toppings\": [\"Cheese\",\"Pepperoni\"] } ] }"
)
]
}
);
from semantic_kernel.contents import ChatMessageContent, FunctionResultContent
from semantic_kernel.contents.utils.author_role import AuthorRole
chat_history.add_message(
ChatMessageContent(
role=AuthorRole.TOOL,
items=[
FunctionResultContent(
name="OrderPizza-add_pizza_to_cart",
id="0001",
result="{ \"new_items\": [ { \"id\": 1, \"size\": \"Medium\", \"toppings\": [\"Cheese\",\"Pepperoni\"] } ] }"
)
]
)
)
Si l’appel automatique est désactivé dans le comportement de l’appel d’outil, une application Java doit appeler les appels de fonction et ajouter le résultat de AuthorRole.TOOL
la fonction en tant que message à l’historique des conversations.
messages.stream()
.filter (it -> it instanceof OpenAIChatMessageContent)
.map(it -> ((OpenAIChatMessageContent<?>) it).getToolCall())
.flatMap(List::stream)
.forEach(toolCall -> {
String content;
try {
// getFunction will throw an exception if the function is not found
var fn = kernel.getFunction(toolCall.getPluginName(),
toolCall.getFunctionName());
FunctionResult<?> fnResult = fn
.invokeAsync(kernel, toolCall.getArguments(), null, null).block();
content = (String) fnResult.getResult();
} catch (IllegalArgumentException e) {
content = "Unable to find function. Please try again!";
}
chatHistory.addMessage(
AuthorRole.TOOL,
content,
StandardCharsets.UTF_8,
FunctionResultMetadata.build(toolCall.getId()));
});
Notez que le résultat est une chaîne JSON que le modèle doit ensuite traiter. Comme précédemment, le modèle devra dépenser des jetons consommant ces informations. C’est pourquoi il est important de garder les types de retour aussi simples que possible. Dans ce cas, le retour inclut uniquement les nouveaux éléments ajoutés au panier, et non l’intégralité du panier.
Conseil
Soyez aussi succinct que possible avec vos retours. Dans la mesure du possible, retournez uniquement les informations dont le modèle a besoin ou récapitulez les informations à l’aide d’une autre invite LLM avant de le renvoyer.
Répétez les étapes 2 à 6
Une fois le résultat retourné au modèle, le processus se répète. Le modèle traite l’historique de conversation le plus récent et génère une réponse. Dans ce cas, le modèle peut demander à l’utilisateur s’il souhaite ajouter une autre pizza à son panier ou s’il souhaite extraire.
Appels de fonction parallèles
Dans l’exemple ci-dessus, nous avons montré comment un LLM peut appeler une fonction unique. Souvent, cela peut être lent si vous avez besoin d’appeler plusieurs fonctions en séquence. Pour accélérer le processus, plusieurs llms prennent en charge les appels de fonction parallèles. Cela permet au LLM d’appeler plusieurs fonctions à la fois, accélérant le processus.
Par exemple, si un utilisateur souhaite commander plusieurs pizzas, le LLM peut appeler la add_pizza_to_cart
fonction pour chaque pizza en même temps. Cela peut réduire considérablement le nombre d’allers-retours vers le LLM et accélérer le processus de commande.
Étapes suivantes
Maintenant que vous comprenez le fonctionnement de l’appel de fonction, vous pouvez continuer à apprendre à configurer différents aspects de l’appel de fonction qui correspondent mieux à vos scénarios spécifiques en faisant référence à l’article sur le comportement du choix de fonction