채팅 완성에서의 함수 호출(function calling)

채팅 완성에서의 함수 호출(function calling)

채팅 완성(chat completion)의 가장 강력한 기능은 모델로부터 함수를 호출할 수 있다는 거예요. 이 덕분에 기존 코드와 상호작용하는 챗봇을 만들어 비즈니스 프로세스를 자동화하고 코드 스니펫을 만들 수 있습니다.

Semantic Kernel은 함수와 파라미터를 모델에 자동으로 설명하고, 모델과 여러분 코드 사이의 왕복 커뮤니케이션을 처리함으로써 함수 호출 사용 과정을 단순화해요. 다만 함수 호출을 쓸 때는 코드를 최적화하고 이 기능을 최대한 활용하기 위해, 뒤에서 무엇이 실제로 일어나는지 이해하는 게 좋습니다.

출처: 공식 문서 — Function calling with chat completion

자동 함수 호출은 어떻게 동작하나

참고. 아래 섹션은 Semantic Kernel에서 자동 함수 호출이 어떻게 동작하는지 설명합니다. 자동 함수 호출은 Semantic Kernel의 기본 동작이지만, 원한다면 함수를 수동으로 호출할 수도 있어요. 수동 함수 호출에 대한 자세한 내용은 함수 호출 문서를 참고하세요.

함수 호출을 활성화한 채 모델에 요청을 보내면, Semantic Kernel은 다음 단계를 수행합니다.

# 단계 설명
1 함수 직렬화 커널의 모든 사용 가능한 함수(및 입력 파라미터)를 JSON 스키마로 직렬화합니다.
2 메시지와 함수를 모델에 전송 직렬화된 함수(그리고 현재 채팅 기록)를 입력의 일부로 모델에 보냅니다.
3 모델이 입력 처리 모델이 입력을 처리하고 응답을 생성합니다. 응답은 채팅 메시지 또는 하나 이상의 함수 호출일 수 있어요.
4 응답 처리 응답이 채팅 메시지면 호출자에게 반환합니다. 함수 호출이면 Semantic Kernel이 함수 이름과 파라미터를 추출합니다.
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: ***
    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 데코레이터가 붙은 함수만 직렬화되어 모델에 전송됩니다. 모델에 노출하고 싶지 않은 헬퍼 함수를 가질 수 있어요.

Java의 경우 플러그인은 @DefineKernelFunction 어노테이션과 @KernelFunctionParameter 어노테이션으로 함수와 파라미터를 정의하며, KernelPluginFactory.createFromObject로 플러그인을 만들고 Kernel.builder().withPlugin(...)으로 추가합니다. Java에서도 @DefineKernelFunction이 붙은 함수만 직렬화되어 모델에 전송됩니다. (자세한 Java 코드 샘플은 문서 원문의 OrderPizzaPlugin 클래스와 Kernel.builder() 부분을 참고하세요.)

자동 함수 호출의 예약 파라미터 이름

KernelFunction에서 자동 함수 호출을 쓸 때 일부 파라미터 이름은 예약되어 특별 처리를 받습니다. 이 예약 이름 덕분에 함수 실행에 필요한 핵심 객체에 자동으로 접근할 수 있어요.

예약된 파라미터 이름은 다음과 같습니다.

  • kernel
  • service
  • execution_settings
  • arguments

