Compartir a través de


Llamada a funciones con finalización del chat

La característica más eficaz de finalización del chat es la capacidad de llamar a funciones desde el modelo. Esto le permite crear un bot de chat que pueda interactuar con el código existente, lo que permite automatizar procesos empresariales, crear fragmentos de código, etc.

Con el kernel semántico, simplificamos el proceso de uso de llamadas de función mediante la descripción automática de las funciones y sus parámetros en el modelo y, a continuación, se controla la comunicación de ida y vuelta entre el modelo y el código.

Sin embargo, cuando se usa la llamada a funciones, es bueno comprender lo que sucede realmente en segundo plano para que pueda optimizar el código y aprovechar la mayor parte de esta característica.

Funcionamiento de las llamadas a funciones

Al realizar una solicitud a un modelo con una llamada de función habilitada, el kernel semántico realiza los pasos siguientes:

Paso Description
1 Serializar funciones Todas las funciones disponibles (y sus parámetros de entrada) en el kernel se serializan mediante el esquema JSON.
2 Envío de mensajes y funciones al modelo Las funciones serializadas (y el historial de chat actual) se envían al modelo como parte de la entrada.
3 El modelo procesa la entrada El modelo procesa la entrada y genera una respuesta. La respuesta puede ser un mensaje de chat o una llamada de función.
4 Control de la respuesta Si la respuesta es un mensaje de chat, se devuelve al desarrollador para imprimir la respuesta en la pantalla. Sin embargo, si la respuesta es una llamada de función, el kernel semántico extrae el nombre de la función y sus parámetros.
5 Invocación de la función El nombre de la función extraída y los parámetros se usan para invocar la función en el kernel.
6 Devolver el resultado de la función El resultado de la función se devuelve al modelo como parte del historial de chat. Los pasos 2-6 se repiten hasta que el modelo envía una señal de terminación

En el diagrama siguiente se muestra el proceso de llamada a funciones:

Llamada a la función kernel semántica

En la sección siguiente se usará un ejemplo concreto para ilustrar cómo funciona la llamada a funciones en la práctica.

Ejemplo: Pedir una pizza

Supongamos que tiene un complemento que permite al usuario pedir una pizza. El complemento tiene las siguientes funciones:

  1. get_pizza_menu: devuelve una lista de pizzas disponibles.
  2. add_pizza_to_cart: agrega una pizza al carro del usuario.
  3. remove_pizza_from_cart: quita una pizza del carro del usuario.
  4. get_pizza_from_cart: devuelve los detalles específicos de una pizza en el carro del usuario.
  5. get_cart: devuelve el carro actual del usuario.
  6. checkout: desprotete el carro del usuario

En C#, el complemento podría tener este aspecto:

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

A continuación, agregaría este complemento al kernel de la siguiente manera:

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

En Python, el complemento podría tener este aspecto:

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(name="get_pizza_menu")
    async def get_pizza_menu(self):
        return await self.pizza_service.get_menu()

    @kernel_function(
        name="add_pizza_to_cart",
        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(
        name="remove_pizza_from_cart",
        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(
        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."
    )
    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(
        name="get_cart",
        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(
        name="checkout",
        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)

A continuación, agregaría este complemento al kernel de la siguiente manera:

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

1) Serialización de las funciones

Al crear un kernel con OrderPizzaPlugin, el kernel serializará automáticamente las funciones y sus parámetros. Esto es necesario para que el modelo pueda comprender las funciones y sus entradas.

Para el complemento anterior, las funciones serializadas tendría el siguiente aspecto:

[
  {
    "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": []
      }
    }
  }
]

