🧚

OpenAI APIのFunction callingと自作のプロンプトを比較してみる

2023/06/18に公開

https://openai.com/blog/function-calling-and-other-api-updates

OpenAIの06/13のアップデートで関数呼び出しに対応したFunction Calling機能が入りました。
これはJSON Schema形式で関数を定義し、その関数の呼び出しに必要な情報を返してくれる機能になります。

https://platform.openai.com/docs/guides/gpt/function-calling

import openai
import json


# Example dummy function hard coded to return the same weather
# In production, this could be your backend API or an external API
def get_current_weather(location, unit="fahrenheit"):
    """Get the current weather in a given location"""
    weather_info = {
        "location": location,
        "temperature": "72",
        "unit": unit,
        "forecast": ["sunny", "windy"],
    }
    return json.dumps(weather_info)


def run_conversation():
    # Step 1: send the conversation and available functions to GPT
    messages = [{"role": "user", "content": "What's the weather like in Boston?"}]
    functions = [
        {
            "name": "get_current_weather",
            "description": "Get the current weather in a given location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city and state, e.g. San Francisco, CA",
                    },
                    "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
                },
                "required": ["location"],
            },
        }
    ]
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=messages,
        functions=functions,
        function_call="auto",  # auto is default, but we'll be explicit
    )
    response_message = response["choices"][0]["message"]

    # Step 2: check if GPT wanted to call a function
    if response_message.get("function_call"):
        # Step 3: call the function
        # Note: the JSON response may not always be valid; be sure to handle errors
        available_functions = {
            "get_current_weather": get_current_weather,
        }  # only one function in this example, but you can have multiple
        function_name = response_message["function_call"]["name"]
        fuction_to_call = available_functions[function_name]
        function_args = json.loads(response_message["function_call"]["arguments"])
        function_response = fuction_to_call(
            location=function_args.get("location"),
            unit=function_args.get("unit"),
        )

        # Step 4: send the info on the function call and function response to GPT
        messages.append(response_message)  # extend conversation with assistant's reply
        messages.append(
            {
                "role": "function",
                "name": function_name,
                "content": function_response,
            }
        )  # extend conversation with function response
        second_response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo-0613",
            messages=messages,
        )  # get a new response from GPT where it can see the function response
        return second_response


print(run_conversation())

サンプルとしては上記のような形で、functionsで定義したget_current_weatherを呼び出しています。
パラメータのfunction_callではnoneauto{"name": "[function name]"}を指定することができ、noneでは常に従来のようなメッセージを返す、autoではメッセージかfunctionsに定義された特定の関数を返す、{"name": "[function name]"}では常に指定した関数をfunctionsから選択するという動作になります。
このサンプルの例なら{"name": "get_current_weather"}と指定した方がより関数を呼び出せる確率が高くなる形ですね。(ただし天気が欲しい以外のメッセージに対応できなくなる)

このような関数呼び出しはLLMではよくあり、どのようなプロンプトにするか試行錯誤が必要でしたが、Function callingがサポートされたことでフレームワークを入れたときのような楽さを得ることができました。

とは言うものの、実際のところ関数の選択の精度はどの程度でしょうか?ユースケースによっては開発の容易さと精度をトレードオフできないケースもあり、導入前にはやはり検証は大切です。
と言うわけで適当な例を元にFunction callingの精度はどのぐらいかを検証してみましょう。

検証コード

https://developer.chatwork.com/reference
検証ではChatworkのAPIをFunction callingの形式で定義します。

https://gist.github.com/k-kinzal/e2d7a6b5b2060694d210936a72830993

あまりガッツリ作り込む気はなかったので、今回は下記のような形でコードを作成しています。

  1. Chatwork APIのドキュメントからFunction callingのJSONを生成するように指示
  2. Function callingのJSONからテストケースを作成
  3. 手動で微調整

そのため、関数名やテストのプロンプトなど微妙なところは残った状態にはなります。おそらくこの辺を整理すると間違いなく精度はあがると思います。

また、本来は関数の引数に期待した値が入っているかもみた方がいいのですが、面倒臭かったので割愛して期待した関数が選択されることを検証しています。そのため、引数の検証したときよりも精度より少し高めに出るかと思われます。

Function Calling

https://gist.github.com/k-kinzal/e2d7a6b5b2060694d210936a72830993#file-prompt1-js

プロンプトは上記の形でモデルだけ切り替えて検証を行なっています。

gpt-3.5-turbo-0613 (prompt1)

