AIと友達になろう③ : Function calling

はじめに

Chat Completions APIの機能の一つであるFunction callingについて記載する。(イメージとしてはAndroidの暗黙的インテントのような感じなのかなという認識だが違うのかな?)

Function callingについて: Link


Function calling

概要

2023年に追加されたChat Completions APIの新機能。
function(使える関数とその使い方一覧)とmessage(〇〇について教えて)をLLMに渡し、LLMの方で「そのmessageの応答にはこの関数(function)が必要で、パラメタはこれを渡せばよさそうだ」という具合に判断(※)してもらう仕組み。

※あくまで判断するところまでをLLMで行う。関数の実行自体はしない。また、「そもそも関数を呼ぶ必要があるのか」という判断もする。

大まかな流れ

  1. プログラムからLLMにリクエストを送る。送られたリクエストから、LLMの方で「この質問に答えるためには 〇〇という引数で △△関数を呼ぶ必要がある」と判断。

  2. LLMの判断結果にもとづき、プログラム側であらかじめ定義していた△△関数を実行して、その結果を取得する。

  3. その実行結果を使って、LLMの方でリクエストに対する回答を出力してもらう。

※あえて3を実行させず、2の結果(Json形式)を取得して、別の処理に使うという応用もできる。

実施項目①:リクエストに回答するための関数と引数を定義する

「大まかな流れ1.」に該当。

あらかじめ関数を定義しておき、実行する関数の候補をまとめたリストを用意する。
今回はget_current_weather関数と、その関数を含むリストであるfunctions_listを定義することとする。

[get_current_weather関数]

import json

def get_current_weather(location, unit="celsius"):
  weather_info = {
      "location": location,
      "temperature": 25,
      "unit": "celsius",
      "forecast": ["sunny", "windy"]
  }
  return json.dumps(weather_info)

[get_current_weather関数を呼ぶ側]
・functions_list
  - get_current_weather関数に関する情報を格納。
  - name に関数名を記載。
  - description に関数の説明を記載。
  - parameters に関数の引数情報を記載。
  - required に必須のパラメタが何かを記載。
・messages
  - ユーザからの問い合わせ内容を記載。

これらを使ってChat Completions APIを実行し、「東京の天気を教えて」という問い合わせへの応答として何の関数が必要なのかをLLMに判断してもらう。その結果がresponseに格納されている。

functions_list = [
    {
        # get_current_weather関数について
        "name": "get_current_weather",
        "description": "Get the current weather in a given location",
        "parameters": {
            "type": "object",
            # get_current_weather関数の引数: locationとunit それぞれについて
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The city and state, e.g. Tokyo",
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"]
                },
            },
            # 必須パラメタはstringのlocation (unitはデフォルト値が定義されているから必須ではない)
            "required": ["location"],
        },
    }
]


messages = [
    {"role": "user", "content": "What's the weather like in Tokyo?"}
]

response = openai.chat.completions.create(
    model = "gpt-3.5-turbo",
    messages = messages,
    functions = functions_list
)

print(response)

[responseの中身(例)]

ChatCompletion(
  id='chatcmpl-8ovavyj5rAmkeGGYk4YIXyUOJm6cD',
  choices=[
    Choice(
      finish_reason='function_call',
      index=0,
      logprobs=None,
      message=ChatCompletionMessage(
        content=None,
        role='assistant',
        function_call=FunctionCall(
          arguments='{\n  "location": "Tokyo"\n}',
          name='get_current_weather'
        ),
        tool_calls=None
      )
    )
  ],
  created=1707148485,
  model='gpt-3.5-turbo-0613',
  object='chat.completion',
  system_fingerprint=None,
  usage=CompletionUsage(
    completion_tokens=17,
    prompt_tokens=79,
    total_tokens=96
  )
)

response内でのポイントは以下。
get_current_weather関数に location: "Tokyo" というパラメタを渡すことで問い合わせに対する応答を得られそうだ、とLLMが判断したということ。

function_call=FunctionCall(
    arguments='{\n "location": "Tokyo"\n}', name='get_current_weather'
),

ちなみに、Function callを使わないと判断された応答の場合、function_callの値としてNoneが入る。こちらの「実行結果(例)」にもしれっと書いていたりする。

function_call=None

実施項目②:LLMによって必要があると判断された関数を実行

「大まかな流れ2.」に該当。

response_message = response.choices[0].message

available_functions = {
    "get_current_weather": get_current_weather
}

# get_current_weather
function_name = response_message.function_call.name
function_to_call = available_functions[function_name]
function_args = json.loads(response_message.function_call.arguments)


function_response = function_to_call(
    location = function_args.get("location"),
    unit = function_args.get("unit")
)

print(function_response)

response.choices[0].message の中身は以下。これを変数response_messageに代入し、関数名やパラメタの属性を指定して値を取得している。