함수 호출 중에 gather_function_parameters 메서드가 각 파라미터를 검사합니다. 파라미터 이름이 예약 이름 중 하나와 일치하면 특정 객체로 채워집니다.

  • kernel: 커널 객체가 주입됩니다.
  • service: 제공된 인자를 기반으로 선택된 AI 서비스로 채워집니다.
  • 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)

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 파라미터는 enum이고, toppings는 enum 배열이에요. 이러면 모델이 더 정확한 응답을 생성하는 데 도움이 됩니다.

    팁. 가능하면 파라미터 타입으로 string을 쓰는 걸 피하세요. 모델은 string의 타입을 추론할 수 없어 모호한 응답으로 이어질 수 있습니다. 대신 가능하면 enum이나 다른 타입(예: int, float, 복합 타입)을 쓰세요.

  3. 필수 파라미터 — 어떤 파라미터가 필수인지 지정할 수도 있어요. 모델이 함수가 동작하는 데 실제로 필요한 파라미터가 무엇인지 이해하는 데 중요합니다. 3단계에서 모델은 이 정보를 사용해 함수 호출에 필요한 최소 정보만 제공합니다.

    팁. 정말 필요한 파라미터만 필수로 표시하세요. 그러면 모델이 함수를 더 빠르고 정확하게 호출하는 데 도움이 됩니다.

  4. 함수 설명 — 함수 설명은 선택이지만 모델이 더 정확한 응답을 생성하는 데 도움이 됩니다. 특히 설명은 반환 타입이 스키마에 직렬화되지 않으므로 응답에서 무엇을 기대할지 모델에 알려줄 수 있어요. 모델이 함수를 잘못 사용하고 있다면 설명으로 예시와 안내를 추가할 수도 있습니다.

    예를 들어 get_pizza_from_cart 함수에서 설명은 이전 메시지에 의존하는 대신 이 함수를 쓰라고 알려줘요. 장바구니가 지난 메시지 이후 바뀌었을 수 있기 때문입니다.

    팁. 설명을 추가하기 전에 "모델이 응답을 생성하는 데 정말 이 정보가 필요한가?"라고 자문하세요. 아니라면 장황함을 줄이기 위해 빼는 걸 고려하세요. 모델이 함수를 제대로 쓰지 못하면 나중에 언제든 설명을 추가할 수 있어요.

  5. 플러그인 이름 — 직렬화된 함수에서 볼 수 있듯이 각 함수에는 name 속성이 있어요. Semantic Kernel은 플러그인 이름으로 함수를 네임스페이스화(namespace)합니다. 이는 같은 이름의 함수를 가진 여러 플러그인을 가질 수 있게 해 주므로 중요해요. 예를 들어 여러 검색 서비스용 플러그인을 각각의 search 함수와 함께 가질 수 있습니다. 함수를 네임스페이스화하면 충돌을 피하고 모델이 어떤 함수를 호출해야 할지 이해하기 쉽게 만들어 줍니다.

    이를 알았다면, 고유하고 설명적인 플러그인 이름을 선택해야 해요. 위 예시에서 플러그인 이름은 OrderPizza로, 함수들이 피자 주문과 관련돼 있다는 걸 분명히 합니다.

    팁. 플러그인 이름을 지을 때 "plugin"이나 "service" 같은 불필요한 단어를 제거하길 권장합니다. 장황함을 줄이고 모델이 플러그인 이름을 이해하기 쉽게 만들어 주니까요.

    참고. 기본적으로 함수 이름의 구분자는 -예요. 대부분의 모델에 잘 동작하지만 Gemini처럼 다른 요구사항이 있는 모델도 있어요. 커널이 자동으로 처리하지만, 직렬화된 함수에서 약간 다른 함수 이름이 보일 수 있습니다.

2) 메시지와 함수를 모델에 전송하기

함수가 직렬화되면 현재 채팅 기록과 함께 모델로 전송됩니다. 이러면 모델이 대화의 컨텍스트와 사용 가능한 함수를 이해할 수 있어요.

이 시나리오에서 사용자가 조수에게 장바구니에 피자를 추가해 달라고 요청했다고 상상해 볼게요.

ChatHistory 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() 동작을 사용해요. 사용 가능한 몇 가지 동작 중 하나입니다. 다른 함수 선택 동작에 대한 자세한 내용은 함수 선택 동작 문서를 확인하세요.

3) 모델이 입력을 처리하기

채팅 기록과 직렬화된 함수를 모두 갖고, 모델은 응답할 최선의 방법을 결정할 수 있어요. 이 경우 모델은 사용자가 피자를 주문하려 한다는 걸 알아챕니다. add_pizza_to_cart 함수를 호출하고 싶겠지만, size와 toppings를 필수 파라미터로 지정했기 때문에 모델은 이 정보를 사용자에게 물어봅니다.

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

모델은 사용자가 다음에 응답하길 원하므로, Semantic Kernel은 자동 함수 호출을 멈추고 제어권을 사용자에게 돌려줍니다. 이 시점에서 사용자는 주문하려는 피자의 크기와 토핑으로 응답할 수 있어요.

chatHistory.AddUserMessage("I'd like a medium pizza with cheese and pepperoni, please.");

response = await chatCompletion.GetChatMessageContentAsync(
    chatHistory,
    kernel: kernel)

이제 모델은 필요한 정보를 갖게 됐으니, 사용자 입력으로 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를 쓰는 걸 볼 수 있어요. 모델에게 한두 자리 숫자를 보내라고 하는 게 GUID를 요구하는 것보다 훨씬 쉽습니다.