Hay algunas cosas que hay que tener en cuenta aquí que pueden afectar tanto al rendimiento como a la calidad de la finalización del chat:

  1. Detalle del esquema de función: la serialización de funciones para el modelo que se va a usar no es gratuita. Cuanto más detallado sea el esquema, más tokens tiene que procesar el modelo, lo que puede ralentizar el tiempo de respuesta y aumentar los costos.

    Sugerencia

    Mantenga las funciones lo más sencillas posible. En el ejemplo anterior, observará que no todas las funciones tienen descripciones en las que el nombre de la función se explica automáticamente. Esto es intencional para reducir el número de tokens. Los parámetros también se mantienen sencillos; todo lo que el modelo no debe tener que saber (como o cartIdpaymentId) se mantienen ocultos. En su lugar, los servicios internos proporcionan esta información.

    Nota:

    Lo único que no necesita preocuparse es la complejidad de los tipos devueltos. Observará que los tipos devueltos no se serializan en el esquema. Esto se debe a que el modelo no necesita conocer el tipo de valor devuelto para generar una respuesta. Sin embargo, en el paso 6, veremos cómo los tipos de valor devuelto demasiado detallados pueden afectar a la calidad de la finalización del chat.

  2. Tipos de parámetros : con el esquema, puede especificar el tipo de cada parámetro. Esto es importante para que el modelo comprenda la entrada esperada. En el ejemplo anterior, el size parámetro es una enumeración y el toppings parámetro es una matriz de enumeraciones. Esto ayuda al modelo a generar respuestas más precisas.

    Sugerencia

    Evite, siempre que sea posible, el uso string como tipo de parámetro. El modelo no puede deducir el tipo de cadena, lo que puede provocar respuestas ambiguas. En su lugar, use enumeraciones u otros tipos (por ejemplo, int, floaty tipos complejos) siempre que sea posible.

  3. Parámetros obligatorios : también puede especificar qué parámetros son necesarios. Esto es importante para que el modelo comprenda qué parámetros son realmente necesarios para que la función funcione. Más adelante en el paso 3, el modelo usará esta información para proporcionar la información mínima según sea necesario para llamar a la función.

    Sugerencia

    Solo marque los parámetros según sea necesario si realmente son necesarios. Esto ayuda a las funciones de llamada de modelo más rápidas y precisas.

  4. Descripciones de funciones: las descripciones de funciones son opcionales, pero pueden ayudar al modelo a generar respuestas más precisas. En concreto, las descripciones pueden indicar al modelo qué esperar de la respuesta, ya que el tipo de valor devuelto no se serializa en el esquema. Si el modelo usa funciones de forma incorrecta, también puede agregar descripciones para proporcionar ejemplos e instrucciones.

    Por ejemplo, en la get_pizza_from_cart función , la descripción indica al usuario que use esta función en lugar de confiar en mensajes anteriores. Esto es importante porque el carro puede haber cambiado desde el último mensaje.

    Sugerencia

    Antes de agregar una descripción, pregúntese si el modelo necesita esta información para generar una respuesta. Si no es así, considere la posibilidad de dejarla fuera para reducir el nivel de detalle. Siempre puede agregar descripciones más adelante si el modelo tiene dificultades para usar la función correctamente.

  5. Nombre del complemento: como puede ver en las funciones serializadas, cada función tiene una name propiedad . El kernel semántico usa el nombre del complemento para el espacio de nombres de las funciones. Esto es importante porque permite tener varios complementos con funciones del mismo nombre. Por ejemplo, puede tener complementos para varios servicios de búsqueda, cada uno con su propia search función. Mediante el espacio de nombres de las funciones, puede evitar conflictos y facilitar la comprensión del modelo de la función a la que se debe llamar.

    Sabiendo esto, debe elegir un nombre de complemento que sea único y descriptivo. En el ejemplo anterior, el nombre del complemento es OrderPizza. Esto hace evidente que las funciones están relacionadas con el pedido de pizza.

    Sugerencia

    Al elegir un nombre de complemento, se recomienda quitar palabras superfluas como "plugin" o "service". Esto ayuda a reducir el nivel de detalle y facilita la comprensión del nombre del complemento para el modelo.

2) Envío de mensajes y funciones al modelo

Una vez serializadas las funciones, se envían al modelo junto con el historial de chat actual. Esto permite al modelo comprender el contexto de la conversación y las funciones disponibles.

En este escenario, podemos imaginar al usuario que pide al asistente que agregue una pizza a su carro:

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

A continuación, podemos enviar este historial de chat y las funciones serializadas al modelo. El modelo usará esta información para determinar la mejor manera de responder.

IChatCompletionService chatCompletion = kernel.GetRequiredService<IChatCompletionService>();

OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new() 
{
    ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
};

ChatResponse response = await chatCompletion.GetChatMessageContentAsync(
    chatHistory,
    executionSettings: openAIPromptExecutionSettings,
    kernel: kernel)
chat_completion = kernel.get_service(type=ChatCompletionClientBase)

execution_settings = AzureChatPromptExecutionSettings(tool_choice="auto")
execution_settings.function_call_behavior = FunctionCallBehavior.EnableFunctions(auto_invoke=True, filters={})

response = (await chat_completion.get_chat_message_contents(
      chat_history=history,
      settings=execution_settings,
      kernel=kernel,
      arguments=KernelArguments(),
  ))[0]

3) El modelo procesa la entrada

Con el historial de chat y las funciones serializadas, el modelo puede determinar la mejor manera de responder. En este caso, el modelo reconoce que el usuario quiere pedir una pizza. Es probable que el modelo quiera llamar a la add_pizza_to_cart función, pero dado que especificamos el tamaño y los ingredientes como parámetros necesarios, el modelo pedirá al usuario esta información:

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?"

Dado que el modelo quiere que el usuario responda a continuación, se enviará una señal de terminación al kernel semántico para detener la llamada automática a funciones hasta que el usuario responda.

En este momento, el usuario puede responder con el tamaño y los ingredientes de la pizza que desea 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]

Ahora que el modelo tiene la información necesaria, ahora puede llamar a la función con la add_pizza_to_cart entrada del usuario. En segundo plano, agrega un nuevo mensaje al historial de chat que tiene este aspecto:

