OpenAI ChatGPTのFunction callingを調べてみた

OpenAIからChatGPTの新しいモデル(gpt-3.5-turbo-0613、gpt-3.5-turbo-16k-0613)が発表されました。16kのつくモデルは最大トークン数が従来の4倍になっていて、より多くのコンテキストを使うことができます。また、0613モデルは、Function callingという新しい機能を使うことができます。

Function calling

In an API call, you can describe functions to gpt-3.5-turbo-0613 and gpt-4-0613, and have the model intelligently choose to output a JSON object containing arguments to call those functions. The Chat Completions API does not call the function; instead, the model generates JSON that you can use to call the function in your code.

The latest models (gpt-3.5-turbo-0613 and gpt-4-0613) have been fine-tuned to both detect when a function should to be called (depending on the input) and to respond with JSON that adheres to the function signature. With this capability also comes potential risks. We strongly recommend building in user confirmation flows before taking actions that impact the world on behalf of users (sending an email, posting something online, making a purchase, etc).

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

「Function calling」という名前から、関数を呼び出してくれるものと推測されますが、説明を読んだだけではよくわかりません。サンプルを動かしながら、その機能の使い道について、考えてみました。

サンプルプログラム

まずは、公式のドキュメントを見ながらサンプルプログラムを実行してみます。実験的に色々試してみたいので、Google Colaboratoryを使います。

いつものように、ライブラリのインストールをします。

!pip install openai

次は、認証キーの入力です。

openai.organization = "org-*******+*****"
openai.api_key = "sk-*******************"

その後、公式ドキュメントにある以下のサンプルコードをセルに入力します。

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

このセルを実行すると、結果は以下のようになります。

{
  "id": "chatcmpl-7b0CZp26RA1CFp3ZkvdCkzAbXxgog",
  "object": "chat.completion",
  "created": 1689053267,
  "model": "gpt-3.5-turbo-0613",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "The weather in Boston is currently sunny and windy with a temperature of 72 degrees Fahrenheit."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 72,
    "completion_tokens": 18,
    "total_tokens": 90
  }
}

この動作は、

Step1 ChatGPTに対して、最初の入力を行う。

"What's the weather like in Boston?"

 

Step2 ChatGPTがFunction callが必要か判断する。

Step3 Function callが必要であれば、定義されたダミーのお天気関数”get_current_weather”を呼び出し、以下の結果を得る。

    weather_info = {
        "location": location,
        "temperature": "72",
        "unit": unit,
        "forecast": ["sunny", "windy"],
    }

Step4 Funcrionの結果(weather_info)をメッセージに追加して、2回目のChatGPTを呼び出しを行う。

その結果、Chat GPTは以下の回答を生成します。

The weather in Boston is currently sunny and windy with a temperature of 72 degrees Fahrenheit."

サンプルプログラムの詳細

このプログラムの中で、1回目のChatGPTは、Function callが必要か否かを判断するとともに、最初の"What's the weather like in Boston?"の中から、”Boston”と言うlocationに関する文字列を抽出し、Functionに投げる引数を生成しています。

また、2回目のChatGPTは、Functionから得られたレスポンスと最初のユーザ入力とを入力とし、適切に応答していることがわかります。

もし、1回目にlocation情報がなければどうなるでしょう?

サンプルプログラムを少し修正して、途中経過を表示できるようにしました。

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(input):
    # Step 1: send the conversation and available functions to GPT
    messages = [{"role": "user", "content": input}]
    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"]
    print('--first response--')
    print(response_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"),
        )
        print('--Function response--')
        print(function_response)

        # 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
        print('--second response--')
        print(second_response["choices"][0]["message"]["content"])

ここからは日本語で試していきます。

run_conversation("明日の京都の天気は?")

--first response--
{
  "role": "assistant",
  "content": null,
  "function_call": {
    "name": "get_current_weather",
    "arguments": "{\n  \"location\": \"Kyoto\"\n}"
  }
}
--Function response--
"{\"location\": \"Kyoto\", \"temperature\": \"72\", \"unit\": null, \"forecast\": [\"sunny\", \"windy\"]}"
--second response--
明日の京都の天気は晴れで、風も強くなる予報です。

最初のレスポンスで、Kyotoを引数にして、get_current_weatherを読んでいるのがわかります。

次に、場所を指定せずに、天気を聞いてみます。

run_conversation("明日の天気は?")

この回答は不定でした。

