[初心者向け]AWS LambdaでOpenAI API のFunction Callingを試してみた

2023.10.17

はじめに

OpenAI API のFunction Callingを触ったことがなかったため、AWS Lambdaを使いを試してみました。

Function Callingとは、ユーザーから受け取った入力から、事前に定義した呼び出すべき関数を判断して、関数の入力形式通りにJSON形式で出力する機能です。

メリットとしては、指定した型に沿ってJSON形式で出力してくれるため、外部ツールとの連携が容易な点です。

Function Callingでない場合、指定した型に沿ってJSON形式で出力するように、プロンプトを工夫する必要があったり、ユーザーの入力によっては、指定していない型で出力される可能性があります。。

ちなみに、Function Callingの動きや仕組みは、下記の記事が分かりやすかったのでご参考ください。

OpenAIアカウントAPIキーの発行

OpenAIアカウント作成後、APIキーの発行をします。 APIキーの発行は、アカウントの View API keys をクリックします。

Create new secret key をクリックすると、API keyが発行されますので、コピーしておきます。

Lambdaを作成

OpenAIが提供するPython向けのライブラリがありますので、インストールし、Lambda Layerにアップロードします。(直接Lambdaに.zipでのアップロードも可)

MacBook Pro M2の場合、下記のコマンドで、ライブラリをインストールしてZip化します

$ mkdir python
$ python3 -m pip install -t ./python openai
$ zip -r openai.zip ./python

Lambdaレイヤーでopenai.zipをアップロードします。

ランタイムPython 3.11を選択し、Lambda関数を作成します。

作成後、下記の設定を行います

  • タイムアウトは、3秒から30秒に変更
  • 環境変数では、キーはAPI_Key、値はAPIキーの値を入力
  • Lambdaレイヤーを追加します

コードは、下記の通りです。

やっていることとしては、アーティスト名と曲名を含めたユーザー入力内容から、Function Callingで、アーティスト名と曲名を抽出し、JSON形式でレスポンスします。

import json
import os
import openai

openai.api_key = os.environ['API_Key']

def lambda_handler(event, context):
    messages = [
        {"role": "user", "content": "オフィシャルひげだんのプリテンダー"},
    ]
    functions = [
        {
            "name": "get_artist_and_song_title",
            "description": "アーティスト名と曲名を取得します",
            "parameters": {
                "type": "object",
                "properties": {
                    "artist": {
                        "type": "string",
                        "description": "アーティスト名。アーティスト名が省略されている場合、フルネームにしてください。 例:Ado "
                    },
                    "song": {
                        "type": "string",
                        "description": "曲名。 曲名が省略されている場合、フルネームにしてください。例:うっせぇわ "
                    },
                },
                "required": ["artist", "song"]
            },
        },
    ]
    response = openai.ChatCompletion.create(
        # model="gpt-3.5-turbo-0613",
        model="gpt-4-0613",
        messages=messages,
        functions=functions,
        temperature=0,
        max_tokens=512,
        # function_call="auto"
        function_call={"name": "get_artist_and_song_title"}
    )

    print(json.dumps(response['choices'][0], ensure_ascii=False))
    return json.loads(response["choices"][0]["message"]["function_call"]["arguments"])

コード解説

  • messagescontentでユーザーの入力内容を記載します
  • ドキュメントに記載の最新のモデル (gpt-3.5-turbo-0613とgpt-4-0613) のうち、gpt-4-0613を使用しました
  • nameは、呼び出したい関数名を記載し、descriptionには関数の概要を記載します。Chat APIがdescriptionの内容を参考にした上で、ユーザーの入力に対して呼び出す関数を決めます。
    • 今回は、get_artist_and_song_titleという名前の関数を作成しました。
  • プロパティ(properties)内
    • descriptionでも関数名のdescriptionと同様に、的確に説明します
    • プロパティ名は、単数名もしくは複数名という点も精度に関わってくるので、考慮して決めましょう。
      • typeも同様です。stringやnumberなどを指定します。
  • requiredは、JSON形式で必ず返したいプロパティ名を指定します。なくても構いません。
  • function_callでは、特定の関数名の呼び出しを必須にしたい場合に限り、関数名を指定します。
    • autoの場合、ユーザー入力によってChat APIが判断し、関数名や通常の会話を返答します。
  • temperatureを低くするとユーザーの入力が同じ場合、一貫性のある回答になり、値を高くすると多様な回答が生成されるため、今回は0にしてます。

Lambdaを実行

Lambdaを実行すると、下記の通り、JSON形式でアーティスト名と曲が返りました。正式名称にも変換されてます。

Response
{
  "artist": "Official髭男dism",
  "song": "Pretender"
}

ログ出力内容では、関数名get_artist_and_song_titleが呼ばれていることが確認できますね。

{
  "index": 0,
  "message": {
    "role": "assistant",
    "content": null,
    "function_call": {
      "name": "get_artist_and_song_title",
      "arguments": "{\n  \"artist\": \"Official髭男dism\",\n  \"song\": \"Pretender\"\n}"
    }
  },
  "finish_reason": "stop"
}

ユーザー入力やdescriptionを変更してみた

ユーザーの入力をオフィシャルひげだんのプリテンダーではなく、ひげだんのプリテンダーとした場合は、正式名称に変換されませんでした。AIの性能面が上がれば、クリアはしそうではあります。

実行したコード (クリックすると展開します)
import json
import os
import openai

openai.api_key = os.environ['API_Key']

