チャットの完了を伴う関数呼び出し

チャット完了の最も強力な機能は、モデルから関数を呼び出す機能です。 これにより、既存のコードと対話できるチャット ボットを作成でき、ビジネス プロセスの自動化、コード スニペットの作成などを行うことができます。

セマンティック カーネルを使用すると、関数とそのパラメーターをモデルに自動的に記述し、モデルとコード間の前後の通信を処理することで、関数呼び出しを使用するプロセスが簡略化されます。

ただし、関数呼び出しを使用する場合は、コードを最適化してこの機能を最大限に活用できるようにバックグラウンドで何が実際に起こっているかを理解することをお勧めします。

関数呼び出しのしくみ

関数呼び出しが有効になっているモデルに対して要求を行うと、セマンティック カーネルは次の手順を実行します。

Step 説明
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();

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

その後、次のようにカーネルにこのプラグインを追加します。

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) 関数のシリアル化

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. 関数スキーマの詳細度 – 使用するモデルの関数のシリアル化は無料で提供されません。 スキーマの詳細度が高いほど、モデルで処理する必要があるトークンが多くなり、応答時間が遅くなり、コストが増加する可能性があります。

    ヒント

    可能な限り単純な関数を保持します。 上記の例では、 すべてではない関数 関数名がわかりやすい説明を持っていることに気付くでしょう。 これは、トークンの数を減らすために意図的です。 パラメーターも単純に保たれます。モデルが知る必要のない ( cartIdpaymentIdなど) はすべて非表示に保たれます。 この情報は、代わりに内部サービスによって提供されます。

    Note

    心配する必要がない 1 つは、戻り値の型の複雑さです。 戻り値の型がスキーマでシリアル化されていないことに気付くでしょう。 これは、モデルが応答を生成するために戻り値の型を知る必要がないためです。 ただし、手順 6 では、過度に詳細な戻り値の種類がチャットの完了の品質に与える影響を確認します。

  2. パラメーターの型 – スキーマを使用して、各パラメーターの型を指定できます。 これは、モデルが予想される入力を理解するために重要です。 上記の例では、 size パラメーターは列挙型であり、 toppings パラメーターは列挙型の配列です。 これは、モデルがより正確な応答を生成するのに役立ちます。

    ヒント

    stringをパラメーター型として使用することは、可能な限り避けてください。 モデルでは文字列の型を推測できないため、あいまいな応答が発生する可能性があります。 代わりに、可能な限り列挙型またはその他の型 ( intfloat、複合型など) を使用します。

  3. 必須パラメーター - 必要なパラメーターを指定することもできます。 これは、関数が機能するために必要な実際にどのパラメーターがモデルで理解されているかを理解するために重要です。 手順 3 の後半で、モデルはこの情報を使用して、関数を呼び出すために必要な最小限の情報を提供します。

    ヒント

    パラメーターが実際に 必要な場合にのみ 必要に応じてマークします。 これは、モデル呼び出し関数をより迅速かつ正確に呼び出すのに役立ちます。

  4. 関数の説明 – 関数の説明は省略可能ですが、モデルがより正確な応答を生成するのに役立ちます。 特に、戻り値の型はスキーマでシリアル化されないため、応答から何が期待されるかをモデルに伝えることができます。 モデルで関数が不適切に使用されている場合は、説明を追加して例とガイダンスを提供することもできます。

    たとえば、 get_pizza_from_cart 関数では、前のメッセージに依存するのではなく、この関数を使用するようにユーザーに指示します。 これは、最後のメッセージ以降にカートが変更された可能性があるため、重要です。

    ヒント

    説明を追加する前に、モデル 必要があるかどうかを確認し この情報を使用して応答を生成します。 それ以外の場合は、詳細性を減らすために、そのままにすることを検討してください。 モデルが関数を適切に使用するのに苦労している場合は、後でいつでも説明を追加できます。

  5. プラグイン名 – シリアル化された関数でわかるように、各関数には name プロパティがあります。 セマンティック カーネルは、プラグイン名を使用して関数の名前空間を設定します。 同じ名前の関数を持つ複数のプラグインを使用できるため、これは重要です。 たとえば、複数の検索サービス用のプラグインがあり、それぞれが独自の search 関数を持つ場合があります。 関数に名前を付けることで、競合を回避し、呼び出す関数をモデルが理解しやすくなります。

    これを知っている場合は、一意でわかりやすいプラグイン名を選択する必要があります。 上記の例では、プラグイン名は OrderPizza。 これにより、関数がピザの注文に関連することが明らかになります。

    ヒント

    プラグイン名を選択するときは、"plugin" や "service" などの余分な単語を削除することをお勧めします。 これにより、詳細度が低下し、モデルのプラグイン名が理解しやすくなります。

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

