Sdílet prostřednictvím


Volání funkcí s dokončením chatu

Nejvýkonnější funkcí dokončování chatu je možnost volat funkce z modelu. Díky tomu můžete vytvořit chatovacího robota, který může pracovat s vaším stávajícím kódem, což umožňuje automatizovat obchodní procesy, vytvářet fragmenty kódu a provádět další činnosti.

Sémantickým jádrem zjednodušujeme proces použití volání funkcí tím, že automaticky popíšeme funkce a jejich parametry modelu a pak zpracujeme komunikaci mezi modelem a vaším kódem.

Při volání funkcí je ale dobré pochopit, co se skutečně děje na pozadí, abyste mohli optimalizovat kód a využít tuto funkci na maximum.

Jak funguje volání funkcí

Když provedete požadavek na model s povoleným voláním funkce, provede sémantické jádro následující kroky:

Krok Description
1 Serializace funkcí Všechny dostupné funkce (a její vstupní parametry) v jádru se serializují pomocí schématu JSON.
2 Odeslání zpráv a funkcí do modelu Serializované funkce (a aktuální historie chatu) se do modelu odesílají jako součást vstupu.
3 Model zpracovává vstup. Model zpracuje vstup a vygeneruje odpověď. Odpověď může být buď zpráva chatu, nebo volání funkce.
4 Zpracování odpovědi Pokud je odpovědí chatová zpráva, vrátí se vývojáři a vytiskne odpověď na obrazovku. Pokud je odpovědí volání funkce, ale sémantické jádro extrahuje název funkce a jeho parametry.
5 Vyvolání funkce Extrahovaný název a parametry funkce se používají k vyvolání funkce v jádru.
6 Vrácení výsledku funkce Výsledek funkce se pak odešle zpět do modelu jako součást historie chatu. Kroky 2–6 se pak opakují, dokud model neodešle signál ukončení.

Následující diagram znázorňuje proces volání funkce:

Sémantické volání funkce jádra

Následující část použije konkrétní příklad k ilustraci fungování volání funkcí v praxi.

Příklad: Objednání pizzy

Předpokládejme, že máte modul plug-in, který uživateli umožňuje objednat pizzu. Modul plug-in má následující funkce:

  1. get_pizza_menu: Vrátí seznam dostupnýchpizzch
  2. add_pizza_to_cart: Přidá pizzu do košíku uživatele.
  3. remove_pizza_from_cart: Odebere pizzu z košíku uživatele.
  4. get_pizza_from_cart: Vrátí konkrétní podrobnosti pizzy v košíku uživatele.
  5. get_cart: Vrátí aktuální košík uživatele.
  6. checkout: Zkontroluje košík uživatele.

V jazyce C# může modul plug-in vypadat takto:

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

Pak byste tento modul plug-in přidali do jádra takto:

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

V Pythonu může modul plug-in vypadat takto:

from semantic_kernel.functions import kernel_function

class OrderPizzaPlugin:
    def __init__(self, pizza_service, user_context, payment_service):
        self.pizza_service = pizza_service
        self.user_context = user_context
        self.payment_service = payment_service

    @kernel_function(name="get_pizza_menu")
    async def get_pizza_menu(self):
        return await self.pizza_service.get_menu()

    @kernel_function(
        name="add_pizza_to_cart",
        description="Add a pizza to the user's cart; returns the new item and updated cart"
    )
    async def add_pizza_to_cart(self, size: PizzaSize, toppings: List[PizzaToppings], quantity: int = 1, special_instructions: str = ""):
        cart_id = await self.user_context.get_cart_id()
        return await self.pizza_service.add_pizza_to_cart(cart_id, size, toppings, quantity, special_instructions)

    @kernel_function(
        name="remove_pizza_from_cart",
        description="Remove a pizza from the user's cart; returns the updated cart"
    )
    async def remove_pizza_from_cart(self, pizza_id: int):
        cart_id = await self.user_context.get_cart_id()
        return await self.pizza_service.remove_pizza_from_cart(cart_id, pizza_id)

    @kernel_function(
        name="get_pizza_from_cart",
        description="Returns the specific details of a pizza in the user's cart; use this instead of relying on previous messages since the cart may have changed since then."
    )
    async def get_pizza_from_cart(self, pizza_id: int):
        cart_id = await self.user_context.get_cart_id()
        return await self.pizza_service.get_pizza_from_cart(cart_id, pizza_id)

    @kernel_function(
        name="get_cart",
        description="Returns the user's current cart, including the total price and items in the cart."
    )
    async def get_cart(self):
        cart_id = await self.user_context.get_cart_id()
        return await self.pizza_service.get_cart(cart_id)

    @kernel_function(
        name="checkout",
        description="Checkouts the user's cart; this function will retrieve the payment from the user and complete the order."
    )
    async def checkout(self):
        cart_id = await self.user_context.get_cart_id()
        payment_id = await self.payment_service.request_payment_from_user(cart_id)
        return await self.pizza_service.checkout(cart_id, payment_id)

