1. システムとオフィスの融合
  2. media
  3. Azure Open AI
  4. Function calling 機能を使って空き予定を検索する仕組みを考える

QESブログ

Function calling 機能を使って空き予定を検索する仕組みを考える

  • LINEで送る
  • このエントリーをはてなブックマークに追加
こんにちは。今回は、OpenAI にある Function calling機能(→公式サイト)を試してみます。

この機能は、Azure OpenAIでも利用できるため、本記事では Azure での動作検証を進めていきます。

Function calling機能とは

機能名を見ると、OpenAIが関数を呼んでくれそうな機能に見えそうですが、実際に呼び出してくれるものではありません。呼び出すべき関数を選択してくれる機能になります。
それだけ?と思われそうですが、ユーザーからの自然言語での要求から、使用する関数を選んでくれるだけでも便利な機能です。
関数自体はOpenAIの外部にある関数のため、戻り値として得られた情報をGPTに連携することで、追加された情報を含めた回答を生成することができます。
外部機能と接続するための手段になります。


シンプルなFunction calling

実際にどのように動作するか試してみます。
学習済のGPTモデルを利用して今の天気を聞いても、リアルタイムの情報を持っていないため回答できません。Function Calling機能を用いて外部機能と連携することで、以下のような流れを作ることができます。
※呼び出すことができる関数は、予め定められた仕様を定義しておく必要があります。

No 担当 動作
1 ユーザー ユーザーが天気について質問する。

例)「今日の東京の天気は?」
2 GPT
(Function
  calling)
指示文および予め定義された関数から呼び出すべき関数を返す。

例)「getWeather 関数 引数:場所=東京、日付=2023/12/10」で関数コールしてください。
3 内部ロジック GPTの回答をもとに、外部機能である getWeather関数を呼び出し、実行結果を得る。

例)getWeatherの応答(「東京」「2023/12/10」「晴れ」)をGPTの入力に追加設定。
4 GPT

ユーザーからの質問と外部関数から得られた情報をもとに、メッセージを生成し回答する。

例)「今日の東京の天気は、晴れです。」

実装例

以下、実装例です。
※ openaiライブラリは、v1.x対応で、Parallel function calling 呼び出し対応用の実装となっています。(1106モデルで試しています)
import json
import os
import datetime
from openai import AzureOpenAI

deployment_id = "gpt-4"

client = AzureOpenAI(
  api_key=os.environ['OPENAI_API_KEY'],
  azure_endpoint=os.environ['OPENAI_API_BASE'],
  api_version="2023-12-01-preview"
)

# functionの定義
tools = [
  {
    "type": "function",
    "function": {
      "name": "getWeather",
      "description": "指定された日付、場所の天気を検索します。",
      "parameters": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "description": "確認したい天気の日付(YYYY/MM/DD形式)"
          },
          "location": {
            "type": "string",
            "description": "確認したい天気の場所(東京、大阪など)"
          }
        },
        "required": ["date", "location"]
      }
    }
  },
]

# 天気を返すダミー関数
def getWeather(args):

  return {
    "date": args.get("date"),
    "location": args.get("location"),
    "weather" : "晴れ"
  }

FunctionList = {
  "getWeather": getWeather,
}

# システムメッセージの設定
system_message = f'''
あなたは、ユーザーからの問い合わせに回答するチャットボットです。
日本語で回答してください。
今日の日付: {datetime.datetime.now().strftime('%Y/%m/%d')}
'''
messages = [{"role": "system", "content": system_message}]

# ユーザーリクエストの設定
user_message = input("\n■質問を入力してください:")
messages.append({ "role": "user", "content": user_message})

# チャット問い合わせ
response = client.chat.completions.create(
  model=deployment_id,
  messages=messages,
  tools=tools,
  tool_choice="auto",
)

assistant_message = response.choices[0].message

if assistant_message.tool_calls:
  messages.append({
    "role": assistant_message.role,
    "tool_calls": assistant_message.tool_calls,
    "content": None
  })

  for tool_call in assistant_message.tool_calls:
    function_name = tool_call.function.name
    function_to_call = FunctionList[function_name]
    function_args = json.loads(tool_call.function.arguments)

    print(f"\n■ 選択された関数")
    print(f"関数: {function_name}")
    print( f"引数: {json.dumps(function_args, ensure_ascii=False)}")
    
    function_response = function_to_call(function_args)
    
    print( f"\n\n関数レスポンス: {json.dumps(function_response, ensure_ascii=False)}")

    # 関数のレスポンスを追加
    messages.append({
      "tool_call_id": tool_call.id,
      "role": "tool",
      "name": function_name,
      "content": json.dumps(function_response)
    })  

  # functionの戻りを受けて、もう一度メッセージの生成依頼
  response = client.chat.completions.create(
    model=deployment_id,
    messages=messages,
  )
  assistant_message = response.choices[0].message

print(f"\n■ GPT回答:\n{assistant_message.content}")
messages.append({"role": assistant_message.role, "content": assistant_message.content})

動作確認

実行するとこのような流れになります。Function callingの戻りとダミー関数が分かるようにコンソール出力しています。
FunctionCalling-001.png

今日という質問をしていますが、GPT自身は当日の日付を知りません。システムメッセージで今日の日付を指示して、今日の日付を認識させています。ダミー関数は、どの location でも「晴れ」しか返しませんが、その結果をもとにGPTが回答を生成しています。

ちなみに、Parallel function calling(並列関数呼び出し)実装なので、複数の場所の天気を聞くとこのようになります。3つの地点分の関数呼び出しが [tool_calls]のリストで返ります。3回分、ダミー関数をコールしてから、GPTに回答を生成を依頼しています。
※Parallel対応していない場合も同じような動きは実現できますが、関数1コール毎にGPTから関数選択の回答を受けることになります。

一つしか関数を定義していませんが、基本的な動きはこのようになります。複数の関数を定義すると、その仕様に合った関数を選んでくれます。ユーザーからの質問の内容が、関数を選択できる内容では無い(今回のケースでは天気とは関係の無い)場合は、普通にチャット応答を返します。

空き時間予定検索の仕組みの検討

基本的な仕組みを理解したところで、応用していきます。

「会議出席予定者の共通の空き時間を探すのが面倒。チャットで適当に聞いたら、答えてくれる感じのボットできない?」といったような話がありました。
ボット実装するなら、Function Calling機能でいい感じにできないかなと思い、考えてみることにしました。

ユーザーの問いかけとしては、以下のような要求があると思います。
「今日の山田さんと小林さんの空き時間を教えて」
「2023/12/10~12/15までの間で、部長と鈴木氏と一郎さんの空いている時間をください」

参加者の言い方も姓だけだったり、敬称があったり無かったり、下の名前だけだったり、役職だけだったり、日付の指定の様々で、要求内容は無限にあります。
これをうまくハンドリングできるかが、今回のポイントになってきます。

用意する関数の検討

この機能を実現するには、以下のような関数があれば、実装できそうな感じがします。

No 関数 実装内容
1 参加予定者の検索 ユーザー要求に含まれた人の呼び方から、予定表システムのキーとなる情報(メールアドレスなど)を返します。候補が複数あれば、複数ユーザーの情報を返します。

【実装イメージ】
Microsoft Entra ID 、Googleアカウントの問い合わせまたは、独自マスタ管理されたユーザー情報の検索など
2 参加予定者の予定取得 参加予定者のキー情報とユーザの要求内容に含まれた期間から、登録済の予定情報を取得します。

【実装イメージ】
Exchange予定表への問い合わせ、Googleカレンダーへの問い合わせ など
3 共通の空き時間確認 取得した予定情報から、共通の空き時間を検出します。


関数定義の実装

関数の定義は、以下のようにしました。3つの関数を用意することを検討してみましたが、まずは「参加予定者の検索」と「参加予定者の予定取得」の関数を定義し、空き時間は取得した予定情報をもとにGPTに判断してもらう形で進めます。
[
  {
    "type": "function",
    "function": {
      "name": "searchMember",
      "description": "メンバー名からメンバーの情報を検索します。",
      "parameters": {
        "type": "object",
        "properties": {
          "user_name": {
            "type": "string",
            "description": "メンバー情報を検索するためのキー情報(ユーザ名)"
          }
        },
        "required": ["user_name"]
      }
    }
  },
  {
    "type": "function",
    "function": {
      "name": "searchMemberSchedule",
      "description": "メンバーのメールアドレスから、予定がはいっている日時を検索します。",
      "parameters": {
        "type": "object",
        "properties": {
          "mail_address": {
            "type": "string",
            "description": "メンバーのメールアドレス"
          },
          "from_date": {
            "type": "string",
            "description": "予定検索期間の開始日(YYYY/MM/DD形式)"
          },
          "to_date": {
            "type": "string",
            "description": "予定検索期間の終了日(YYYY/MM/DD形式)"
          }

        },
        "required": ["meil_address", "from_date", "to_date"]
      }
    }
  }
]

テスト用ダミー関数の実装

ダミー関数は、次のように固定応答するものを作ります。 メンバーの検索機能で、あいまい検索をした場合に複数のユーザーがヒットする可能性があります。そのため、「鈴木」というキーワードに対しては、複数の候補を返すようにしています。予定についても登録済予定があるように固定で応答を返します。
def searchMember(args):
  user_name = args.get("user_name")
  print(f"searchMember 検索キー:{user_name}")

  if user_name == "小林":
    return [
      {
        "mail_address": "taro.kobayashi@qes.dummy",
        "member_name" : "小林 太郎"
      }
    ]

  elif user_name == "鈴木":
    return [
      {
        "mail_address": "ichiro.suzuki@qes.dummy",
        "member_name" : "鈴木 一郎"
      },
      {
        "mail_address": "jiro.suzuki@qes.dummy",
        "member_name" : "鈴木 次郎"
      },
    ]

else: return [] def searchMemberSchedule(args): mail_address = args.get("mail_address") if mail_address == "taro.kobayashi@qes.dummy": return { "mail_address": "taro.kobayashi@qes.dummy", "schedule":[ { "from": "2023-12-04 10:00", "to": "2023-12-04 13:30", }, { "from": "2023-12-04 15:00", "to": "2023-12-04 16:00", }, ] } elif mail_address == "ichiro.suzuki@qes.dummy": return { "mail_address": "ichiro.suzuki@qes.dummy", "schedule":[ { "from": "2023-12-04 15:00", "to": "2023-12-04 16:30", }, { "from": "2023-12-04 12:30", "to": "2023-12-04 13:30", }, ] } return {}

処理部の実装

処理部は、以下のようにしています。(一部省略)
def chat_completion_request(messages):
  try:
    response = client.chat.completions.create(
      model=deployment_id,
      messages=messages,
      tools=tools,
      tool_choice="auto",
    )
    return response
  
  except Exception as e:
    print(f"chat_completion_error: {e}")
    return e

def request(messages):

  try:
    chat_response = chat_completion_request(messages)

    assistant_message = chat_response.choices[0].message

    if assistant_message.tool_calls is None:

      if assistant_message.content:
        print(f"\n■GPT回答:\n{assistant_message.content}")
        messages.append({
          "role": assistant_message.role,
          "content": assistant_message.content
        })

      user_message = input("\n■ユーザー入力:")
      if user_message == "終了":
        return None
      elif user_message == "リセット":
        return messages
      
      messages.append({
          "role": "user",
          "content": user_message
      })

      return request(messages)
    
    else:

      messages.append({
          "role": assistant_message.role,
          "tool_calls": assistant_message.tool_calls,
          "content": None
      })

      for tool_call in assistant_message.tool_calls:
        function_name = tool_call.function.name
        function_to_call = FunctionList[function_name]
        function_args = json.loads(tool_call.function.arguments)

        #print(f"関数: {function_name}")
        #print( f"引数: {json.dumps(function_args, ensure_ascii=False)}")

        function_response = function_to_call(function_args)

        #print(f"関数の戻り値: {function_response}\n")

        messages.append({
          "tool_call_id": tool_call.id,
          "role": "tool",
          "name": function_name,
          "content": json.dumps(function_response)
        })
      
      return request(messages)

  except Exception as e:
    print(f"request error: {e}")
    return e

FunctionList = {
  "searchMember": searchMember,
  "searchMemberSchedule": searchMemberSchedule,
  "searchFreeTime": searchFreeTime,
}

system_message = f''''
あなたは、メンバーの予定を管理する秘書です。日本語で回答してください。
ユーザーからの問い合わせに対して、指定されたユーザーの空き情報を確認します。
メンバーのキー情報は、メールアドレスとなっています。
ユーザーからの問い合わせで、メールアドレスが識別できない場合は、searchMember関数を呼び出して、ユーザー情報を調べます。
問い合わせ内のメンバー名には、敬称(さん、くん、様など)が含まれていることがあります。その場合は敬称を外して、searchMember関数を呼び出してください。
searchMember関数の戻り値が空の場合は、そのユーザーは存在しないため、ユーザーに再確認してください。複数の候補がある場合は、どのユーザーか確認してください。
メンバー全員の予定が確認できたら、共通の空き時間を探してください。
ユーザーからの要求があいまいな場合は、勝手に決めつけず、ユーザーに確認の質問をしてください。
指定が無い場合の検索対象は、平日の 9:00-17:30 とします。
今日の日付:{datetime.datetime.now().strftime('%Y/%m/%d')}
'''

messages = []
while messages is not None:

  messages = [{"role": "system", "content": system_message}]

  messages = request(messages)

動作確認

実装したコードを動かしてみます。


ダミー関数で、「鈴木」を検索すると2名返すように設定してます。このため、GPT側では勝手にユーザーを決めつけずに、どちらの鈴木氏から確認の質問をしてくれることが確認できました。実際にメンバー検索関数を実装する際で、複数候補が存在する場合に単純に複数返してあげれば良さそうです。「山田」氏はダミー関数に登録していないので、見つからないのは想定通りの動作となります。
メンバー検索関数は、どこまで曖昧さを許すかは検索関数の実装次第となります。下の名前だけでの検索を許したり、役職名だけで候補を返すことも実装次第です。

検索ユーザーが特定できたら、それぞれの予定情報を取得し、生成AIに渡して空き時間を返してもらいました。ぱっと見、うまく言っているように見えますが、いろいろ試してみると、空いていない時間を案内するケースもあります。こういった処理はLLMが得意としていない領域のため、現時点の生成AIでは誤った情報を出力することがよくあります(ハルシネーション)。

生成AIは万能ではありません。すべてを生成AIにお任せするのではなく、個別実装を検討した方が良い処理もあります(その実装のためのコードを生成AIに書いてもらうというのもアリ)。今回のケースでは、予定情報さえ取得できれば、空き時間を検索するロジックはそれほど難しくないと考えられますので、当初予定通り個別実装を検討することにします。

上記サンプルは、自然言語での問い合わせパターンをいろいろ試したわけではありませんが、Function Calling 機能を組み合わせれば、いろいろな用途に使えるのではないかと思います。

おわりに

今回は、空き時間を検索する仕組みを検討してみました。実際にメンバー情報を検索する関数やメンバーの予定情報を取得する関数を用意すれば、空き時間を検索する仕組みができそうです。この実装を Teams ボットなどに組み込めば、使い慣れたチャットインターフェースで、やりとりができると考えられます。その流れで、そのまま予定登録するということも、頑張ればできそうです。基本的な仕組みは使いまわせそうですので、Microsoft系、Google系で実装できるか試してみたいと思います。

QESでは、「AIチャットボット構築サービス」をはじめとして、各AIサービスを利用したシステム導入のお手伝いをしております。それ以外でも QESでは様々なアプリケーションの開発・導入を行っております。提供するサービス・ソリューションにつきましては こちら に掲載しております。

システム開発・構築でお困りの問題や弊社が提供するサービス・ソリューションにご興味を抱かれましたら、是非一度 お問い合わせ ください。
また、QESでは採用活動を強化しております。ブログを読んで弊社の業務内容に興味を持っていただけましたら、採用情報にもお目通しいただければ幸いです。

※このブログで参照されている、Microsoft、Azure、Azure OpenAI、Teams、その他のマイクロソフト製品およびサービスは、米国およびその他の国におけるマイクロソフトの商標または登録商標です。
※その他の会社名、製品名は各社の登録商標または商標です。

  • LINEで送る
  • このエントリーをはてなブックマークに追加

お気軽にお問い合わせください。

ページのトップへ