その後、このチャット履歴とシリアル化された関数をモデルに送信できます。 モデルは、この情報を使用して、最適な応答方法を決定します。

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) モデルが入力を処理する

チャット履歴とシリアル化された関数の両方で、モデルは最適な応答方法を決定できます。 この場合、モデルは、ユーザーがピザを注文することを認識します。 モデルは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?"

モデルはユーザーが次に応答することを望んでいるため、ユーザーが応答するまで、セマンティック カーネルに終了シグナルが送信され、関数の自動呼び出しが停止されます。

この時点で、ユーザーは注文するピザのサイズとトッピングで応答できます。

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]

これでモデルに必要な情報が含まれたので、ユーザーの入力を使用して 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 など) は避けてください。 たとえば、pizzaIdintを使用していることに注意してください。 モデルに 1 から 2 桁の数字を送信するように依頼する方が、GUID を要求するよりもはるかに簡単です。

重要

この手順により、関数呼び出しが非常に強力になります。 以前は、AI アプリ開発者は、意図とスロットフィル関数を抽出するために個別のプロセスを作成する必要がありました。 関数呼び出しでは、モデルは関数を呼び出し、提供する情報を決定できます。

4) 応答を処理する

セマンティック カーネルは、モデルから応答を受信すると、応答が関数呼び出しであるかどうかを確認します。 その場合、セマンティック カーネルは関数名とそのパラメーターを抽出します。 この場合、関数名は OrderPizzaPlugin-add_pizza_to_cartされ、引数はピザのサイズとトッピングです。

この情報を使用して、セマンティック カーネルは入力を適切な型にマーシャリングし、OrderPizzaPluginadd_pizza_to_cart関数に渡すことができます。 この例では、引数は JSON 文字列として生成されますが、セマンティック カーネルによって PizzaSize 列挙型と List<PizzaToppings>に逆シリアル化されます。

Note

入力を正しい型にマーシャリングすることは、セマンティック カーネルを使用する主な利点の 1 つです。 モデルのすべてが 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"]})
            )
        ]
    )
)

5) 関数を呼び出す

セマンティック カーネルが正しい型を持つ場合は、最終的に add_pizza_to_cart 関数を呼び出すことができます。 プラグインは依存関係の挿入を使用するため、関数は pizzaServiceuserContext などの外部サービスと対話して、ピザをユーザーのカートに追加できます。

ただし、すべての関数が成功するわけではありません。 関数が失敗した場合、セマンティック カーネルはエラーを処理し、モデルに既定の応答を提供できます。 これにより、モデルは問題の原因を理解し、ユーザーに対する応答を生成できます。

ヒント

モデルが自己修正できるようにするには、問題の原因とその修正方法を明確に伝えるエラー メッセージを提供することが重要です。 これは、モデルが正しい情報を使用して関数呼び出しを再試行するのに役立ちます。

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

結果は、モデルが処理する必要がある JSON 文字列であることに注意してください。 以前と同様に、モデルでは、この情報を使用するトークンを使用する必要があります。 このため、戻り値の型をできるだけ単純に保つことが重要です。 この場合、返品には、カート全体ではなく、カートに追加された新しい項目のみが含まれます。

ヒント

返品を可能な限り簡潔にしてください。 可能な場合は、モデルに必要な情報のみを返すか、別の LLM プロンプトを使用して情報を要約してから返してください。

手順 2 から 6 を繰り返します

結果がモデルに返されると、プロセスが繰り返されます。 このモデルは、最新のチャット履歴を処理し、応答を生成します。 この場合、モデルは、別のピザをカートに追加するか、チェックアウトするかをユーザーに尋ねる場合があります。

並列関数呼び出し

上記の例では、LLM が 1 つの関数を呼び出す方法を示しました。 多くの場合、複数の関数を順番に呼び出す必要がある場合、これは遅くなる可能性があります。 プロセスを高速化するために、複数の LLM で並列関数呼び出しがサポートされています。 これにより、LLM は一度に複数の関数を呼び出し、プロセスを高速化できます。

たとえば、ユーザーが複数のピザを注文する場合、LLM はピザごとに add_pizza_to_cart 関数を同時に呼び出すことができます。 これにより、LLM へのラウンド トリップの数を大幅に減らし、注文プロセスを高速化できます。

次のステップ

関数呼び出しのしくみを理解したら、 計画に関する記事を参照して、セマンティック カーネルで関数呼び出しを実際に使用する方法を学習できるようになりました