「どこの天気ですか?」と場所を聞いてくる場合もありますし、東京をlocationとしてしまう場合もありました。

後者の場合、1回目のChatGPTがlocationを”東京”と返していましたので、「日本語で聞かれているから東京のことだろう」と、勝手に解釈してしまっているようです。必ず、場所を聞いてくるようにするためには、1回目の呼び出しのmessagesに、{”role”:”system”,"content":" ** "}を追加すると良いかと思います。

  # Step 1: send the conversation and available functions to GPT
    messages = [{"role": "system", "content": "天気を聞かれたらどこの天気が知りたいか場所を確認します。"},{"role": "user", "content": input}]
   

そうすると最初のChatGPTの回答が以下のようになり、FunctionCallは呼び出されません。

--first response--
{
  "role": "assistant",
  "content": "どこの天気を知りたいですか?"
}

Function callingの使い道

Function callingの使い道として最初に思いつくのは、サンプルにあったような天気や、ニュースなどのように、従来のChatGPTでは回答できなかったリアルタイム情報を外部のAPIと組み合わせることです。サンプルではダミーのget_current_weather()を呼び出していましたが、get_current_weatherの中で、公開されているお天気の APIから情報を取得すれば、リアルタイムな回答ができるようになります。

また、独自の予約サイトと繋げることも、できるかもしれません。

先ほどのサンプルに、日付を入れたら予約の空き情報を返すダミーのFunctionを追加してみます。

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)

 ### 予約状況を確認するFunctionを追加
def get_yoyaku(date):                                    
    """
    入力された日付の空き情報を確認します。
    空いていたらOK,空いていなかったらNGを返すようにします。
        今回は、ダミーで常にOKを返しています。
    """
   
    yoyaku_info = {
        "date": date,
        "yoyaku":"OK"
    }
    return json.dumps(yoyaku_info)


def run_conversation(input):
    # Step 1: send the conversation and available functions to GPT
       ### 今日の日付をsystem contentに追加することで、明日とか来週といった今日を起点とする日付表現を使うことができる。
    messages = [{"role": "system", "content": "今日は2023年7月11日です。天気を聞かれたらどこの天気が知りたいか場所を確認します。"},{"role": "user", "content": input}] 
       
       ### functionsの配列に追加
       functions = [
        {
            "name": "get_current_weather",
            "description": "与えられたlocationの天気を返します。",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "都市名、もしくは都道府県名",
                    },
                    "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
                },
                "required": ["location"],
            },
        },
        {
            "name": "get_yoyaku",
            "description": "与えられた日付の空き情報を返します。",
            "parameters": {
                "type": "object",
                "properties": {
                    "date": {
                        "type": "string",
                        "description": "問い合わせの日付",
                    },
                },
                "required": ["date"],
            },
        }
    ]
    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"]
    print('--first response--')
    print(response["id"])
    first_response_text = json.dumps(response_message,indent=2,ensure_ascii=False)
    print(first_response_text)


    # 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,
            "get_yoyaku": get_yoyaku,
        }  # 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"])

                ### Fuctionによって、取得する引数の場合分け
        if function_name == "get_current_weather":
            function_response = fuction_to_call(  
                location=function_args.get("location"),
                unit=function_args.get("unit"),
            )
        if function_name == "get_yoyaku":
            function_response = fuction_to_call(  
                date=function_args.get("date"),
            )
        
        print('--Function response--')
        function_response_text = json.dumps(function_response,indent=2,ensure_ascii=False)
        print(function_response_text)

        # 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
        print('--second response--')
        print(second_response["id"])
        print(second_response["choices"][0]["message"]["content"])

run_conversation("今度の金曜日の予約をしたいのですが、空いてますか?")

--first response--
{
  "role": "assistant",
  "content": null,
  "function_call": {
    "name": "get_yoyaku",
    "arguments": "{\n\"date\": \"2023-07-14\"\n}"
  }
}
--Function response--
"{\"date\": \"2023-07-14\", \"yoyaku\": \"OK\"}"
--second response--
2023年7月14日(金曜日)の予約は空いています。どのような予約を希望されますか?

このように比較的簡単に実装することができました。

Function callingを使わなくても、コードを書くことはできましたが、汎用的なAI-botの場合はコードが煩雑になりがちでした。Function callingを使うことによって、すっきりと実装できるように思います。

ただし、意図しないFunction callingを避けるためにChatGPT呼び出しの際のsystem roleのcontentや、Functionのdescriptionの表現には工夫が必要かもしれません。