Поделиться через


Вызов функции с завершением чата

Самая мощная функция завершения чата — возможность вызова функций из модели. Это позволяет создать чат-бот, который может взаимодействовать с существующим кодом, что позволяет автоматизировать бизнес-процессы, создавать фрагменты кода и многое другое.

С помощью семантического ядра мы упрощаем процесс вызова функции, автоматически описывая функции и их параметры в модели, а затем обрабатываем обратную и вперед связь между моделью и кодом.

Однако при использовании вызова функции важно понять, что на самом деле происходит в фоне, чтобы оптимизировать код и использовать эту функцию максимально эффективно.

Как работает автоматическое вызов функции

Примечание.

В следующем разделе описывается, как выполняется автоматическое вызов функции в семантическом ядре. Автоматическое вызов функции — это поведение по умолчанию в семантическом ядре, но при желании можно также вручную вызывать функции. Дополнительные сведения о вызове функции вручную см. в статье о вызове функции.

При выполнении запроса на модель с включенным вызовом функций семантический ядро выполняет следующие действия:

# Этап Описание:
1 Сериализация функций Все доступные функции (и его входные параметры) в ядре сериализуются с помощью схемы JSON.
2 Отправка сообщений и функций в модель Сериализованные функции (и текущая история чата) отправляются в модель в рамках входных данных.
3 Модель обрабатывает входные данные Модель обрабатывает входные данные и создает ответ. Ответ может быть сообщением чата или одним или несколькими вызовами функции.
4 Обработка ответа Если ответ является сообщением чата, он возвращается вызывающему. Однако если ответ является вызовом функции, семантический ядро извлекает имя функции и его параметры.
5 Вызов функции Извлеченные имя и параметры функции используются для вызова функции в ядре.
6 Верните результат функции Результат функции затем отправляется обратно в модель как часть журнала чата. Затем шаги 2-6 повторяются до тех пор, пока модель не вернет сообщение чата или достигнуто максимальное число итераций.

На следующей схеме показан процесс вызова функции:

Вызов функции семантического ядра

В следующем разделе показано, как работает вызов функций на практике.

Пример: заказ пиццы

Предположим, у вас есть подключаемый модуль, позволяющий пользователю заказать пиццу. Подключаемый модуль имеет следующие функции:

  1. get_pizza_menu: возвращает список доступных пицц
  2. add_pizza_to_cart: добавляет пиццу в корзину пользователя
  3. remove_pizza_from_cart: удаляет пиццу из корзины пользователя
  4. get_pizza_from_cart: возвращает конкретные сведения о пицце в корзине пользователя
  5. get_cart: возвращает текущую корзину пользователя
  6. 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();

Примечание.

Сериализуются и отправляются в модель только функции с KernelFunction атрибутом. Это позволяет использовать вспомогательные функции, которые не видны модели.

В 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

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

Примечание.

Сериализуются только функции с декоратором kernel_function и отправляются в модель. Это позволяет иметь вспомогательные функции, которые не предоставляются модели.

Зарезервированные имена параметров для автоматического вызова функций

При использовании автоматического вызова функции в KernelFunctions определенные имена параметров зарезервированы и получают специальную обработку. Эти зарезервированные имена позволяют автоматически получать доступ к ключевым объектам, необходимым для выполнения функции.

Зарезервированные имена

Зарезервированы следующие имена параметров:

  • kernel
  • service
  • execution_settings
  • arguments

Как они работают

Во время вызова функции метод gather_function_parameters проверяет каждый параметр. Если имя параметра соответствует одному из зарезервированных имен, оно заполняется определенными объектами:

  • kernel: внедрено с использованием объекта ядра.
  • service: заполняется службой ИИ, выбранной на основе предоставленных аргументов.
  • execution_settings: содержит параметры, относящиеся к выполнению функции.
  • arguments: получает весь набор аргументов ядра, переданных во время вызова.

Эта конструкция гарантирует автоматическое управление этими параметрами, устраняя необходимость извлечения или назначения вручную.

Пример использования

Рассмотрим следующий пример:

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

Пользовательские имена зарезервированных параметров для автоматического вызова функций

Это поведение также можно настроить. Чтобы сделать это, необходимо значить параметр, который требуется исключить из определения вызова функции, как показано ниже.

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.

При вызове этой функции обязательно передайте special_arg параметр, в противном случае возникает ошибка.

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

Или добавьте его в KernelArguments объект, чтобы использовать его с автоматическим вызовом функции в агенте следующим образом:

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

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

Примечание.

Сериализуются и отправляются в модель только функции с DefineKernelFunction заметкой. Это позволяет иметь вспомогательные функции, которые не предоставляются модели.

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

