Function Callingを使ってAIに独自の関数を実行させる
Function CallingはChatGPT APIの公式機能の一つで、これを使用すると、呼び出し元で定義された任意の関数をAIに実行させることができます。処理のフローは通常の会話の流れと少し異なり、次のステップで構成されます。
- ユーザーからの質問文と共に、実行可能な関数の名称、用途、引数の仕様をChatGPTに送信する
- AIが関数の実行が必要だと判断した場合、対象の関数名と、具体的な引数の内容が返却される
- 指定された引数を使用して、呼び出し元で対象の関数を実行する
- 実行結果をChatGPTに再送信する
- ChatGPTから最終的な返答を受け取り、ユーザーに返却する
この流れからもわかるように、「AIが関数を実行する」と言っても、ChatGPTが関数を直接実行するわけではありません。API経由でChatGPTに送信されるのは、関数の本体ではなく、その使用方法を示した仕様のみで、ChatGPTはそれを基に「必要な関数の選択」と「引数の指定」を行います。その後、呼び出し元で関数の実行が行われます。
ここで特に重要なのは、引数の受け渡し方法です。通常の会話で生成される自由な形式の文章では、文中にある本当に必要なデータをプログラムで拾うのが非常に困難ですが、Function Callingを使用すると、AIは必要な引数を仕様通りに構造化されたJSONデータとして返却してくれます。これにより、関数の実行に必要な値を確実に取得できる仕組みになっています。
次は、この実装方法についてさらに詳しく解説していきます。
実装方法
例として、冒頭でお見せしたようなスマート電球を操作するAIを作ってみましょう。
ご紹介するコードは、Google Colab等に上から順に貼り付けて実行すれば動作する想定ですが、説明の都合上、一度だけ会話を行なったら終了するようになっています。もし連続した会話を試したい場合は、前回の記事でご紹介した内容と組み合わせて実装してみてください。
事前準備
まずは前回の記事と同様に必要なライブラリをインストールします。
!pip install openai==1.3.4
本記事では、執筆時点で最新であるVer 1.3.4を対象とします。2023年11月に正式リリースされた1系と、それ以前の0系では使用方法に差異がありますので、ローカル環境で過去のバージョンを利用している方はご注意ください。
次に、OpenAIのアクセスキーをセットして、クライアントオブジェクトを作成します。
import os from openai import OpenAI # アクセスキーを環境変数にセット os.environ['OPENAI_API_KEY'] = "<アクセスキー>" # クライアントを作成 client = OpenAI()
<アクセスキー> の部分にはご自身のアクセスキーを入力してください。
前回の記事とほぼ同様の内容となりますが、事前準備はこちらで完了です。ここから、Function Callingを実装していきます。
1. 関数を定義する
まず、AIに実行させる関数を定義します。今回は以下のような仕様で作成します。
- 関数名: light_control
-
用途: ユーザーの部屋の照明を操作する
-
引数:
-
power: 電源の状態。 “ON”または“OFF”の文字列で指定する。
-
brightness: 明るさ。0〜100の数値で指定する。
-
color: 照明の色。RGBを表す3要素の配列で指定する。内容は0〜255の数値。
-
関数の実体は以下のように実装してみました。実際に電球の操作を行うには対応デバイスを用意する必要があるため、ここでは「どのように操作が実行されたか」をコンソールに表示するだけのものとして実装してあります。この内容であれば、実物のデバイスがなくてもAIによる家電操作を擬似的に体験できますので、余裕があればエアコンなど照明以外の家電を操作する関数も追加してみてください。
import json
# 照明の初期状態
light_state = {"power": "OFF", "brightness": 100, "color": [255, 255, 255]}
# 照明を操作する関数
def light_control(power=None, brightness=None, color=None):
# 照明を変更
new_state = {"power": power, "brightness": brightness, "color": color}
new_state = {key: value for key, value in new_state.items() if value is not None}
light_state.update(new_state)
result = "照明操作: {}".format(json.dumps(light_state))
# 操作結果をコンソールに表示(赤字)
print('\033[31m' + result + '\033[0m')
# 操作結果を文字列で返却
return result
関数の内容は基本的には自由ですが、最終的な実行結果は文字列としてChatGPTに返却しなければならない点だけ注意してください。
2. 関数の仕様を書き出す
次に、関数の使い方をChatGPTに教えるための仕様書を記述します。上記で作った関数は、Function Callingの書き方に直すと以下のような内容で定義できます。
tools = [
{
"type": "function",
"function": {
# 関数名
"name": "light_control",
# 関数の説明
"description" : "照明を操作します。",
# 引数の定義
"parameters": {
"type": "object",
"properties": {
# 引数① 電源状態
"power": {
"type": "string",
"enum": ["ON", "OFF"],
"description": "電源のオン/オフを指定します。"
},
# 引数② 明るさ
"brightness": {
"type": "integer",
"description": "明るさを指定します。(0〜100)"
},
# 引数③ 色
"color":{
"type": "array",
"description": "照明の色をRGBの配列で指定します。(0〜255)",
"items": {
"type":"integer",
}
}
}
},
"required": []
}
}
]
toolsという変数に代入したリストの中に関数の仕様を記述しています。
ここでは一件のみですが、複数の関数を指定すれば、AIがそれらの中から必要なものを選択して使用してくれます。少々長くて複雑に見えるかもしれませんが、落ち着いて見れば難しいことは何もありません。以下、定義内容について詳しく解説していきます。
関数名
nameプロパティには関数の名称を指定します。実際の関数名と一致する必要はありませんが、複数の関数を指定した場合は、実行対象を識別するのにこちらの名称を使用するため、ユニークな内容を指定する必要があります。
関数の説明
descriptionプロパティに関数の説明を入力します。その関数の具体的な内容や、どんなときに役立つのかを記載しておくことで、AIが実行の要否を判断するのに使用されます。
引数の定義
parametersプロパティには関数の実行に必要な引数の詳細な情報を入力します。関数の実行が必要だと判断されたとき、AIはここに記載されている定義にしたがって実行パラメーターを生成するので、ここはFunction Callingの実装において非常に大切な部分です。基本的には以下の項目を組み合わせて定義します。
- type: "object"や"string"など、 対象要素の型を指定します。
- description: 対象要素に対する説明を記述します。説明しなくてもわかりそうな場合は省略することも可能です。
- properties: typeが"object"の場合のみ必須の項目です。中身の要素の項目名と内容を定義します。
- required: 同じくtypeが"object"の場合のみ指定可能です。ここに挙げられているプロパティは必ず指定され、それ以外は不要なら省略されます。対象関数に必須パラメーターがある場合は忘れずに追加しておきましょう。
- enum: typeが"string"や"integer"などのとき、内容を限られた選択肢から選ばせたい場合に指定することができます。上記の例では、powerの内容は「ON」か「OFF」かのどちらかである必要があるため、こちらで指定しています。
- items: typeが"array"の場合に、中身の要素の形式等を定義します。
上記のコードはまず一番上の階層を「type:object」として定義し、その内部要素(properties)としてそれぞれの引数の内容を定義しています。もし引数が一つだけであっても、必ずこのように指定しなければなりません。
このように関数の仕様を記述してChatGPTに送信することで、AIは概ねこの通りの形式で実行内容を指定してくれます。ただし、100%これに準じたものが返される事が保証されているわけではないという点には注意が必要です。指定したものと矛盾する内容が生成される可能性があることは常に念頭に置いておきましょう。
また、この内容は各発言とともに毎回ChatGPT APIに送信され、入力トークンとしてもカウントされます。可能であれば、全て英語で記述した方が少ないトークン数で内容を伝えられるため、コストの削減に繋がります。
3. 会話内容と共に関数の仕様をChatGPTに送信する
# メッセージ入力
user_message = input("User > ")
# 会話履歴
messages = [
{"role": "system", "content": "あなたは優秀なライティングコーディネーターです。ユーザーの心理状態や状況をヒアリングし、最適なライティングを提供してください。"},
{"role": "user", "content": user_message}
]
# 会話実行
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=messages,
tools=tools
)
定義した内容をChatGPTに送信する方法は、通常の会話を行う際とあまり変わりません。toolsパラメーターに先ほど定義した仕様リストを指定するだけで大丈夫です。システムプロンプトの内容は適宜変更してください。
4. 関数を実行する
関数を実行する必要があるとAIが判断した場合、返答オブジェクトのtool_callsというプロパティに実行すべき関数の情報が格納されて返ってきます。この内容の有無で処理を切り分けて、必要であれば関数を実行します。実行後、最終的にユーザーへ返答を返すまでの一連の処理は以下のようになります。
# ChatGPTの返答内容を取得
response_message = response.choices[0].message
tool_calls = response_message.tool_calls
# 関数の実行要否を確認
if tool_calls:
# 関数の実行が必要な場合
# 関数名と関数本体の紐づけを定義
available_functions = {
"light_control": light_control,
}
# 会話履歴に1回目の返答を追加
messages.append(response_message)
# 指定されている関数を全て実行する
for tool_call in tool_calls:
function_name = tool_call.function.name # 関数名
function_args = json.loads(tool_call.function.arguments) # 引数
function_to_call = available_functions[function_name] # 関数本体
# 関数実行
function_response = function_to_call(**function_args)
# 実行結果を会話履歴に追加
messages.append(
{
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": function_response,
}
)
# 全ての関数を実行したら、実行結果を再びAPIに送信
second_response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=messages,
)
# ユーザーへの最終的な返答を取得
message_assistant = second_response.choices[0].message.content
else:
# 関数の実行が不要な場合
# contentの内容をユーザーへの返答としてそのまま取得
message_assistant = response.choices[0].message.content
# 最終返答を会話履歴に追加
messages.append(
{
"role": "assistant",
"content": message_assistant,
}
)
# 最終返答を表示
print("AI > ", message_assistant)
上述の通り、tool_callsの内容の有無で処理を切り分けています。
tool_callsに関数の指定がある場合は、ユーザーに回答を返却する前に関数を実行します。tool_callsはリスト形式となっており、複数指定されている場合もあるので、全ての関数が実行されるようにしましょう。
関数の実行結果は"tool"ロールのメッセージとして会話履歴に加えます。この時、履歴内で"tool"ロールのメッセージは必ず関数の実行要求と連続している必要がある点に注意してください。特に、連続した会話を実装する上で、実行結果を履歴に残し続けたい場合は、履歴の増加により実行要求の部分のみが削除されてしまうとエラーが発生します。
その後、実行結果を含む会話履歴をChatGPTに再送信すると、最終的な返答(または次の関数要求)が得られます。ChatGPTとのやりとりが複数回に渡っても、ユーザーが目にするのは基本的に最後の返答のみとなるでしょう。こうして、独自関数の実行を伴ったChatGPTとの会話が実現されます。
以上で、Function Callingの基本的な実装は完了です。Google Colaboratry等に上から順に貼り付けて実行すると、AIが照明を操作する様子が疑似体験できますので、ぜひ試してみて下さい。