def lambda_handler(event, context):
    messages = [
        {"role": "user", "content": "ひげだんのプリテンダー"},
    ]
    functions = [
        {
            "name": "get_artist_and_song_title",
            "description": "アーティスト名と曲名を取得します",
            "parameters": {
                "type": "object",
                "properties": {
                    "artist": {
                        "type": "string",
                        "description": "アーティスト名。アーティスト名が省略されている場合、フルネームにしてください。 例:Ado "
                    },
                    "song": {
                        "type": "string",
                        "description": "曲名。 曲名が省略されている場合、フルネームにしてください。例:うっせぇわ "
                    },
                },
                "required": ["artist", "song"]
            },
        },
    ]
    response = openai.ChatCompletion.create(
        # model="gpt-3.5-turbo-0613",
        model="gpt-4-0613",
        messages=messages,
        functions=functions,
        temperature=0,
        max_tokens=512,
        # function_call="auto"
        function_call={"name": "get_artist_and_song_title"}
    )

    print(json.dumps(response['choices'][0], ensure_ascii=False))
    return json.loads(response["choices"][0]["message"]["function_call"]["arguments"])
Response
{
  "artist": "ひげだん",
  "song": "プリテンダー"
}

また、プロパティ内のdescriptionの「フルネーム」という文言を「正式名称」に変えた場合も、正式名称に変換されませんでした。

実行したコード(クリックすると展開します)
import json
import os
import openai

openai.api_key = os.environ['API_Key']

def lambda_handler(event, context):
    messages = [
        {"role": "user", "content": "オフィシャルひげだんのプリテンダー"},
    ]
    functions = [
        {
            "name": "get_artist_and_song_title",
            "description": "アーティスト名と曲名を取得します",
            "parameters": {
                "type": "object",
                "properties": {
                    "artist": {
                        "type": "string",
                        "description": "アーティスト名。アーティスト名が省略されている場合、正式名称に変換してください。 例:Ado "
                    },
                    "song": {
                        "type": "string",
                        "description": "曲名。 曲名が省略されている場合、正式名称に変換してください。例:うっせぇわ "
                    },
                },
                "required": ["artist", "song"]
            },
        },
    ]
    response = openai.ChatCompletion.create(
        # model="gpt-3.5-turbo-0613",
        model="gpt-4-0613",
        messages=messages,
        functions=functions,
        temperature=0,
        max_tokens=512,
        # function_call="auto"
        function_call={"name": "get_artist_and_song_title"}
    )

    print(json.dumps(response['choices'][0], ensure_ascii=False))
    return json.loads(response["choices"][0]["message"]["function_call"]["arguments"])
Response
{
  "artist": "オフィシャルひげだん",
  "song": "プリテンダー"
}

descriptionの値によって、結果が変わるので、プロンプトエンジニアリングスキルが必要だと分かります。

複数の関数

呼び出す関数名をもう一つ追加して、ユーザーからの入力に対して、descriptionを参考に適切な関数が返せるか確認します。

ユーザーからの入力は、明日の10時に東京スカイツに行きたいで、時間と建物名を抽出するかんせすを追加しました。

import json
import os
import openai

openai.api_key = os.environ['API_Key']

def lambda_handler(event, context):
    messages = [
        {"role": "user", "content": "明日の10時に東京スカイツに行きたい"},
    ]
    functions = [
        {
            "name": "get_artist_and_song_title",
            "description": "アーティスト名と曲名を取得します",
            "parameters": {
                "type": "object",
                "properties": {
                    "artist": {
                        "type": "string",
                        "description": "アーティスト名。アーティスト名が省略されている場合、フルネームにしてください。 例:Ado "
                    },
                    "song": {
                        "type": "string",
                        "description": "曲名。 曲名が省略されている場合、フルネームにしてください。例:うっせぇわ "
                    },
                },
                "required": ["artist", "song"]
            },
        },
        {
            "name": "get_building",
            "description": "建物名と時間を取得します",
            "parameters": {
                "type": "object",
                "properties": {
                    "building": {
                        "type": "string",
                        "description": "建物名。曲名が省略されている場合、フルネームにしてください 例:東京タワー "
                    },
                    "time": {
                        "type": "string",
                        "description": "時間。 例:2023年11月1日 10:00:00 "
                    }
                },
                "required": ["building", "time"]
            }
        }
    ]
    response = openai.ChatCompletion.create(
        # model="gpt-3.5-turbo-0613",
        model="gpt-4-0613",
        messages=messages,
        functions=functions,
        temperature=0,
        max_tokens=512,
        function_call="auto"
        # function_call={"name": "get_artist_and_song_title"}
    )

    print(json.dumps(response['choices'][0], ensure_ascii=False))
    return json.loads(response["choices"][0]["message"]["function_call"]["arguments"])

建物名が正式名称に変換されつつ、建物名と時間がJSON形式でレスポンスされました。

Response
{
  "building": "東京スカイツリー",
  "time": "明日の10時"
}

ログ出力内容では、関数名get_buildingが呼ばれていることが確認できますね。

{
  "index": 0,
  "message": {
    "role": "assistant",
    "content": null,
    "function_call": {
      "name": "get_building",
      "arguments": "{\n  \"building\": \"東京スカイツリー\",\n  \"time\": \"明日の10時\"\n}"
    }
  },
  "finish_reason": "function_call"
}

最後に

今回の検証では、Lambdaを実行しJSON形式で返るとき、descriptionの内容によっては、プロパティ名が正式名称に変換されることが確認できました。

この調整というのは、いわゆるプロンプトエンジニアリングの分野なので、意図した通りの回答になるよう色々試したいと思います。

参考