テスト対象 結果 失敗ケース
getMe 10/10 -
getStatus 10/10 -
getMyTasks 10/10 -
getContacts 10/10 -
getRoomList 10/10 -
createGroupChat 10/10 -
getRoom 10/10 -
editRoom 10/10 -
deleteOrLeaveRoom (delete) 10/10 -
deleteOrLeaveRoom (leave) 10/10 -
getMessages 10/10 -
sendMessage 9/10 {"getRoom":1,"sendMessage":9}
markMessageAsRead 9/10 {"getChatMessage":1,"markMessageAsRead":9}
updateMessageUnread 10/10 -
getChatMessage 10/10 -
editChatMessage 9/10 {"editChatMessage":9,"getChatMessage":1}
deleteMessage 10/10 -
getTasks 10/10 -
addTaskToChat 10/10 -
getTask 10/10 -
changeTaskStatus 10/10 -
getRoomFiles 10/10 -
uploadFile 9/10 {"getRoom":1,"uploadFile":9}
getChatFile 10/10 -
getRoomInviteLink 10/10 -
createChatroomInviteLink 10/10 -
changeChatroomLink 10/10 {"changeChatroomLink":9,"getRoomInviteLink": 1}
deleteChatInviteLink 10/10 -
getContactRequests 10/10 -
approveContactRequest 10/10 -

傾向としては10/10または9/10で、稀に8/10になるというぐらいでほぼ正解を取ることができます。おそらくtemperatureを少し弄れば安定する結果になっています。

gpt-3.5-turbo-16k-0613 (prompt1)

テスト対象 結果 失敗ケース
getMe 10/10 -
getStatus 10/10 -
getMyTasks 10/10 -
getContacts 10/10 -
getRoomList 10/10 -
createGroupChat 10/10 -
getRoom 10/10 -
editRoom 10/10 -
deleteOrLeaveRoom (delete) 10/10 -
deleteOrLeaveRoom (leave) 10/10 -
getMessages 10/10 -
sendMessage 9/10 {"getMessages":1,"sendMessage":9}
markMessageAsRead 10/10 -
updateMessageUnread 10/10 -
getChatMessage 10/10 -
editChatMessage 10/10 -
deleteMessage 10/10 -
getTasks 10/10 -
addTaskToChat 10/10 -
getTask 10/10 -
changeTaskStatus 10/10 -
getRoomFiles 10/10 -
uploadFile 10/10 -
getChatFile 10/10 -
getRoomInviteLink 10/10 -
createChatroomInviteLink 10/10 -
changeChatroomLink 10/10 -
deleteChatInviteLink 10/10 -
getContactRequests 10/10 -
approveContactRequest 10/10 -

傾向としてほぼ10/10で成功を取ることができました。この結果は想定外だったので何度か実行してみましたが、ほぼこの傾向になります。

gpt-4-0613 (prompt1)

テスト対象 結果 失敗ケース
getMe 10/10 -
getStatus 10/10 -
getMyTasks 10/10 -
getContacts 10/10 -
getRoomList 10/10 -
createGroupChat 10/10 -
getRoom 10/10 -
editRoom 10/10 -
deleteOrLeaveRoom (delete) 10/10 -
deleteOrLeaveRoom (leave) 10/10 -
getMessages 10/10 -
sendMessage 10/10 -
markMessageAsRead 10/10 -
updateMessageUnread 10/10 -
getChatMessage 10/10 -
editChatMessage 10/10 -
deleteMessage 10/10 -
getTasks 10/10 -
addTaskToChat 10/10 -
getTask 10/10 -
changeTaskStatus 10/10 -
getRoomFiles 10/10 -
uploadFile 10/10 -
getChatFile 10/10 -
getRoomInviteLink 10/10 -
createChatroomInviteLink 10/10 -
changeChatroomLink 10/10 -
deleteChatInviteLink 10/10 -
getContactRequests 10/10 -
approveContactRequest 10/10 -

GPT4では3系と比べると精度が出やすいため、想定通り精度は出るようです。

自作プロンプト

https://gist.github.com/k-kinzal/e2d7a6b5b2060694d210936a72830993#file-prompt2-js

プロンプトは上記の形で、結果がJSONになるようにAssistantの指定や、結果の補正が行っています。
prompt1と同様にモデルを切り替えて検証を行なっています。

gpt-3.5-turbo-0613 (prompt2)