Pak byste tento modul plug-in přidali do jádra takto:

from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase

kernel = Kernel()
kernel.add_service(AzureChatCompletion(model_id, endpoint, api_key))

# Create the services needed for the plugin: pizza_service, user_context, and payment_service
# ...

# Add the plugin to the kernel
kernel.add_plugin(OrderPizzaPlugin(pizza_service, user_context, payment_service), plugin_name="OrderPizza")

1) Serializace funkcí

Když vytvoříte jádro s jádrem OrderPizzaPlugin, jádro automaticky serializuje funkce a jejich parametry. To je nezbytné, aby model rozuměl funkcím a jejich vstupům.

U výše uvedeného modulu plug-in by serializované funkce vypadaly takto:

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

Tady je několik věcí, které můžou ovlivnit výkon i kvalitu dokončení chatu:

  1. Podrobnosti schématu funkcí – Serializace funkcí pro model, který se má použít, není zadarmo. Čím více podrobného schématu, tím více tokenů musí model zpracovat, což může zpomalit dobu odezvy a zvýšit náklady.

    Tip

    Udržujte funkce co nejjednodušší. V předchozím příkladu si všimnete, že ne všechny funkce mají popisy, ve kterých je název funkce vysvětlovaný. To je úmyslné snížit počet tokenů. Parametry jsou také zachovány jednoduché; Cokoli, co by model neměl vědět (jako je ten cartId nebo paymentId) je skrytý. Tyto informace jsou místo toho poskytovány interními službami.

    Poznámka:

    Jedna věc, se kterou si nemusíte dělat starosti, je složitost návratových typů. Všimněte si, že návratové typy nejsou serializovány ve schématu. Důvodem je to, že model nepotřebuje znát návratový typ pro vygenerování odpovědi. V kroku 6 ale uvidíme, jak příliš podrobné návratové typy můžou ovlivnit kvalitu dokončení chatu.

  2. Typy parametrů – Pomocí schématu můžete zadat typ každého parametru. To je důležité pro model, aby porozuměl očekávanému vstupu. V předchozím příkladu size je parametr výčtem a toppings parametr je pole výčtů. To pomáhá modelu generovat přesnější odpovědi.

    Tip

    Pokud je to možné, nepoužívejte string jako typ parametru. Model nemůže odvodit typ řetězce, což může vést k nejednoznačným odpovědím. Místo toho použijte výčty nebo jiné typy (např int. , float, a komplexní typy), pokud je to možné.

  3. Požadované parametry – Můžete také určit, které parametry se vyžadují. To je důležité pro model, aby porozuměl tomu, které parametry jsou skutečně nezbytné pro fungování funkce. Později v kroku 3 použije model tyto informace k zadání minimálních informací, které jsou potřeba k volání funkce.

    Tip

    Parametry označte jako povinné pouze v případě, že jsou skutečně povinné. To pomáhá volat funkce modelu rychleji a přesněji.

  4. Popisy funkcí – Popisy funkcí jsou volitelné, ale můžou modelu pomoct generovat přesnější odpovědi. Konkrétně popisy můžou modelu sdělit, co očekávat od odpovědi, protože návratový typ není serializován ve schématu. Pokud model nesprávně používá funkce, můžete také přidat popisy, které vám poskytnou příklady a pokyny.

    Například ve get_pizza_from_cart funkci popis uživateli říká, aby tuto funkci používal, místo aby se spoléhal na předchozí zprávy. To je důležité, protože se košík mohl od poslední zprávy změnit.

    Tip

    Než přidáte popis, zeptejte se sami sebe, jestli model potřebuje tyto informace k vygenerování odpovědi. Pokud ne, zvažte jeho vynechání, abyste snížili úroveň podrobností. Popisy můžete kdykoli později přidat, pokud se model snaží funkci správně používat.

  5. Název modulu plug-in – jak vidíte v serializovaných funkcích, každá funkce má name vlastnost. Sémantické jádro používá název modulu plug-in k oboru názvů funkcí. To je důležité, protože umožňuje mít více modulů plug-in s funkcemi stejného názvu. Můžete mít například moduly plug-in pro více vyhledávacích služeb, z nichž každá má vlastní search funkci. Když funkce pojmenujete, můžete se vyhnout konfliktům a usnadnit tak, aby model rozuměl tomu, která funkce se má volat.

    Znáte to, měli byste zvolit název modulu plug-in, který je jedinečný a popisný. V předchozím příkladu je OrderPizzanázev modulu plug-in . To znamená, že funkce souvisí s objednávání pizzy.

    Tip

    Při výběru názvu modulu plug-in doporučujeme odebrat nadbytečná slova, jako je "plugin" nebo "služba". To pomáhá snížit úroveň podrobností a usnadňuje pochopení názvu modulu plug-in pro model.

