Udostępnij za pośrednictwem


Wywoływanie funkcji za pomocą uzupełniania czatu

Najbardziej zaawansowaną funkcją uzupełniania czatu jest możliwość wywoływania funkcji z modelu. Dzięki temu można utworzyć czatbota, który może wchodzić w interakcje z istniejącym kodem, co umożliwia automatyzację procesów biznesowych, tworzenie fragmentów kodu i nie tylko.

W przypadku jądra semantycznego upraszczamy proces wywoływania funkcji przez automatyczne opisywanie funkcji i ich parametrów w modelu, a następnie obsługę komunikacji między modelem a kodem.

W przypadku wywoływania funkcji warto jednak zrozumieć, co faktycznie dzieje się za kulisami, aby można było zoptymalizować kod i jak najlepiej wykorzystać tę funkcję.

Jak działa wywoływanie funkcji

Po wysłaniu żądania do modelu z włączonym wywołaniem funkcji jądro semantyczne wykonuje następujące kroki:

Krok opis
1 Serializowanie funkcji Wszystkie dostępne funkcje (i jego parametry wejściowe) w jądrze są serializowane przy użyciu schematu JSON.
2 Wysyłanie komunikatów i funkcji do modelu Serializowane funkcje (i bieżąca historia czatu) są wysyłane do modelu w ramach danych wejściowych.
3 Model przetwarza dane wejściowe Model przetwarza dane wejściowe i generuje odpowiedź. Odpowiedź może być komunikatem czatu lub wywołaniem funkcji
100 Obsługa odpowiedzi Jeśli odpowiedź jest wiadomością na czacie, zostanie zwrócona deweloperowi, aby wydrukować odpowiedź na ekran. Jeśli jednak odpowiedź jest wywołaniem funkcji, semantyczne jądro wyodrębnia nazwę funkcji i jej parametry.
5 Wywoływanie funkcji Wyodrębniona nazwa i parametry funkcji są używane do wywoływania funkcji w jądrze.
6 Zwraca wynik funkcji Wynik funkcji jest następnie wysyłany z powrotem do modelu w ramach historii czatów. Kroki 2–6 są następnie powtarzane, dopóki model nie wyśle sygnału zakończenia

Na poniższym diagramie przedstawiono proces wywoływania funkcji:

Wywołanie funkcji jądra semantycznego

Poniższa sekcja będzie używać konkretnego przykładu, aby zilustrować sposób działania wywoływania funkcji w praktyce.

Przykład: Zamawianie pizzy

Załóżmy, że masz wtyczkę, która umożliwia użytkownikowi zamawianie pizzy. Wtyczka ma następujące funkcje:

  1. get_pizza_menu: Zwraca listę dostępnych pizz
  2. add_pizza_to_cart: Dodaje pizzę do koszyka użytkownika
  3. remove_pizza_from_cart: Usuwa pizzę z koszyka użytkownika
  4. get_pizza_from_cart: zwraca szczegółowe informacje o pizzy w koszyku użytkownika
  5. get_cart: Zwraca bieżący koszyk użytkownika
  6. checkout: wyewidencjonowywanie koszyka użytkownika

W języku C#wtyczka może wyglądać następująco:

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

Następnie należy dodać tę wtyczkę do jądra w następujący sposób:

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

W języku Python wtyczka może wyglądać następująco:

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)

Następnie należy dodać tę wtyczkę do jądra w następujący sposób:

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

W języku Java wtyczka może wyglądać następująco:

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

Następnie należy dodać tę wtyczkę do jądra w następujący sposób:

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) Serializowanie funkcji

Podczas tworzenia jądra za pomocą OrderPizzaPluginpolecenia jądro automatycznie serializuje funkcje i ich parametry. Jest to konieczne, aby model mógł zrozumieć funkcje i ich dane wejściowe.

W przypadku powyższej wtyczki serializowane funkcje wyglądają następująco:

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

