Chiamata di funzioni con completamento della chat
La funzionalità più potente di completamento della chat è la possibilità di chiamare funzioni dal modello. In questo modo è possibile creare un chatbot in grado di interagire con il codice esistente, rendendo possibile automatizzare i processi aziendali, creare frammenti di codice e altro ancora.
Con il kernel semantico si semplifica il processo di utilizzo della chiamata di funzione descrivendo automaticamente le funzioni e i relativi parametri al modello e quindi gestendo la comunicazione avanti e indietro tra il modello e il codice.
Quando si usa la chiamata a funzioni, tuttavia, è consigliabile comprendere cosa accade in realtà dietro le quinte in modo da poter ottimizzare il codice e sfruttare al meglio questa funzionalità.
Funzionamento della chiamata di funzioni
Quando si effettua una richiesta a un modello con chiamata di funzione abilitata, il kernel semantico esegue i passaggi seguenti:
Procedi | Description | |
---|---|---|
1 | Serializzare le funzioni | Tutte le funzioni disponibili (e i relativi parametri di input) nel kernel vengono serializzate usando lo schema JSON. |
2 | Inviare messaggi e funzioni al modello | Le funzioni serializzate (e la cronologia della chat corrente) vengono inviate al modello come parte dell'input. |
3 | Il modello elabora l'input | Il modello elabora l'input e genera una risposta. La risposta può essere un messaggio di chat o una chiamata di funzione |
4 | Gestire la risposta | Se la risposta è un messaggio di chat, viene restituita allo sviluppatore per stampare la risposta sullo schermo. Se la risposta è una chiamata di funzione, tuttavia, Semantic Kernel estrae il nome della funzione e i relativi parametri. |
5 | Richiamare la funzione | Il nome e i parametri della funzione estratti vengono usati per richiamare la funzione nel kernel. |
6 | Restituire il risultato della funzione | Il risultato della funzione viene quindi inviato di nuovo al modello come parte della cronologia delle chat. I passaggi da 2 a 6 vengono quindi ripetuti fino a quando il modello non invia un segnale di terminazione |
Il diagramma seguente illustra il processo di chiamata di funzione:
La sezione seguente userà un esempio concreto per illustrare il funzionamento delle chiamate di funzione in pratica.
Esempio: ordinamento di una pizza
Si supponga di avere un plug-in che consente a un utente di ordinare una pizza. Il plug-in ha le funzioni seguenti:
get_pizza_menu
: restituisce un elenco di pizze disponibiliadd_pizza_to_cart
: aggiunge una pizza al carrello dell'utenteremove_pizza_from_cart
: rimuove una pizza dal carrello dell'utenteget_pizza_from_cart
: restituisce i dettagli specifici di una pizza nel carrello dell'utenteget_cart
: restituisce il carrello corrente dell'utentecheckout
: estrae il carrello dell'utente
In C# il plug-in potrebbe essere simile al seguente:
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);
}
}
Si aggiungerà quindi questo plug-in al kernel come segue:
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();
In Python il plug-in potrebbe essere simile al seguente:
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)
Si aggiungerà quindi questo plug-in al kernel come segue:
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")
In Java il plug-in potrebbe essere simile al seguente:
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));
}
}
Si aggiungerà quindi questo plug-in al kernel come segue:
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) Serializzazione delle funzioni
Quando si crea un kernel con , OrderPizzaPlugin
il kernel serializzerà automaticamente le funzioni e i relativi parametri. Questa operazione è necessaria in modo che il modello possa comprendere le funzioni e i relativi input.
Per il plug-in precedente, le funzioni serializzate sono simili alle seguenti:
[
{
"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": []
}
}
}
]
Ecco alcuni aspetti da notare che possono influire sulle prestazioni e sulla qualità del completamento della chat:
Dettaglio dello schema delle funzioni: la serializzazione delle funzioni da usare per il modello non è gratuita. Più dettagliato è lo schema, maggiore è il numero di token che il modello deve elaborare, che può rallentare il tempo di risposta e aumentare i costi.
Suggerimento
Mantenere le funzioni il più semplice possibile. Nell'esempio precedente si noterà che non tutte le funzioni hanno descrizioni in cui il nome della funzione è autoesplicativo. Questo è intenzionale per ridurre il numero di token. I parametri sono anche mantenuti semplici; qualsiasi elemento che il modello non deve conoscere (ad esempio
cartId
opaymentId
) viene mantenuto nascosto. Queste informazioni vengono invece fornite dai servizi interni.Nota
L'unica cosa che non è necessario preoccuparsi è la complessità dei tipi restituiti. Si noterà che i tipi restituiti non vengono serializzati nello schema. Ciò è dovuto al fatto che il modello non deve conoscere il tipo restituito per generare una risposta. Nel passaggio 6, tuttavia, vedremo come i tipi restituiti eccessivamente dettagliato possono influire sulla qualità del completamento della chat.
Tipi di parametri: con lo schema è possibile specificare il tipo di ogni parametro. Questo aspetto è importante per il modello per comprendere l'input previsto. Nell'esempio precedente il
size
parametro è un'enumerazione e iltoppings
parametro è una matrice di enumerazioni. Questo consente al modello di generare risposte più accurate.Suggerimento
Evitare, se possibile, di usare
string
come tipo di parametro. Il modello non può dedurre il tipo di stringa, che può causare risposte ambigue. Usare invece enumerazioni o altri tipi (ad esempio,int
,float
e tipi complessi) laddove possibile.Parametri obbligatori: è anche possibile specificare i parametri necessari. Questo è importante per il modello per comprendere quali parametri sono effettivamente necessari per il funzionamento della funzione. Più avanti nel passaggio 3, il modello userà queste informazioni per fornire le informazioni minime necessarie per chiamare la funzione.
Suggerimento
Contrassegnare solo i parametri necessari se sono effettivamente necessari. Ciò consente alle funzioni di chiamare il modello in modo più rapido e accurato.
Descrizioni delle funzioni: le descrizioni delle funzioni sono facoltative, ma consentono al modello di generare risposte più accurate. In particolare, le descrizioni possono indicare al modello cosa aspettarsi dalla risposta poiché il tipo restituito non viene serializzato nello schema. Se il modello usa funzioni in modo non corretto, è anche possibile aggiungere descrizioni per fornire esempi e indicazioni.
Ad esempio, nella funzione la
get_pizza_from_cart
descrizione indica all'utente di usare questa funzione invece di basarsi sui messaggi precedenti. Questo è importante perché il carrello potrebbe essere cambiato dopo l'ultimo messaggio.Suggerimento
Prima di aggiungere una descrizione, chiedere se il modello necessita di queste informazioni per generare una risposta. In caso contrario, è consigliabile evitare di ridurre il livello di dettaglio. È sempre possibile aggiungere descrizioni in un secondo momento se il modello ha difficoltà a usare correttamente la funzione.
Nome del plug-in: come si può vedere nelle funzioni serializzate, ogni funzione ha una
name
proprietà . Il kernel semantico usa il nome del plug-in per spazi dei nomi delle funzioni. Questo è importante perché consente di avere più plug-in con funzioni con lo stesso nome. Ad esempio, si potrebbero avere plug-in per più servizi di ricerca, ognuno con la propriasearch
funzione. Assegnando nomi alle funzioni, è possibile evitare conflitti e semplificare la comprensione della funzione da chiamare dal modello.Sapendo questo, è necessario scegliere un nome di plug-in univoco e descrittivo. Nell'esempio precedente il nome del plug-in è
OrderPizza
. Ciò rende chiaro che le funzioni sono correlate all'ordinamento della pizza.Suggerimento
Quando si sceglie un nome di plug-in, è consigliabile rimuovere parole superflue, ad esempio "plug-in" o "service". In questo modo è possibile ridurre il livello di dettaglio e semplificare la comprensione del nome del plug-in per il modello.
2) Invio di messaggi e funzioni al modello
Dopo aver serializzato le funzioni, vengono inviate al modello insieme alla cronologia della chat corrente. In questo modo il modello può comprendere il contesto della conversazione e le funzioni disponibili.
In questo scenario, è possibile immaginare l'utente che chiede all'assistente di aggiungere una pizza al carrello:
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!");
È quindi possibile inviare la cronologia delle chat e le funzioni serializzate al modello. Il modello userà queste informazioni per determinare il modo migliore per rispondere.
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();
3) Il modello elabora l'input
Con la cronologia delle chat e le funzioni serializzate, il modello può determinare il modo migliore per rispondere. In questo caso, il modello riconosce che l'utente vuole ordinare una pizza. È probabile che il modello voglia chiamare la add_pizza_to_cart
funzione, ma poiché sono stati specificati le dimensioni e i condimenti come parametri obbligatori, il modello chiederà all'utente queste informazioni:
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?"
Poiché il modello vuole che l'utente risponda successivamente, un segnale di terminazione verrà inviato al kernel semantico per arrestare la chiamata automatica della funzione fino a quando l'utente non risponde.
A questo punto, l'utente può rispondere con le dimensioni e i condimenti della pizza che vogliono ordinare:
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();
Ora che il modello ha le informazioni necessarie, ora può chiamare la add_pizza_to_cart
funzione con l'input dell'utente. Dietro le quinte, aggiunge un nuovo messaggio alla cronologia delle chat simile al seguente:
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "OrderPizzaPlugin-add_pizza_to_cart",
"arguments": "{\n\"size\": \"Medium\",\n\"toppings\": [\"Cheese\", \"Pepperoni\"]\n}"
}
}
]
Suggerimento
È consigliabile ricordare che ogni argomento necessario deve essere generato dal modello. Ciò significa che i token di spesa per generare la risposta. Evitare argomenti che richiedono molti token (ad esempio un GUID). Si noti, ad esempio, che per int
.pizzaId
Chiedere al modello di inviare un numero da uno a due cifre è molto più semplice rispetto alla richiesta di un GUID.
Importante
Questo passaggio rende la chiamata di funzione così potente. In precedenza, gli sviluppatori di app di intelligenza artificiale dovevano creare processi separati per estrarre funzioni di riempimento finalità e slot. Con la chiamata di funzione, il modello può decidere quando chiamare una funzione e quali informazioni fornire.
4) Gestire la risposta
Quando il kernel semantico riceve la risposta dal modello, verifica se la risposta è una chiamata di funzione. In caso affermativo, il kernel semantico estrae il nome della funzione e i relativi parametri. In questo caso, il nome della funzione è OrderPizzaPlugin-add_pizza_to_cart
e gli argomenti sono le dimensioni e i condimenti della pizza.
Con queste informazioni, il kernel semantico può effettuare il marshalling degli input nei tipi appropriati e passarli alla add_pizza_to_cart
funzione in OrderPizzaPlugin
. In questo esempio gli argomenti hanno origine come stringa JSON, ma vengono deserializzati dal kernel semantico in un'enumerazione PizzaSize
e in un oggetto List<PizzaToppings>
.
Nota
Il marshalling degli input nei tipi corretti è uno dei vantaggi principali dell'uso del kernel semantico. Tutto il contenuto del modello viene fornito come oggetto JSON, ma il kernel semantico può deserializzare automaticamente questi oggetti nei tipi corretti per le funzioni.
Dopo il marshalling degli input, Semantic Kernel può anche aggiungere la chiamata di funzione alla cronologia delle chat:
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"]})
)
]
)
)
Il kernel semantico per Java gestisce la funzione che chiama in modo diverso da C# e Python quando il comportamento di chiamata dello strumento di richiamo automatico è false. Non si aggiunge il contenuto della chiamata di funzione alla cronologia delle chat; piuttosto, l'applicazione viene resa responsabile della chiamata delle chiamate di funzione. Passare alla sezione successiva , "Invoke the function", per un esempio di gestione delle chiamate di funzione in Java quando il richiamo automatico è false.
5) Richiamare la funzione
Una volta che il kernel semantico ha i tipi corretti, può infine richiamare la add_pizza_to_cart
funzione. Poiché il plug-in usa l'inserimento delle dipendenze, la funzione può interagire con servizi esterni come pizzaService
e userContext
per aggiungere la pizza al carrello dell'utente.
Non tutte le funzioni avranno esito positivo, tuttavia. Se la funzione ha esito negativo, il kernel semantico può gestire l'errore e fornire una risposta predefinita al modello. In questo modo il modello può comprendere cosa è andato storto e generare una risposta all'utente.
Suggerimento
Per assicurarsi che un modello possa essere corretto in modo automatico, è importante fornire messaggi di errore che comunicano chiaramente cosa è andato storto e come risolverlo. Ciò consente al modello di ripetere la chiamata di funzione con le informazioni corrette.
Nota
Il kernel semantico richiama automaticamente le funzioni per impostazione predefinita. Tuttavia, se si preferisce gestire manualmente la chiamata di funzione, è possibile abilitare la modalità di chiamata manuale della funzione. Per altre informazioni su come eseguire questa operazione, vedere l'articolo relativo alla chiamata di funzione.
6) Restituire il risultato della funzione
Dopo aver richiamato la funzione, il risultato della funzione viene restituito al modello come parte della cronologia delle chat. In questo modo il modello può comprendere il contesto della conversazione e generare una risposta successiva.
Dietro le quinte, Semantic Kernel aggiunge un nuovo messaggio alla cronologia delle chat dal ruolo dello strumento simile al seguente:
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\"] } ] }"
)
]
)
)
Se il richiamo automatico è disabilitato nel comportamento di chiamata dello strumento, un'applicazione Java deve richiamare le chiamate di funzione e aggiungere il risultato della funzione come AuthorRole.TOOL
messaggio alla cronologia delle chat.
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()));
});
Si noti che il risultato è una stringa JSON che il modello deve quindi elaborare. Come in precedenza, il modello dovrà spendere i token che usano queste informazioni. Questo è il motivo per cui è importante mantenere i tipi restituiti il più semplice possibile. In questo caso, il ritorno include solo i nuovi articoli aggiunti al carrello, non l'intero carrello.
Suggerimento
Essere il più conciso possibile con i tuoi ritorni. Se possibile, restituire solo le informazioni necessarie al modello o riepilogare le informazioni usando un altro prompt LLM prima di restituirlo.
Ripetere i passaggi da 2 a 6
Dopo che il risultato viene restituito al modello, il processo viene ripetuto. Il modello elabora la cronologia delle chat più recente e genera una risposta. In questo caso, il modello potrebbe chiedere all'utente se vuole aggiungere un'altra pizza al carrello o se vuole eseguire il check-out.
Chiamate di funzioni parallele
Nell'esempio precedente è stato illustrato come un LLM può chiamare una singola funzione. Spesso questo può essere lento se è necessario chiamare più funzioni in sequenza. Per velocizzare il processo, diversi LLM supportano chiamate di funzione parallele. In questo modo l'LLM può chiamare più funzioni contemporaneamente, velocizzando il processo.
Ad esempio, se un utente vuole ordinare più pizze, l'LLM può chiamare la add_pizza_to_cart
funzione per ogni pizza contemporaneamente. Ciò può ridurre significativamente il numero di round trip al LLM e velocizzare il processo di ordinamento.
Passaggi successivi
Ora che si è appreso come funziona la chiamata di funzioni, è possibile imparare a configurare vari aspetti della chiamata di funzione che corrispondono meglio agli scenari specifici facendo riferimento all'articolo relativo al comportamento di scelta della funzione