Вызов функции с завершением чата
Самая мощная функция завершения чата — возможность вызова функций из модели. Это позволяет создать чат-бот, который может взаимодействовать с существующим кодом, что позволяет автоматизировать бизнес-процессы, создавать фрагменты кода и многое другое.
С помощью семантического ядра мы упрощаем процесс вызова функции, автоматически описывая функции и их параметры в модели, а затем обрабатываем обратную и вперед связь между моделью и кодом.
Однако при использовании вызова функции хорошо понять, что на самом деле происходит за кулисами, чтобы оптимизировать код и сделать большую часть этой функции.
Как работает вызов функции
При выполнении запроса на модель с включенным вызовом функций семантический ядро выполняет следующие действия:
Этап | Описание: | |
---|---|---|
1 | Сериализация функций | Все доступные функции (и его входные параметры) в ядре сериализуются с помощью схемы JSON. |
2 | Отправка сообщений и функций в модель | Сериализованные функции (и текущая история чата) отправляются в модель в рамках входных данных. |
3 | Модель обрабатывает входные данные | Модель обрабатывает входные данные и создает ответ. Ответ может быть сообщением чата или вызовом функции |
4 | Обработка ответа | Если ответ является сообщением чата, он возвращается разработчику, чтобы распечатать ответ на экран. Однако если ответ является вызовом функции, семантический ядро извлекает имя функции и его параметры. |
5 | Вызов функции | Извлеченные имя и параметры функции используются для вызова функции в ядре. |
6 | Возврат результата функции | Результат функции затем отправляется обратно в модель в рамках журнала чата. Затем шаги 2-6 повторяются до тех пор, пока модель не отправляет сигнал завершения |
На следующей схеме показан процесс вызова функции:
В следующем разделе показано, как работает вызов функций на практике.
Пример: заказ пиццы
Предположим, у вас есть подключаемый модуль, позволяющий пользователю заказать пиццу. Подключаемый модуль имеет следующие функции:
get_pizza_menu
: возвращает список доступных пиццadd_pizza_to_cart
: добавляет пиццу в корзину пользователяremove_pizza_from_cart
: удаляет пиццу из корзины пользователяget_pizza_from_cart
: возвращает конкретные сведения о пицце в корзине пользователяget_cart
: возвращает текущую корзину пользователяcheckout
: извлекает корзину пользователя
В C#подключаемый модуль может выглядеть следующим образом:
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);
}
}
Затем вы добавите этот подключаемый модуль в ядро следующим образом:
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();
В Python подключаемый модуль может выглядеть следующим образом:
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)
Затем вы добавите этот подключаемый модуль в ядро следующим образом:
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")
В Java подключаемый модуль может выглядеть следующим образом:
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));
}
}
Затем вы добавите этот подключаемый модуль в ядро следующим образом:
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) Сериализация функций
При создании ядра с OrderPizzaPlugin
помощью ядра ядро автоматически сериализует функции и их параметры. Это необходимо, чтобы модель понимала функции и их входные данные.
Для приведенного выше подключаемого модуля сериализованные функции будут выглядеть следующим образом:
[
{
"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": []
}
}
}
]
Здесь есть несколько вещей, которые могут повлиять как на производительность, так и качество завершения чата:
Подробные сведения о схеме функций — сериализация функций для используемой модели не предоставляется бесплатно. Чем более подробно схема, тем больше маркеров модель должна обрабатывать, что может замедлить время отклика и увеличить затраты.
Совет
Как можно проще сохранить свои функции. В приведенном выше примере вы заметите, что не все функции имеют описания, в которых имя функции является самообязательным. Это намеренно для уменьшения количества маркеров. Параметры также сохраняются простыми; все, что модель не должна знать (например
cartId
, илиpaymentId
) хранится скрыто. Вместо этого эти сведения предоставляются внутренними службами.Примечание.
Единственное, что вам не нужно беспокоиться, заключается в сложности типов возвращаемых значений. Вы заметите, что возвращаемые типы не сериализуются в схеме. Это связано с тем, что модель не должна знать тип возврата для создания ответа. Однако на шаге 6 мы увидим, как чрезмерно подробные типы возвращаемых данных могут повлиять на качество завершения чата.
Типы параметров — с помощью схемы можно указать тип каждого параметра. Это важно для модели, чтобы понять ожидаемые входные данные. В приведенном выше примере
size
параметр является перечислением, иtoppings
параметр является массивом перечислений. Это помогает модели создавать более точные ответы.Совет
Избегайте, если это возможно, использование
string
в качестве типа параметров. Модель не может определить тип строки, что может привести к неоднозначным ответам. Вместо этого используйте перечисления или другие типы (например,int
,float
и сложные типы), где это возможно.Обязательные параметры — можно также указать необходимые параметры. Это важно для модели, чтобы понять, какие параметры фактически необходимы для работы функции. Далее на шаге 3 модель будет использовать эту информацию для предоставления минимальной информации, как необходимо для вызова функции.
Совет
Пометьте только необходимые параметры, если они действительно необходимы. Это помогает функции вызова модели быстрее и точно.
Описания функций — описания функций являются необязательными , но могут помочь модели создавать более точные ответы. В частности, описания могут указывать модели, чего ожидать от ответа, так как тип возвращаемого значения не сериализуется в схеме. Если модель неправильно использует функции, можно также добавить описания для предоставления примеров и рекомендаций.
Например, в
get_pizza_from_cart
функции описание сообщает пользователю использовать эту функцию, а не полагаться на предыдущие сообщения. Это важно, так как корзина может измениться с момента последнего сообщения.Совет
Прежде чем добавить описание, попросите себя, нужна ли эта модель для создания ответа. Если нет, рекомендуется отказаться от него, чтобы уменьшить детализацию. Вы всегда можете добавить описания позже, если модель пытается правильно использовать функцию.
Имя подключаемого модуля— как можно увидеть в сериализованных функциях, каждая функция имеет
name
свойство. Семантический ядро использует имя подключаемого модуля для пространства имен функций. Это важно, так как это позволяет иметь несколько подключаемых модулей с функциями одного и того же имени. Например, у вас могут быть подключаемые модули для нескольких служб поиска, каждая из которых имеет собственнуюsearch
функцию. При использовании имен функций можно избежать конфликтов и упростить работу модели, чтобы понять, какую функцию следует вызывать.Зная это, следует выбрать имя подключаемого модуля, уникальное и описательное. В приведенном выше примере имя подключаемого модуля —
OrderPizza
. Это дает понять, что функции связаны с заказом пиццы.Совет
При выборе имени подключаемого модуля рекомендуется удалить лишние слова, такие как "подключаемый модуль" или "служба". Это помогает уменьшить детализацию и упрощает понимание имени подключаемого модуля для модели.
2) Отправка сообщений и функций в модель
После сериализации функций они отправляются в модель вместе с текущим журналом чата. Это позволяет модели понять контекст беседы и доступных функций.
В этом сценарии можно представить, что пользователь просит помощника добавить пиццу в корзину:
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!");
Затем мы можем отправить этот журнал чата и сериализованные функции в модель. Эта информация будет использоваться для определения оптимального способа реагирования.
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();
Примечание.
В этом примере используется FunctionChoiceBehavior.Auto()
поведение, одно из немногих доступных. Дополнительные сведения о поведении других функций см. в статье о поведении выбора функции.
3) Модель обрабатывает входные данные
При использовании журнала чата и сериализованных функций модель может определить оптимальный способ реагирования. В этом случае модель распознает, что пользователь хочет заказать пиццу. Модель, скорее всего , захотите вызвать add_pizza_to_cart
функцию, но так как мы указали размер и начинки в качестве необходимых параметров, модель попросит пользователя об этом:
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?"
Так как модель хочет, чтобы пользователь ответил следующим образом, сигнал завершения будет отправлен в семантический ядро, чтобы остановить автоматический вызов функции до тех пор, пока пользователь не ответит.
На этом этапе пользователь может ответить с размером и начинками пиццы, которую они хотят заказать:
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();
Теперь, когда у модели есть необходимые сведения, она теперь может вызвать add_pizza_to_cart
функцию с входными данными пользователя. За кулисами он добавляет новое сообщение в журнал чата, который выглядит следующим образом:
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "OrderPizzaPlugin-add_pizza_to_cart",
"arguments": "{\n\"size\": \"Medium\",\n\"toppings\": [\"Cheese\", \"Pepperoni\"]\n}"
}
}
]
Совет
Хорошо помнить, что каждый необходимый аргумент должен быть создан моделью. Это означает, что токены расходов для создания ответа. Избегайте аргументов, требующих большого количества маркеров (например, GUID). Например, обратите внимание, что мы используем int
для объекта pizzaId
. Запрос модели отправить одно-двухзначное число гораздо проще, чем запрашивать GUID.
Внимание
Этот шаг — это то, что делает функцию вызовом столь мощной. Ранее разработчики приложений ИИ должны были создавать отдельные процессы для извлечения функций намерения и заполнения слотов. При вызове функции модель может решить , когда следует вызывать функцию и какие сведения необходимо предоставить.
4) Обработка ответа
Когда семантический ядро получает ответ от модели, он проверяет, является ли ответ вызовом функции. Если это так, семантический ядро извлекает имя функции и его параметры. В этом случае имя функции равно OrderPizzaPlugin-add_pizza_to_cart
, а аргументы — размер и начинки пиццы.
С помощью этой информации семантический ядро может маршалировать входные данные в соответствующие типы и передавать их в функцию add_pizza_to_cart
в этой OrderPizzaPlugin
функции. В этом примере аргументы создаются как строка JSON, но десериализированы семантической ядром в PizzaSize
перечисление и a List<PizzaToppings>
.
Примечание.
Маршалинг входных данных в правильные типы является одним из ключевых преимуществ использования семантического ядра. Все, что происходит из модели в виде объекта JSON, но семантический ядро может автоматически десериализировать эти объекты в правильные типы для ваших функций.
После маршаллинга входных данных семантический ядро также может добавить вызов функции в журнал чата:
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"]})
)
]
)
)
Семантический ядро для Java обрабатывает вызовы функций, отличных от C# и Python, если поведение вызова средства автоматического вызова равно false. Вы не добавляете содержимое вызова функции в журнал чата; вместо этого приложение отвечает за вызовы функций. Перейдите к следующему разделу "Вызов функции", например, обработка вызовов функций в Java при автоматическом вызове имеет значение false.
5) Вызов функции
После того как семантический ядро имеет правильные типы, он может, наконец, вызвать функцию add_pizza_to_cart
. Так как подключаемый модуль использует внедрение зависимостей, функция может взаимодействовать с внешними службами, как pizzaService
и userContext
добавлять пиццу в корзину пользователя.
Однако не все функции будут успешными. Если функция завершается ошибкой, семантический ядро может обрабатывать ошибку и предоставлять ответ по умолчанию модели. Это позволяет модели понять, что пошло не так и создать ответ пользователю.
Совет
Чтобы обеспечить самокорректную модель, важно предоставить сообщения об ошибках, которые четко сообщают о том, что пошло не так, и как исправить его. Это может помочь модели повторить вызов функции с правильными сведениями.
Примечание.
Семантический ядро автоматически вызывает функции по умолчанию. Однако если вы предпочитаете управлять вызовом функции вручную, можно включить режим вызова функции вручную. Дополнительные сведения о том, как это сделать, см. в статье о вызове функции.
6) Возврат результата функции
После вызова функции результат функции отправляется обратно в модель в рамках журнала чата. Это позволяет модели понять контекст беседы и создать последующий ответ.
За кулисами семантический ядро добавляет новое сообщение в журнал чата из роли средства, которая выглядит следующим образом:
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\"] } ] }"
)
]
)
)
Если автоматическое вызов отключено в поведении вызова средства, приложение Java должно вызвать вызовы функции и добавить результат функции в AuthorRole.TOOL
журнал чата.
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()));
});
Обратите внимание, что результатом является строка JSON, которую модель затем необходимо обработать. Как и раньше, модели потребуется потратить маркеры, потребляющие эти сведения. Поэтому важно сохранить типы возвращаемых данных как можно проще. В этом случае возврат включает только новые элементы, добавленные в корзину, а не всю корзину.
Совет
Будьте насколько это возможно с вашим возвращением. Когда это возможно, возвращаются только сведения, необходимые модели, или суммируют сведения с помощью другого запроса LLM перед возвратом.
Повторите шаги 2-6
После возвращения результата в модель процесс повторяется. Модель обрабатывает последнюю историю чата и создает ответ. В этом случае модель может попросить пользователя добавить другую пиццу в корзину или проверить ее.
Параллельные вызовы функций
В приведенном выше примере мы показали, как LLM может вызывать одну функцию. Часто это может быть медленно, если необходимо вызвать несколько функций в последовательности. Для ускорения процесса несколько LLM поддерживают параллельные вызовы функций. Это позволяет LLM одновременно вызывать несколько функций, ускоряя процесс.
Например, если пользователь хочет заказать несколько пицц, LLM может вызывать add_pizza_to_cart
функцию для каждой пиццы одновременно. Это может значительно сократить количество цикловых путей в LLM и ускорить процесс упорядочивания.
Следующие шаги
Теперь, когда вы узнаете, как работает вызов функции, вы можете узнать, как настроить различные аспекты вызова функций, которые лучше соответствуют конкретным сценариям, ссылаясь на статью о поведении выбора функции.