Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

Azure OpenAI Service で、Function calling を試してみる

こんにちは、igaです。
台風が接近していて、外出の予定と重ならないかドキドキしています。


今回は、Azure OpenAI Service(以下、Azure OpenAIと記載します)で7月から利用できるようになった「Function calling」を試してみます。
Function callingとは、実行可能な関数を予め定義しておき、自然言語から実行すべき関数と引数を特定してくれる機能です。
OpenAIのAPIで、6月にリリースされた機能ですが、それがAzure OpenAIでも利用できるようになりました。

この機能のポイントは、「関数を呼び出す」ことではなく、「呼び出す関数(とその引数)を特定してくれる」ことにあります。
(名前が「Function calling」なのに、ややこしいですね)


Azure OpenAIの利用開始方法とAPIの呼び出し方については、前回の記事を参考にしてください。
acro-engineer.hatenablog.com

Function callingのポイント

Function calling(関数呼び出し)と言っていますが、Azure OpenAIが関数を呼び出すわけではありません
関数を呼び出すのは、ユーザ(プログラム)が行う必要があります。
Azure OpenAIは、「この関数を、この引数で呼び出すのが適切」と判断するまでをやるので、その判断結果に基づいてプログラムで関数を呼び出す必要があります。

今回のプログラムでは、Azure OpenAIに呼び出す関数と引数を判断してもらい、その結果プログラムで関数を呼び出す、というところを試してみます。

Function callingを利用する条件

Azure OpenAIでFunction callingを利用するには、デプロイされたモデルとバージョン、APIバージョンが指定された値になっている必要があります。

デプロイされたモデル モデルバージョン APIバージョン
gpt-35-turbo
gpt-35-turbo-16k
gpt-4
gpt-4-32k
0613 2023-07-01-preview

Function callingを利用する

Azure OpenAIを利用するAPI呼び出しのPythonコードは、前回記事 で使用したコードを流用します。
この時、条件に記述したようにAPIバージョンを「2023-07-01-preview」に変更します。

使用する関数の定義内容については、公式のブログに記述された内容を利用します。
techcommunity.microsoft.com

#Note: The openai-python library support for Azure OpenAI is in preview.
import os
import pprint
import math
import json
import openai

openai.api_type = "azure"
openai.api_base = "https://xxxxxx.openai.azure.com/"
openai.api_version = "2023-07-01-preview"  # Function callingを使用できるAPIバージョンを指定する
openai.api_key = os.getenv("OPENAI_API_KEY")

# この関数が、呼び出される関数の定義です
def calculator(num1, num2, operator):
    """簡単な計算機
    - 対応していない演算子が指定された場合はNoneを返す
    - 計算できない場合はNoneを返す

    Args:
        num1 (int): 数値1
        num2 (int): 数値2
        operator (str): 演算子
    """
    if operator == "+":
        return num1 + num2
    elif operator == "-":
        return num1 - num2
    elif operator == "*":
        return num1 * num2
    elif operator == "/":
        if num2 == 0:
            return None
        return num1 / float(num2)
    elif operator == "**":
        return num1 ** num2
    elif operator == "sqrt":
        return math.sqrt(num1)
    else:
        return None

# Azure OpenAIに対する関数の定義です
functions= [
    {
        "name": "calculator",
        "description": "A simple calculator",
        "parameters": {
            "type": "object",
            "properties": {
                "num1": {"type": "number"},
                "num2": {"type": "number"},
                "operator": {"type": "string", "enum": ["+", "-", "*", "/", "**", "sqrt"]},
            },
            "required": ["num1", "num2", "operator"],
        },
    }
]

# 最初の問い合わせ内容
messages = [
    {
        "role": "user",
        "content": "Last month Fabrikam made $73,846 in sales. Based on that, what would the annual run rate be?"
    }
]

# 指定されたメッセージを送る
response = openai.ChatCompletion.create(
    engine="ChatTest",
    messages=messages,
    functions=functions,
    function_call='auto')

pp = pprint.PrettyPrinter(indent=2)
res_message = response['choices'][0]['message']
pp.pprint(res_message)

if res_message.get('function_call'):
    # Functionが利用可能と判断された

    function_name = res_message['function_call']['name']
    arguments = json.loads(res_message['function_call']['arguments'])

    if function_name == 'calculator':
        function_result = calculator(
            num1=int(arguments.get('num1')),
            num2=int(arguments.get('num2')),
            operator=arguments.get('operator')
        )
    else:
        print(f'unknown function {function_name}')
        exit

    if not function_result:
        exit

    print(f'\ncalculator result: {function_result}\n')

    # 最初の問い合わせに、Azure OpenAIからの回答を追加する
    messages.append(
        {
            "role": res_message['role'],
            "name": function_name,
            "content": res_message['function_call']['arguments']
        }
    )

    # 関数の呼び出し結果を追加する
    messages.append(
        {
            "role": "function",
            "name": function_name,
            "content": str(function_result)
        }
    )

    # 関数の実行結果を使用して、問い合わせを行う
    # Function callingは行わないので、functionsの指定はしない
    second_response = openai.ChatCompletion.create(
        engine="ChatTest",
        messages = messages)

    second_res_message = second_response['choices'][0]['message']
    pp.pprint(second_res_message)

Functionの定義

定義するFunctionには、3つのパラメータを指定する必要があります。

パラメータ 説明
name 定義するFunctionの名前を指定します
description モデルが、Functionをいつどのように呼び出せばよいか、決定するために使用します
Functionの動作について、意味のある説明を与える必要があります
parameters Functionが受け入れる引数について、JSON形式で指定します

今回定義したFunctionについて

今回定義したFunctionは、以下のようになっています。

functions = [
    {
        "name": "calculator",
        "description": "A simple calculator",
        "parameters": {
            "type": "object",
            "properties": {
                "num1": {"type": "number"},
                "num2": {"type": "number"},
                "operator": {"type": "string", "enum": ["+", "-", "*", "/", "**", "sqrt"]},
            },
            "required": ["num1", "num2", "operator"],
        },
    }
]

このFunctionは、次のような意味になります。

項目 説明
name calculator
description 簡単な計算機
parameters num1、num2の2つの数値と、operatorの文字列を受け取る
operatorは、「+」「-」「*」「/」「**」「sqrt」のいずれかが指定される
num1、num2、operatorはいずれも指定が必須

Function calling を行う

Function callingを行うには、openai.ChatCompletion.create()の引数で、Functionの定義を指定する必要があります。

response = openai.ChatCompletion.create(
    engine="ChatTest",
    messages = messages,
    functions = functions,
    function_call = "auto")

Function callingに関連する引数は、「functions」と「function_call」の2つになります。

  • functions
    • Functionの定義を指定する
  • function_call
    • auto
      • Functionの呼び出しが必要になるか、モデルが決定します
    • none
      • モデルにFunctionの呼び出しを行わせない
    • { "name": 呼び出すFunction名 }
      • 決まったFunctionの呼び出しが必要ということを、モデルに指示します

Function callingの結果

先ほどのサンプルプログラムを実行すると、responseのchoices[0]['message']には以下の値が設定されていました。

{
  'role': 'assistant',
  'function_call': {
    'name': 'calculator',
    'arguments': '{\n'
                 '  "num1": 73846,\n'
                 '  "num2": 12,\n'
                 '  "operator": "*"\n'
                 '}'
    },


messageにfunction_callが存在しているので、モデルがFunction callingが必要と判断しました。
呼び出すFunctionはどうなっているかは、nameに呼び出すFunction名が、argumentsに引数が指定されています。
引数の内容は、「num1が73846、num2が12、operatorが*」ということになっています。

これは、メッセージに指定した「Last month Fabrikam made $73,846 in sales. Based on that, what would the annual run rate be?」という文章から、モデルが「1か月の売り上げ額(73846) × 1年の月数(12) = 年間の売り上げ額」という計算を行う必要がある、と判断して引数の値を決定しています。

Function callingの結果を使って処理を行う

Function callingの結果で、messageにfunction_callが含まれていたので、関数の呼び出しをプログラムで行います。
今回のプログラムでは、以下の部分で関数呼び出しを行っています。
プログラムで呼び出せる関数が指定された場合に、関数「calculator」を呼び出しています。

if res_message.get('function_call'):
    # Functionが利用可能と判断された

    function_name = res_message['function_call']['name']
    arguments = json.loads(res_message['function_call']['arguments'])

    if function_name == 'calculator':
        function_result = calculator(
            num1=int(arguments.get('num1')),
            num2=int(arguments.get('num2')),
            operator=arguments.get('operator')
        )
    else:
        print(f'unknown function {function_name}')
        exit

関数呼び出しの結果を使ってAzure OpenAIに問い合わせする

関数呼び出しの結果を使って、もう一度Azure OpenAIに問い合わせを行います。
呼び出す際には、これまでのチャットのやり取りとして、最初の質問、Azure OpenAIからの回答、関数呼び出しの結果、の3つをmessagesに設定してAzure OpenAIの呼び出しを行います。
2回目の呼び出し処理は、以下のようになっています。

# 最初の問い合わせに、Azure OpenAIからの回答を追加する
messages.append(
    {
        "role": res_message['role'],
        "name": function_name,
        "content": res_message['function_call']['arguments']
    }
)

# 関数の呼び出し結果を追加する
messages.append(
    {
        "role": "function",
        "name": function_name,
        "content": str(function_result)
    }
)

# 関数の実行結果を使用して、問い合わせを行う
# Function callingは行わないので、functionsの指定はしない
second_response = openai.ChatCompletion.create(
    engine="ChatTest",
    messages = messages)

2回目の呼び出し結果(サンプルプログラムの、second_response['choices'][0]['message']の内容)は次のようになりました。

{
  'role': 'assistant',
  'content': 'The annual run rate based on last month's sales of $73,846 would be $886,152.'
}

contentの内容が、「The annual run rate based on last month's sales of $73,846 would be $886,152.」という文章になり、関数の呼び出し結果を使った文章を生成していることが分かります。

メッセージを日本語で記述する

先ほどのサンプルは、メッセージが英語になっていました。
これを日本語で書いてみると、どのような結果になるでしょうか。

先ほどのサンプルコードの、messagesの定義を日本語に変更します。

# 最初の問い合わせ内容
messages = [
    {
        "role": "user",
        "content": "先月、太郎さんは5,340,000円の売り上げを上げました。これに基づいた年間売り上げはいくらになりますか?"
    }
]

response = openai.ChatCompletion.create(
    engine="ChatTest",
    messages=messages,
    functions=functions,
    function_call='auto',
)

メッセージを書き換えて、Pythonスクリプトを実行した結果は以下のようになりました。
今回も、指定した文章から必要な計算式を判断して、calculatorに対して渡す引数を生成できています。

{
  'role': 'assistant',
  'function_call': {
    'name': 'calculator',
    'arguments': '{\n  \'num1\': 5340000,\n  \'num2\': 12,\n  \'operator\': \'*\'\n}'
  }
}

この結果を元に、関数を呼び出した結果を使ってAzure OpenAIに問い合わせした結果は以下のようになりました。
こちらも、関数を呼び出した結果を使った文章が生成されました。

{
  'role': 'assistant',
  'content': '太郎さんの年間売り上げは64,080,000円になります。'
}

関数が特定できないメッセージの場合

メッセージの内容から、関数が特定できない場合(今回のサンプルでは、計算を行わないメッセージの場合)は、どのような結果がAzure OpenAIから返ってくるか確認します。
サンプルプログラムの、messagesの内容を以下のように書き換えます。
Azure OpenAIに問い合わせを行う、openai.ChatCompletion.create() には、そのままfunctionsを指定します。

# 最初の問い合わせ内容
messages = [
    {
        "role": "user",
        "content": "こんにちは、今日もいい天気ですね。"
    }
]

response = openai.ChatCompletion.create(
    engine="ChatTest",
    messages=messages,
    functions=functions,
    function_call='auto')

Azure OpenAIの呼び出し結果は、以下の通りです。
Function callingを使わずにチャットを行った時と同じような結果が返ってきました。
Function callingを行う「function_call」は含まれていません。
正常に関数が特定できたかどうかは、レスポンスの内容に「function_call」のパラメータが含まれるかどうかで、判断できそうです。

{
  'role': 'assistant',
  'content': 'こんにちは!そうですね、いい天気ですね。何かお手伝いできることはありますか?'
}

まとめ

これまで、チャットの機能として、文章を要約したり、プログラムのコードを出力するようなことはできましたが、その応答の内容は基本的に人が判断するものでした。
このFunction callingの機能によって、プログラムからAPIを呼び出し、その結果を受け取って別の処理を実行するようなことを、自動で行えるようになります。

Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

Kaggle Grandmasterと一緒に働きたエンジニアWanted! - Acroquest Technology株式会社のデータサイエンティストの採用 - Wantedlywww.wantedly.com