중요. 이 단계가 함수 호출을 강력하게 만드는 핵심이에요. 이전에는 AI 앱 개발자가 의도 추출(intent)과 슬롯 채움(slot fill) 함수를 위한 별도 프로세스를 만들어야 했습니다. 함수 호출 덕분에 모델이 언제 함수를 호출하고 무엇을 제공할지 결정할 수 있어요.

4) 응답 처리하기

Semantic Kernel은 모델로부터 응답을 받으면 그 응답이 함수 호출인지 확인합니다. 함수 호출이라면 함수 이름과 파라미터를 추출합니다. 이 경우 함수 이름은 OrderPizzaPlugin-add_pizza_to_cart이고, 인자는 피자의 크기와 토핑이에요.

이 정보로 Semantic Kernel은 입력을 적절한 타입으로 마샬링하고 OrderPizzaPluginadd_pizza_to_cart 함수에 전달합니다. 이 예에서 인자는 JSON 문자열로 시작하지만, Semantic Kernel이 PizzaSize enum과 List<PizzaToppings>로 역직렬화합니다.

참고. 입력을 올바른 타입으로 마샬링하는 것은 Semantic Kernel을 쓰는 핵심 이점 중 하나예요. 모델에서 오는 모든 것은 JSON 객체로 오지만, Semantic Kernel은 이 객체들을 함수에 맞는 올바른 타입으로 자동 역직렬화할 수 있습니다.

5) 함수 호출하기

Semantic Kernel이 올바른 타입을 갖게 되면, 마침내 add_pizza_to_cart 함수를 호출할 수 있습니다. 플러그인이 의존성 주입을 사용하므로, 함수는 pizzaServiceuserContext 같은 외부 서비스와 상호작용해 사용자 장바구니에 피자를 추가할 수 있어요.

하지만 모든 함수가 성공하는 건 아닙니다. 함수가 실패하면 Semantic Kernel은 오류를 처리하고 모델에 기본 응답을 제공할 수 있어요. 이러면 모델이 무엇이 잘못됐는지 이해하고 재시도하거나 사용자에게 응답을 생성할지 결정할 수 있습니다.

팁. 모델이 스스로 교정하도록 하려면, 무엇이 잘못됐고 어떻게 고칠지를 분명히 전달하는 오류 메시지를 제공하는 게 중요해요. 이러면 모델이 올바른 정보로 함수 호출을 재시도하는 데 도움이 됩니다.

참고. Semantic Kernel은 기본적으로 함수를 자동으로 호출합니다. 하지만 함수 호출을 직접 관리하고 싶다면 수동 함수 호출 모드를 활성화할 수 있어요. 자세한 내용은 함수 호출 문서를 참고하세요.

6) 함수 결과 반환하기

함수가 호출된 후, 함수 결과는 채팅 기록의 일부로 모델에 다시 보내집니다. 이러면 모델이 대화의 컨텍스트를 이해하고 후속 응답을 생성할 수 있어요. 뒷단에서 Semantic Kernel은 tool 역할의 새 메시지를 채팅 기록에 추가합니다.

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

결과는 모델이 처리해야 하는 JSON 문자열이라는 점을 확인하세요. 이전과 마찬가지로 모델은 이 정보를 소비하는 데 토큰을 써야 합니다. 그래서 반환 타입을 가능한 한 단순하게 유지하는 게 중요합니다. 이 경우 반환은 장바구니에 추가된 새 항목만 포함하며, 장바구니 전체가 아닙니다.

팁. 반환을 가능한 한 간결하게 하세요. 가능하면 모델이 필요한 정보만 반환하거나, 반환하기 전에 다른 LLM 프롬프트로 정보를 요약하세요.

2~6단계 반복하기

결과가 모델에 반환되면 과정이 반복됩니다. 모델은 최신 채팅 기록을 처리하고 응답을 생성합니다. 이 경우 모델이 사용자에게 장바구니에 피자를 또 추가할지, 결제할지 물을 수 있어요.

병렬 함수 호출

위 예시에서 LLM이 단일 함수를 호출하는 법을 보여드렸어요. 여러 함수를 순차적으로 호출해야 한다면 종종 느릴 수 있습니다. 과정을 빠르게 하기 위해 여러 LLM이 병렬 함수 호출을 지원합니다. LLM이 여러 함수를 한 번에 호출할 수 있어 과정이 빨라집니다.

예를 들어 사용자가 여러 피자를 주문하려 하면, LLM이 각 피자에 대해 add_pizza_to_cart 함수를 동시에 호출할 수 있어요. 이러면 LLM으로의 왕복 횟수를 크게 줄이고 주문 과정을 빠르게 만들 수 있습니다.

더 알아보기 (Learn more)