テスト対象 結果 失敗ケース
getMe 10/10 -
getStatus 10/10 -
getMyTasks 10/10 -
getContacts 10/10 -
getRoomList 10/10 -
createGroupChat 10/10 -
getRoom 10/10 -
editRoom 10/10 -
deleteOrLeaveRoom (delete) 10/10 -
deleteOrLeaveRoom (leave) 10/10 -
getMessages 10/10 -
sendMessage 10/10 -
markMessageAsRead 10/10 -
updateMessageUnread 10/10 -
getChatMessage 10/10 -
editChatMessage 10/10 -
deleteMessage 10/10 -
getTasks 10/10 -
addTaskToChat 10/10 -
getTask 10/10 -
changeTaskStatus 10/10 -
getRoomFiles 10/10 -
uploadFile 10/10 -
getChatFile 10/10 -
getRoomInviteLink 10/10 -
createChatroomInviteLink 10/10 -
changeChatroomLink 10/10 -
deleteChatInviteLink 10/10 -
getContactRequests 10/10 -
approveContactRequest 10/10 -

傾向としてほぼ10/10になり、稀にJSONのパースエラーが発生することがあります。
発生するJSONのパースエラーのケースは"action""アクション"になるぐらいで、補正ケースの漏れというような形で同一のエラーを起こさないように補正は可能です。

gpt-4-0613 (prompt2)

テスト対象 結果 失敗ケース
getMe 10/10 -
getStatus 10/10 -
getMyTasks 10/10 -
getContacts 10/10 -
getRoomList 10/10 -
createGroupChat 10/10 -
getRoom 10/10 -
editRoom 10/10 -
deleteOrLeaveRoom (delete) 10/10 -
deleteOrLeaveRoom (leave) 10/10 -
getMessages 10/10 -
sendMessage 10/10 -
markMessageAsRead 10/10 -
updateMessageUnread 10/10 -
getChatMessage 10/10 -
editChatMessage 10/10 -
deleteMessage 10/10 -
getTasks 10/10 -
addTaskToChat 10/10 -
getTask 10/10 -
changeTaskStatus 10/10 -
getRoomFiles 10/10 -
uploadFile 10/10 -
getChatFile 10/10 -
getRoomInviteLink 10/10 -
createChatroomInviteLink 10/10 -
changeChatroomLink 10/10 -
deleteChatInviteLink 10/10 -
getContactRequests 10/10 -
approveContactRequest 10/10 -

gpt-3.5-turbo-0613の傾向から分かる通りGPT4でもほぼ成功する傾向になります。

gpt-3.5-turbo-0301 (prompt2)

テスト対象 結果 失敗ケース
getMe 10/10 -
getStatus 9/10 {"action: \"getStatus\",args: []}":1,"getStatus": 9}
getMyTasks 8/10 {"getMyTasks": 8,"アクション:\"getMyTasks\",args: []}":1,"アクション: \"getMyTasks\",引数: []}":1}
getContacts 3/10 {"getContacts": 3,"アクション: getContacts()":1,"以下のような形式でAPIを実行してください。\n\n{\n\"action\": \"getContacts\"\n}":1,"以下のようにgetUserメソッドを使用して自分自身の情報を取得し、そのresponseから自分自身のaccountIdを取得し、getContactsメソッドを使用して自分のコンタクト一覧を取得します。\n\n```\n{\n \"action\": \"getContacts\",\n \"args\": []\n}":1, ... }
getRoomList 8/10 {"'action': 'getRoomList','args': []}":1,"getRoomList":8,"以下のような形式のAPIリクエストを送信してください。\n\n```python\n/api/v2/rooms.get\n```\n\n\nAPIリクエストを送信するためには、APIトークンが必要です。また、APIの詳細についてはドキュメント(https://developer.nulab.com/docs/backlog/api/2/get-room-list/)を参照してください。\n\n回答は下記のフォーマットで回答してください。\n{\n\"action\": \"getRoomList\",\n\"args\": []\n}":1}
createGroupChat 7/10 {"\"action\": \"createGroupChat\",\n\"args\": [\n\"明日の予定相談\",\n\"明日の予定について話し合うためのグループチャットです\",\n]\n}":1,"\"action\": \"createGroupChat\",\n\"args\": [\n\"明日の予定相談\",\n\"明日の予定を相談するためのチャットです\",\n]\n}":1,"createGroupChat":7,"下記のようにcreateGroupChat()アクションを使用して、新しいグループチャットを作成します。\n\n```\n{\n\"action\": \"createGroupChat\",\n\"args\": [\n\"明日の予定相談\",\n\"明日の予定相談について話し合うチャットルームです\"\n]\n}":1}
getRoom 6/10 { "getRoom":6,"アクション: getRoom()\n\n引数:\n - roomId: 1\n\n 以下が、ルームID1のチャット情報になります。\n\n ```\n{\n\"action\": \"getRoom\",\n\"args\": [\n1\n]\n}":1,"アクション: getRoom()\n\n引数: roomId=1\n\n以下のように回答します。 \n\n{\n\"action\": \"getRoom\",\n\"args\": [\n1\n]\n}":1,"以下のように、\"getRoom\"というアクションにルームIDを引数として渡してください。\n\n{\n\"action\": \"getRoom\",\n\"args\": [\n1\n]\n}":1,"以下のように「getRoom」アクションを選択してください。\n\n{\n\"action\": \"getRoom\",\n\"args\": [\n1\n]\n\n}":1}
editRoom 10/10 -
deleteOrLeaveRoom (delete) 9/10 {"'action': 'deleteOrLeaveRoom',\n'args': [\n1, # 削除するグループチャットのRoom ID\n'delete' # 削除する場合は'delete'、退席する場合は'leave'を指定\n]\n}":1,"deleteOrLeaveRoom": 9}
deleteOrLeaveRoom (leave) 10/10 -
getMessages 10/10 -
sendMessage 10/10 -
markMessageAsRead 10/10 -
updateMessageUnread 9/10 {"'action': 'updateMessageUnread',\n'args': [1, '234']\n}":1,"updateMessageUnread":9}
getChatMessage 10/10 -
editChatMessage 10/10 -
deleteMessage 10/10 -
getTasks 10/10 -
addTaskToChat 10/10 -
getTask 10/10 -
changeTaskStatus 10/10 -
getRoomFiles 10/10 -
uploadFile 9/10 {"'action': 'uploadFile',\n'args': [1, 'foo.txt']\n}":1,"uploadFile":9}
getChatFile 10/10 -
getRoomInviteLink 10/10 -
createChatroomInviteLink 8/10 {"\"name\": \"createChatroomInviteLink\",\n\"args\": [\n1,\n\"foo-bar\",\n1\n]\n}":1,"'action': 'createChatroomInviteLink',\n'args': [\n1, # RoomID\n'foo-bar', # code\n0, # needAcceptance\nNone # description(optional)\n]\n\n}":1,"createChatroomInviteLink": 8}
changeChatroomLink 10/10 -
deleteChatInviteLink 9/10 {"deleteChatInviteLink": 9,"以下の情報を入力してください:\n\n```\nアクション:deleteChatInviteLink\n引数:roomId=1\n```\n\n回答例:\n\n```\n{\n\"action\": \"deleteChatInviteLink\",\n\"args\": [\n{\n\"roomId\": 1\n}":1}
getContactRequests 0/10 {"以下のような形式で回答します。\n\n{\n\"action\": \"getContactRequests\"\n}": 1, ...}
approveContactRequest 10/10 -

