Chamada de função com conclusão de bate-papo
O recurso mais poderoso do preenchimento de bate-papo é a capacidade de chamar funções do modelo. Isso permite que você crie um bot de bate-papo que pode interagir com seu código existente, possibilitando automatizar processos de negócios, criar trechos de código e muito mais.
Com o Semantic Kernel, simplificamos o processo de uso da chamada de função descrevendo automaticamente suas funções e seus parâmetros para o modelo e, em seguida, lidando com a comunicação de ida e volta entre o modelo e seu código.
Ao usar a chamada de função, no entanto, é bom entender o que realmente está acontecendo nos bastidores para que você possa otimizar seu código e aproveitar ao máximo esse recurso.
Como funciona a chamada de função
Quando você faz uma solicitação a um modelo com a chamada de função habilitada, o Kernel Semântico executa as seguintes etapas:
Etapa | Descrição | |
---|---|---|
1 | Serializar funções | Todas as funções disponíveis (e seus parâmetros de entrada) no kernel são serializadas usando o esquema JSON. |
2 | Enviar as mensagens e funções para o modelo | As funções serializadas (e o histórico de chat atual) são enviadas para o modelo como parte da entrada. |
3 | O modelo processa a entrada | O modelo processa a entrada e gera uma resposta. A resposta pode ser uma mensagem de chat ou uma chamada de função |
4 | Lidar com a resposta | Se a resposta for uma mensagem de chat, ela será devolvida ao desenvolvedor para imprimir a resposta na tela. No entanto, se a resposta for uma chamada de função, o Semantic Kernel extrairá o nome da função e seus parâmetros. |
5 | Invocar a função | O nome e os parâmetros da função extraída são usados para invocar a função no kernel. |
6 | Retornar o resultado da função | O resultado da função é então enviado de volta ao modelo como parte do histórico de bate-papo. As etapas 2 a 6 são repetidas até que o modelo envie um sinal de terminação |
O diagrama a seguir ilustra o processo de chamada de função:
A seção a seguir usará um exemplo concreto para ilustrar como a chamada de função funciona na prática.
Exemplo: Pedir uma pizza
Vamos supor que você tenha um plug-in que permite que um usuário peça uma pizza. O plugin tem as seguintes funções:
get_pizza_menu
: Retorna uma lista de pizzas disponíveisadd_pizza_to_cart
: Adiciona uma pizza ao carrinho do usuárioremove_pizza_from_cart
: Remove uma pizza do carrinho do usuárioget_pizza_from_cart
: Retorna os detalhes específicos de uma pizza no carrinho do usuárioget_cart
: Retorna o carrinho atual do usuáriocheckout
: Faz check-out do carrinho do usuário
Em C#, o plug-in pode ter esta aparência:
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);
}
}
Você então adicionaria este plugin ao kernel assim:
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();
Em Python, o plug-in pode ter esta aparência:
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)
Você então adicionaria este plugin ao kernel assim:
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")
Em Java, o plug-in pode ter esta aparência:
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));
}
}
Você então adicionaria este plugin ao kernel assim:
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) Serialização das funções
Quando você cria um kernel com o OrderPizzaPlugin
, o kernel serializa automaticamente as funções e seus parâmetros. Isso é necessário para que o modelo possa entender as funções e suas entradas.
Para o plug-in acima, as funções serializadas ficariam assim:
[
{
"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": []
}
}
}
]
Há algumas coisas a serem observadas aqui que podem afetar o desempenho e a qualidade da conclusão do bate-papo:
Verbosidade do esquema de função – A serialização de funções para o modelo usar não é gratuita. Quanto mais detalhado o esquema, mais tokens o modelo precisa processar, o que pode diminuir o tempo de resposta e aumentar os custos.
Dica
Mantenha suas funções o mais simples possível. No exemplo acima, você observará que nem todas as funções têm descrições em que o nome da função é autoexplicativo. Isso é intencional para reduzir o número de tokens. Os parâmetros também são mantidos simples; qualquer coisa que o modelo não precise saber (como o
cartId
oupaymentId
) é mantido oculto. Em vez disso, essas informações são fornecidas por serviços internos.Observação
A única coisa com a qual você não precisa se preocupar é a complexidade dos tipos de retorno. Você observará que os tipos de retorno não são serializados no esquema. Isso ocorre porque o modelo não precisa saber o tipo de retorno para gerar uma resposta. Na etapa 6, no entanto, veremos como os tipos de retorno excessivamente detalhados podem afetar a qualidade da conclusão do chat.
Tipos de parâmetro – Com o esquema, você pode especificar o tipo de cada parâmetro. Isso é importante para que o modelo entenda a entrada esperada. No exemplo acima, o
size
parâmetro é uma enumeração e otoppings
parâmetro é uma matriz de enumerações. Isso ajuda o modelo a gerar respostas mais precisas.Dica
Evite, sempre que possível, usar
string
como um tipo de parâmetro. O modelo não pode inferir o tipo de cadeia de caracteres, o que pode levar a respostas ambíguas. Em vez disso, use enumerações ou outros tipos (por exemplo,int
, ,float
e tipos complexos) sempre que possível.Parâmetros obrigatórios - Você também pode especificar quais parâmetros são necessários. Isso é importante para que o modelo entenda quais parâmetros são realmente necessários para que a função funcione. Mais adiante na etapa 3, o modelo usará essas informações para fornecer o mínimo de informações necessárias para chamar a função.
Dica
Marque os parâmetros como obrigatórios apenas se eles forem realmente necessários. Isso ajuda as funções de chamada de modelo com mais rapidez e precisão.
Descrições de função – As descrições de função são opcionais, mas podem ajudar o modelo a gerar respostas mais precisas. Em particular, as descrições podem dizer ao modelo o que esperar da resposta, já que o tipo de retorno não é serializado no esquema. Se o modelo estiver usando funções incorretamente, você também poderá adicionar descrições para fornecer exemplos e diretrizes.
Por exemplo, na função, a
get_pizza_from_cart
descrição informa ao usuário para usar essa função em vez de depender de mensagens anteriores. Isso é importante porque o carrinho pode ter mudado desde a última mensagem.Dica
Antes de adicionar uma descrição, pergunte a si mesmo se o modelo precisa dessas informações para gerar uma resposta. Caso contrário, considere deixá-lo de fora para reduzir a verbosidade. Você sempre pode adicionar descrições posteriormente se o modelo estiver com dificuldades para usar a função corretamente.
Nome do plug-in – Como você pode ver nas funções serializadas, cada função tem uma
name
propriedade. O Semantic Kernel usa o nome do plug-in para namespace das funções. Isso é importante porque permite que você tenha vários plugins com funções de mesmo nome. Por exemplo, você pode ter plug-ins para vários serviços de pesquisa, cada um com sua própriasearch
função. Ao espaçar as funções, você pode evitar conflitos e facilitar a compreensão do modelo sobre qual função chamar.Sabendo disso, você deve escolher um nome de plugin que seja único e descritivo. No exemplo acima, o nome do plug-in é
OrderPizza
. Isso deixa claro que as funções estão relacionadas ao pedido de pizza.Dica
Ao escolher um nome de plug-in, recomendamos remover palavras supérfluas como "plug-in" ou "serviço". Isso ajuda a reduzir o detalhamento e torna o nome do plug-in mais fácil de entender para o modelo.
2) Envio das mensagens e funções para o modelo
Depois que as funções são serializadas, elas são enviadas para o modelo junto com o histórico de bate-papo atual. Isso permite que o modelo entenda o contexto da conversa e as funções disponíveis.
Nesse cenário, podemos imaginar o usuário pedindo ao assistente para adicionar uma pizza ao carrinho:
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!");
Podemos então enviar esse histórico de bate-papo e as funções serializadas para o modelo. O modelo usará essas informações para determinar a melhor maneira de responder.
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();
Observação
Este exemplo usa o FunctionChoiceBehavior.Auto()
comportamento, um dos poucos disponíveis. Para obter mais informações sobre outros comportamentos de escolha de função, confira o artigo Comportamentos de escolha de função.
3) O modelo processa a entrada
Com o histórico de chat e as funções serializadas, o modelo pode determinar a melhor maneira de responder. Nesse caso, o modelo reconhece que o usuário deseja pedir uma pizza. O modelo provavelmente gostaria de chamar a add_pizza_to_cart
função, mas como especificamos o tamanho e as coberturas como parâmetros necessários, o modelo solicitará ao usuário essas informações:
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?"
Como o modelo deseja que o usuário responda em seguida, um sinal de encerramento será enviado ao Kernel Semântico para interromper a chamada automática de função até que o usuário responda.
Neste ponto, o usuário pode responder com o tamanho e as coberturas da pizza que deseja pedir:
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();
Agora que o modelo tem as informações necessárias, ele pode chamar a add_pizza_to_cart
função com a entrada do usuário. Nos bastidores, ele adiciona uma nova mensagem ao histórico de bate-papo que se parece com esta:
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "OrderPizzaPlugin-add_pizza_to_cart",
"arguments": "{\n\"size\": \"Medium\",\n\"toppings\": [\"Cheese\", \"Pepperoni\"]\n}"
}
}
]
Dica
É bom lembrar que todos os argumentos necessários devem ser gerados pelo modelo. Isso significa gastar tokens para gerar a resposta. Evite argumentos que exijam muitos tokens (como um GUID). Por exemplo, observe que usamos um int
para o pizzaId
. Pedir ao modelo para enviar um número de um a dois dígitos é muito mais fácil do que solicitar um GUID.
Importante
Esta etapa é o que torna a chamada de função tão poderosa. Anteriormente, os desenvolvedores de aplicativos de IA precisavam criar processos separados para extrair funções de intenção e preenchimento de slot. Com a chamada de função, o modelo pode decidir quando chamar uma função e quais informações fornecer.
4) Lidar com a resposta
Quando o Kernel Semântico recebe a resposta do modelo, ele verifica se a resposta é uma chamada de função. Se for, o Semantic Kernel extrai o nome da função e seus parâmetros. Nesse caso, o nome da função é OrderPizzaPlugin-add_pizza_to_cart
, e os argumentos são o tamanho e as coberturas da pizza.
Com essas informações, o Semantic Kernel pode empacotar as entradas nos tipos apropriados e passá-las para a add_pizza_to_cart
função no OrderPizzaPlugin
. Neste exemplo, os argumentos se originam como uma cadeia de caracteres JSON, mas são desserializados pelo Semantic Kernel em uma PizzaSize
enumeração e um List<PizzaToppings>
.
Observação
Empacotar as entradas nos tipos corretos é um dos principais benefícios do uso do Kernel Semântico. Tudo do modelo vem como um objeto JSON, mas o Semantic Kernel pode desserializar automaticamente esses objetos nos tipos corretos para suas funções.
Depois de organizar as entradas, o Semantic Kernel também pode adicionar a chamada de função ao histórico de bate-papo:
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"]})
)
]
)
)
O Kernel Semântico para Java lida com a chamada de função de forma diferente do C# e do Python quando o comportamento de chamada da ferramenta de invocação automática é false. Você não adiciona conteúdo de chamada de função ao histórico de bate-papo; em vez disso, o aplicativo é responsável por invocar as chamadas de função. Pule para a próxima seção, "Invocar a função", para obter um exemplo de como lidar com chamadas de função em Java quando a invocação automática é falsa.
5) Invocar a função
Depois que o Semantic Kernel tiver os tipos corretos, ele poderá finalmente invocar a add_pizza_to_cart
função. Como o plug-in usa injeção de dependência, a função pode interagir com serviços externos como pizzaService
e userContext
para adicionar a pizza ao carrinho do usuário.
No entanto, nem todas as funções serão bem-sucedidas. Se a função falhar, o Kernel Semântico poderá lidar com o erro e fornecer uma resposta padrão ao modelo. Isso permite que o modelo entenda o que deu errado e gere uma resposta ao usuário.
Dica
Para garantir que um modelo possa se autocorrigir, é importante fornecer mensagens de erro que comuniquem claramente o que deu errado e como corrigi-lo. Isso pode ajudar o modelo a repetir a chamada de função com as informações corretas.
Observação
O Kernel Semântico invoca automaticamente funções por padrão. No entanto, se preferir gerenciar a invocação de função manualmente, você pode ativar o modo de invocação de função manual. Para obter mais detalhes sobre como fazer isso, consulte o artigo de invocação de função.
6) Retornar o resultado da função
Depois que a função é invocada, o resultado da função é enviado de volta ao modelo como parte do histórico de chat. Isso permite que o modelo entenda o contexto da conversa e gere uma resposta subsequente.
Nos bastidores, o Kernel Semântico adiciona uma nova mensagem ao histórico de bate-papo da função de ferramenta que se parece com esta:
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 a chamada automática estiver desativada no comportamento de chamada de ferramenta, um aplicativo Java deverá chamar as chamadas de função e adicionar o resultado da função como uma AuthorRole.TOOL
mensagem ao histórico de bate-papo.
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()));
});
Observe que o resultado é uma cadeia de caracteres JSON que o modelo precisa processar. Como antes, o modelo precisará gastar tokens consumindo essas informações. É por isso que é importante manter os tipos de devolução o mais simples possível. Nesse caso, a devolução inclui apenas os novos itens adicionados ao carrinho, não o carrinho inteiro.
Dica
Seja o mais sucinto possível com seus retornos. Sempre que possível, retorne apenas as informações de que o modelo precisa ou resuma as informações usando outro prompt do LLM antes de retorná-las.
Repita as etapas 2 a 6
Depois que o resultado é retornado ao modelo, o processo se repete. O modelo processa o histórico de chat mais recente e gera uma resposta. Nesse caso, o modelo pode perguntar ao usuário se ele deseja adicionar outra pizza ao carrinho ou se deseja finalizar a compra.
Chamadas de função paralelas
No exemplo acima, demonstramos como um LLM pode chamar uma única função. Muitas vezes, isso pode ser lento se você precisar chamar várias funções em sequência. Para acelerar o processo, vários LLMs suportam chamadas de função paralelas. Isso permite que o LLM chame várias funções ao mesmo tempo, acelerando o processo.
Por exemplo, se um usuário quiser pedir várias pizzas, o LLM pode chamar a add_pizza_to_cart
função para cada pizza ao mesmo tempo. Isso pode reduzir significativamente o número de viagens de ida e volta para o LLM e acelerar o processo de pedido.
Próximas etapas
Agora que você entende como funciona a chamada de função, pode continuar aprendendo a configurar vários aspectos da chamada de função que correspondem melhor aos seus cenários específicos, consultando o artigo de comportamento de escolha de função