OpenAI Function callingで複雑なタスクを簡単に実現

# Function callingの登場

6/13ごろにOpenAI社から発表された新しい`gpt-3.5-turbo`のインスタンスにFunction callingという機能が追加され話題を呼んでいます。このFunction calling、非常に強力な機能なのですが、仕組みがいまいちピンとこないといった方も多いのではないでしょうか。筆者もその一人で、ドキュメントを3回くらい読んでもしっくり来なかったのですが、実際にFunction callingを実装してみてなるほど、これは凄いな、となったので紹介します。

ここでは、具体的なソースコードを紹介しながら、実際に動作するサンプルを作っていきます。

## TL;DR

* ソースコードだけ見られればいい！という方は以下へ
    * https://github.com/canada/openai-function/blob/master/app.py
* ソースコードを解説してくればいい！という方は「実際にコードを書いてみる」までジャンプしてください

## Function callingは何ができるのか

Function callingを使うと、特にフレームワークを用意しなくても非常に簡単に思考連鎖（chain of thought）が実現できるようになります。今までLangChainを使ったり、OpenAI Pluginを使って実現していたものが、とても短いプログラムで可能になりました。例えば

**「東京オリンピックの開会式から今日まで何秒経過したか」**

という質問にChatGPTは答えることができません。

* 東京オリンピックの開催日を知らない
* 今が何月何日か知らない
* 秒数の計算が苦手

という前提があるからですね。これを実現するためには

* Google/Bing検索をして東京オリンピックの開催日を得る
* Pythonなどをを呼び出して、現在時刻からまでの経過秒数の計算をさせる

という２つのChatGPT単体ではできないタスクをこなさなければならないのです。これまでこういった機能を実現するには、

* Pluginを実装してホスティングし、OpenAIアプリから呼び出す
* LangChainやsemantic kernelといったフレームワークでゴリゴリと実装する

の２つの方法くらいしかありませんでした。Google検索やPython呼び出しなどはLangChainのAgentという機能で比較的簡単に実装できますが、例えば社内業務システムとつなぎこみたい、となった場合はAgentを手書きしなければならず、やや辛みがあります。しかもLangChainの場合、AgentのDescription（概要）だけでどのAgentを使うか判断していたため、なかなか思う通りに動作しないといった欠点がありました。

## Function callingの動作原理

今までのChatGPT APIは、自然言語を入力として自然言語をアウトプットすることしかできませんでした。

```
人間: 人生、宇宙、すべての答えは？
AI: 42です。
```
こういった見慣れたやり取りですね。しかしFunction callingを実装したモデルでは、「回答を生成するために、関数の呼び出しを依頼する」といった出力もするようになりました。「これはFunctionがないと答えられないな」ということをChatGPTが判断し、必要であれば

```python
人間: 東京オリンピックの開催日は
AI: {"ask_google"}関数に{"arguments": "東京オリンピック 開催日"}を渡し結果を教えてください
```

こんな感じの答えを出すようになったのです（※イメージです）。ここで凄いことが２つ起きています。

* どの関数を呼び出せばタスクが達成できるか判断している
* 関数への引数を自動生成している

筆者はこれまで業務でこのようなChatGPTと関数やAPIとのつなぎ込みの実装をずっとやってきたのですが、正しいJSONの出力はプロンプトの調整が大変です。これが関数の定義をするだけで簡単にできるようになったのです。

## 実際にコードを書いてみる

そろそろ説明はもう飽きた、という方もいるでしょうから実際にコードを書いてみます。Googleの例はLangChainで比較的サクッとできてしまうため、今回は社内システムとのつなぎ込みを考えてみます。そうですね、

* 在庫の数が確認できるAPI
* メールを送信できるAPI

の２つの機能をChatGPTにもたせてみましょう。

まず、在庫の数が確認できるAPIを示します。

```python
stock = [
    {"item": "みかん", "stock": 0, "supplier_for_item": "温州コーポレーション"},
    {"item": "りんご", "stock": 10, "supplier_for_item": "ハローKiddy Industory"},
    {"item": "バナナ", "stock": 0, "supplier_for_item": "The Donkey Foods"},
    {"item": "パイナップル", "stock": 1000, "supplier_for_item": "ペンパイナッポー流通"},
    {"item": "ぶどう", "stock": 100, "supplier_for_item": "グレープ Fruits inc."},
]

# 在庫チェック関数
def inventory_search(arguments):
    # 名前で在庫を探す
    inventory_names = json.loads(arguments)["inventory_names"]
    inventories = []
    for x in inventory_names.split(","):
        inventories.append(next((item for item in stock if item["item"] == x), None))

    return json.dumps(inventories)
```

ちょっと長いですが、カンマ区切りで商品名を受け取り、配列をサーチしてレコードごと返却しているだけです。次にメール送信部分を示します。

```python
# メール送信関数
def send_mail(arguments):
    args = json.loads(arguments)
    print("""
mail sent as follows
=====
{}さま
いつもお世話になっております。
商品名：{}
{}
よろしくお願いします。

""".format(args["supplier_name"], args["items"], args["message_body"]))
    return json.dumps({"status": "success"})
```

宛先、商品名、本文の3つの引数をJSON文字列で受け取ってメールを送信しています。

では、どのようにしてこの関数の仕様をChatGPTに教えればよいのでしょう。Function callingでは「利用可能な関数の仕様書を渡す」という方法を取っています。以上のシンプルな関数２つの仕様は以下のようになります。

```python
# 呼び出し可能な関数の定義
functions=[
    # 在庫チェック関数の定義
    {
        # 関数名
        "name": "inventory_search",
        # 関数の説明
        "description": "在庫商品を検索します。商品名は必ずカンマ区切りである必要があります。",
        # 関数の引数の定義
        "parameters": {
            "type": "object",
            "properties": {
                "inventory_names": {
                    "type": "string",
                    "description": "検索文字列",
                },
            },
            # 必須引数の定義
            "required": ["input"],
        },
    },
    # メール送信関数の定義
    {
        # 関数名
        "name": "send_mail",
        # 関数の説明
        "description": "サプライヤにメールします。メールは一度に１人にしか送れません",
        # 関数の引数の定義
        "parameters": {
            "type": "object",
            "properties": {
                "supplier_name": {
                    "type": "string",
                    "description": "商品のサプライヤー",
                },
                "message_body": {
                    "type": "string",
                    "description": "サプライヤーへのメッセージ",
                },
                "items": {
                    "type": "string",
                    "description": "サプライヤーに通知する商品名。商品は一度に１つしか指定できません",
                },
            },
            # 必須引数の定義
            "required": ["item_shortage"],
        },
    },
]
```
思ったより長いですが大丈夫、落ち着いて読むと大したことは書いていません。関数の目的、受け取る変数名、変数にどういった値が入るかを自然言語で書いてあります（実際には英語ですが、ここでは分かりやすく和訳してあります）。この構造をChatGPTが読んで、どの関数を使ってどういった引数を渡すべきかを判断するのです。いわば「関数呼び出し専用のプロンプト」と言ったわけですね。

## ChatGPTを呼び出す

準備が整ったのでいよいよAIの呼び出しです。呼び出しは以下のように非常に簡単です。

```python
# ユーザープロンプト
query = "みかん、ぶどう、バナナについて、在庫が0であるか調べ、在庫が0の場合は商品のサプライヤーに追加注文のメールを送ってください。"
user_prompt = {"role": "user", "content": query}

# 初回問い合わせ
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[user_prompt],
    # Function callを使うことを明示
    functions=functions,
    function_call="auto",
)
# AIの返答を取得
message = response.choices[0]["message"]
```

引数に`functions`, `function_call`が増えただけで従来と全く同じですね。messageの受け取り方も同じです。ではAIのレスポンスを見てみましょう。

```json
{
    "role": "assistant",
    "content": null,
    "function_call": {
        "name": "inventory_search",
        "arguments": "{\n  \"inventory_names\": \"みかん,ぶどう,バナナ\"\n}"
    }
}
```

なんと今まで「人間向けの回答」が入っていた`content`が`null`になっています。代わりに`function_call`という構造が追加されているのが分かりますね。ただしChatGPTは「関数の実行」は自動的には行ってくれません。ここは自前で呼び出しましょう。関数名が文字列で、引数がJSON文字列で返ってきているので、力技で次のように実行します。

```python
    f_call = message["function_call"]

    # 関数の呼び出し、レスポンスの取得
    function_response = globals()[f_call["name"]](f_call["arguments"])
```

今回のAIの依頼は「在庫を確認して」だったので、

```python 
function_response = inventory_search('{"inventory_names": "みかん,ぶどう,バナナ"}')
```

が呼び出され、結果が配列に入ります。検索文字列がちゃんとカンマ区切りになっていますね。ここがおかしい場合は先ほどのFunctionの定義を調整します。とはいえプロンプトで頑張るよりはずっと楽ですね。

さて問題はここからです。次のChatGPT呼び出しはどうしましょう？答えは簡単で、先ほどのAIの回答（関数の指示）と関数の結果を会話履歴のプロンプトに追加してもう一度呼び出せばいいだけです。そうすると、次回呼び出された時は、在庫状況を知っているので、次のアクション、つまりメール送信を試みるようになります。AIの返答と関数の結果は以下のようにして履歴（プロンプト）に追加します。

```python
    # AIの回答を会話履歴に追加する
    messages.append(response.choices[0]["message"])

    # 関数のレスポンスを追加する
    messages.append({
        "role": "function", # いままではsystem, human, assistantの3つだった
        "name": f_call["name"], # 関数名
        "content": function_response, # 関数からのレスポンス（JSON string）
    })
```


こうやって関数を複数回呼び出し、会話履歴から自然文が生成できるようになったら関数の呼び出しではなく、`contents`に自然文が返るようになります。つまり`message["function_call"]`(関数呼び出しの指示)がAIから返ってこなくなるまで`openai.ChatCompletion.create()`を繰り返し呼べばいいわけです。

## 動作サンプル

以下で公開しています。

https://github.com/canada/openai-function/

## 動作結果

実行結果も載せておきます。

```
AI response: 
{
    "role": "assistant",
    "content": null,
    "function_call": {
        "name": "inventory_search",
        "arguments": "{\n  \"inventory_names\": \"みかん,ぶどう,バナナ\"\n}"
    }
}

Function call: inventory_search()
Params: {
  "inventory_names": "みかん,ぶどう,バナナ"
}

Function:
 returns [{'item': 'みかん', 'stock': 0, 'supplier_for_item': '温州コーポレーション'}, {'item': 'ぶどう', 'stock': 100, 'supplier_for_item': 'グレープ Fruits inc.'}, {'item': 'バナナ', 'stock': 0, 'supplier_for_item': 'The Donkey Foods'}]

AI response: 
{
    "role": "assistant",
    "content": null,
    "function_call": {
        "name": "send_mail",
        "arguments": "{\n  \"supplier_name\": \"温州コーポレーション\",\n  \"message_body\": \"在庫が0の商品があります。追加注文をお願いします。\",\n  \"items\": \"みかん\"\n}"
    }
}

Function call: send_mail()
Params: {
  "supplier_name": "温州コーポレーション",
  "message_body": "在庫が0の商品があります。追加注文をお願いします。",
  "items": "みかん"
}

Function:
returns 
{'status': 'success'}

mail sent as follows
=====
温州コーポレーションさま
いつもお世話になっております。
商品名：みかん
在庫が0の商品があります。追加注文をお願いします。
よろしくお願いします。


AI response: 
{
    "role": "assistant",
    "content": null,
    "function_call": {
        "name": "send_mail",
        "arguments": "{\n  \"supplier_name\": \"The Donkey Foods\",\n  \"message_body\": \"在庫が0の商品があります。追加注文をお願いします。\",\n  \"items\": \"バナナ\"\n}"
    }
}

Function call: send_mail()
Params: {
  "supplier_name": "The Donkey Foods",
  "message_body": "在庫が0の商品があります。追加注文をお願いします。",
  "items": "バナナ"
}

Function:
returns 
{'status': 'success'}

mail sent as follows
=====
The Donkey Foodsさま
いつもお世話になっております。
商品名：バナナ
在庫が0の商品があります。追加注文をお願いします。
よろしくお願いします。


AI response: 
{
    "role": "assistant",
    "content": "在庫が0の商品は以下の通りです：\n\n- みかん：温州コーポレーション\n- バナナ：The Donkey Foods\n\nサプライヤーに追加注文のメールを送信しました。"
}

Chain finished!
```