Istnieje kilka kwestii, które należy zwrócić uwagę, co może mieć wpływ zarówno na wydajność, jak i jakość ukończenia czatu:

  1. Szczegółowość schematu funkcji — serializowanie funkcji dla modelu do użycia nie jest dostępne bezpłatnie. Tym bardziej pełny schemat, tym więcej tokenów, które model musi przetworzyć, co może spowolnić czas odpowiedzi i zwiększyć koszty.

    Napiwek

    Zachowaj swoje funkcje tak proste, jak to możliwe. W powyższym przykładzie zauważysz, że nie wszystkie funkcje zawierają opisy, w których nazwa funkcji jest objaśniona. Jest to zamierzone, aby zmniejszyć liczbę tokenów. Parametry są również proste; wszystkie elementy, których model nie powinien wiedzieć (na przykład lub cartId paymentId) są ukryte. Te informacje są zamiast tego udostępniane przez usługi wewnętrzne.

    Uwaga

    Jedyną rzeczą, o którą nie musisz się martwić, jest złożoność zwracanych typów. Zauważysz, że zwracane typy nie są serializowane w schemacie. Wynika to z faktu, że model nie musi znać typu powrotu, aby wygenerować odpowiedź. Jednak w kroku 6 zobaczymy, jak nadmiernie pełne typy zwracane mogą mieć wpływ na jakość ukończenia czatu.

  2. Typy parametrów — przy użyciu schematu można określić typ każdego parametru. Jest to ważne dla modelu, aby zrozumieć oczekiwane dane wejściowe. W powyższym przykładzie size parametr jest wyliczeniowym, a toppings parametr jest tablicą wyliczenia. Pomaga to modelowi wygenerować dokładniejsze odpowiedzi.

    Napiwek

    Unikaj, jeśli to możliwe, używając string jako typu parametru. Model nie może wywnioskować typu ciągu, co może prowadzić do niejednoznacznych odpowiedzi. Zamiast tego należy używać wyliczenia lub innych typów (np. int, floati typów złożonych), jeśli to możliwe.

  3. Wymagane parametry — można również określić, które parametry są wymagane. Jest to ważne dla modelu, aby zrozumieć, które parametry są rzeczywiście niezbędne do działania funkcji. W dalszej części kroku 3 model użyje tych informacji, aby dostarczyć jak najmniejsze informacje niezbędne do wywołania funkcji.

    Napiwek

    Oznacz parametry jako wymagane, jeśli są one rzeczywiście wymagane. Ułatwia to szybsze i dokładniejsze wywoływanie funkcji modelu.

  4. Opisy funkcji — opisy funkcji są opcjonalne, ale mogą pomóc modelowi wygenerować dokładniejsze odpowiedzi. W szczególności opisy mogą powiedzieć modelowi, czego można oczekiwać od odpowiedzi, ponieważ typ zwracany nie jest serializowany w schemacie. Jeśli model używa funkcji nieprawidłowo, możesz również dodać opisy, aby podać przykłady i wskazówki.

    Na przykład w get_pizza_from_cart funkcji opis informuje użytkownika o użyciu tej funkcji zamiast polegać na poprzednich komunikatach. Jest to ważne, ponieważ koszyk mógł ulec zmianie od ostatniego komunikatu.

    Napiwek

    Przed dodaniem opisu zadaj sobie pytanie, czy model potrzebuje tych informacji, aby wygenerować odpowiedź. Jeśli nie, rozważ pozostawienie go w celu zmniejszenia szczegółowości. Zawsze możesz dodawać opisy później, jeśli model ma problemy z prawidłowym użyciem funkcji.

  5. Nazwa wtyczki — jak widać w funkcjach serializowanych, każda name funkcja ma właściwość . Semantyczne jądro używa nazwy wtyczki do przestrzeni nazw funkcji. Jest to ważne, ponieważ umożliwia posiadanie wielu wtyczek z funkcjami o tej samej nazwie. Na przykład możesz mieć wtyczki dla wielu usług wyszukiwania, z których każda ma własną search funkcję. Nazwując funkcje, można uniknąć konfliktów i ułatwić modelowi zrozumienie, która funkcja ma być wywoływana.

    Wiedząc o tym, należy wybrać nazwę wtyczki, która jest unikatowa i opisowa. W powyższym przykładzie nazwa wtyczki to OrderPizza. To jasno pokazuje, że funkcje są związane z zamawianiem pizzy.

    Napiwek

    Podczas wybierania nazwy wtyczki zalecamy usunięcie zbędnych słów, takich jak "plugin" lub "service". Pomaga to zmniejszyć szczegółowość i ułatwia zrozumienie nazwy wtyczki dla modelu.

