MENU

Knowledge Oasisは主にAIとAWSの知識を共有するブログです。その他ITに関する知識やまれに生活に役立つ知識も共有するかもしれません。

KOふみ
名前はKOふみ(こふみ)。独立系SIerで20年のキャリアを持ち、新人研修の講師から請負開発まで幅広く経験。現在はAIを駆使したソリューション開発に従事。資格は応用情報技術者、データベーススペシャリスト、プロジェクトマネージャー、PMP、簿記2級。AWS学習中で、将来はAWSアンバサダーを目指す。

OpenAIのAPI完全制覇パート3 関数呼び出し編

OpenAIのAPI完全制覇パート3 関数呼び出し編
  • URLをコピーしました!

今回はOpenAI APIで関数呼び出し(Function calling)をおこなうためのパラメータtoolsとtool_choiceについて解説します。GPTsのアクションと動きが違うので注意が必要です。

目次

今回解説するパラメータ

今回解説するパラメータはマーカーを付けた2つです。2つとも関数呼び出し(Function calling)に関するパラメータです。

  • model (string, 必須): 使用するモデルのID。
  • messages (array, 必須): 会話を構成するメッセージのリスト。
  • temperature (number, オプション): 0から2.0までの範囲。出力のランダム性を制御。
  • top_p (number, オプション): 0から1の範囲。上位の確率質量のトークンのみを考慮。
  • n (integer, オプション): デフォルトは1。各入力メッセージに対して生成される回答の数。
  • stop (string or array, オプション): APIがトークン生成を停止するシーケンス。
  • max_tokens (integer, オプション): 生成される最大トークン数。
  • presence_penalty (number, オプション): -2.0から2.0の範囲。新しいトピックについて話す可能性を制御。
  • frequency_penalty (number, オプション): -2.0から2.0の範囲。繰り返しを減少。
  • logit_bias (map, オプション): トークンの出現確率を調整。
  • logprobs (bool, オプション): 出力トークンの対数確率を返すかどうか
  • top_logprobs (integer, オプション): 各出力トークンについて採用されなかったトークンを出力する
  • response_format (object, オプション): 出力する形式を指定する。 jsonを指定可能。
  • seed (integer, オプション): 2024/05/24時点ではベータ版。seed値を指定する。
  • stream (boolean, オプション): デフォルトはfalse。部分的なメッセージをストリーミングするかどうか。
  • stream_options (object, オプション): stream時の動作を制御するオプション。
  • tools (array, オプション): モデルが呼び出せる関数を定義する。
  • tool_choice (string, オプション): toolsで指定した関数の呼び出しを制御する。
  • user (string, オプション): エンドユーザーの一意の識別子。

OpenAI APIの関数呼び出し(Function calling)とは

LLM(大規模言語モデル)では対応しきれない複雑な処理や特定の決まった処理を行いたい場合に定義した関数を呼び出す手法が関数呼び出し(Function calling)です。

例えばある会社では職級と号俸から複雑な計算を経て給与が決まるとします。プロンプトエンジニアリングで複雑な計算方法を覚えさせても良いのですが、LLMに任せると間違える可能性があります。そこでLLMには問い合わせのあった職級と号俸を抽出してもらって複雑な計算給与計算を行う関数を呼び出してもらいます。その結果をLLMに戻してLLMに最終的な回答を作ってもらうのが関数呼び出し(Function calling)です。

GPTsのアクションではOpen APIの定義を設定しておくと呼び出しまでやってくれますが、APIの関数呼び出し(Function calling)では呼び出しはやってくれません。その代わりにOpen APIだけではなくローカルプログラムの関数も呼び出すことができます。既存の関数をAPI化することなく呼び出すことができるのです。

パラメータ解説

tools

関数をJSONの配列で定義します。JSONの定義は以下の通りです。toolsは128個まで登録できます。

  • type(string): 必須。2024年6月6日時点では”function”のみ
  • function(object): 必須
    • description(string): 関数の説明。この内容から関数呼び出しに必要な情報を判断している。
    • name(string): 必須。関数名。
    • parameters(object)
      • name(string): パラメーター名。
      • type(string): 型
    • required(object): 必須パラメータを指定。

tool_choice

toolsで定義した関数の呼び出し方法を定義します。以下の4種類を設定できます。

  • none: 関数呼び出しを行いません。
  • auto: APIがプロンプトを分析して呼び出す関数を判断します。
  • required: 必ずいずれかの関数を呼び出します。
  • 関数指定: {"type": "function", "function": {"name": "関数名"}} の形式で呼び出す関数を指定します

動かし方

toolsを指定して実行

toolsを指定してAPIを実行してみます。

from openai import OpenAI
client = OpenAI()

