10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

OpenAIによるChat APIの新機能Function callingをPythonで使ってみた

Posted at

Supershipの名畑です。Dr.STONEは分割2クールなのですね。1期、2期も面白かったけれど、今回の3期はさらに面白い。何度涙腺が緩んだことか。さて、10月まで秒数をカウントしますかね……。

はじめに

OpenAIChatGPTの提供元)によるChat APIについてまた書かせていただきます。

先日Function calling and other API updatesにて、APIのアップデートが告知されました。
アップデート内容の一つであるFunction calling(関数呼び出し)が話題になっておりますが、こちらについて私も遅ればせながら試してみたので記事として残します。

Function callingにおける一連の流れを順次説明してまとめていますので、皆様の理解に繋がると嬉しいです。

Chat API自体が初耳という方は過去の記事「OpenAIのChat API(Chat completions)を用いての会話をPythonで実装してみた記録」を読んでいただけると嬉しいです。

そもそもFunction callingって?

先述したFunction calling and other API updatesに説明が書かれています。

These models have been fine-tuned to both detect when a function needs to be called (depending on the user’s input) and to respond with JSON that adheres to the function signature. Function calling allows developers to more reliably get structured data back from the model. For example, developers can:
・ Create chatbots that answer questions by calling external tools (e.g., like ChatGPT Plugins)

Google翻訳にかけると下記

これらのモデルは、関数を呼び出す必要があるとき (ユーザーの入力に応じて) を検出し、関数の署名に準拠した JSON で応答するように微調整されています。関数呼び出しにより、開発者はモデルから構造化データをより確実に取得できるようになります。たとえば、開発者は次のことができます。
・ 外部ツール(ChatGPT プラグインなど)を呼び出して質問に答えるチャットボットを作成する

イメージつきますでしょうか。

要は以下の流れのようです。

  1. 関数の定義(名称、役割、引数)をあらかじめ決めておく
  2. Chat APIを呼び出す(質問等をする)
  3. Chat APIは定義済みの関数を呼び出すべきかどうかを判断する
  4. 関数を呼び出すべきと判断した場合、Chat APIは関数名と引数をJSON形式で返してくる
  5. 関数のレスポンスをJSON形式で用意する
  6. 4と5のJSONを渡してChat APIを再び呼び出す
  7. Chat APIは渡されたJSONを利用した回答を行う

5の段階で好きな情報を関数のレスポンスとして設定することができます。データベースから引っ張ってきた値を渡すのでも、外部のAPIを呼び出した結果を渡すのでも良いです。要点としては関数のレスポンスを用意するのはChat APIではなく、実装者です。
つまりはリアルタイム性のある情報でも良いです。Chat APIのモデルで学習されていないデータを元にしたファインチューニングが実現できます。

ただ、これらは、Function callingが提供される前から、プロンプトや実装の工夫によって実現可能ではあります。

Function callingの登場とはつまり「APIのI/F改善によりユーザーがやりたいことをよりやりやすくなった」ということですかね。

説明だけだとピンとこないと思うので、実装してみます。

環境設定

まずはお決まりの環境確認です。
私が使っているのはmacOSです。

Pythonはインストール済み。

$ python --version
Python 3.10.7

openaiのライブラリもインストール済みです。

$ openai --version
openai 0.27.6

openaiが入っていない人はinstallしておきましょう。

$ pip install openai

API KeyはOpenAI platformで発行済みで、OPENAI_API_KEYという環境変数名で保存してあるとします。

export OPENAI_API_KEY=ここに取得したAPI Keyを書く

関数の定義

今回は get_anime_information という、アニメの情報を取得する関数を定義してみます。定義だけなので、実装はありません。

my_functions = [
    {
        "name": "get_anime_information",
        "description": "与えられたアニメタイトルの情報を返します",
        "parameters": {
            "type": "object",
            "properties": {
                "title": {
                    "type": "string", "description": "アニメのタイトル"
                },
            },
            "required": ["title"]
        }
    }
]

本当はすべて英語の方が精度が上がるのかもしれませんが、descriptionは可読性を考慮して日本語にしました。変数名はわかりやすくmy_functionsとします。

引数はtitleのみです。アニメのタイトルを受け取ります。

関数の定義について詳細は公式ページのfunctionsをご覧ください。
nameのみが必須です。

importなど

使用するパッケージのimportとAPI Keyのセットをしておきます。

import os
import openai
openai.api_key = os.getenv("OPENAI_API_KEY")

質問を投げてみる

Developers can now describe functions to gpt-4-0613 and gpt-3.5-turbo-0613, and have the model intelligently choose to output a JSON object containing arguments to call those functions.

現状ではFunction callingが使えるモデルはgpt-4-0613gpt-3.5-turbo-0613です。
ですのでgpt-3.5-turbo-0613を指定した上で質問を投げます。

質問文は「TVアニメ「スキップとローファー」について教えてください」としました。

さきほど定義したmy_functionsも一緒に渡します。

completion = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": "TVアニメ「スキップとローファー」について教えてください"},
    ],
    functions=my_functions
)

すると、下記のレスポンスがcompletionに格納されています。

{
  "choices": [
    {
      "finish_reason": "function_call",
      "index": 0,
      "message": {
        "content": null,
        "function_call": {
          "arguments": "{\n\"title\": \"スキップとローファー\"\n}",
          "name": "get_anime_information"
        },
        "role": "assistant"
      }
    }
  ],

  

}

function_callがあります。つまり、Chat APIが関数呼び出しをするべきと判断したわけです。私たち実装者はfunction_callの有無で以降の処理をどうするか分岐することになります。

function_callの中にはget_anime_informationという関数名とその引数があります。引数はJSONで下記です。関数の定義に合っていますね。

{
	"title": "スキップとローファー"
}

関数レスポンスの用意

関数の回答を用意します。
つまりは「スキップとローファーという引数を与えられたときの回答を用意する」ということです。

公式のサンプルコード(天気を答えるfunction)だと、Bostonの天気を聞かれて天気情報取得のAPIを呼び出す設定になっています。

今回は手間をかけないよう、手動で用意しました。
TVアニメ「スキップとローファー」公式サイトから紹介文を持ってきました。
余談ですが個人的にはかなり好きな作品なので、皆様も是非。胸が熱くなります。

地方の小さな中学校から、東京の高偏差値高校に首席入学した岩倉美津未。カンペキな生涯設計を胸に、ひとり上京してきた田舎の神童は、勉強はできるけれど距離感が独特でちょっとズレてる。だから失敗することもあるけれど、その天然っぷりにクラスメイトたちはやわらかに感化されて、十人十色の個性はいつしか重なっていく。知り合って、だんだんわかって、気づけば互いに通じ合う。だれもが経験する心のもやもや、チリチリした気持ち。わかりあえるきっかけをくれるのは、かけがえのない友達。ときどき不協和音スレスレ、だけどいつのまにかハッピーなスクールライフ・コメディ!

こちらをそれっぽくJSONにします。

{"information": "地方の小さな中学校から、東京の高偏差値高校に首席入学した岩倉美津未。カンペキな生涯設計を胸に、ひとり上京してきた田舎の神童は、勉強はできるけれど距離感が独特でちょっとズレてる。だから失敗することもあるけれど、その天然っぷりにクラスメイトたちはやわらかに感化されて、十人十色の個性はいつしか重なっていく。知り合って、だんだんわかって、気づけば互いに通じ合う。だれもが経験する心のもやもや、チリチリした気持ち。わかりあえるきっかけをくれるのは、かけがえのない友達。ときどき不協和音スレスレ、だけどいつのまにかハッピーなスクールライフ・コメディ!"}

Chat APIにJSONを渡す

では2つのJSONをassistantfunctionというroleで追加した上でAPIを再び呼び出してみます。

completion = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": "TVアニメ「スキップとローファー」について教えてください"},
        {"role": "assistant", "content": None, "function_call": {"name": "get_anime_information", "arguments": "{\"title\": \"スキップとローファー\"}"}},
        {"role": "function", "name": "get_anime_information", "content": "{\"information\": \"地方の小さな中学校から、東京の高偏差値高校に首席入学した岩倉美津未。カンペキな生涯設計を胸に、ひとり上京してきた田舎の神童は、勉強はできるけれど距離感が独特でちょっとズレてる。だから失敗することもあるけれど、その天然っぷりにクラスメイトたちはやわらかに感化されて、十人十色の個性はいつしか重なっていく。知り合って、だんだんわかって、気づけば互いに通じ合う。だれもが経験する心のもやもや、チリチリした気持ち。わかりあえるきっかけをくれるのは、かけがえのない友達。ときどき不協和音スレスレ、だけどいつのまにかハッピーなスクールライフ・コメディ!\"}"}
    ],
    functions=my_functions
)
print(completion["choices"][0]["message"]["content"])

表示されたレスポンスは下記です。

TVアニメ「スキップとローファー」は、地方の小さな中学校から東京の高偏差値高校に首席入学した岩倉美津未の物語です。岩倉美津未は、勉強はできるけれど距離感が独特でちょっとズレている主人公で、失敗することもありますが、その天然っぷりにクラスメイトたちはやわらかに感化されて、十人十色の個性が重なっていきます。互いに理解し合える友達の存在が、心のもやもややチリチリした気持ちに対する解決策のきっかけを提供します。不協和音スレスレな面もあるけれど、いつの間にかハッピーなスクールライフ・コメディとなります。

渡した内容がしっかり使われていますね。

Function callingを用いた実装の一連の流れは以上となります。

関数とまったく関係ない質問をすると?

東京の観光名所について教えてください」と聞いてみます。

completion = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": "東京の観光名所について教えてください"},
    ],
    functions=my_functions
)
print(completion["choices"][0]["message"]["content"])

下記のようなfunctionsを無視した回答が表示されます。つまりはfunctionsを設定していないときと同じ振る舞いです。

東京には多くの観光名所があります。以下にいくつかの代表的な観光名所をご紹介します。

  1. 東京ディズニーランド/シー: 東京ディズニーリゾート内にある世界的に有名なテーマパークです。ディズニーキャラクターとの出会いやアトラクション、ショーを楽しむことができます。

  2. 東京スカイツリー: 東京都墨田区にある高さ634メートルの電波塔です。展望台からは360度のパノラマビューが楽しめるほか、レストランやショップもあります。

以下略

複数の関数を用意すると?

get_anime_informationに加えて新たにget_manga_informationも定義してみました。

my_functions = [
    {
        "name": "get_anime_information",
        "description": "与えられたアニメタイトルの情報を返します",
        "parameters": {
            "type": "object",
            "properties": {
                "title": {
                    "type": "string", "description": "アニメのタイトル"
                },
            },
            "required": ["title"]
        }
    },
    {
        "name": "get_manga_information",
        "description": "与えられた漫画タイトルの情報を返します",
        "parameters": {
            "type": "object",
            "properties": {
                "title": {
                    "type": "string", "description": "漫画のタイトル"
                },
            },
            "required": ["title"]
        }
    }
]

その上で「漫画「スキップとローファー」について教えてください」という質問を投げてみます。

completion = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": "漫画「スキップとローファー」について教えてください"},
    ],
    functions=my_functions
)
print(completion["choices"][0]["message"]["content"])

レスポンスは下記です。

{
  "choices": [
    {
      "finish_reason": "function_call",
      "index": 0,
      "message": {
        "content": null,
        "function_call": {
          "arguments": "{\n\"title\": \"スキップとローファー\"\n}",
          "name": "get_manga_information"
        },
        "role": "assistant"
      }
    }
  ],

  

}

漫画についての質問ということでget_manga_informationを呼び出すべきだと正しく判定されていますね。

おまけ

前に「OpenAI APIでGPT-3にFine-tuningでアニメの放送開始日時を学習してもらうPython実装の記録」という記事を書きましたが、似たようなことをしてみましょう。
スキップとローファーの放送日時をAPIに答えてもらいます。

まずはget_anime_scheduleという関数の定義をします。titleでアニメのタイトルを、channelで放送局の名称を受け取る関数とします。

my_functions = [
    {
        "name": "get_anime_schedule",
        "description": "与えられたアニメタイトルの放送曜日と時間を取得します",
        "parameters": {
            "type": "object",
            "properties": {
                "title": {
                    "type": "string", "description": "アニメのタイトル"
                },
                "channel": {
                    "type": "string", "description": "放送局の名称"
                },
            },
            "required": ["title", "channel"]
        }
    },
]

TVアニメ「スキップとローファー」のTOKYO MXでの放送日時を教えてください」という質問を投げます。

completion = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": "TVアニメ「スキップとローファー」のTOKYO MXでの放送日時を教えてください"},
    ],
    functions=my_functions
)

下記はレスポンス抜粋です。titlechannelの二つの引数が正しく設定されていますね。

"function_call": {
  "arguments": "{\n\"title\": \"スキップとローファー\",\n\"channel\": \"TOKYO MX\"\n}",
  "name": "get_anime_schedule"
},

回答のJSONを用意します。本来はここでTOKYO MXの情報だけを書くのが関数のレスポンスとしては正しいのでしょうが、試しに5局分書いてみました。まあ、TOKYO MXのデータも含まれるから、ギリギリセーフということで。

 {
    "data":[
        {"channel": "TOKYO MX", "schedule": "火曜日23:00〜"},
        {"channel": "AT-X", "schedule": "水曜日22:00〜"},
        {"channel": "北陸朝日放送", "schedule": "水曜日25:58〜"},
        {"channel": "BS朝日", "schedule": "金曜日23:00〜"},
        {"channel": "関西テレビ放送", "schedule": "日曜日25:59〜"},
    ]
 }

2つのJSONを渡して再び呼び出します。

completion = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": "TVアニメ「スキップとローファー」のTOKYO MXでの放送日時を教えてください"},
        {"role": "assistant", "content": None,
         "function_call": {"name": "get_anime_schedule", "arguments": "{\"title\": \"スキップとローファー\",\"channel\": \"TOKYO MX\"}"}},
        {"role": "function", "name": "get_anime_information",
         "content": """
         {
            "data":[
                {"channel": "TOKYO MX", "schedule": "火曜日23:00〜"},
                {"channel": "AT-X", "schedule": "水曜日22:00〜"},
                {"channel": "北陸朝日放送", "schedule": "水曜日25:58〜"},
                {"channel": "BS朝日", "schedule": "金曜日23:00〜"},
                {"channel": "関西テレビ放送", "schedule": "日曜日25:59〜"},
            ]
         }
         """
        }
    ],
    functions=my_functions
)
print(completion["choices"][0]["message"]["content"])

表示されたレスポンスは下記です。

TVアニメ「スキップとローファー」はTOKYO MXで火曜日23:00に放送されています。

ばっちりTOKYO MXの放送時間だけを返してくれてますね。

userの質問文を「スキップとローファー」のBS朝日での放送日時を教えてください」に変えてみます。

{"role": "user", "content": "TVアニメ「スキップとローファー」のBS朝日での放送日時を教えてください"},

これに連動してget_anime_scheduleに引数として渡すchannelBS朝日に変わります。

最終的なレスポンスは下記です。

TVアニメ「スキップとローファー」はBS朝日で金曜日の23:00〜に放送されています。

正しい回答です。

ちなみに質問を「TVアニメ「スキップとローファー」の放送日時を教えてください」として局名を省くとどうなるでしょう。

{"role": "user", "content": "TVアニメ「スキップとローファー」の放送日時を教えてください"},

レスポンスは下記でした。channelが空要素になっています。

"function_call": {
  "arguments": "{\n\"title\": \"\u30b9\u30ad\u30c3\u30d7\u3068\u30ed\u30fc\u30d5\u30a1\u30fc\",\n\"channel\": \"\"\n}",
  "name": "get_anime_schedule"
},

これを踏まえてchannelが空要素のままで呼び出してみました。

completion = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": "TVアニメ「スキップとローファー」の放送日時を教えてください"},
        {"role": "assistant", "content": None,
         "function_call": {"name": "get_anime_schedule", "arguments": "{\"title\": \"スキップとローファー\",\"channel\": \"\"}"}},
        {"role": "function", "name": "get_anime_information",
         "content": """
         {
            "data":[
                {"channel": "TOKYO MX", "schedule": "火曜日23:00〜"},
                {"channel": "AT-X", "schedule": "水曜日22:00〜"},
                {"channel": "北陸朝日放送", "schedule": "水曜日25:58〜"},
                {"channel": "BS朝日", "schedule": "金曜日23:00〜"},
                {"channel": "関西テレビ放送", "schedule": "日曜日25:59〜"},
            ]
         }
         """
        }
    ],
    functions=my_functions
)

レスポンスは以下です。箇条書き部分は実際にはMarkdown形式(行頭が-)でした。

TVアニメ「スキップとローファー」の放送日時は以下の通りです:

  • TOKYO MX:火曜日23:00〜
  • AT-X:水曜日22:00〜
  • 北陸朝日放送:水曜日25:58〜
  • BS朝日:金曜日23:00〜
  • 関西テレビ放送:日曜日25:59〜

ご参考までにどうぞ。

5局全部を表示してくれました。

Function callingを用いなくても実現できる内容ではありましたが、まあ、おまけということで。

最後に

変化が激しくて楽しい。

最後に2

記事の題材としてなぜ冒頭のDr.STONEを選ばなかったのかというと、Dr.STONEのTVアニメ1期は2019年に放映されているため、学習データにすでになにかしら含まれている可能性があるかもしれないと考えたからです。GPT-3.5の説明を見るとgpt-3.5-turbo-0613TRAINING DATAUp to Sep 2021のため。

宣伝

SupershipのQiita Organizationを合わせてご覧いただけますと嬉しいです。他のメンバーの記事も多数あります。

Supershipではプロダクト開発やサービス開発に関わる方を絶賛募集しております。
興味がある方はSupership株式会社 採用サイトよりご確認ください。

10
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?