2) Wysyłanie komunikatów i funkcji do modelu

Po serializacji funkcji są one wysyłane do modelu wraz z bieżącą historią czatu. Dzięki temu model może zrozumieć kontekst konwersacji i dostępnych funkcji.

W tym scenariuszu możemy sobie wyobrazić, że użytkownik prosi asystenta o dodanie pizzy do koszyka:

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

Następnie możemy wysłać tę historię czatu i serializowane funkcje do modelu. Model użyje tych informacji, aby określić najlepszy sposób reagowania.

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

Uwaga

W tym przykładzie użyto FunctionChoiceBehavior.Auto() zachowania , jednego z niewielu dostępnych. Aby uzyskać więcej informacji na temat innych zachowań wyboru funkcji, zapoznaj się z artykułem dotyczącym zachowań wyboru funkcji.

3) Model przetwarza dane wejściowe

Zarówno w przypadku historii czatu, jak i zserializowanej funkcji model może określić najlepszy sposób reagowania. W takim przypadku model rozpoznaje, że użytkownik chce zamówić pizzę. Model prawdopodobnie będzie chciał wywołać add_pizza_to_cart funkcję, ale ponieważ określono rozmiar i wartości topping jako wymagane parametry, model poprosi użytkownika o następujące informacje:

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

Ponieważ model chce, aby użytkownik odpowiedział dalej, sygnał zakończenia zostanie wysłany do jądra semantycznego, aby zatrzymać automatyczne wywoływanie funkcji, dopóki użytkownik nie odpowie.

W tym momencie użytkownik może odpowiedzieć za pomocą rozmiaru i doładowania pizzy, którą chce zamówić:

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

Teraz, gdy model ma niezbędne informacje, może teraz wywołać add_pizza_to_cart funkcję z danymi wejściowymi użytkownika. W tle dodaje nową wiadomość do historii czatów, która wygląda następująco:

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

Napiwek

Warto pamiętać, że każdy wymagany argument musi być generowany przez model. Oznacza to, że tokeny wydatków generują odpowiedź. Unikaj argumentów wymagających wielu tokenów (takich jak identyfikator GUID). Zwróć na przykład uwagę, że używamy elementu int dla elementu pizzaId. Prośba modelu o wysłanie jednego do dwóch cyfr jest znacznie łatwiejsza niż prośba o identyfikator GUID.

Ważne

Ten krok sprawia, że wywoływanie funkcji jest tak zaawansowane. Wcześniej deweloperzy aplikacji sztucznej inteligencji musieli utworzyć oddzielne procesy w celu wyodrębnienia funkcji wypełniania intencji i miejsca. W przypadku wywoływania funkcji model może zdecydować , kiedy wywołać funkcję i jakie informacje należy podać.

4) Obsługa odpowiedzi

Gdy semantyczne jądro odbiera odpowiedź z modelu, sprawdza, czy odpowiedź jest wywołaniem funkcji. Jeśli tak jest, semantyczne jądro wyodrębnia nazwę funkcji i jej parametry. W takim przypadku nazwa funkcji to OrderPizzaPlugin-add_pizza_to_cart, a argumenty to rozmiar i dodatki pizzy.

Dzięki tym informacjom semantyczne jądro może przesłonić dane wejściowe do odpowiednich typów i przekazać je do add_pizza_to_cart funkcji w pliku OrderPizzaPlugin. W tym przykładzie argumenty pochodzą z ciągu JSON, ale są deserializowane przez jądro semantyczne do PizzaSize wyliczenia i List<PizzaToppings>.

Uwaga

Przeprowadzanie marshalingu danych wejściowych do poprawnych typów jest jedną z kluczowych zalet korzystania z jądra semantycznego. Wszystko, od modelu jest obiektem JSON, ale semantyczne jądro może automatycznie deserializować te obiekty w poprawne typy funkcji.

Po przesłaniu danych wejściowych jądro semantyczne może również dodać wywołanie funkcji do historii czatów:

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

