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:
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:
get_pizza_menu
: devuelve una lista de pizzas disponibles.add_pizza_to_cart
: agrega una pizza al carro del usuario.remove_pizza_from_cart
: quita una pizza del carro del usuario.get_pizza_from_cart
: devuelve los detalles específicos de una pizza en el carro del usuario.get_cart
: devuelve el carro actual del usuario.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
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)
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")
En Java, el complemento podría tener este aspecto:
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));
}
}
A continuación, agregaría este complemento al kernel de la siguiente manera:
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) 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:
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
cartId
paymentId
) 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.
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 eltoppings
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
,float
y tipos complejos) siempre que sea posible.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.
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.
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 propiasearch
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!")
ChatHistory chatHistory = new ChatHistory();
chatHistory.addUserMessage("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()
{
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:
En este ejemplo se usa el FunctionChoiceBehavior.Auto()
comportamiento, uno de los pocos disponibles. Para obtener más información sobre otros comportamientos de elección de función, consulte el artículo Comportamientos de elección de funciones.
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?"
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?"
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]
chatHistory.addUserMessage("I'd like a medium pizza with cheese and pepperoni, please.");
responses = chatCompletion.GetChatMessageContentAsync(
chatHistory,
kernel,
null).block();
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_cart
y 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"]})
)
]
)
)
El kernel semántico para Java controla la función que llama de forma diferente a C# y Python cuando el comportamiento de llamada a la herramienta de invocación automática es false. No agrega contenido de llamada de función al historial de chats; en su lugar, la aplicación se hace responsable de invocar las llamadas de función. Vaya a la sección siguiente, "Invocar la función", para obtener un ejemplo de control de llamadas de función en Java cuando la invocación automática es false.
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.
Nota:
El kernel semántico invoca automáticamente las funciones de forma predeterminada. Sin embargo, si prefiere administrar manualmente la invocación de funciones, puede habilitar el modo de invocación de función manual. Para obtener más información sobre cómo hacerlo, consulte el artículo de invocación de funciones.
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\"] } ] }"
)
]
)
)
Si la invocación automática está deshabilitada en el comportamiento de la llamada a la herramienta, una aplicación Java debe invocar las llamadas de función y agregar el resultado de la función como mensaje AuthorRole.TOOL
al historial de chat.
messages.stream()
.filter (it -> it instanceof OpenAIChatMessageContent)
.map(it -> ((OpenAIChatMessageContent<?>) it).getToolCall())
.flatMap(List::stream)
.forEach(toolCall -> {
String content;
try {
// getFunction will throw an exception if the function is not found
var fn = kernel.getFunction(toolCall.getPluginName(),
toolCall.getFunctionName());
FunctionResult<?> fnResult = fn
.invokeAsync(kernel, toolCall.getArguments(), null, null).block();
content = (String) fnResult.getResult();
} catch (IllegalArgumentException e) {
content = "Unable to find function. Please try again!";
}
chatHistory.addMessage(
AuthorRole.TOOL,
content,
StandardCharsets.UTF_8,
FunctionResultMetadata.build(toolCall.getId()));
});
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, puede continuar para aprender a configurar varios aspectos de las llamadas a funciones que mejor se correspondan con sus escenarios específicos haciendo referencia al artículo comportamiento de elección de función.