より高速に効率的にFunction callingを実行できる!!OpenAI DevDayで発表されたFunction callingの並列実行について試してみた。

2023.11.07

こんちには。

データアナリティクス事業本部 インテグレーション部 機械学習チームの中村です。

今回は前回の記事で書ききれなかった「Function callingの並列化」にフォーカスして試してみます。

機能の概要

Function callingが更新され、1つのメッセージで複数の関数を呼び出すように動作するようになりました。(正確には複数呼び出すようにレスポンス側に指示が来ます)

こちらはgpt-4-1106-previewgpt-3.5-turbo-1106で使用可能です。

また、Function calling自体の精度も向上され、正しい関数パラメータを返す可能性が高くなっているようです。

上記の公式ガイドでは並列的なFunction callingの例として、例えば3箇所の天気を取得する機能を例として説明されています。

こちらについて試してみようと思います。

試してみた

前提

ツールの定義

まずはツールとして呼び出したい関数を定義します。

import json

def get_current_weather(location, unit="celsius"):
    """Get the current weather in a given location"""
    if "tokyo" in location.lower():
        return json.dumps({"location": location, "temperature": "10", "unit": "celsius"})
    elif "san francisco" in location.lower():
        return json.dumps({"location": location, "temperature": "72", "unit": "fahrenheit"})
    else:
        return json.dumps({"location": location, "temperature": "22", "unit": "celsius"})

次にこの関数をツールとしてもつtoolsを定義します。(データ型はList[ChatCompletionToolParam]となるようにします)

from openai.types.chat import ChatCompletionToolParam

tools = [
    ChatCompletionToolParam({
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "指定した場所の現在の天気を取得する",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "県名や都市名(例:東京、サンフランシスコ)",
                    },
                    "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
                },
                "required": ["location"],
            },
        },
    })
]

こちらのtoolsの定義ですが、functionsを使う場合とは構造が変わっており注意が必要です。

toolsの場合は"type": "function"というtypeの指定が増え、ネストが1段階増えているようです。

関数以外の様々な用途を想定したtoolsという形に変わっており、今後にアップデートにも期待が膨らむところです。

ツールを指定してチャット

そして以下のようにtoolsを引数に指定してチャットAPIを呼び出します。

from openai import OpenAI

client = OpenAI(
    api_key = "ここにOpenAI APIキーを記載"
)

response = client.chat.completions.create(
    model="gpt-3.5-turbo-1106",
    temperature=0.0,
    messages=[
        {"role": "user", "content": "サンフランシスコ、東京、大阪の天気は?"},
    ],
    tools=tools,
    tool_choice="auto",  # auto is default, but we'll be explicit
)

呼び出し時もtoolstool_choiceという新しい引数を使用します。(以前はfunctionsfunction_callでしたね)

結果を確認してみましょう。

print(response.model_dump_json(indent=2))

# {
#   "id": "chatcmpl-8IDRfTGaObZEqfL5kpuVx2IeVjIDH",
#   "choices": [
#     {
#       "finish_reason": "tool_calls",
#       "index": 0,
#       "message": {
#         "content": null,
#         "role": "assistant",
#         "function_call": null,
#         "tool_calls": [
#           {
#             "id": "call_uVBN0txk9txPyqkmhkAZrJnf",
#             "function": {
#               "arguments": "{\"location\": \"San Francisco\"}",
#               "name": "get_current_weather"
#             },
#             "type": "function"
#           },
#           {
#             "id": "call_5YDoZydCJaoRrE8NEZg9Rjmu",
#             "function": {
#               "arguments": "{\"location\": \"Tokyo\"}",
#               "name": "get_current_weather"
#             },
#             "type": "function"
#           },
#           {
#             "id": "call_fEGI3Wf2ZPFvW9S95RtmFtR8",
#             "function": {
#               "arguments": "{\"location\": \"Osaka\"}",
#               "name": "get_current_weather"
#             },
#             "type": "function"
#           }
#         ]
#       }
#     }
#   ],
#   "created": 1699352279,
#   "model": "gpt-3.5-turbo-1106",
#   "object": "chat.completion",
#   "system_fingerprint": "fp_eeff13170a",
#   "usage": {
#     "completion_tokens": 63,
#     "prompt_tokens": 114,
#     "total_tokens": 177
#   }
# }

"finish_reason": "tool_calls"となっており、messageにもtool_callsが含まれています。

tool_callsにはどのツールを呼び出すべきかのリストが渡されていることが分かります。

これにより1回のレスポンスで複数のツール(今回はfunction)を開発者側で呼び出すことができます。

ツールを開発者側で呼び出す

各ツールの結果を以下のようなフォーマットでコンテキストして格納して渡すと、続きのチャットが可能です。

{
    "tool_call_id": tool_call.id,
    "role": "tool",
    "name": function_name,
    "content": function_response,
}

まずは関数呼び出しの結果を取得します。

from openai.types.chat import ChatCompletionToolMessageParam, ChatCompletionUserMessageParam

tool_response_messages = []

if response.choices[0].message.tool_calls is not None:
    tool_calls = response.choices[0].message.tool_calls

    for tool_call in tool_calls:
        if tool_call.type == "function":
            function_call = tool_call.function
            function_name = function_call.name

            available_functions = [t.get("function").get("name") for t in tools if t.get("type") == "function"]

            if function_name in available_functions:
                function_arguments = function_call.arguments
                function_response = eval(function_name)(**eval(function_arguments))
            else:
                raise Exception

            tool_response_messages.append(
                ChatCompletionToolMessageParam({
                    "tool_call_id": tool_call.id,
                    "role": "tool",
                    "content": function_response,
                })
            )  # extend conversation with function response

このツールの結果と、初回のリクエストとレスポンスを一つのリストにまとめます。

history_messages = [
    ChatCompletionUserMessageParam({"role": "user", "content": "サンフランシスコ、東京、大阪の天気は?"}),
    response.choices[0].message,
    *tool_response_messages
]

これを次のリクエストに渡すことで、続きを得ることができます。

続きのチャットを得る

以下で続きを得るためのリクエストを実行します。

response = client.chat.completions.create(
    model="gpt-3.5-turbo-1106",
    temperature=0.0,
    messages=[
        *history_messages
    ],
)

print(response["choices"][0]["message"]["content"])

# サンフランシスコの天気は摂氏22度で晴れ、東京の天気は摂氏10度で曇り、大阪の天気は摂氏22度で晴れです。

正しく使用できました。

まとめ

いかがでしたでしょうか。

ほぼ従来のFunction callingと使い方は同じですが、構造や引数名が変わっている点には注意が必要そうです。

本記事がご参考になれば幸いです。