2) Odesílání zpráv a funkcí do modelu

Jakmile jsou funkce serializovány, posílají se do modelu spolu s aktuální historií chatu. To umožňuje modelu porozumět kontextu konverzace a dostupným funkcím.

V tomto scénáři si můžeme představit, že uživatel požádá asistenta o přidání pizzy do košíku:

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

Pak můžeme do modelu odeslat tuto historii chatu a serializované funkce. Tento model použije tyto informace k určení nejlepšího způsobu reakce.

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

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

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

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

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

3) Model zpracovává vstup

S historií chatu i serializovanými funkcemi může model určit nejlepší způsob, jak reagovat. V tomto případě model rozpozná, že uživatel chce objednat pizzu. Model by pravděpodobně chtěl volat add_pizza_to_cart funkci, ale protože jsme zadali velikost a zastavování podle požadovaných parametrů, model požádá uživatele o tyto informace:

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

Vzhledem k tomu, že model chce, aby uživatel odpověděl příště, odešle se signál ukončení do sémantického jádra, aby zastavil automatické volání funkce, dokud uživatel neodpoví.

V tomto okamžiku může uživatel reagovat na velikost a zastavování pizzy, kterou chce objednat:

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]

Teď, když má model potřebné informace, teď může volat add_pizza_to_cart funkci se vstupem uživatele. Na pozadí přidá novou zprávu do historie chatu, která vypadá takto:

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

Tip

Je dobré si uvědomit, že každý argument, který požadujete, musí být vygenerován modelem. To znamená, že tokeny útraty k vygenerování odpovědi. Vyhněte se argumentům, které vyžadují mnoho tokenů (například IDENTIFIKÁTOR GUID). Všimněte si například, že používáme pro intpizzaId. Žádost modelu o odeslání čísla o jednu až dvě číslice je mnohem jednodušší než požádat o identifikátor GUID.

Důležité

Tento krok znamená, že volání funkcí je tak výkonné. Vývojáři aplikací AI dříve museli vytvářet samostatné procesy pro extrakci funkcí výplně záměru a slotu. Při volání funkce se model může rozhodnout , kdy volat funkci a jaké informace poskytnout.

4) Zpracování odpovědi

Když sémantické jádro obdrží odpověď z modelu, zkontroluje, jestli odpověď je volání funkce. Pokud ano, sémantické jádro extrahuje název funkce a jeho parametry. V tomto případě je OrderPizzaPlugin-add_pizza_to_cartnázev funkce a argumenty jsou velikost a zastavování pizzy.