Здесь есть несколько вещей, которые могут повлиять как на производительность, так и качество завершения чата:

  1. Подробность схемы функций — сериализация функций для использования моделью не обходится бесплатно. Чем более подробно схема, тем больше маркеров модель должна обрабатывать, что может замедлить время отклика и увеличить затраты.

    Совет

    Делайте функции как можно проще. В приведенном выше примере вы заметите, что не все функции имеют описания, в которых имя функции является самообязательным. Это сделано намеренно для уменьшения количества токенов. Параметры также сохраняются простыми; все, что модель не должна знать (например cartId , или paymentId) хранится скрыто. Вместо этого эти сведения предоставляются внутренними службами.

    Примечание.

    То, о чем вам не стоит беспокоиться, это сложность типов возвращаемых значений. Вы заметите, что в схеме не происходит сериализации возвращаемых типов. Это связано с тем, что модель не должна знать тип возврата для создания ответа. Однако на шаге 6 мы увидим, как чрезмерно подробные типы возвращаемых данных могут повлиять на качество завершения чата.

  2. Типы параметров — с помощью схемы можно указать тип каждого параметра. Это важно для модели, чтобы понять ожидаемые входные данные. В приведенном выше примере size параметр является перечислением, и toppings параметр является массивом перечислений. Это помогает модели создавать более точные ответы.

    Совет

    Избегайте, если это возможно, использование string в качестве типа параметров. Модель не может определить тип строки, что может привести к неоднозначным ответам. Вместо этого используйте перечисления или другие типы (например, int, floatи сложные типы), где это возможно.

  3. Обязательные параметры — можно также указать необходимые параметры. Это важно для модели, чтобы понять, какие параметры фактически необходимы для работы функции. Далее на шаге 3 модель будет использовать эту информацию для предоставления минимальной информации по мере необходимости для вызова функции.

    Совет

    Пометьте только необходимые параметры, если они действительно необходимы. Это помогает модели вызывать функции более быстро и точно.

  4. Описания функций — описания функций являются необязательными , но могут помочь модели создавать более точные ответы. В частности, описания могут указывать модели, чего ожидать от ответа, так как тип возвращаемого значения не сериализуется в схеме. Если модель неправильно использует функции, можно также добавить описания для предоставления примеров и рекомендаций.

    Например, в get_pizza_from_cart функции описание сообщает пользователю использовать эту функцию, а не полагаться на предыдущие сообщения. Это важно, так как корзина может измениться с момента последнего сообщения.

    Совет

    Прежде чем добавить описание, спросите себя, нужна ли модели эта информация для генерации ответа. Если нет, рассмотрите возможность отказа от него, чтобы уменьшить многословие. Вы всегда можете добавить описания позже, если у модели возникают трудности с правильным использованием функции.

  5. Имя подключаемого модуля— как можно увидеть в сериализованных функциях, каждая функция имеет name свойство. Семантический ядро использует имя подключаемого модуля для пространства имен функций. Это важно, так как это позволяет иметь несколько подключаемых модулей с функциями одного и того же имени. Например, у вас могут быть плагины для нескольких поисковых служб, и каждая из них обладает собственной search функцией. При использовании имен функций можно избежать конфликтов и упростить работу модели, чтобы понять, какую функцию следует вызывать.

    Зная это, следует выбрать имя подключаемого модуля, уникальное и описательное. В приведенном выше примере имя подключаемого модуля — OrderPizza. Это дает понять, что функции связаны с заказом пиццы.

    Совет

    При выборе имени подключаемого модуля рекомендуется удалить лишние слова, такие как "подключаемый модуль" или "служба". Это помогает уменьшить многословность и облегчает понимание названия плагина моделью.

    Примечание.

    По умолчанию разделитель для имени функции — -. Хотя это работает для большинства моделей, некоторые из них могут иметь разные требования, такие как Gemini. Это выполняется автоматически ядром, однако в сериализованных функциях могут отображаться немного разные имена функций.

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)

Примечание.

В этом примере используется FunctionChoiceBehavior.Auto() тип поведения, один из немногих доступных. Дополнительные сведения о поведении других функций см. в статье о поведении выбора функции.

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

Примечание.

В этом примере используется поведение FunctionChoiceBehavior.Auto(), одно из немногих доступных. Дополнительные сведения о поведении других функций см. в статье о поведении выбора функции.

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

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

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

Внимание

Ядро должно быть передано в службу для использования вызовов функций. Это связано с тем, что подключаемые модули зарегистрированы в ядре, и служба должна знать, какие подключаемые модули доступны.

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

Теперь, когда у модели есть необходимые сведения, она теперь может вызвать 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, когда параметр автоматического вызова отключен. Вы не добавляете содержимое вызова функции в журнал чата; вместо этого приложение отвечает за вызовы функций. Перейдите к следующему разделу "Вызов функции", чтобы ознакомиться с примером обработки вызовов функций в 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 должно выполнить вызовы функций и добавить результат функции в журнал чата в виде сообщения.

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 и ускорить процесс оформления заказа.

Следующие шаги

Теперь, когда вы узнаете, как работает вызов функции, вы можете узнать, как настроить различные аспекты вызова функций, которые лучше соответствуют конкретным сценариям, перейдя к следующему шагу: