Azure OpenAI Service 09 - Function Calling 介紹和實做串接

OpenAI 在前陣子公布了新的模型版本 (gpt-4-0613gpt-3.5-turbo-0613 ) 以及新增了 Function Calling 的功能,Function Calling 這個功能可以讓我們預先定義好函示名稱和回傳的參數結果,模型會判斷聊天內容是否符合設定的函示,是的話就會把聊天內容取出函示所需要的參數並且回傳 Json 格式的資料,方便我們接收到之後可以給函示來處理,使用 Function Calling 的好處在於不需要針對每個對話的提示詞多加上說明要回傳的格式,也只會回傳 Json 格式的結果,不會包含多於文字,變成我們要另外處理才可以正確取出要的 Json 內容,如此一來我們寫聊天機器人或是外掛程式的時候就不用針對使用的傳的訊息預先判斷再加上特定提示詞來保證回傳結果,可以完全透過模型的判斷即可。而 Azure OpenAI Service 雖然沒第一時間就支援,但是也在稍晚也推出新版的模型,後面就來說明要如何實做以及測試的結果。

實做

不管是 OpenAI 或是 Azure OpenAI 都要確保模型是 GPT 3.5 或 GPT4 的 0613 版本,這一個版本的模型才有辦法支援 Function calling 。

接下來我們一樣用 Azure OpenAI client library for .NET 來寫我們的程式,但是要注意版本要是 beta 6 以上才有支援 Function calling 這個功能。

我定義了兩個函示和簡單實做函示,定義裡函數以及參數的 Description 寫的越清楚,模型未來在判斷的時候就會更準確,參數的類型也是要注意,只是文件裡面都沒特別提到可以吃的參數名稱,像是整數需要用完整的名稱 integer 而不行用 int ,設定不對的話模型也會回傳錯誤就是,Required 則為必填的參數。

CalFoodPrice

定義

var calFoodPriceChatTool = ChatTool.CreateFunctionTool(
    functionName: "CalFoodPrice",
    functionDescription: "計算客戶餐點價錢",
    functionParameters: BinaryData.FromObjectAsJson(
        new
        {
            Type = "object",
            Properties = new
            {
                Count = new
                {
                    Type = "integer",
                    Description = "客戶點的餐點數量,比如說一份"
                },
                Food = new
                {
                    Type = "string",
                    Enum = new[] { "牛排", "豬排" },
                    Description = "客戶點的餐點,比如說牛排或是豬排。"
                }
            },
            Required = new[] { "count", "food" },
        }, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
);

方法實做

public static decimal CalFoodPrice(FoodInfo foodInfo)
{
    switch (foodInfo.Food)
    {
        case "牛排":
            return 200 * foodInfo.Count;
        case "豬排":
            return 100 * foodInfo.Count;
        default:
            return 0;
    }
}

GetCurrentWeather

定義

var getWeatherChatTool = ChatTool.CreateFunctionTool(
    functionName: "GetCurrentWeather",
    functionDescription: "取得指定地點的天氣資訊",
    functionParameters: BinaryData.FromObjectAsJson(
        new
        {
            Type = "object",
            Properties = new
            {
                Location = new
                {
                    Type = "string",
                    Description = "城市或鄉鎮地點",
                },
                Unit = new
                {
                    Type = "string",
                    Enum = new[] { "攝氏", "華式" },
                }
            },
            Required = new[] { "location" },
        }, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
);

函示實做

public static string GetCurrentWeather(WeatherConfig weatherConfig)
{
    switch (weatherConfig.Unit)
    {

        case "華式":
            return $"{weatherConfig.Location} 目前華式 86 度";
        case "攝氏":
        default:
            return $"{weatherConfig.Location} 目前攝氏 30 度";
    }
}

定義完成之後再傳進去的聊天設定不一樣的點就是要設定我們傳進去的函數有哪些。

var chatCompletionsOptions = new ChatCompletionOptions() { 
     Tools = { 
        calFoodPriceChatTool,
        getWeatherChatTool
      }
};

再來定義幾組測試的問題。

List<ChatMessage> conversationMessages =
[
    //new UserChatMessage("我想要點兩份牛排"),
    //new UserChatMessage("請問台北市的天氣?"),
    new UserChatMessage("請問 PS5 多少錢?"),
];

拿到回應結果之後可以透過 FinishReason 來判斷是否有被判斷成函示呼叫,有的話就會回傳判斷應該使用的函數名稱以及應該傳進去的參數內容。

var client = new AzureOpenAIClient(new Uri(apiUrl), new AzureKeyCredential(apikey)).GetChatClient(deploymentName);
ChatCompletion completion = client.CompleteChat(conversationMessages, options: chatCompletionsOptions);

if (completion.FinishReason == ChatFinishReason.ToolCalls)
{
    foreach (var toolCall in completion.ToolCalls)
    {
        switch (toolCall.FunctionName)
        {
            case "CalFoodPrice":
                var foodInfo = JsonSerializer.Deserialize<FoodInfo>(toolCall.FunctionArguments);
                var foodPrice = Functions.CalFoodPrice(foodInfo);
                Console.WriteLine($"您點了 {foodInfo.Count} 份 {foodInfo.Food} 總共 {foodPrice} 元");
                break;
            case "GetCurrentWeather":
                var weatherConfig = JsonSerializer.Deserialize<WeatherConfig>(toolCall.FunctionArguments);
                Console.WriteLine(Functions.GetCurrentWeather(weatherConfig));
                break;
            default:
                break;
        }
    }
}
else
{
    Console.Write(completion);
}

測試結果

我準備了三個情境,剛好可以測試出我的兩個函示和一般情境的結果,前面兩個都有被正確的判斷成我們要的函示並且取出我要的參數,然後我再傳進去寫好的函示處理完之後拿到結果再回應給使用者,第三個情境就都不在我定義的兩個函示內,就會回應正常的一般回應了。

完整範例程式可以到 GitHub 下載。

結論

簡單的針對 Function calling 做了介紹和實做,這個功能又讓 ChatGPT 模型方便使用了,尤其我們使用情境如果會很常需要透過模型幫我們判斷對話的意圖的話,判斷完之後就可以幫我們整理成之後我們要呼叫的函示以及參數,就不用預先針對對話要先判斷要呼叫哪一個函示然後再要他回應特定格式的 Json ,如果函示多的時候就會讓聊天機器人的設計和判斷邏輯變得困難,這時候透過 Function calling 就可以方便我們後續的串接和使用。

參考資料