Semantyczne jądro dla języka Java obsługuje funkcję wywołującą inaczej niż C# i Python, gdy zachowanie wywołania narzędzia automatycznego jest fałszywe. Nie dodajesz zawartości wywołania funkcji do historii czatu; zamiast tego aplikacja jest odpowiedzialna za wywoływanie wywołań funkcji. Przejdź do następnej sekcji "Wywołaj funkcję", aby uzyskać przykład obsługi wywołań funkcji w języku Java, gdy automatyczne wywołanie jest fałszywe.

5) Wywoływanie funkcji

Gdy semantyczne jądro ma poprawne typy, może na koniec wywołać add_pizza_to_cart funkcję. Ponieważ wtyczka używa iniekcji zależności, funkcja może wchodzić w interakcje z usługami zewnętrznymi, takimi jak pizzaService i userContext , aby dodać pizzę do koszyka użytkownika.

Jednak nie wszystkie funkcje powiedzą się. Jeśli funkcja zakończy się niepowodzeniem, jądro semantyczne może obsłużyć błąd i dostarczyć domyślną odpowiedź na model. Dzięki temu model może zrozumieć, co poszło nie tak i wygenerować odpowiedź na użytkownika.

Napiwek

Aby zapewnić, że model może się samodzielnie poprawić, ważne jest dostarczenie komunikatów o błędach, które wyraźnie informują o tym, co poszło nie tak i jak go naprawić. Może to pomóc modelowi ponowić wywołanie funkcji przy użyciu poprawnych informacji.

Uwaga

Semantyczne jądro automatycznie wywołuje funkcje domyślnie. Jeśli jednak wolisz ręcznie zarządzać wywołaniem funkcji, możesz włączyć tryb wywołania funkcji ręcznej. Aby uzyskać więcej informacji na temat tego, jak to zrobić, zapoznaj się z artykułem dotyczącym wywołania funkcji.

6) Zwróć wynik funkcji

Po wywołaniu funkcji wynik funkcji jest wysyłany z powrotem do modelu w ramach historii czatów. Dzięki temu model może zrozumieć kontekst konwersacji i wygenerować kolejną odpowiedź.

W tle jądro semantyczne dodaje nową wiadomość do historii czatów z roli narzędzia, która wygląda następująco:

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

Jeśli automatyczne wywołanie jest wyłączone w zachowaniu wywołania narzędzia, aplikacja Java musi wywołać wywołania funkcji i dodać wynik funkcji jako AuthorRole.TOOL komunikat do historii czatów.

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

Zwróć uwagę, że wynikiem jest ciąg JSON, który następnie musi przetworzyć model. Tak jak wcześniej, model będzie musiał wydać tokeny korzystające z tych informacji. Dlatego ważne jest, aby zachować jak najprostsze typy zwracane. W takim przypadku zwrot obejmuje tylko nowe elementy dodane do koszyka, a nie cały koszyk.

Napiwek

Bądź tak zwięzły, jak to możliwe w przypadku zwrotów. Jeśli to możliwe, przed zwróceniem informacji należy zwrócić tylko informacje wymagane przez model lub podsumowywać informacje przy użyciu innego monitu LLM.

Powtórz kroki 2–6

Po powrocie wyniku do modelu proces powtarza się. Model przetwarza najnowszą historię czatów i generuje odpowiedź. W takim przypadku model może zapytać użytkownika, czy chce dodać kolejną pizzę do koszyka, czy też chcesz wyewidencjonować.

Równoległe wywołania funkcji

W powyższym przykładzie pokazano, jak usługa LLM może wywołać pojedynczą funkcję. Często może to być powolne, jeśli trzeba wywołać wiele funkcji w sekwencji. Aby przyspieszyć proces, kilka modułów LLM obsługuje równoległe wywołania funkcji. Dzięki temu usługa LLM może jednocześnie wywoływać wiele funkcji, przyspieszając proces.

Jeśli na przykład użytkownik chce zamówić wiele pizz, llM może wywołać add_pizza_to_cart funkcję dla każdej pizzy jednocześnie. Może to znacznie zmniejszyć liczbę rund do usługi LLM i przyspieszyć proces zamawiania.

Następne kroki

Teraz, gdy rozumiesz, jak działa wywoływanie funkcji, możesz dowiedzieć się, jak skonfigurować różne aspekty wywoływania funkcji, które lepiej odpowiadają konkretnym scenariuszom, korzystając z artykułu o zachowaniu wyboru funkcji