【Function Calling】指定したフォーマットで日付を返してくれるか検証してみた

# はじめに

[Function Calling](https://platform.openai.com/docs/guides/function-calling)とは、「自然言語の入力から、実行する関数とその引数を決定してくれる」機能です。
この機能は、OpenAIが提供する言語モデルである`gpt-3.5-turbo-0301`、`gpt-4`で利用できます。

本記事では、Function Callingで決定したい引数に日付がある場合に、どのようにして指定した日付フォーマットで返させるのがよいのかを検証します。

# 検証

Function Callingでは、関数の引数の各項目には、「型」と「説明文」を設定できます。
今回の検証では、この「説明文」を工夫することで言語モデルが出力する引数の日付フォーマットを制御する、というアプローチをとります。
なお、「型」は`string`や`number`などのプリミティブな型を指定でき、日付の型は指定できません。

## 検証環境

- Python
    - rye `0.15.2`
    - python `3.11.6`
    - openai `1.3.7`
- 言語モデル
    - モデル：`gpt-3.5-turbo`
    - バージョン：`0613`
    ※Azure OpenAI Service上でデプロイ

## 検証コード

<details><summary>ソースコード全体</summary><div>

```python
import json
import openai

# 接続設定

CONFIG = {
    "azure_endpoint": "<Your API Endpoint>",
    "azure_deployment": "<Your Deployment>",
    "api_key": "<Your API Key>",
    "api_version": "2023-07-01-preview",
}


# 関数の定義

description = "日付フォーマットを制御するための説明文"

FUNCTION = {
    "name": "get_weather",
    "description": "指定した場所と日時の天気を取得する",
    "parameters": {
        "type": "object",
        "properties": {
            "location": {
                "type": "string",
                "description": "場所 (例：東京, 名古屋, 博多, etc.)",
            },
            "datetime": {
                "type": "string",
                "description": f"日時{description}"
            }
        },
        "required": ["location", "datetime"],
    }
}


# メッセージ

MESSAGES = [
  {"role": "system", "content": "現在時刻は2023年12月1日12時20分です。"},
  {"role": "user", "content": "今日の東京の天気を教えてください。"},
]


# リクエスト

client = openai.AzureOpenAI(**CONFIG)

completion = client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=MESSAGES,
    functions=[FUNCTION],
    function_call="auto",
)

# 結果の表示
result = completion.choices[0].message.function_call
print(f"args = {json.loads(result.arguments)}")

```

</div></details>

### 接続設定

Azure OpenAI Service上でデプロイしたモデルを利用する場合の接続設定は次の通りです。

```python
CONFIG = {
    "azure_endpoint": "<Your API Endpoint>",
    "azure_deployment": "<Your Deployment>",
    "api_key": "<Your API Key>",
    "api_version": "2023-07-01-preview",
}
```

:::note warn
Function Callingは、「2023-07-01-preview」以降のAPIバージョンで利用できます。
古いAPIバージョンを利用している場合はエラーとなることに注意してください。
詳しくは「[Azure OpenAI Service の REST API リファレンス](https://learn.microsoft.com/ja-jp/azure/ai-services/openai/reference)」をご覧ください。
:::

### 関数の定義

今回の検証では、以下のような「指定した場所と日時の天気を返してくれる関数」を想定します。

```python
def get_weather(location: str, datetime: str) -> str:
   ...
```

この`get_weather`をFunction CallingするためのJSONスキーマは次の通りです。

```python
description = "＜日付フォーマットを制御するための説明文＞"

FUNCTION = {
    "name": "get_weather",
    "description": "指定した場所と日時の天気を取得する",
    "parameters": {
        "type": "object",
        "properties": {
            "location": {
                "type": "string",
                "description": "場所 (例：東京, 名古屋, 博多, etc.)",
            },
            "datetime": {
                "type": "string",
                "description": f"日時{description}"
            }
        },
        "required": ["location", "datetime"],
    }
}
```

ここで、`description`は日付フォーマットを制御するための説明文です。
この値で言語モデルが返す日付フォーマットを制御できるか検証します。
実際の値は「[検証の実施](#検証の実施結果)」をご覧ください。

### メッセージ

```python
MESSAGES = [
  {"role": "system", "content": "日本の現在時刻は2023年12月1日12時20分です。"},
  {"role": "user", "content": "今日の東京の天気を教えてください。"},
]
```

ここでは、日本の現在時刻をシステムメッセージとして与えることで、
「今日」とか「明日」などの曖昧な日時の基準とさせる狙いがあります。

このメッセージから期待するFunction Callingの結果は、以下の範囲に収まる日付が指定したフォーマットの文字列で返されることです。

```
2023/12/1 00:00:00 < datetime < 2023/12/1 12:59:59
```

### リクエスト＆結果の表示

Azure OpenAI Service上でデプロイしたモデルを利用する場合は`AzureOpenAI`クラスを利用します。
また、言語モデルが決定した関数の引数はJSON文字列で返されるため、プログラムで利用するには以下のようにパースする必要があります。

```python
client = openai.AzureOpenAI(**CONFIG)

completion = client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=MESSAGES,
    functions=[FUNCTION],
    function_call="auto",
)

result = completion.choices[0].message.function_call
print(f"args = {json.loads(result.arguments)}")
```


## 検証の実施＆結果

`description`の値は、大きく以下のパターンで検証します。

- 日付フォーマットを指定しない場合
- ISO8601を指定する
- `yyyyMMddHHmmss`のような文字列で日付フォーマットを指定する場合


<details><summary>検証結果の一覧</summary><div>

```python
# 日付フォーマットを指定しない場合
description = ""
"2023-12-01T12:20:00": 48 回
"2023-12-01T00:00:00Z": 12 回
"2023-12-01T12:20:00+09:00": 11 回
"2023-12-01": 9 回
"2023-12-01T00:00:00": 8 回
"2023-12-01T12:00:00": 4 回
"2023-12-01T12:00:00+09:00": 3 回
"2023-12-01T00:00:00+09:00": 3 回
"2023-12-01 00:00:00": 1 回
"2023-12-1T12:00:00Z": 1 回

# ISO8601のみ指定
description = " (ISO8601形式の文字列)"
"2023-12-01T12:20:00+09:00": 48 回
"2023-12-01T12:20:00": 18 回
"2023-12-01T00:00:00+09:00": 18 回
"2023-12-01T00:00:00Z": 13 回
"2023-12-01T12:20:00Z": 2 回
"2023-12-01T12:00:00+09:00": 1 回

# ISO8601拡張形式を指定
description = " (ISO8601拡張形式の文字列)"
"2023-12-01T12:20:00+09:00": 54 回
"2023-12-01T00:00:00+09:00": 32 回
"2023-12-01T12:20:00": 9 回
"2023-12-01T00:00:00Z": 3 回
"2023-12-01T12:00:00+09:00": 1 回
"2023-12-01T12:20:00Z": 1 回

# ISO8601基本形式を指定
description = " (ISO8601基本形式の文字列)"
"2023-12-01T12:20:00+09:00": 58 回
"2023-12-01T00:00:00+09:00": 18 回
"2023-12-01T12:20:00": 17 回
"2023-12-01T00:00:00Z": 5 回
"2023-12-01T00:00:00": 1 回
"2023-12-01T12:20:00Z": 1 回

# ISO8601基本形式を指定（例を与えた場合）
description = " (ISO8601基本形式の文字列. 例：20201231T153558+09:00)"
"20231201T122000+09:00": 48 回
"20231201T1220+09:00": 42 回
"2023-12-01T12:20:00+09:00": 5 回
"20231130T122000+09:00": 2 回
"20231130T120000+09:00": 1 回
"20231101T1200+09:00": 1 回
"20231201122000+09:00": 1 回

# `yyyy-MM-dd HH:mm:ss`形式
description = " (yyyy-MM-dd HH:mm:ss形式の文字列)"
"2023-12-01 12:20:00": 92 回
"2023-12-01 00:00:00": 5 回
"2023-12-01 12:00:00": 2 回
"2022-12-01 12:20:00": 1 回

# `yyyyMMddHHmmss`形式
description = " (yyyyMMddHHmmss形式の文字列)"
"20231201122000": 100 回

# `yyyy-MM-dd HH:mm:ss.SSS`形式
description = " (yyyy-MM-dd HH:mm:ss.SSS形式の文字列)"
"2023-12-01 12:20:00.000": 87 回
"2023-12-01 12:20:00": 11 回
"2023-12-01 12:00:00.000": 1 回
"2023-12-01 00:00:00.000": 1 回

# `yyyyMMddHHmmssSSS`形式
description = " (yyyyMMddHHmmssSSS形式の文字列)"
"20231201122000000": 98 回
"20231201000000000": 1 回
"2023120112200000000": 1 回

# `yyyy-MM-dd HH:mm:ss.SSS`形式（文字の説明を追加した場合）
description = " (yyyy-MM-dd HH:mm:ss.SSS形式の文字列. y:年, M:月, d:日, H:時, m:分, s:秒, S:ミリ秒.)"
"2023-12-01 12:20:00.000": 92 回
"2023-12-01 12:00:00.000": 1 回
"2023-12-01 12:20:00": 3 回
"2023-12-01 00:00:00.000": 4 回

# `yyyy-MM-dd HH:mm:ss.SSS`形式（例を与えた場合）
description = " (yyyy-MM-dd HH:mm:ss.SSS形式の文字列. 例：2020-05-17 15:35:58.000)"
"2023-12-01 12:20:00.000": 96 回
"2023-12-01 12:20:00": 3 回
"2023-12-01 12:00:00.000": 1 回
```

</div></details>


### 日付フォーマットを指定しない場合

まずは、説明文で日付フォーマットを指定しない場合を見てみます。
100回実行してみて返ってきた`datetime`を集計した結果は次の通りです。

```python
# 日付フォーマットを指定しない場合
description = ""
"2023-12-01T12:20:00": 48 回
"2023-12-01T00:00:00Z": 12 回
"2023-12-01T12:20:00+09:00": 11 回
"2023-12-01": 9 回
"2023-12-01T00:00:00": 8 回
"2023-12-01T12:00:00": 4 回
"2023-12-01T12:00:00+09:00": 3 回
"2023-12-01T00:00:00+09:00": 3 回
"2023-12-01 00:00:00": 1 回
"2023-12-1T12:00:00Z": 1 回
```

おお、日付フォーマットを指定しなくても結構ちゃんと返してくれるものですね。
`+09:00`を見るに日本標準時とかも理解してくれてそうです。
しかし、タイムゾーンがあったりなかったり、時刻があったりなかったり、とバラバラであるため、このまま利用するには不安が残ります。

### ISO8601を指定する

次は、ISOで定められた日付と時刻の表記に関する国際規格である「ISO8601」を指定してみます。
「ISO8601」は、日付と時刻を`T`で区切るのが特徴で、以下の「拡張形式」と「基本形式」があります。

- 拡張形式: 2023-04-01T05:00:30+09:00
- 基本形式: 20230401T050030+09:00

今回は、以下のパターンで指定した日付フォーマットで返ってくるかをそれぞれ検証します。

- ISO8601のみ指定
- ISO8601拡張形式を指定
- ISO8601基本形式を指定

100回実行して返ってきた`datetime`を集計した結果はそれぞれ次の通りです。

```python
# ISO8601のみ指定
description = " (ISO8601形式の文字列)"
"2023-12-01T12:20:00+09:00": 48 回
"2023-12-01T12:20:00": 18 回
"2023-12-01T00:00:00+09:00": 18 回
"2023-12-01T00:00:00Z": 13 回
"2023-12-01T12:20:00Z": 2 回
"2023-12-01T12:00:00+09:00": 1 回

# ISO8601拡張形式を指定
description = " (ISO8601拡張形式の文字列)"
"2023-12-01T12:20:00+09:00": 54 回
"2023-12-01T00:00:00+09:00": 32 回
"2023-12-01T12:20:00": 9 回
"2023-12-01T00:00:00Z": 3 回
"2023-12-01T12:00:00+09:00": 1 回
"2023-12-01T12:20:00Z": 1 回

# ISO8601基本形式を指定
description = " (ISO8601基本形式の文字列)"
"2023-12-01T12:20:00+09:00": 58 回
"2023-12-01T00:00:00+09:00": 18 回
"2023-12-01T12:20:00": 17 回
"2023-12-01T00:00:00Z": 5 回
"2023-12-01T00:00:00": 1 回
"2023-12-01T12:20:00Z": 1 回
```

何も指定しないよりだいぶマシになりましたね。
「拡張形式」を指定した場合は、タイムゾーンを気にしないならば、このままでも利用できそうです。
一方、「基本形式を指定」した場合は、すべて拡張形式で返ってきており、「ISO8601基本形式」という説明だけでは不十分だったようです。
「基本形式とは何か」を細かく説明してあげる必要がありそうです。

追加で、「基本形式」の説明として例を与えてみましょう。
同様に100回実行して集計した結果は次の通りです。

```python
description = " (ISO8601基本形式の文字列. 例：20201231T153558+09:00)"
"20231201T122000+09:00": 48 回
"20231201T1220+09:00": 42 回
"2023-12-01T12:20:00+09:00": 5 回
"20231130T122000+09:00": 2 回
"20231130T120000+09:00": 1 回
"20231101T1200+09:00": 1 回
"20231201122000+09:00": 1 回
```

．．．想定より良くない結果になりました。
例を与えたことによって「基本形式」で返ってくるようになりました。
しかし、「基本形式」っぽいものでも、秒数がなかったり、`T`で区切られていなかったり、と利用するには不安が残る結果となりました。

### `yyyyMMddHHmmss`のような文字列で日付フォーマットを指定する

最後に、`yyyyMMddHHmmss`のような文字列で日付フォーマットを指定してみます。

今回はよくあるシンプルな以下のパターンで、指定した日付フォーマットで返ってくるかをそれぞれ検証します。

- `yyyy-MM-dd HH:mm:ss`形式
- `yyyyMMddHHmmss`形式
- `yyyy-MM-dd HH:mm:ss.SSS`形式
- `yyyyMMddHHmmssSSS`形式

```python
# `yyyy-MM-dd HH:mm:ss`形式
description = " (yyyy-MM-dd HH:mm:ss形式の文字列)"
"2023-12-01 12:20:00": 92 回
"2023-12-01 00:00:00": 5 回
"2023-12-01 12:00:00": 2 回
"2022-12-01 12:20:00": 1 回

# `yyyyMMddHHmmss`形式
description = " (yyyyMMddHHmmss形式の文字列)"
"20231201122000": 100 回

# `yyyy-MM-dd HH:mm:ss.SSS`形式
description = " (yyyy-MM-dd HH:mm:ss.SSS形式の文字列)"
"2023-12-01 12:20:00.000": 87 回
"2023-12-01 12:20:00": 11 回
"2023-12-01 12:00:00.000": 1 回
"2023-12-01 00:00:00.000": 1 回

# `yyyyMMddHHmmssSSS`形式
description = " (yyyyMMddHHmmssSSS形式の文字列)"
"20231201122000000": 98 回
"20231201000000000": 1 回
"2023120112200000000": 1 回
```

これまでの日付フォーマットを指定する方法と比較すると、全体的に安定して返してくれました。
特に、「`yyyyMMddHHmmss`形式」が最も安定しています。

また、「`yyyy-MM-dd HH:mm:ss.SSS`形式」では、ミリ秒の位がないケースが10%程度あります。
これについて、説明を追加して改善するか見てみましょう。
説明を追加し、100回実行して集計した結果は次の通りです。

```python
# `yyyy-MM-dd HH:mm:ss.SSS`形式（文字の説明を追加した場合）
description = " (yyyy-MM-dd HH:mm:ss.SSS形式の文字列. y:年, M:月, d:日, H:時, m:分, s:秒, S:ミリ秒.)"
"2023-12-01 12:20:00.000": 92 回
"2023-12-01 12:00:00.000": 1 回
"2023-12-01 12:20:00": 3 回
"2023-12-01 00:00:00.000": 4 回

# `yyyy-MM-dd HH:mm:ss.SSS`形式（例を与えた場合）
description = " (yyyy-MM-dd HH:mm:ss.SSS形式の文字列. 例：2020-05-17 15:35:58.000)"
"2023-12-01 12:20:00.000": 96 回
"2023-12-01 12:20:00": 3 回
"2023-12-01 12:00:00.000": 1 回
```

説明を与えた結果、ミリ秒の位がないケースの割合は減少しており、さらに安定しましたね。

ここまでミリ秒の位についても検証しておいてなんですが、ミリ秒の位まで具体的に値を指定したいことは稀であると思います。
より高い精度で言語モデルが指定したフォーマットで返してくれることを求める場合は、今回最も安定していた「`yyyyMMddHHmmss`形式」で言語モデルには返させて、一度、日付オブジェクトに変換し、任意の日付フォーマットの文字列に変換する、という方法が良いかもしれません。


# まとめ

今回の検証では、関数の引数項目の「説明文」を工夫することで言語モデルが出力する引数の日付フォーマットを制御する、というアプローチをとりました。

以下のパターンについて、それぞれ100回実行してどのくらい指定した日付フォーマットで返ってくるかを集計しました
。

- 日付フォーマットを指定しない場合
- ISO8601を指定する
    - ISO8601のみ指定
    - ISO8601拡張形式を指定
    - ISO8601基本形式を指定
    - ISO8601基本形式を指定（例を与えた場合）
- `yyyyMMddHHmmss`のような文字列で日付フォーマットを指定する場合
    - `yyyy-MM-dd HH:mm:ss`形式
    - `yyyyMMddHHmmss`形式
    - `yyyy-MM-dd HH:mm:ss.SSS`形式
    - `yyyy-MM-dd HH:mm:ss.SSS`形式（文字の説明を追加した場合）
    - `yyyy-MM-dd HH:mm:ss.SSS`形式（例を与えた場合）
    - `yyyyMMddHHmmssSSS`形式

その結果、以下のことがわかりました。

- Function Callingでは、関数の引数項目の「説明文」によって日付フォーマットをある程度制御できる。
- 「説明文」に詳細情報や例を追加することで、日付が指定のフォーマットで返されやすくなる。
- 今回の検証した範囲では、「`yyyyMMddHHmmss`のような文字列で日付フォーマットを指定する方法」が安定して日付を指定したフォーマットで返してくれる。
特に、以下の「説明文」の場合に最も安定していた。

    ```python
    "日時 (yyyyMMddHHmmss形式の文字列)"
    ```