S touto informací může sémantické jádro zařazovat vstupy do příslušných typů a předat je add_pizza_to_cart funkci v OrderPizzaPlugin. V tomto příkladu pocházejí argumenty jako řetězec JSON, ale jsou deserializovány sémantickým jádrem do výčtu PizzaSize a .List<PizzaToppings>

Poznámka:

Zařazování vstupů do správných typů je jednou z klíčových výhod použití sémantického jádra. Všechno od modelu přichází jako objekt JSON, ale sémantické jádro může tyto objekty automaticky deserializovat do správných typů vašich funkcí.

Po zařazování vstupů může sémantické jádro také přidat volání funkce do historie chatu:

chatHistory.Add(
    new() {
        Role = AuthorRole.Assistant,
        Items = [
            new FunctionCallContent(
                functionName: "add_pizza_to_cart",
                pluginName: "OrderPizza",
                id: "call_abc123",
                arguments: new () { {"size", "Medium"}, {"toppings", ["Cheese", "Pepperoni"]} }
            )
        ]
    }
);
from semantic_kernel.contents import ChatMessageContent, FunctionCallContent
from semantic_kernel.contents.utils.author_role import AuthorRole

chat_history.add_message(
    ChatMessageContent(
        role=AuthorRole.ASSISTANT,
        items=[
            FunctionCallContent(
                name="OrderPizza-add_pizza_to_cart",
                id="call_abc123",
                arguments=str({"size": "Medium", "toppings": ["Cheese", "Pepperoni"]})
            )
        ]
    )
)

5) Vyvolání funkce

Jakmile má sémantické jádro správné typy, může funkci nakonec vyvolat add_pizza_to_cart . Vzhledem k tomu, že modul plug-in používá injektáž závislostí, může funkce pracovat s externími službami, jako je pizzaServiceuserContext a přidat pizzu do košíku uživatele.

Ne všechny funkce však budou úspěšné. Pokud funkce selže, může sémantické jádro chybu zpracovat a poskytnout výchozí odpověď na model. Díky tomu může model pochopit, co se nepovedlo, a vygenerovat odpověď uživateli.

Tip

Aby se zajistilo, že model dokáže správně opravit, je důležité poskytnout chybové zprávy, které jasně komunikují o tom, co se nepovedlo a jak ho opravit. To může pomoci modelu zopakovat volání funkce se správnými informacemi.

6) Vrátí výsledek funkce.

Po vyvolání funkce se výsledek funkce odešle zpět do modelu jako součást historie chatu. Díky tomu může model pochopit kontext konverzace a vygenerovat následnou odpověď.

Na pozadí přidá sémantické jádro do historie chatu novou zprávu z role nástroje, která vypadá takto:

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

Všimněte si, že výsledkem je řetězec JSON, který pak model potřebuje zpracovat. Stejně jako předtím bude model muset utratit tokeny, které tyto informace spotřebovávají. Proto je důležité, aby návratové typy byly co nejjednodušší. V tomto případě vrácení zahrnuje pouze nové položky přidané do košíku, ne celý košík.

Tip

Buďte co nejsušnější s vašimi výnosy. Pokud je to možné, vraťte pouze informace, které model potřebuje, nebo shrnout informace pomocí jiné výzvy LLM před vrácením.

Opakování kroků 2–6

Po vrácení výsledku do modelu se proces opakuje. Model zpracuje nejnovější historii chatu a vygeneruje odpověď. V tomto případě se model může zeptat uživatele, jestli chce do košíku přidat další pizzu nebo jestli si chce rezervovat.

Paralelní volání funkcí

V předchozím příkladu jsme ukázali, jak LLM může volat jednu funkci. Často to může být pomalé, pokud potřebujete volat více funkcí v posloupnosti. Aby se proces urychlil, podporuje několik LLM paralelní volání funkcí. LLM tak může volat více funkcí najednou a urychlit proces.

Pokud například chce uživatel objednat více pizz, může LLM volat add_pizza_to_cart funkci pro každou pizzu najednou. To může výrazně snížit počet odezvy do LLM a urychlit proces řazení.

Další kroky

Teď, když rozumíte tomu, jak funguje volání funkcí, teď se dozvíte, jak ve skutečnosti používat volání funkcí v sémantickém jádru, a to v článku o plánování.