Chamada de função com conclusão de chat
O recurso mais poderoso da conclusão do 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, tornando possível automatizar processos de negócios, criar trechos de código e muito mais.
Com o Semantic Kernel, simplificamos o processo de usar a chamada de função descrevendo automaticamente suas funções e seus parâmetros para o modelo e, em seguida, manipulando 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 para um modelo com a chamada de função habilitada, o Semantic Kernel executa as seguintes etapas:
Passo | Description | |
---|---|---|
5 | 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 bate-papo 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 | Manipular a resposta | Se a resposta for uma mensagem de chat, ela será retornada ao desenvolvedor para imprimir a resposta na tela. Se a resposta for uma chamada de função, no entanto, o Kernel Semântico extrai o nome da função e seus parâmetros. |
5 | Invoque a função | O nome da função extraída e os parâmetros 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 para o modelo como parte do histórico de bate-papo. Os passos 2 a 6 são então repetidos 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 plugin que permite que um usuário peça uma pizza. O plugin tem as seguintes funções:
get_pizza_menu
: Devolve 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
: Devolve os detalhes específicos de uma pizza no carrinho do utilizadorget_cart
: Devolve o carrinho atual do utilizadorcheckout
: 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ê 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 plugin 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ê 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 plugin 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ê 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) Serializando as funções
Quando você cria um kernel com o OrderPizzaPlugin
, o kernel serializará 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 teriam esta aparência:
[
{
"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 observar aqui que podem afetar o desempenho e a qualidade da conclusão do bate-papo:
Verbosidade do esquema de função – Serializar funções para o modelo usar não vem de graça. Quanto mais detalhado o esquema, mais tokens o modelo tem que processar, o que pode retardar o tempo de resposta e aumentar os custos.
Gorjeta
Mantenha as suas funções o mais simples possível. No exemplo acima, você notará 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; Tudo o que o modelo não precisa saber (como o
cartId
ORpaymentId
) é mantido escondido. Em vez disso, estas informações são fornecidas por serviços internos.Nota
A única coisa com a qual você não precisa se preocupar é com a complexidade dos tipos de retorno. Você notará 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 tipos de retorno excessivamente detalhados podem afetar a qualidade da conclusão do bate-papo.
Tipos de parâmetros – Com o esquema, você pode especificar o tipo de cada parâmetro. Isso é importante para que o modelo compreenda a entrada esperada. No exemplo acima, o
size
parâmetro é um enum, e otoppings
parâmetro é uma matriz de enums. Isso ajuda o modelo a gerar respostas mais precisas.Gorjeta
Evite, sempre que possível, usar
string
como 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 enums ou outros tipos (por exemplo,int
,float
, e tipos complexos) sempre que possível.Parâmetros necessá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 tarde, na etapa 3, o modelo usará essas informações para fornecer informações tão mínimas quanto necessário para chamar a função.
Gorjeta
Marque os parâmetros apenas conforme necessário se eles forem realmente necessários. Isso ajuda as funções de chamada de modelo de forma mais rápida e precisa.
Descrições de funções – As descrições de funções 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, uma vez 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 orientações.
Por exemplo, na
get_pizza_from_cart
função, a descrição diz ao usuário para usar essa função em vez de confiar em mensagens anteriores. Isso é importante porque o carrinho pode ter mudado desde a última mensagem.Gorjeta
Antes de adicionar uma descrição, pergunte-se se o modelo precisa dessas informações para gerar uma resposta. Se não, considere deixá-lo de fora para reduzir a verbosidade. Você sempre pode adicionar descrições mais tarde se o modelo estiver lutando para usar a função corretamente.
Nome do plugin – Como você pode ver nas funções serializadas, cada função tem uma
name
propriedade. O Kernel Semântico usa o nome do plugin para namespace das funções. Isso é importante porque permite que você tenha vários plugins com funções do mesmo nome. Por exemplo, você pode ter plugins para vários serviços de pesquisa, cada um com sua própriasearch
função. Ao nomear as funções, você pode evitar conflitos e tornar mais fácil para o modelo entender qual função chamar.Sabendo disso, você deve escolher um nome de plugin que seja exclusivo e descritivo. No exemplo acima, o nome do plugin é
OrderPizza
. Isso deixa claro que as funções estão relacionadas ao pedido de pizza.Gorjeta
Ao escolher um nome de plugin, recomendamos remover palavras supérfluas como "plugin" ou "serviço". Isso ajuda a reduzir a verbosidade e torna o nome do plugin mais fácil de entender para o modelo.
2) Enviar as mensagens e funções para o modelo
Uma vez 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 compreenda o contexto da conversa e as funções disponíveis.
Neste cenário, podemos imaginar o usuário pedindo ao assistente para adicionar uma pizza ao seu 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();
Nota
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, consulte o artigo Comportamentos de escolha de função.
3) O modelo processa a entrada
Com o histórico de bate-papo e as funções serializadas, o modelo pode determinar a melhor maneira de responder. Neste caso, o modelo reconhece que o usuário quer 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 estas 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 quer que o usuário responda em seguida, um sinal de terminação será enviado para o Semantic Kernel 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 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 isto:
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "OrderPizzaPlugin-add_pizza_to_cart",
"arguments": "{\n\"size\": \"Medium\",\n\"toppings\": [\"Cheese\", \"Pepperoni\"]\n}"
}
}
]
Gorjeta
É bom lembrar que todo argumento necessário deve ser gerado 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 pedir um GUID.
Importante
Este passo é o que torna a chamada de função tão poderosa. Anteriormente, os desenvolvedores de aplicativos de IA tinham que criar processos separados para extrair funções de intenção e preenchimento de slots. Com a chamada de função, o modelo pode decidir quando chamar uma função e quais informações fornecer.
4) Manipule a resposta
Quando o Semantic Kernel recebe a resposta do modelo, ele verifica se a resposta é uma chamada de função. Se for, o Kernel Semântico extrai o nome da função e seus parâmetros. Neste 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 organizar 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 Kernel Semântico em um PizzaSize
enum e um List<PizzaToppings>
.
Nota
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 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"]})
)
]
)
)
O Kernel Semântico para Java lida com a chamada de função de forma diferente de C# e Python quando o comportamento de chamada da ferramenta de invocação automática é falso. Você não adiciona conteúdo de chamada de função ao histórico de bate-papo; em vez disso, o aplicativo é feito responsável por invocar as chamadas de função. Vá para a próxima seção, "Invoke the function", para obter um exemplo de manipulação de chamadas de função em Java quando a auto-invoke é false.
5) Invoque a função
Uma vez que o Kernel Semântico tenha os tipos corretos, ele pode finalmente invocar a add_pizza_to_cart
função. Como o plugin 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 terão êxito. Se a função falhar, o Semantic Kernel pode lidar com o erro e fornecer uma resposta padrão para o modelo. Isso permite que o modelo entenda o que deu errado e gere uma resposta para o usuário.
Gorjeta
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.
Nota
O Kernel Semântico invoca automaticamente funções por padrão. No entanto, se preferir gerir manualmente a invocação de funções, pode ativar o modo de invocação manual de funções. 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 for invocada, o resultado da função será enviado de volta ao modelo como parte do histórico de bate-papo. Isso permite que o modelo compreenda o contexto da conversa e gere uma resposta subsequente.
Nos bastidores, o Semantic Kernel adiciona uma nova mensagem ao histórico de bate-papo da função de ferramenta que se parece com isto:
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 invocação automática estiver desabilitada no comportamento de chamada de ferramenta, um aplicativo Java deverá invocar 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. Neste caso, a devolução inclui apenas os novos itens adicionados ao carrinho, não o carrinho inteiro.
Gorjeta
Seja o mais sucinto possível com as suas devoluções. Sempre que possível, retorne apenas as informações de que o modelo precisa ou resuma as informações usando outro prompt LLM antes de devolvê-las.
Repita os passos 2 a 6
Depois que o resultado é retornado ao modelo, o processo se repete. O modelo processa o histórico de bate-papo mais recente e gera uma resposta. Neste caso, o modelo pode perguntar ao usuário se ele deseja adicionar outra pizza ao carrinho ou se deseja fazer check-out.
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óximos passos
Agora que você entende como a chamada de função funciona, você pode continuar a aprender como configurar vários aspetos da chamada de função que melhor correspondem aos seus cenários específicos, consultando o artigo de comportamento de escolha de função