message=ChatCompletionMessage(
    content=None,
    role='assistant',
    function_call=FunctionCall(
        arguments='{\n "location": "Tokyo"\n}',
        name='get_current_weather'
    ), tool_calls=None
)

available_functions では、関数名をkey、関数そのものをvalueとしたペアが辞書形式で登録されている。
その辞書に対して、

function_name = response_message.function_call.name

で取得した値: "get_current_weather" を渡して、それに対応する関数:get_current_weather を取得。

Pythonでは、下記にある通り、辞書のValueとして関数そのものを指定することができるらしい。
Pythonの関数を辞書(dict)のValueに格納して、Keyに応じた関数呼び出しをする方法

実行する関数が決まったら、それに必要な引数を指定して、その関数を実行してレスポンスを取得する。

function_response = function_to_call(
    location = function_args.get("location"),         # Tokyo
    unit = function_args.get("unit")                       # None
)

[get_current_weather関数を実行結果]
東京の温度は25度で、予報は晴れ・風は強い という結果。

{"location": "Tokyo", "temperature": 25, "unit": "celsius", "forecast": ["sunny", "windy"]}

この結果はLLMが出したわけではなく、単にPythonで関数を実行しただけ。
Pythonで関数の事項結果が得られたら、LLMに再度リクエストを送る。

[ここまでのまとめ]

  • 「東京の天気を知りたいんだけど」というリクエスト送ったらその指示内容から、LLMの方で「こ質問に答のえるためには Tokyo という引数で get_current_weatherを呼ぶ必要がある」と判断

  • Pythonで事前に定義していたget_current_weather関数を実行して、その結果を取得する(←今ココ)

  • その実行結果を使って、LLMの方で「東京の天気を知りたいんだけど」というリクエストに対する回答を出力してもらう(←次のステップ)

実施項目③:get_current_weatherの結果から問い合わせへの応答を得る

実施項目②にて、get_current_weather関数を実行して「東京の温度は25度で、予報は晴れ・風は強い」という結果が得られた。
この結果を使って、もともとの「東京の天気を教えて」という問い合わせへの応答を得る。

Pythonで定義した関数get_current_weatherの実行結果が得られたら、LLMに再度リクエストを送る。
最初に送ったリクエスト、

messages = [
    {"role": "user", "content": "What's the weather like in Tokyo?"}
]

にLLMの応答のmesssagesを追加する。
具体的には以下の通り。

messages.append(response_message)          # response_messageはresponse.choices[0].message の値
messages.append(
    {
        "role": "function",
        "name": function_name,             # "get_current_weather"
        "content": function_response       # {"location": "Tokyo", "temperature": 25, "unit": "celsius", "forecast": ["sunny", "windy"]}
    }
)

この時点で、messageの値は以下のようになっている。

# messageの値の中身

[
  {
    'role': 'user', 
    'content': "What's the weather like in Tokyo?"
  }, 

  ChatCompletionMessage(
    content=None, 
    role='assistant', 
    function_call=FunctionCall(
      arguments='{"location":"Tokyo"}', 
      name='get_current_weather'), 
      tool_calls=None
  ), 
  {
    'role': 'function', 
    'name': 'get_current_weather', 
    'content': '{"location": "Tokyo", "temperature": 25, "unit": "celsius", "forecast": ["sunny", "windy"]}'
  }, 

  ChatCompletionMessage(
    content=None, 
    role='assistant', 
    function_call=FunctionCall(
      arguments='{"location":"Tokyo"}', 
      name='get_current_weather'
    ), 
    tool_calls=None
  ), 
  {
    'role': 'function', 
    'name': 'get_current_weather', 
    'content': '{"location": "Tokyo", "temperature": 25, "unit": "celsius", "forecast": ["sunny", "windy"]}'
  }
]

そのmessagesを使って、改めて「東京の天気を知りたいんだけど」というリクエストを送る。そのレスポンスには、「東京の温度は25度、晴れで風が強い」という値が入っている。

second_response = openai.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=messages                # messages(右辺)の値はappendしてできた値
)

print(second_response.choices[0].message.content)
# 結果: The weather in Tokyo is currently sunny and windy with a temperature of 25 degrees Celsius.


まとめ

Function callingについて記載した。
プログラムからLLMへの問い合わせは2回。

1回目は、「このリクエストへの応答を用意するにはどの関数を使えばいいと思う?」という内容。LLMが判断した「この関数じゃないですかね」という結果に基づいて、プログラム側でその関数を実行する。(実施事項①②)

2回目は、その関数の実行結果をもとに「実行結果はこれなのでいい感じの応答文を書いてこちらにください」という内容。(実施事項③)
messageの内容を連結して、「こういうやり取りを踏まえて文言ください」というリクエストを組み立てる。

Function callingにより、リクエストに応じて処理させる関数をハンドリングさせることができるようになる。今回のサンプルでは関数の数は1個だけだったが、うまく使えれば色々応用が利きそうな気がした。


参考文献