Chamada de função com conclusão de bate-papo

O recurso mais poderoso da conclusão de chat é a capacidade de fazer com que o modelo chame funções. 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 Kernel semântico, 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 automática de função

Observação

A seção a seguir descreve como a chamada de função automática funciona em Kernel semântico. A chamada de função automática é o comportamento padrão em Kernel semântico, mas você também pode invocar manualmente funções se preferir. Para obter mais informações sobre invocação de função manual, consulte o artigo de invocação 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 ou mais chamadas de função.
4 Tratar a resposta Se a resposta for uma mensagem de chat, ela será retornada ao chamador. No entanto, se a resposta for uma chamada de função, o Kernel semântico 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-6 são repetidas até que o modelo retorne uma mensagem de chat ou o número máximo de iteração tenha sido atingido.

O diagrama a seguir ilustra o processo de chamada de função:

Chamada de função do Kernel Semântico

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:

  1. get_pizza_menu: Retorna uma lista de pizzas disponíveis
  2. add_pizza_to_cart: Adiciona uma pizza ao carrinho do usuário
  3. remove_pizza_from_cart: Remove uma pizza do carrinho do usuário
  4. get_pizza_from_cart: Retorna os detalhes específicos de uma pizza no carrinho do usuário
  5. get_cart: Retorna o carrinho atual do usuário
  6. checkout: Finaliza o 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();

Observação

Somente as funções com o KernelFunction atributo serão serializadas e enviadas para o modelo. Isso permite que você tenha funções auxiliares que não são expostas ao modelo.

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

kernel = Kernel()

# 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")

Observação

Somente as funções com o decorador kernel_function serão serializadas e enviadas ao modelo. Isso permite que você tenha funções auxiliares que não são expostas ao modelo.

Nomes de parâmetro reservados para chamada de função automática

Ao usar a chamada de função automática em KernelFunctions, determinados nomes de parâmetro são reservados e recebem tratamento especial. Esses nomes reservados permitem que você acesse automaticamente os objetos de chave necessários para a execução da função.

Nomes Reservados

Os seguintes nomes de parâmetro são reservados:

  • kernel
  • service
  • execution_settings
  • arguments

Como eles funcionam

Durante a invocação de função, o método gather_function_parameters inspeciona cada parâmetro. Se o nome do parâmetro corresponder a um dos nomes reservados, ele será preenchido com objetos específicos:

  • kernel: Injetado com o objeto de kernel.
  • service: preenchido com o serviço de IA selecionado com base nos argumentos fornecidos.
  • execution_settings: contém configurações pertinentes à execução da função.
  • arguments: recebe todo o conjunto de argumentos kernel passados durante a invocação.

Esse design garante que esses parâmetros sejam gerenciados automaticamente, eliminando a necessidade de extração ou atribuição manual.

Exemplo de Uso

Considere o seguinte exemplo:

class SimplePlugin:
    @kernel_function(name="GetWeather", description="Get the weather for a location.")
    async def get_the_weather(self, location: str, arguments: KernelArguments) -> str:
        # The 'arguments' parameter is reserved and automatically populated with KernelArguments.
        return f"Received user input: {location}, the weather is nice!"

Nomes de parâmetros reservados personalizados para chamada de função automática

Você também pode personalizar esse comportamento. Para fazer isso, você precisa anotar o parâmetro que deseja excluir da definição de chamada de função, desta forma:

class SimplePlugin:
    @kernel_function(name="GetWeather", description="Get the weather for a location.")
    async def get_the_weather(self, location: str, special_arg: Annotated[str, {"include_in_function_choices": False}]) -> str:
        # The 'special_arg' parameter is reserved and you need to ensure it either has a default value or gets passed.

Ao chamar essa função, certifique-se de passar o special_arg parâmetro, caso contrário, gerará um erro.

response = await kernel.invoke_async(
    plugin_name=...,
    function_name="GetWeather",
    location="Seattle",
    special_arg="This is a special argument"
)

Ou adicione-o ao objeto KernelArguments para usá-lo com a chamada automática de função em um agente desta forma:

arguments = KernelArguments(special_arg="This is a special argument")
response = await agent.get_response(
    messages="what's the weather in Seattle?"
    arguments=arguments)

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();

Observação

Somente as funções com a DefineKernelFunction anotação serão serializadas e enviadas para o modelo. Isso permite que você tenha funções auxiliares que não são expostas ao modelo.

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:

  1. Verbosidade do esquema de função – Serializar funções para uso pelo modelo não sai de graça. 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 ou paymentId) é 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 tipos de retorno excessivamente detalhados podem afetar a qualidade da conclusão do chat.

  2. 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 o toppings 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, , floate tipos complexos) sempre que possível.

  3. 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. Posteriormente, na Etapa 3, o modelo usará essas informações para fornecer informações mínimas, conforme necessário, para chamar a função.

    Dica

    Marque os parâmetros como obrigatórios apenas se eles forem realmente necessários. Isso ajuda o modelo a chamar funções com mais rapidez e precisão.

  4. 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 get_pizza_from_cart, a descrição informa ao usuário que use essa função em vez de contar com 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.

  5. Nome do plug-in – Como você pode ver nas funções serializadas, cada função tem uma name propriedade. O Kernel semântico 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ópria search 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.

    Observação

    Por padrão, o delimitador para o nome da função é -. Embora isso funcione para a maioria dos modelos, alguns deles podem ter requisitos diferentes, como Gemini. Isso é cuidado pelo kernel automaticamente, no entanto, você pode ver nomes de função ligeiramente diferentes nas funções serializadas.

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)

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.

chat_completion = kernel.get_service(type=ChatCompletionClientBase)

execution_settings = AzureChatPromptExecutionSettings()
execution_settings.function_choice_behavior = FunctionChoiceBehavior.Auto()

response = await chat_completion.get_chat_message_content(
    chat_history=history,
    settings=execution_settings,
    kernel=kernel,
)

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.

ChatCompletionService chatCompletion = kernel.getService(I)ChatCompletionService.class);

InvocationContext invocationContext = InvocationContext.builder()
    .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(false));

List<ChatResponse> responses = chatCompletion.getChatMessageContentsAsync(
    chatHistory,
    kernel,
    invocationContext).block();

Importante

O kernel deve ser passado ao serviço para poder usar chamadas de função. Isso ocorre porque os plug-ins são registrados com o kernel e o serviço precisa saber quais plug-ins estão disponíveis.

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 vai querer chamar a função add_pizza_to_cart, mas, como especificamos o tamanho e os ingredientes como parâmetros obrigatórios, o modelo solicitará essas informações ao usuário:

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, Kernel semântico interromperá a chamada automática de função e retornará o controle ao usuário. 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_content(
    chat_history=history,
    settings=execution_settings,
    kernel=kernel,
)
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 intenções e preencher slots. 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 Kernel semântico 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 Kernel semântico pode empacotar as entradas nos tipos apropriados e passá-las para a add_pizza_to_cart função no OrderPizzaPlugin. Neste exemplo, os argumentos são originalmente fornecidos como uma string JSON, mas são desserializados pelo Kernel semântico em um enum PizzaSize 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 Kernel semântico pode desserializar automaticamente esses objetos nos tipos corretos para suas funções.

Depois de preparar os dados de entrada, o Kernel semântico também adicionará a chamada de função ao histórico da conversa:

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 chamadas de função de forma diferente de C# e Python quando o comportamento de chamada automática de ferramenta é falso. 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 Kernel semântico 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 decida repetir ou gerar 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 sobre 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 do chat com o papel de ferramenta, que se parece com a seguinte:

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 desativada no comportamento de chamada da ferramenta, um aplicativo Java deverá invocar as chamadas de função e adicionar o resultado da função como uma mensagem AuthorRole.TOOL ao histórico da conversa.

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 em suas respostas. 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 a chamada de função funciona, você pode continuar a aprender a configurar vários aspectos da chamada de função que correspondem melhor aos seus cenários específicos indo para a próxima etapa: