AzureOpenAI入門:FunctionCallingで生成AI×ツールを実現する

サイオステクノロジーのひろです。今回はAzureOpenAIを使用したFunctionCallingについて解説していきます。

このブログのゴール

  • FunctionCallingとは何かわかる
  • AzureOpenAIモデルを使用したFunctionCallingの実装方法がわかる

FunctionCallingとは

FunctionCallingはLLMに関数を使わせることができる機能です。

関数は予め作成しておく必要がありますが、このFunctionCallingを使用することで、LLMができることを拡張することが可能です。

例えば、LLMは文章の生成が可能で、様々な質問に回答してくれます。しかしなんでも正しい答えを教えてくれるわけではなく、最新の情報(例えば今日の天気等)やクローズドな情報には回答できません。

しかしながら、最新の情報を検索して取得する関数や、データベースを検索してクローズドな情報を取得する関数を用意し、FunctionCallingによってLLMに関数を使用させれば、回答させることも可能になります。

さらにそれどころではなく、外部APIを操作する関数を実行させればGoogleカレンダーへのイベント登録やメール送信も可能ですし、ロボットを動作させる関数を実行させれば現実へ干渉させることも可能になります。

組み合わせ次第で新たな価値を生み出すことができるかもしれません。

FunctionCallingでできることについて理解していただけたでしょうか。

ここから具体的な内容に入っていきたいと思います。

FunctionCallingのシーケンス

どのようにLLMが関数を実行するかというと、以下のシーケンス図を見ていただければと思います。

まず、アプリケーションからLLMに対して使用可能な関数とプロンプトを渡します。

次に、LLMはプロンプトに沿って独自にどの関数を使用すべきか判断し、アプリケーションへ関数の呼び出しをリクエストします。

実行する関数を教えてもらったアプリケーションは関数を実行し、実行結果をLLMに渡します。

最終的に、LLMが実行結果をもとに回答を生成します。

このような流れでFunctionCallingが行われます。

最終的な回答を生成するまでに必要な情報が足りない場合は複数の関数を実行させることもあります。

例えば、「八王子の天気と現在時刻を教えて」とプロンプトを投げたとします。

その場合、FunctionCallingは並列で2つの関数(天気取得関数と時刻取得関数)を実行して回答します。

また、アプリケーション側で、最終的な回答がされるまでツールの実行を続ける実装を行った場合、LLMが続けて関数を実行できます。

例えば、「天気情報を取得してそれを表にまとめて」とプロンプトを投げたとします。

その場合、1回目のFunctionCallingで天気情報を取得し、2回目に表を操作する関数を使って天気情報を書きこむ等といった処理が可能になります。

生成AIが行っているのは関数と引数を渡すことのみで、関数の実行はアプリケーション側で行います。そのため、アプリケーション側でLLMから渡される関数と引数を読み取って、関数を実行する機能を実装する必要があります。

FunctionCallingの説明については以上です。

続いてAzure OpenAIにおけるFunctionCallingについて解説していきます。

Azure OpenAIのFunctionCalling

Azure OpenAIのFunctionCallingは、APIを叩く際のパラメータとして、toolsとtool_choiceを追加することで可能になります。

Azure OpenAIモデルのデプロイ方法はこちらのブログで解説しております。

以下にサンプルコードを記載しています。また、サンプルコードはこちらのリポジトリで公開しています。実行方法をreadmeに記載しているのでクローンして是非試してみていただければと思います。

まずは今回FunctionCallingで使用する関数を作成しましょう。

今回非常に簡易的な、辞書型で値を返す関数を2つ作成します。

サンプルコード

Python
import datetime

def current_time(location: str):
    # 簡易的に現在時刻を送信
    current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    return {
        "current_time": current_time,
        "timezone": location,
    }


def current_weather(location: str):
    # ダミーの天気データを返す
    return {
        "location": location,
        "temperature": 15,
        "description": "晴れ",
    }

今回作成した関数では、地域名を引数として、疑似的にその地域の現在時刻を返す関数と天気情報を返す関数を作成しました。これらの関数をFunctionCallingで実行していきます。

次にFunctionCallingで渡すツールについてです。

渡すツール定義のJSONは以下のようになっています。

JSON
{
  "type": "function",
  "function": {
    "name": "関数名",
    "description": "関数の説明(生成AIがこの説明を読んで関数を実行するか判断します)",
    "parameters": {
      "type": "object",
      "properties": {
        "【引数名】": {
          "type": "データ型(string, integer, booleanなど)",
          "description": "引数の説明"
        },
      },
      "required": [
        "必須の引数名",
      ]
    }
  }
}

関数名と関数の説明、引数の説明や引数のデータ型を定義して渡します。

ツール定義はJSONをそのまま記述すると量が多く、メンテナンスし辛いことがあるため、今回はPythonのライブラリであるPydanticを使用します。これにより、メンテナンスし易くなるだけでなく、後ほど引数のバリデーションに使用することができます。

以下はサンプルコードです。

Python
from pydantic import BaseModel, Field
from typing import Dict, Any


class WeatherArgs(BaseModel):
    """get_current_weather関数の引数モデル"""

    location: str = Field(
        ...,
        description="天気情報を取得したい都市名(日本語)。例: '東京'",
    )


class TimeArgs(BaseModel):
    """get_current_time関数の引数モデル"""

    location: str = Field(
        ...,
        description="時刻を取得したい都市名(日本語)。例: '東京', 'ニューヨーク'",
    )


class FunctionSchemaManager:
    """複数の関数スキーマを管理するクラス"""

    @staticmethod
    def get_weather_tool() -> Dict[str, Any]:
        """天気取得ツールのスキーマ"""
        return {
            "type": "function",
            "function": {
                "name": "current_weather",
                "description": "指定された都市の現在の天気情報を取得します",
                "parameters": WeatherArgs.model_json_schema(),
            },
        }

    @staticmethod
    def get_time_tool() -> Dict[str, Any]:
        """時刻取得ツールのスキーマ"""
        return {
            "type": "function",
            "function": {
                "name": "current_time",
                "description": "指定された都市の現在時刻を取得します",
                "parameters": TimeArgs.model_json_schema(),
            },
        }

    @staticmethod
    def get_all_tools() -> list[Dict[str, Any]]:
        """利用可能な全てのツールを返す"""
        return [
            FunctionSchemaManager.get_weather_tool(),
            FunctionSchemaManager.get_time_tool(),
        ]

descriptionには引数の例を入れておくことで正しい引数が期待できます。

最後にFunctionCallingを行う部分のサンプルコードです。

Python
import os
import json
from openai import AzureOpenAI
from dotenv import load_dotenv
from schemas import FunctionSchemaManager, TimeArgs, WeatherArgs
from functions import current_time, current_weather


# .envファイルから環境変数を読み込み
load_dotenv()

api_key = os.getenv("AZURE_OPENAI_API_KEY")
endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
api_version = os.getenv("AZURE_OPENAI_API_VERSION")
deployment = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME")

# Azure OpenAIクライアントの作成
client = AzureOpenAI(
    api_version=api_version,
    azure_endpoint=endpoint,
    api_key=api_key,
)
# ツール取得
tools = FunctionSchemaManager.get_all_tools()

messages = [
    {
        "role": "system",
        "content": "あなたは優秀なアシスタントAIです。",
    },
    {"role": "user", "content": "八王子の今の天気と時刻を教えてください。"},
]

# 1回目のFunctionCallingリクエスト
response = client.chat.completions.create(
    messages=messages,
    max_tokens=1000,
    temperature=0.7,
    model=deployment,
    tools=tools,
    tool_choice="auto",
)

response_message = response.choices[0].message
messages.append(response_message)

print("レスポンス:")
print(response_message)

# 関数実行
if response_message.tool_calls:
    for tool_call in response_message.tool_calls:
        function_name = tool_call.function.name
        function_args = json.loads(tool_call.function.arguments)
        print(f"関数呼び出し: {function_name}")
        print(f"引数: {function_args}")

        if function_name == "current_weather":
            args = WeatherArgs.model_validate(function_args) # 引数チェック
            tool_response = current_weather(location=args.location)
        elif function_name == "current_time":
            args = TimeArgs.model_validate(function_args) # 引数チェック
            tool_response = current_time(location=args.location)
        else:
            tool_response = {"error": "不明な関数"}

        messages.append(
            {
                "tool_call_id": tool_call.id,
                "role": "tool",
                "name": function_name,
                "content": json.dumps(tool_response, ensure_ascii=False),
            }
        )
else:
    print("関数呼び出しはありませんでした。")
# 最終回答生成
final_response = client.chat.completions.create(
    messages=messages,
    max_tokens=1000,
    temperature=0.7,
    model=deployment,
)

print("最終レスポンス:")
print(final_response.choices[0].message.content)

生成AIモデルからのレスポンスには使用する関数と引数が書かれているため、それを使用して関数の実行を行います。

Azure OpenAIのFunctionCallingは並列関数呼び出しが可能で、一回のレスポンスで複数の使用する関数名を返してくれます。

実行結果

実行結果は以下のようになりました。

レスポンス:
ChatCompletionMessage(content=None, refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=[ChatCompletionMessageFunctionToolCall(id='call_iFwCt1t3Lzl2WMqQJDvP26Z7', function=Function(arguments='{"location": "八王子"}', name='current_weather'), type='function'), ChatCompletionMessageFunctionToolCall(id='call_6LbONbarZs3sCqoPiZstpieN', function=Function(arguments='{"location": "八王子"}', name='current_time'), type='function')])
関数呼び出し: current_weather
引数: {'location': '八王子'}
関数呼び出し: current_time
引数: {'location': '八王子'}
最終レスポンス:
八王子の現在の天気は晴れで、気温は15度です。また、現在の時刻は2026年2月24日の13時36分です。

current_weatherとcurrent_timeの2つの関数を一度のレスポンスで呼び出していることがわかります。

また、関数の実行結果をもとにして回答してくれています。

messagesを変更することで、1つの関数だけ実行させることも可能です。

以下にmessagesを変更した場合の実行結果を示します。

時刻を聞くmessages

Python
messages = [
    {
        "role": "system",
        "content": "あなたは優秀なアシスタントAIです。",
    },
    {"role": "user", "content": "八王子の時刻を教えてください。"},
]

実行結果

レスポンス:
ChatCompletionMessage(content=None, refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=[ChatCompletionMessageFunctionToolCall(id='call_5m0PwZa4t2AIty1trBlKTGAs', function=Function(arguments='{"location":"八王子"}', name='current_time'), type='function')])
関数呼び出し: current_time
引数: {'location': '八王子'}
最終レスポンス:
現在の八王子の時刻は、2026年2月24日 14:22です。

天気を聞くmessages

Python
messages = [
    {
        "role": "system",
        "content": "あなたは優秀なアシスタントAIです。",
    },
    {"role": "user", "content": "八王子の天気を教えてください。"},
]

実行結果

レスポンス:
ChatCompletionMessage(content=None, refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=[ChatCompletionMessageFunctionToolCall(id='call_MZmeJ11LfOQufB1dFzyztQEu', function=Function(arguments='{"location":"八王子"}', name='current_weather'), type='function')])
関数呼び出し: current_weather
引数: {'location': '八王子'}
最終レスポンス:
現在の八王子の天気は晴れで、気温は15度です。

傘が必要か聞くmessages

Python
messages = [
    {
        "role": "system",
        "content": "あなたは優秀なアシスタントAIです。",
    },
    {"role": "user", "content": "今から八王子に行きます。傘はいるかな"},
]

実行結果

レスポンス:
ChatCompletionMessage(content=None, refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=[ChatCompletionMessageFunctionToolCall(id='call_Hoa7qoq13vJa4nVqzCdegVJp', function=Function(arguments='{"location":"八王子"}', name='current_weather'), type='function')])
関数呼び出し: current_weather
引数: {'location': '八王子'}
最終レスポンス:
八王子の天気は晴れで、気温は15度です。今のところ傘は必要なさそうですが、念のため天気予報を確認しておくと良いかもしれません。安全に行ってきてください!

直接天気や時刻を聞かなくとも関数を実行して答えてくれます。

まとめ

今回はAzure OpenAIでFunctionCallingを行う方法についてまとめました。

Azure OpenAIのFunctionCallingは並列関数呼び出しが可能で、前章の例のように複数の関数を一度のレスポンスで呼び出すことができます。

どのような関数を実行させるかはアイデア次第です。是非FunctionCallingを使用して生成AIとツールを組み合わせてみてください。

今後もAzure OpenAIについて学んだことを共有していきたいと思います。

読んでいただきありがとうございました。

関連記事

これまでのAzure OpenAI入門

Azure OpenAI入門:モデルのデプロイとpythonからAPIを実行

AzureOpenAI入門:JSON形式のデータを出力させる

参考文献

https://learn.microsoft.com/ja-jp/azure/ai-foundry/openai/how-to/function-calling?view=foundry-classic&tabs=python-secure

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。
エンジニア募集中!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です