# 給与計算関数
def payroll(grade, rank):
  print(f"call payroll: grade={grade}, rank={rank}")
  # 複雑な給与計算
  payment_structure = {
    1: {1: 11000, 2: 12000, 3: 13000},
    2: {1: 21000, 2: 22000, 3: 23000},
    3: {1: 31000, 2: 32000, 3: 33000},
    4: {1: 41000, 2: 42000, 3: 43000}
  }
  return payment_structure[grade][rank]

# ツールの定義
tools = [
  {
    "type": "function",
    "function": {
      "name": "payroll",
      "description": "職級(grade)と号俸(rank)から給与を計算する",
      "parameters": {
        "type": "object",
        "properties": {
          "grade": {"type": "integer"},
          "rank": {"type": "integer"},
        },
        "required": ["grade", "rank"]
      }
    }
  },
]

# Step #1: 関数呼び出しが必要となるプロンプトを実行する 
# function callingの結果を追記する必要があるので配列で定義
messages = [
  {"role": "user", "content": "2級3号の給与はいくらですか?"},
]

# function calling
response = client.chat.completions.create(
  model="gpt-4o",
  messages=messages,
  tools=tools,
  tool_choice="auto",
)

print(response.choices[0])

上記を実行した結果が以下です。

Choice(
    finish_reason='tool_calls',
    index=0,
    logprobs=None,
    message=ChatCompletionMessage(
        content=None,
        role='assistant',
        function_call=None,
        tool_calls=[
            ChatCompletionMessageToolCall(
                id='call_hdl1C3zSLbpbGMErOzevVDHY',
                function=Function(
                    arguments='{"grade":2,"rank":3}',
                    name='payroll'
                ),
                type='function'
            )
        ]
    )
)

結果を見ると給与は回答してくれていません。その代わりにpayrollという関数を{"grade":2,"rank":3}というパラメータで呼び出すと給与が取得できるという情報が返ってきます。この情報を元に自分で関数呼び出し(Function calling)を行い、その結果を再びAPIに渡して回答を生成してもらう必要があるのです。

toolsの結果を含めてAPIを呼び出す方法

関数呼び出し(Function calling)を行い回答を生成するコードを解説します。以下のコードを先ほどのコードに続けて記述して動かしてみてください。

# === ポイント1 ===
# function callingの結果を追加
response_message = response.choices[0].message
messages.append(response_message)

# Step 2: 回答にtool_callsが含まれているか判断する
tool_calls = response_message.tool_calls
if tool_calls:
  for tool_call in tool_calls:
    # 回答から関数呼び出しに関する情報を取り出す
    tool_call_id = tool_call.id
    tool_function_name = tool_call.function.name
    tool_grade = eval(tool_call.function.arguments)['grade']
    tool_rank = eval(tool_call.function.arguments)['rank']
    
    # Step 3: 関数を呼び出してプロンプトに結果を追加する
    if tool_function_name == 'payroll':
      results = payroll(tool_grade, tool_rank)

      # === ポイント2 ===
      # 関数の実行結果をmessagesに追加
      messages.append({
        "role":"tool", 
        "tool_call_id":tool_call_id, 
        "content":f"{results}"
      })
        
    else: 
      # 定義されてない関数が指定された場合
      print(f"Error: function {tool_function_name} does not exist")

  # Step 4: 関数呼び出しの結果を含むプロンプトを実行する
  model_response_with_function_call = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
  )
  print(model_response_with_function_call.choices[0].message.content)

else: 
  # 関数呼び出しが必要なかった場合
  print(response_message)

上記を動かした結果が以下です。

ChatCompletion(
    id='chatcmpl-9XENhovwfi8Cq9ab7nIIEsySPX0ws',
    choices=[
        Choice(
            finish_reason='stop',
            index=0,
            logprobs=None,
            message=ChatCompletionMessage(
                content='2級3号の給与は23,000円です。',
                role='assistant',
                function_call=None,
                tool_calls=None
            )
        )
    ],
    created=1717707133,
    model='gpt-4o-2024-05-13',
    object='chat.completion',
    system_fingerprint='fp_319be4768e',
    usage=CompletionUsage(
        completion_tokens=13,
        prompt_tokens=46,
        total_tokens=59
    )
)

正しく「2級3号の給与は23,000円です。」と回答してくれています。

ポイント解説

関数の実行結果を含めてAPIを呼び出すにはいくつかポイントがあります。

toolsを指定した結果をmessagesに追加する

関数の実行結果だけではなく”toolsを指定して実行”の結果をmessagesに含める必要があります。”toolsを指定して実行”の結果にはidが含まれており、API側で最終的な回答を生成する際にどの関数の結果をどこに含めるかを知る必要があるのだと思います。

# === ポイント1 ===
# function callingの結果を追加
response_message = response.choices[0].message
messages.append(response_message)

関数の実行結果をmessagesに追加する

関数を実行した結果をmessagesに追加します。その際のroleには”tool”を指定します。roleとcontent以外にtool_call_idも指定します。tool_call_idを指定することでAPI側がどこに回答を含めれば良いかを判断できるのだと思います。

      # === ポイント2 ===
      # 関数の実行結果をmessagesに追加
      messages.append({
        "role":"tool", 
        "tool_call_id":tool_call_id, 
        "content":f"{results}"
      })

tool_choiceの動きについて

tool_choiceの動きは少し特殊でまだ良く分かっていない部分もあります。現時点で分かっている内容を解説しておきます。分かり次第訂正していきます。

none

その名の通りで関数呼び出し(Function calling)を行いません。先ほどの例で指定すると「ただいま確認しますので少々お待ちください。」という回答になりました。

auto

文脈から判断して関数呼び出し(Function calling)を行います。先ほどの例のプロンプトを「”2級3号と3級2号の給与はいくらですか?”」のように変更した場合、以下のように2回関数を呼び出すように結果が返ってきます。指示通りに2回関数を実行してそれぞれの結果を戻してあげると「2級3号の給与は23,000円、3級2号の給与は32,000円です。」と正しく回答してくれます。

Choice(
    finish_reason='tool_calls',
    index=0,
    logprobs=None,
    message=ChatCompletionMessage(
        content=None,
        role='assistant',
        function_call=None,
        tool_calls=[
            ChatCompletionMessageToolCall(
                id='call_If65muAJb31ZgckRdslWDM59',
                function=Function(
                    arguments='{"grade": 2, "rank": 3}',
                    name='payroll'
                ),
                type='function'
            ),
            ChatCompletionMessageToolCall(
                id='call_deugOxa6srdgyh0Qxk9WoGvi',
                function=Function(
                    arguments='{"grade": 3, "rank": 2}',
                    name='payroll'
                ),
                type='function'
            )
        ]
    )
)

required

関数呼び出し(Function calling)を強制します。autoと同様に複数回呼び出しが必要かも判断してくれます。

先ほどの例でプロンプトを「”2007年に日本一になった球団は?”」と関数と全く関係ない内容にした場合、以下のように存在しない関数名を返してきてエラーとなってしまいました。argumentsの内容も意味不明です。この事象の原因や対処方法は分かっていません。

Choice(
    finish_reason='stop',
    index=0,
    logprobs=None,
    message=ChatCompletionMessage(
        content=None,
        role='assistant',
        function_call=None,
        tool_calls=[
            ChatCompletionMessageToolCall(
                id='call_OAUbPIjx1hc0aQGVjziWa1gL',
                function=Function(
                    arguments='{"tool_uses":[{"recipient_name":"functions.payroll","parameters":{"grade":1,"rank":1}}]}',
                    name='parallel'
                ),
                type='function'
            )
        ]
    )
)

関数と関係ない文脈のプロンプトが来る可能性がある場合はrequiredを指定しない方がよさそうです。

関数指定({"type": "function", "function": {"name": "payroll"}})

関数指定は注意が必要です。複数回呼び出しが必要な場合でも指定した関数を1回しか実行してくれません。

Choice(
  finish_reason='stop',
  index=0,
  logprobs=None,
  message=ChatCompletionMessage(
    content=None,
    role='assistant',
    function_call=None,
    tool_calls=[
      ChatCompletionMessageToolCall(
        id='call_LaH9ju7P3TwSoHM6RSJH4bz8',
        function=Function(
          arguments='{"grade":2,"rank":3}',
          name='payroll'
        ),
        type='function'
      )
    ]
  )
)

tool_choiceによる動きの違い

tool_choiceの値による動きの違いをまとめると以下のようになります。関数呼び出しを行う場合でもfinish_reasonの結果が異なってきます。finish_reasonの値で処理の分岐を考える場合には注意が必要です。

tool_choicefinish_reasontool_calls複数回呼び出し
nonestopなし
autotool_callsありする
requiredstopありする
関数指定stopありしない
tools_choiceによる結果の違い

function_callについて

toolsとtool_choiceに似たパラメータでfunctionsとfunction_callがあります。functionsとfunction_callはDeprecatedになっており、それぞれtoolsとtool_choiceに置き換わっています。実行結果を見てもらうとfunction_call=None が返ってきていますが、これはfunctionsの名残だと思われます。関数呼び出し(Function calling)が行われたかを確認する際に間違えてfunction_call=None を確認しないように注意しましょう。toolsを指定した場合はtool_calls に結果が返ってきます。

まとめ

今回はtoolsとtool_choiceについて解説しました。関数呼び出し(Function calling)を行うことで複雑な業務ロジックなどを確実に実行することができます。自作のアプリやシステムにOpenAIのパワーを取り込むために必須の機能と言えるかもしれません。サンプルコードをもとに色々試してみてください。

  • URLをコピーしました!

コメント

コメントする

目次