Taste of Tech Topics

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

Amazon Bedrock の Tool Use(Function Calling)でプロンプトに応じて処理を振り分ける

はじめに

こんにちは一史です。
最高気温も10℃を下回る日も出てきて、外出する際には、マフラーをするようになりました。
皆様も体調にはお気を付けください。

さて、OpenAIのChatGPTではFunction callingという会話の流れからAIが判断して関数(メソッド)を呼び出す機能がありますが、Amazon BedrockでもTool Useという機能により関数呼び出しをすることができます。
docs.aws.amazon.com

今回はこのTool Useを使って、旅行プランの提案・予約を行う生成AIチャットを作ってみます。
AIエージェントで実現されるような内容ですが、ToolUse(Function calling)が実際にどのように使えるかを生成AIチャットを作り、見ていきます。

概要

本記事ではBedrockで旅行プランの提案・予約を行う生成AIチャットを作り、Tool Useで実際にどのようなことに使えるかを試していきます。

Tool Useとは

Tool Use は何をしてくれるのか

Tool Useとは、事前に定義してある関数をAIが任意に呼び出すことができる機能です。
これにより、インターネットの検索や、データベースの読み取り書き込み、複雑な数値計算など、AI単体ではできなかった処理を、会話の文脈からAIが選び出し、実行することができます。

Tool Use はどのように振る舞うのか

例えばAWS公式のサンプルとして、ラジオ局の人気の曲を返す関数を呼び出しています。
このサンプルでは会話の中から、AIがラジオ局の局名を抽出し、関数に渡すことで、人気の曲名を回答しています。
docs.aws.amazon.com

Tool Use を使って旅行プランの提案・予約を行うプロンプト処理を実現する

概要

今回作成する生成AIチャットの構成図はこちらです。
Bedrockとの会話を行う中で、旅行プラン提案関数、旅行プランの修正関数、予約関数を呼び出していきます。

構成図

具体的な処理の流れは以下です。

  1. ユーザーに「どこで、どんなことをしたいか、出発日はいつごろか」を入力してもらう。
  2. ユーザー入力から旅行の、場所とカテゴリを抽出し、旅行プランをユーザーに提示する。
  3. ユーザーとの会話で適宜以下の処理を呼び出す。
    1. 旅行プランの一部変更。
    2. AIに予約が必要なものを抽出させて、予約する。

実装内容

全体像

今回の実装の全体像はこちらです。全体像を示すために処理の中身は省略してあります。

class ToolUseAdapter:
    """ユーザーの入力から実行するツールを選択するクラス"""

    def define_tools(self) -> list:
        # ToolUseで使用する各関数の定義の一覧を作成する。

    def select_tool(self, prompt: str) -> tuple:
        # ユーザープロンプトに基づいてツールを選択する。
        # 選択した関数名、生成した引数を返す。

    def make_tool_use_response(self, tool_name: str, result_params: dict) -> str:
        # ツール使用結果をもとにユーザーへの回答を作成する。

class ToolUseExecutor:
    """選択されたツールを実行して実行結果をモデルに返すクラス"""

    def execute(self, prompt: str) -> str:
        # select_toolで判定されたツールを実行し、その結果のメッセージを返す。
        # ツールとしてsuggest_base_plan、arrange_plan、reserve、chatのいずれかを実行する。

    def suggest_base_plan(self, destination: str, category: str) -> list:
        # 目的地とカテゴリに基づいて旅行プランを提案する。

    def arrange_plan(self, arranged_plan: list) -> list:
        # 調整された旅行プランを受け取り、返す。

    def reserve(self, reservation_list: list):
        # 予約を行う。

    def chat(self, prompt: str) -> str:
        # ユーザー入力をもとに返答を返す。

def run():
    # 旅行プランの提案と予約処理を実行する。
Tool定義(呼び出される関数の定義)

今回呼び出す関数の定義は、define_toolsメソッド内で以下で定義しています。
ここでは旅行プランの提案、修正、予約用の関数を定義しています。
また、どの関数にも該当せず、返答を生成するだけの処理を選ばせるために、chat関数も定義しています。

    def define_tools(self) -> list:
        # ToolUseで使う各関数の定義を作成する。

        # 旅行プランの提案をする関数の定義。
        base_plan_suggester_def = {
            "toolSpec": {
                "name": "suggest_base_plan",
                "description": "指定された旅行の要件に基づいて旅行プランの一覧を作成する。",
                "inputSchema": {
                    "json": {
                        "type": "object",
                        "properties": {
                            "destination": {
                                "type": "string",
                                "description": "旅行の目的地"
                            },
                            "category": {
                                "type": "string",
                                "description": "旅行のカテゴリ、次のいずれか: 'outdoor', 'culture', 'relaxing', 'food', 'shopping'"
                            }
                        },
                        "required": ["destination", "category"]
                    }
                }
            }
        }

        # 旅行プランの修正をする関数の定義。
        arrange_plan_def = {
            "toolSpec": {
                "name": "arrange_plan",
                "description": "更新した旅行プランを出力する関数",
                "inputSchema": {
                    "json": {
                        "type": "object",
                        "properties": {
                            "arranged_plan_list": {
                                "type": "array",
                                "items": {
                                    "start_time": {
                                        "type": "string",
                                        "description": "時刻 HH:MM形式の文字列"
                                    },
                                    "content": {
                                        "type": "string",
                                        "description": "内容の文字列"
                                    }
                                }
                            }
                        },
                        "required": ["arranged_plan_list"]
                    }
                }
            }
        }

        # 予約をする関数の定義。
        reserve_def = {
            "toolSpec": {
                "name": "reserve",
                "description": "確定した旅行プランの中から、予約が必要な場所の予約を行う。常識的に考えて予約が不要な場所や予約ができない場所は予約しない。",
                "inputSchema": {
                    "json": {
                        "type": "object",
                        "properties": {
                            "reservation_list": {
                                "type": "array",
                                "items": {
                                    "reservation_datetime": {
                                        "type": "string",
                                        "description": "予約の日時 yyyy-mm-dd HH:MM:SS形式"
                                    },
                                    "reservation_dest": {
                                        "type": "string",
                                        "description": "予約した先の店"
                                    }
                                }
                            }
                        },
                        "required": ["reservation_list"]
                    }
                }
            }
        }

        # どの関数定義にも該当しなかった場合Chatを行う関数の定義。
        chat_def = {
                "toolSpec": {
                    "name": "chat",
                    "description": "suggest_base_plan、arrange_plan、reserveの呼び出しに該当しない、自由な回答が必要な場合この関数を呼び出して会話を生成する",
                    "inputSchema": {
                        "json": {
                            "type": "object",
                            "properties": {},
                            "required": []
                        }
                    }
                }
            }

        return [
            base_plan_suggester_def,
            arrange_plan_def,
            reserve_def,
            chat_def
        ]
Toolの判定処理

前節で定義したToolの判定処理と、判定後のツール実行処理について紹介します。

判定は以下コードでBedrockに関数の定義とこれまでの会話履歴(conversation_history)を渡し、判定させています。
また、Tool Useはデフォルトでは、回答の文章か関数呼び出しのためのレスポンスが混在して出力されますが、今回は処理をシンプルにするためにtoolChoiceに"any"を設定することで関数一覧のどれかが必ず選択されるようにしています。

    def select_tool(self, prompt: str) -> tuple:
        # ユーザープロンプトを元に、ツールを決定する。
        self.conversation_history.append({"role": "user", "content": [{"text": prompt}]})
        
        # Bedrock APIを呼び出し、指定されたツールとともにプロンプトを送信してレスポンスを取得する。
        tool_list = self.define_tools()

        response = self.bedrock_client.converse(
            modelId="anthropic.claude-3-haiku-20240307-v1:0",
            messages=self.conversation_history,
            toolConfig={
                "tools": tool_list,
                "toolChoice": {"any": {}}
            }
        )

        tool_use_params = response.get("output", {}).get("message", {}).get("content", [{}])[0].get("toolUse", {})

        function_name = tool_use_params.get("name", "")
        input_params = tool_use_params.get("input", {})
        return function_name, input_params


この判定処理結果の一例を以下に載せます。
以下は「横浜でおいしい中華を食べたい。出発日は1月下旬の土曜日がいいです。」とプロンプトを渡した時に、旅行プラン提案関数が選択されたときのBedrockのレスポンスになります。

{
  "message": {
    "role": "assistant",
    "content": [
      {
        "toolUse": {
          "toolUseId": "tooluse_PaQLCs1yTwGy-PejdbG1jw",
          "name": "suggest_base_plan",
          "input": { "destination": "横浜", "category": "food" }
        }
      }
    ]
  }
}

レスポンスを見てみると"toolUse"の中の"name"で呼び出す関数名が、"input"で関数の引数であるdestination(旅の目的地)やcategory(旅行のカテゴリ)が定義通りに生成されていることがわかります。

このselect_toolメソッドを呼び出し、実際に各関数を実行するexecuteメソッドの実装は以下になります。

    def execute(self, prompt: str) -> list:
        # select_toolで判定されたツールを実行し、その結果のメッセージを返す。
        # ツールとしてsuggest_base_plan、arrange_plan、reserve、chatのいずれかを実行する。

        func_name, input_params = self.tool_use_adapter.select_tool(prompt)

        if func_name == "suggest_base_plan":
            # 旅行プランを提案。
            destination = input_params.get("destination", "")
            category = input_params.get("category", "")
            suggested_plan_contents = self.suggest_base_plan(destination, category)
            
            # ユーザーへの回答を生成。
            response_message = self.tool_use_adapter.make_tool_use_response(func_name, {"result": suggested_plan_contents})

        elif func_name == "arrange_plan":
            # 修正されたプランを取得し、提案プランを更新。
            arranged_plan_list = input_params.get("arranged_plan_list", [])
            arranged_plan_list = self.arrange_plan(arranged_plan_list)

            # ユーザーへの回答を生成。
            response_message = self.tool_use_adapter.make_tool_use_response(func_name, {"result": arranged_plan_list})

        elif func_name == "reserve":
            # 予約を行う。
            reservation_list = input_params.get("reservation_list", [])
            self.reserve(reservation_list)
            response_message = "以下の予約が完了しました。\n"
            for res in reservation_list:
                response_message += f"{res['reservation_dest']} ({res['reservation_datetime']})\n"

        else:
            # チャットモードの場合、チャットを行う。
            response_message = self.chat(prompt)

        return response_message

上記実装の"suggest_base_plan"、"arrange_plan"の処理では、ツールの使用結果を以下メソッドで再度Bedrock側に返し、最終的なユーザーへの回答を生成させています。

    def make_tool_use_response(self, tool_name: str, result_params: dict) -> str:
        # ツール使用結果をもとにユーザーへの回答を作成する。

        # assistant, userのメッセージが交互に会話履歴に含まれる必要があるため、両者のメッセージに分けて会話履歴に追加する。
        messages = [
            {
                'role': 'assistant',
                'content': [
                    {
                        'text': f'{name}の結果を返します。'
                    }
                ]
            },
            {
                'role': 'user',
                'content': [
                    {
                        'text': 'あなたが呼び出したツールの使用結果は以下です。この結果をもとにユーザーに回答してください。'
                    },
                    {
                        'text': json.dumps(result_params, ensure_ascii=False)
                    }
                ]
            }
        ]
        
        self.conversation_history.extend(messages)
        
        response = self.bedrock_client.converse(
            modelId="anthropic.claude-3-haiku-20240307-v1:0",
            messages=self.conversation_history
        )

        response_contents = response.get("output", {}).get("message", {}).get("content", [{}])
        response_message = "\n".join([response["text"] for response in response_contents if "text" in response])
        
        return response_message
ツールとなる各関数の処理

前節のToolの判定処理により呼び出される関数は以下です。

まず旅行プラン提案の関数は、旅行先(destination)と旅行のカテゴリ(category)から旅行プランを返します。

    def suggest_base_plan(self, destination: str, category: str) -> list:
        # 目的地とカテゴリに基づいた旅行プランを返す。
        plan = []
        if destination == "横浜":
            if category == "food":
                plan = [
                        {
                            "content": "中華街の正門前で写真撮影",
                            "start_time": "10:30"
                        },
                        {
                            "content": "食べ歩き",
                            "start_time": "10:45"
                        },
                        {
                            "content": "A飯店で食事",
                            "start_time": "12:00"
                        },
                        {
                            "content": "中華街のお土産購入",
                            "start_time": "13:30"
                        },
                        {
                            "content": "赤レンガ倉庫観光",
                            "start_time": "15:00"
                        },
                        {
                            "content": "ホテルにチェックイン",
                            "start_time": "17:30"
                        }
                    ]
        elif destination == "岐阜":
            if category == "culture":
                plan = [
                        {
                            "content": "岐阜城訪問",
                            "start_time": "09:00"
                        },
                        {
                            "content": "金華山ハイキング",
                            "start_time": "11:00"
                        },
                        {
                            "content": "長良川鵜飼観覧",
                            "start_time": "14:00"
                        },
                        {
                            "content": "郡上八幡で街歩き",
                            "start_time": "16:00"
                        },
                        {
                            "content": "温泉宿で宿泊",
                            "start_time": "18:30"
                        }
                ]
        
        elif destination == "北海道":
            if category == "outdoor":
                plan = [
                        {
                            "content": "旭山動物園訪問",
                            "start_time": "08:00"
                        },
                        {
                            "content": "ラーメン村で昼食",
                            "start_time": "13:00"
                        },
                        {
                            "content": "富良野で観光",
                            "start_time": "15:00"
                        },
                        {
                            "content": "温泉ホテルで宿泊",
                            "start_time": "17:30"
                        }
                    ]

        return plan

また旅行プランの修正、予約を行う関数は以下です。
本来は複雑な処理を必要とするところですが本記事の目的ではないので、処理はかなり簡略化したものとしています。

    def arrange_plan(self, arranged_plan: list) -> list:
        # 引数としてAIに生成された、修正した旅行プランを受け取りそれを返す。
        return arranged_plan

    def reserve(self, reservation_list: list):
        # 予約用のAPIを呼び出す。
        pass

プロンプトの会話例

これら機能を使って今回作成する生成AIチャットとの会話例は以下です。

AI: 旅行プランを提案します。どこでどういったことをやりたいか、出発日はいつかを入力してください。
User: 横浜でおいしい中華を食べたい。出発日は1月下旬の土曜日がいいです。

AI: 以下のような横浜での1日の旅行プランをご提案します。
10:30 - 中華街の正門前で写真撮影
10:45 - 中華街を食べ歩き
12:00 - A飯店で中華料理の昼食
13:30 - 中華街でお土産を買い物
15:00 - 赤レンガ倉庫を観光
17:30 - ホテルにチェックイン
出発日は1月下旬の土曜日を想定しています。中華街での食事やお土産探し、赤レンガ倉庫の見学など、横浜の人気スポットを効率的に回れる1日コースです。ご希望の場合は、このプラン の予約手続きを行うことができます。いかがでしょうか。
User: A飯店以外にどういうお店があるか知りたいので、他の中華料理店の名前と特色を一覧で出してください。

AI: はい、わかりました。他の中華料理店の名前と特色を一覧でお伝えします。
1. 北京ダック専門店 - 北京ダックの調理方法を一流に仕上げており、香り高く、皮はパリッと、肉は柔らかい逸品。
2. 四川料理店 - 四川料理の本格的な辛さと深い旨味が特徴。麻辣な香り豊かな料理を堪能できます。
3. 広東料理店 - 広東の伝統的な調理法を継承し、繊細な味わいと質の高い食材を使用した料理が人気。
4. 点心専門店 - 種類豊富な点心メニューを楽しめる。蒸し点心やフライ点心などの調理技術が高い。
5. 家庭料理店 - 地元の食材を使った家庭的な味わいの料理が自慢。ホームクッキングのような心地良さがある。
以上が他の中華料理店の一覧になります。ご参考にしていただければと思います。
User: 広東料理いいですね、では昼食はそこに変更してください。

AI: はい、ご要望に応じて旅行プランを更新しました。
10:30 - 中華街の正門前で写真撮影
10:45 - 中華街を食べ歩き
12:00 - 広東料理店で昼食
13:30 - 中華街でお土産を買い物
15:00 - 赤レンガ倉庫を観光
17:30 - ホテルにチェックイン
昼食が A飯店からより本格的な広東料理店に変更になりました。中華街の散策、お土産探し、赤レンガ倉庫での観光などを効率的にまわれる1日コースとなっています。ご希望であれば、この旅行プランの予約手続きを行えます。いかがでしょうか。
User: 赤レンガ観光後にカフェでゆっくりする時間を取りたいです。おすすめのカフェはありますか?

AI: 赤レンガ観光後のカフェ探しですね。以下に、おすすめのカフェをいくつかご紹介します。
1. 横浜赤レンガ倉庫 1号館内のカフェ・コロール
- 赤レンガ倉庫内にあるおしゃれな雰囲気のカフェ
- テラス席もあり、赤レンガの景色を楽しめます
- 洋食メニューやスイーツが人気
2. ベイサイドマリーナ内のカフェ・ルノアール
- 港の景色を眺められる立地のカフェ
- カフェラテやケーキなどのメニューが豊富
- 落ち着いた雰囲気で寛げます
3. 山下公園周辺のスターバックス 山下公園店
- 公園を望むロケーションのスターバックス店
- 大きな窓から自然光が差し込む明るい空間
- 定番のコーヒーやフードメニューを楽しめます
いかがでしょうか。それぞれ雰囲気や眺めも異なるので、お好みに合わせてお選びください。観光のあとゆっくりカフェタイムを過ごせそうですね。
User: ゆっくりできるところなら、どれでもいいので適当に選んで予定に入れてください。