一つ前のバージョンのgpt-3.5-turbo-0301では選択自体はほ正解しているが、JSONを返すのに失敗するという形で大部分のテストでエラーが発生しました。

ただし、ここはプロンプトの調整の範疇で、例えば

回答は下記のフォーマットで回答してください。
{
  "action": "{アクション名}",
  "args": [
    "{アクションに渡す引数の値}"
  ]
}

という文字列をアクションの一覧の前に持ってくるなどで、JSONエラーがほぼ起きなくなるようになります。
またJSONであることを明示していないのでJSONで返すように指示する、JSON Schemaを書いてこのスキーマを満たすJSONを返すように指示するなどでも変わると思います。

検証外で起きたエラーケース

今回の検証では発生しなかったのですが、はじめに触った際にはfunction_callが空になるケースがありました。(表に出せないコードなのでプロンプトは提示しない)
そのときには通常のメッセージとして下記のような形になっていました。

functions.XXXにaを呼び出すことができます。

[typescriptコード]

関数と引数はあっているのになぜfunctionを返さずにメッセージで返した!!!!!みたいになることは普通に起こり得るため、function_callが空になることはありえる、このケースではそれなりの頻度で発生する。というのでFunction callingだからと過信せずに適切に検証は行なった方が良いかと思います。

まとめ

  • 傾向としてFunction callingより自作プロンプトの方が選択ミスが少なくなる
    • ただし、JSON Parseでエラーが起きる確率は自作の方が高いため、プロンプトの調整や、結果の補正は必要
  • 料金は高くなるがgpt-3.5-turbo-16k-0613だと精度が高く安定する(謎)
  • 料金は高くなるがGPT4は正義

ただし、あくまでも今回のケースではこのような傾向が出たというだけであることを注意してください。
例えば選択肢に入れる関数の数が少なかったり、指定する各種パラメーターの調整次第ではFunction callingでも十分に精度が出ると思います。

余談

この検証で$50ぐらい飛びました。より傾向を出すためには試行回数を10から100回に増やす、1関数の呼び出しに対して数十ケースするなどすると良いことは分かっているのですが、僕のお財布が大変なことになるので勘弁してください。

Discussion