"tool_calls": [
    {
        "id": "call_abc123",
        "type": "function",
        "function": {
            "name": "OrderPizzaPlugin-add_pizza_to_cart",
            "arguments": "{\n\"size\": \"Medium\",\n\"toppings\": [\"Cheese\", \"Pepperoni\"]\n}"
        }
    }
]

Sugerencia

Es bueno recordar que el modelo debe generar todos los argumentos que necesite. Esto significa que los tokens de gasto generan la respuesta. Evite argumentos que requieran muchos tokens (como un GUID). Por ejemplo, observe que usamos un int para .pizzaId Pedir al modelo que envíe un número de uno a dos dígitos es mucho más fácil que pedir un GUID.

Importante

Este paso es lo que hace que la función llame a tan eficaz. Anteriormente, los desarrolladores de aplicaciones de IA tenían que crear procesos independientes para extraer funciones de relleno de intención y ranura. Con las llamadas a funciones, el modelo puede decidir cuándo llamar a una función y qué información proporcionar.

4) Controlar la respuesta

Cuando el kernel semántico recibe la respuesta del modelo, comprueba si la respuesta es una llamada de función. Si es así, el kernel semántico extrae el nombre de la función y sus parámetros. En este caso, el nombre de la función es OrderPizzaPlugin-add_pizza_to_carty los argumentos son el tamaño y los ingredientes de la pizza.

Con esta información, el kernel semántico puede serializar las entradas en los tipos adecuados y pasarlas a la add_pizza_to_cart función de OrderPizzaPlugin. En este ejemplo, los argumentos se originan como una cadena JSON, pero se deserializan mediante kernel semántico en una PizzaSize enumeración y un List<PizzaToppings>.

Nota:

Serializar las entradas en los tipos correctos es una de las principales ventajas de usar kernel semántico. Todo el contenido del modelo viene como un objeto JSON, pero el kernel semántico puede deserializar automáticamente estos objetos en los tipos correctos para las funciones.

Después de serializar las entradas, el kernel semántico también puede agregar la llamada de función al historial 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"]})
            )
        ]
    )
)

5) Invocar la función

Una vez que el kernel semántico tiene los tipos correctos, finalmente puede invocar la add_pizza_to_cart función . Dado que el complemento usa la inserción de dependencias, la función puede interactuar con servicios externos como pizzaService y userContext agregar la pizza al carro del usuario.

Sin embargo, no todas las funciones se realizarán correctamente. Si se produce un error en la función, el kernel semántico puede controlar el error y proporcionar una respuesta predeterminada al modelo. Esto permite al modelo comprender lo que salió mal y generar una respuesta al usuario.

Sugerencia

Para asegurarse de que un modelo puede corregirse automáticamente, es importante proporcionar mensajes de error que comuniquen claramente lo que salió mal y cómo corregirlo. Esto puede ayudar al modelo a reintentar la llamada de función con la información correcta.

6) Devolver el resultado de la función

Una vez invocada la función, el resultado de la función se devuelve al modelo como parte del historial de chat. Esto permite al modelo comprender el contexto de la conversación y generar una respuesta posterior.

En segundo plano, el kernel semántico agrega un nuevo mensaje al historial de chat del rol de herramienta que tiene este aspecto:

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\"] } ] }"
            )
        ]
    )
)

Observe que el resultado es una cadena JSON que el modelo necesita procesar. Como antes, el modelo tendrá que gastar tokens que consumen esta información. Este es el motivo por el que es importante mantener los tipos de valor devuelto lo más sencillos posible. En este caso, la devolución solo incluye los nuevos artículos agregados al carro, no todo el carro.

Sugerencia

Sea lo más concisa posible con sus devoluciones. Siempre que sea posible, solo devuelva la información que necesita el modelo o resuma la información con otro símbolo del sistema LLM antes de devolverla.

Repita los pasos del 2 al 6

Una vez devuelto el resultado al modelo, el proceso se repite. El modelo procesa el historial de chat más reciente y genera una respuesta. En este caso, el modelo podría preguntar al usuario si desea agregar otra pizza al carro o si desea desproteger.

Llamadas a funciones paralelas

En el ejemplo anterior, se mostró cómo un LLM puede llamar a una sola función. A menudo, esto puede ser lento si necesita llamar a varias funciones en secuencia. Para acelerar el proceso, varias LLM admiten llamadas de función paralelas. Esto permite que LLM llame a varias funciones a la vez, lo que acelera el proceso.

Por ejemplo, si un usuario quiere pedir varias pizzas, LLM puede llamar a la add_pizza_to_cart función para cada pizza al mismo tiempo. Esto puede reducir significativamente el número de recorridos de ida y vuelta al LLM y acelerar el proceso de ordenación.

Pasos siguientes

Ahora que comprende cómo funciona la llamada a funciones, ahora puede aprender a usar realmente las llamadas a funciones en kernel semántico haciendo referencia al artículo de planeación.