ChatGPTのfunction callingの使い方

# この記事は
ChatGPTのfunction callingの使い方の備忘録です。

# API呼び出しに必要なもの
ChatGPTのfunction callingの引数は以下のようになっています。

|引数名|型|説明|
|:-:|:-:|:-|
|`model`|`str`|Completionを行うモデルの名前。function callingを使う場合は`0613`以降のモデルを指定する必要がある。
|`messages`|`list[dict[str, str]]`|ユーザーとアシスタントの会話履歴|
|`functions`|`list[dict[str, Any]]`|function callingで使う関数を定義した辞書のリスト。1つの関数しか登録しない場合でもリストで渡す必要あり。|
|`function_call`|`dict[str, str]`|モデルに関数呼び出しを強制するオプション。この引数を渡すと指定した関数を強制的に呼び出す。|
|`max_tokens`|`int`|モデルに出力させる最大トークン数。課金額を制御するために使う。たぶんモデルはこの値を意識して出力を作ったりはしない。最大トークン数に到達する前にEOSが出現したらそこで生成を打ち切ってレスポンスが返ってくる。
|`temperature`|`float`|温度パラメータ。大きいほど出力の多様性が増す。

この中でfunction callingの呼び出しに重要な`functions`と`function_call`について解説します。

# `functions`と`function_call`
`functions`のサンプルを[OpenAI cookbook](https://github.com/openai/openai-cookbook/blob/main/examples/Function_calling_finding_nearby_places.ipynb)から持ってきました。

```function_definition.py
{
    "name": "call_google_places_api",
    "description": "This function calls the Google Places API to find the top places of a specified type near a specific location. It can be used when a user expresses a need (e.g., feeling hungry or tired) or wants to find a certain type of place (e.g., restaurant or hotel).",
    "parameters": {
        "type": "object",
        "properties": {
            "place_type": {
                "type": "string",
                "description": "The type of place to search for."
            }
        }
    },
    "result": {
        "type": "array",
        "items": {
            "type": "string"
        }
    }
}
```

このサンプル（＋別のサンプル）から関数には以下の要素が必要であることがわかります。

|キー名|型|説明|
|:-:|:-:|:-|
|`name`|`str`|関数の名称。|
|`description`|`str`|関数の機能の説明。どういう場合に使うのかも記述する。
|`parameters`|`dict[str, Union[str, dict[str, str]]]`|関数のパラメータ。`'type': 'object'`部分は固定。`propetries`部分に各引数を書く。引数の`type`に使える型は`[string, number, array]`が使えることはわかっている。`description`には各引数の説明を書く
|`result`|`dict[str, Union[str, dict[str, str]]]`|関数の出力の型を書く。この部分は省略しても動く。
|`required`|`list[str]`|引数のうち必須パラメータを指定する。

ところで、ChatGPTはfunction callingを使おうと思っても必ずしも関数を呼び出してくれません。必ず関数を呼び出したい場合は`function_call`というパラメータを渡します。`function_call`は`name`をキーに持つ辞書で、`functions`で定義した関数の名称を渡すことで関数の引数生成を矯正できます。

```function_call_sample.py
# キーはnameにしてバリューに呼び出したい関数名を入れる
function_call = dict(name='call_google_places_api')

# ChatCompletion.create実行時にfunction_callを渡すと指定した関数を強制的に呼び出す
openai.ChatCompletion.create(
    # 適当にパラメータを設定
    function_call=function_call,
)
```

ただし、強制的に呼び出した場合でも引数はモデルが考えて（next token predictionして）作っているので、引数の内容を間違える可能性があることには注意してください。

# few-shot promptingとの組み合わせ
function callingの機能はかなり便利そうに見えますが、実際使ってみると関数を呼び出してほしい場面で関数を呼び出してくれないケースがあります。特に、`gpt-3.5-turbo`は関数呼び出し能力が弱いです。

これはfew-shot promptingと組み合わせると一定改善します。ただし、モデルはこちらのバックグラウンドを知らない＆状況判断をプロンプティングで全て教え込むのは無理なので完璧に対応することは不可能だと思います。

```few_shot_prompting_messages.py
import json


functions = [
    {
        'name': 'shiminka',  # 市民課/この部分はアルファベットあるいは数字でないとエラーが出る
        'description': '戸籍に関する手続きを扱いたい時に呼び出す。',
        'parameters': {
            'type': 'object',
            'properties': {
                'name': {
                    'type': 'string',
                    'description': '照会者の名前',
                },
            },
        },
    },
    {
        'name': 'seikatsu_eiseika',  # 生活衛生課/この部分はアルファベットあるいは数字でないとエラーが出る
        'description': '犬や猫を飼いたい場合に呼び出す',
        'parameters': {
            'type': 'object',
            'properties': {
                'name': {
                    'type': 'string',
                    'description': '照会者の名前',
                },
            },
        },
    },        
]
messages = [
    dict(role='system', content='You are a helpfull assistant. Think step-by-step.'),
    # nameもアルファベットか数字でないとエラーが出る
    dict(role='user', name='tanaka', content='結婚したいのですが'),
    # few-shot promptingを行って関数呼び出しをシミュレーションする場合でもcontentは必須パラメータなので渡さなくてはいけない。None以外で渡した場合にどうなるかは不明
    # function callingの結果の引数はjsonなのでjsonでfew-shot promptingを行う
    dict(role='assistant', content=None, function_call=dict(name='shiminka', arguments=json.dumps(dict(name='tanaka')))),
    dict(role='user', name='suzuki', content='ペットを飼いたいのですが'),
    dict(role='assistant', content=None, function_call=dict(name='seikatsu_eiseika', arguments=json.dumps(dict(name='suzuki')))),
    # few-shotの例示後に本当に聞きたいことを聞く
    dict(role='user', name='satoh', content='引越ししたいです'),
]

response = openai.ChatCompletion.create(
    model='gpt-3.5-turbo-0613',
    messages=messages,
    temperature=0.0,
    functions=functions,
    max_tokens=128,
)
```

上記を実行すると以下の結果が返ってきます。

```response.py
{
  "name": "shiminka",
  "arguments": "{\"name\": \"sato\"}"
}
```

ちなみにfew-shot promptingをしない場合は上記の質問は関数呼び出しされず、ChatGPT自身が回答を生成します。

```response_failes.py
引越しをするためには、以下の手順を踏む必要があります。

1. 引越し先の物件を探す
2. 引越し先の物件の契約手続きをする
3. 引越し業者を選ぶ
4. 引越しの日程を決める
5. 引越しの荷物の準備をする
6. 引越し
```

<details>
<summary>few-shot promptingしない場合のリクエストコード</summary>

```request.py
functions = [
    {
        'name': 'shiminka',  # 市民課/この部分はアルファベットあるいは数字でないとエラーが出る
        'description': '戸籍に関する手続きを扱いたい時に呼び出す。',
        'parameters': {
            'type': 'object',
            'properties': {
                'name': {
                    'type': 'string',
                    'description': '照会者の名前',
                },
            },
        },
    },
    {
        'name': 'seikatsu_eiseika',  # 生活衛生課/この部分はアルファベットあるいは数字でないとエラーが出る
        'description': '犬や猫を飼いたい場合に呼び出す',
        'parameters': {
            'type': 'object',
            'properties': {
                'name': {
                    'type': 'string',
                    'description': '照会者の名前',
                },
            },
        },
    },        
]
messages = [
    dict(role='system', content='You are a helpfull assistant. Think step-by-step.'),
    dict(role='user', name='satoh', content='引越ししたいです'),
]

response = openai.ChatCompletion.create(
    model='gpt-3.5-turbo-0613',
    messages=messages,
    temperature=0.0,
    functions=functions,
    max_tokens=128,
)
```

</details>

# 試してみたこと
- 関数呼び出しをあんまり使ってくれないことに対してのzero-shot CoTによる効果改善は限定的
    - ベースラインとして入れていたが普通に関数呼び出ししないで自分で回答し始めるケースあり
- 関数の引数を省略することもできる。ただし、エラーが出ずに実行できるというだけで変な回答が返ってくることがある。上記のフォーマットに合わせて学習しているので、未定義のフォーマットで渡されるとよくわからないことになるということなのだと思われる

# 推測している部分
- 関数定義の`type`部分はjsonを想定している気がするのでjsonableな型ならおそらくなんでも使える
- 関数定義の`result`キーはfunction callingで引数生成をするだけなら不要。その後関数の戻り値をアシスタントに渡して会話を続行する場合にアシスタントの出力の認識力をサポートするためのものであると考える
- 関数定義の`required`キーは省略できると思うが、あるとアシスタントに明確にアテンションさせられるのかもしれない。ちなみにこの部分を削ったとしてもpromptのトークン数は特に変わらないので書いても推論コストが上がることはない
- `function_call`の引数はやることが決まっていて、フォーマット通りに情報を抽出したい場合などに使うとよさそう。GPT自身がjson出力をミスらない限り、規定のフォーマットに沿って出力が出てくるのでうれしい場面がありそう