AI: わかりました。ユーザー様のご要望に応じて、以下の旅行プランを作成しました。
10:30 - 中華街の正門前で写真撮影
10:45 - 中華街を食べ歩き
12:00 - 広東料理店で昼食
13:30 - 中華街でお土産を買い物  
15:00 - 赤レンガ倉庫を観光
16:30 - ベイサイドマリーナ内のカフェ・ルノアールでゆっくり過ごす
18:00 - ホテルにチェックイン
赤レンガ倉庫観光後は、カフェ・ルノアールでゆっくりとくつろげるようプランに組み込みました。港の景色を楽しみながら、カフェタイムを過ごすことができます。
ご予約の手続きが必要な場合は、ご連絡ください。
User: いいですね、ではその中で予約が必要なものは予約お願いします。

AI: 以下の予約が完了しました。
広東料理店 (2024-01-27 12:00:00)
ベイサイドマリーナ内のカフェ・ルノアール (2024-01-27 16:30:00)
横浜ホテル (2024-01-27 18:00:00)
User: OKありがとう。

AI: 承知しました。ご旅行の際は、ゆっくりとお楽しみください。 


会話例の結果をもとに、Tool Useを使うことで、期待する処理ができているかを確認してきます。

まず最初の旅行プラン提案の部分ではログから、以下のBedrockからのレスポンスでsuggest_base_plan関数が呼ばれました。

{
  "message": {
    "role": "assistant",
    "content": [
      {
        "toolUse": {
          "toolUseId": "tooluse_PaQLCs1yTwGy-PejdbG1jw",
          "name": "suggest_base_plan",
          "input": { "destination": "横浜", "category": "food" }
        }
      }
    ]
  }
}

この生成させたcategoryは、outdoor、culture、relaxing、food、shoppingのどれかの値を取るように関数定義のdescriptionで指定していました。
Bedrockのレスポンスをみると、categoryは期待通り定義したものの中から選ばれているようです。

次に旅行プランの修正の部分ではログから、以下のレスポンスでarrange_plan関数が呼ばれました。

{
  "message": {
    "role": "assistant",
    "content": [
      {
        "toolUse": {
          "toolUseId": "tooluse_G7ZCQ6z9SReFgk3_aMdebA",
          "name": "arrange_plan",
          "input": {
            "arranged_plan_list": [
              { "content": "中華街の正門前で写真撮影", "start_time": "10:30" },
              { "content": "食べ歩き", "start_time": "10:45" },
              { "content": "広東料理店で昼食", "start_time": "12:00" },
              { "content": "中華街のお土産購入", "start_time": "13:30" },
              { "content": "赤レンガ倉庫観光", "start_time": "15:00" },
              { "content": "ホテルにチェックイン", "start_time": "17:30" }
            ]
          }
        }
      }
    ]
  }
}

今回、修正した旅行プランをJSON形式の引数で関数に渡す、という定義をしました。
レスポンスの内容をみると、関数に渡された引数は期待するJSON形式で生成されているがわかります。
また、start_timeの時刻形式も指定通りに生成できているようです。

最後に予約では以下のレスポンスでreserve関数が呼ばれました。

{
  "message": {
    "role": "assistant",
    "content": [
      {
        "toolUse": {
          "toolUseId": "tooluse_6Nbd1k3FSzqTuT5rE7DCbw",
          "name": "reserve",
          "input": {
            "reservation_list": [
              {
                "reservation_datetime": "2024-01-27 12:00:00",
                "reservation_dest": "広東料理店"
              },
              {
                "reservation_datetime": "2024-01-27 16:30:00",
                "reservation_dest": "ベイサイドマリーナ内のカフェ・ルノアール"
              },
              {
                "reservation_datetime": "2024-01-27 18:00:00",
                "reservation_dest": "横浜ホテル"
              }
            ]
          }
        }
      }
    ]
  }
}

レスポンスをみると、「1月下旬の土曜日」という部分から2024/1/27を生成でき、予約が必要な場所の抽出も期待通りできていることが確認できます。

まとめ

Amazon Bedrock Tool Useを使うことで、生成AIチャットから関数を呼び出し、その結果をBedrockの回答に反映されることができました。

応用範囲が広い機能なため、是非ご利用ください。

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

  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 

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

www.wantedly.com