💁

Bedrock Converse API の Tool Use とはなんぞや?

に公開

この記事は、AWS CommunityのAmazon Bedrock Converse API解説記事の2を日本語訳したものです。親記事に対して行われた2025年4月11日の更新を反映しています。


はじめに

この記事は、Amazon Bedrockでのツール利用に関するシリーズの第2回です。第1回では、Amazon Bedrock Converse APIの簡単なチュートリアルを紹介しました。本記事では、シンプルなツール利用の例を通じて、その仕組みを説明します。今後の記事では、JSON生成エージェントループなど、より高度なユースケースについて掘り下げていきます。

Converse APIは、Amazon Bedrock上の大規模言語モデル(LLM)へ一貫した方法でアクセスできるAPIです。ユーザーと生成AIモデル間のターン制メッセージをサポートし、ツール利用(いわゆる「関数呼び出し(function calling)」)をサポートするモデル向けに、ツール定義の一貫したフォーマットも提供します。

ツール利用とは、大規模言語モデルが、モデル自身が生成したパラメータを使ってアプリケーション側で関数を呼び出すよう指示できる機能です。利用可能な関数とサポートされるパラメータは、プロンプトと一緒にモデルへ渡されます。重要なのは、モデル自身が関数を直接呼び出すのではなく、JSONを返し、呼び出しアプリケーションが実際の処理を行う点です。

ネイティブなツール利用がなぜ重要なのでしょうか?それは、自由形式のコンテンツを自動化や分析に適した構造化データへ変換するための組み込みサポートが得られるからです。高度なプロンプトエンジニアは、既存のLLMで手動でツール利用アプリケーションを構築することにある程度成功していましたが、それは壊れやすかったり、XMLベースだったり、無効なJSONを生成しやすかったりしました。ネイティブサポートにより、ツール利用機能がより多くの人にとって身近で現実的なものになると私は考えています。

Amazon Bedrock Converse APIでのツール利用の流れは以下の通りです:

  1. 呼び出しアプリケーションが (A) ツール定義 と (B) トリガーメッセージ を大規模言語モデルに渡す。
  2. リクエストがツール定義に一致すると、モデルはツール利用リクエストを生成し、ツールに渡すパラメータを含める。
  3. 呼び出しアプリケーションがモデルのツール利用リクエストからパラメータを抽出し、対応するローカル関数に渡す。
  4. 呼び出しアプリケーションはツールの結果を直接使うことも、ツール結果をモデルに戻して追加の応答を得ることもできる。
  5. モデルは最終応答を返すか、さらに別のツールを要求する。

開発環境とAWSアカウントのセットアップ

作業を始める前に、最新のAWS SDKとAmazon Bedrockモデルへのアクセスを設定しておきましょう。

免責事項

  • 大規模言語モデルは非決定的です。この記事の例と異なる結果になる場合があります。
  • ご自身のAWSアカウントでこのコードを実行すると、消費したトークン分の料金が発生します。
  • 私は「最小限のプロンプト」主義を採用しています。ユースケースによっては、より詳細なプロンプトが必要になるかもしれません。
  • すべてのモデルがConverse APIの全機能をサポートしているわけではありません。公式ドキュメントでサポート状況を確認してください。

コード解説:Amazon Bedrock Converse APIの利用

まず、コマンドラインから実行できるPythonスクリプトを書いてみましょう。ここでは、基本的なツール定義、生成されたパラメータの関数への受け渡し、ツール結果のモデルへの返却、エラー処理をデモします。

ツール定義とClaudeにツール利用を促すメッセージの送信

まず、Converse APIのツール定義フォーマットを使ってcosine(コサイン,(余弦))ツールを定義します。このフォーマットについては、シリーズの後の記事でさらに詳しく説明します。

次に、ツール利用リクエストをトリガーするシンプルなメッセージを作成し、空のメッセージリストに追加します。ここでは「user」ロールからのメッセージを作成します。このメッセージ内には、コンテンツブロックのリストを含めることができます。この例では、「7のコサインは何ですか?」とモデルに尋ねるtextコンテンツブロックが1つだけあります。

これでツール定義とメッセージをAmazon Bedrockに渡す準備ができました。ターゲットモデルとしてAnthropicのClaude 3 Sonnetを指定します。モデルの応答のトークン数はmaxTokens値で制限できます。また、応答のばらつきを最小限にするため、temperatureを0に設定します。

ここでシステムメッセージを設定している点に注意してください。Claudeが自分で計算を試みないようにするためです。現世代の大規模言語モデルは、数学計算を確実にこなすことができません。

import boto3, json, math

session = boto3.Session()
bedrock = session.client(service_name='bedrock-runtime')

tool_list = [
    {
        "toolSpec": {
            "name": "cosine",
            "description": "Calculate the cosine of x.",
            "inputSchema": {
                "json": {
                    "type": "object",
                    "properties": {
                        "x": {
                            "type": "number",
                            "description": "The number to pass to the function."
                        }
                    },
                    "required": ["x"]
                }
            }
        }
    }
]

message_list = []

initial_message = {
    "role": "user",
    "content": [
        { "text": "What is the cosine of 7?" } 
    ],
}

message_list.append(initial_message)

response = bedrock.converse(
    modelId="anthropic.claude-3-sonnet-20240229-v1:0",
    messages=message_list,
    inferenceConfig={
        "maxTokens": 2000,
        "temperature": 0
    },
    toolConfig={
        "tools": tool_list
    },
    system=[{"text":"You must only do math by using a tool."}]
)

response_message = response['output']['message']
print(json.dumps(response_message, indent=4))
message_list.append(response_message)

これにより、以下のようなレスポンスが生成されます。

{
    "role": "assistant",
    "content": [
        {
            "text": "Here is how we can calculate the cosine of 7 using the available tool:"
        },
        {
            "toolUse": {
                "toolUseId": "tooluse_xH3ljaGCQwGqx2wdlG8dnA",
                "name": "cosine",
                "input": {
                    "x": 7
                }
            }
        }
    ]
}

ここでいくつか注意すべき点があります:

  1. このケースでは、Claudeはツール利用リクエストの前にテキストも生成しています。Claudeは毎回このようにするわけではなく、時にはテキストなしでツール利用リクエストだけを生成することもあります。
  2. toolUseブロックにはtoolUseIdが含まれています。toolUseIdを使うことで、最初のツールリクエストと、後でClaudeに返すツール結果とを結びつけることができます。
  3. toolUseブロックには呼び出すべきツール名(この場合はcosine)が含まれています。
  4. inputプロパティには、ツールに渡す引数のJSON構造が含まれています。このJSONはそのまま利用することもできます(これについては後の記事でさらに詳しく説明します)。この例では、Claudeは呼び出しアプリケーションに、cosine関数へ引数xに値7を渡すよう求めています。

toolUseコンテンツブロックに基づいて関数を呼び出す

次に、レスポンスメッセージのコンテンツブロックをループ処理します。ツール利用がリクエストされていればcosineツールを使い、LLMからのメッセージに含まれるテキストコンテンツブロックがあればそれを表示します。

response_content_blocks = response_message['content']

for content_block in response_content_blocks:
    if 'toolUse' in content_block:
        tool_use_block = content_block['toolUse']
        tool_use_name = tool_use_block['name']
        
        print(f"Using tool {tool_use_name}")
        
        if tool_use_name == 'cosine':
            tool_result_value = math.cos(tool_use_block['input']['x'])
            print(tool_result_value)
            
    elif 'text' in content_block:
        print(content_block['text'])

これにより、以下のようなレスポンスが生成されます。

Here is how we can calculate the cosine of 7 using the available tool:
Using tool cosine
0.7539022543433046

このパターンは、あなたのユースケースにとって十分かもしれません。もしツールの結果をClaudeに返す必要がなければ、アプリケーション側でツールの呼び出し結果をそのまま使って処理を進めることができます。次のセクションでは、Claudeにフォローアップリクエストを送り、最終的な応答を得る方法を紹介します。

ツールの結果をClaudeに返す

ここからは、レスポンスメッセージのコンテンツブロックをループし、ツール使用リクエストがあるか確認します。ツール使用リクエストがあれば、指定されたツールを呼び出し、Claudeから提供された入力パラメータを渡します。その後、toolResultコンテンツブロックを含むメッセージを作成し、最終応答を得るためにClaudeに返します。

follow_up_content_blocks = []

for content_block in response_content_blocks:
    if 'toolUse' in content_block:
        tool_use_block = content_block['toolUse']
        tool_use_name = tool_use_block['name']

        if tool_use_name == 'cosine':
            tool_result_value = math.cos(tool_use_block['input']['x'])

        follow_up_content_blocks.append({
            "toolResult": {
                "toolUseId": tool_use_block['toolUseId'],
                "content": [
                    {
                        "json": {
                            "result": tool_result_value
                        }
                    }
                ]
            }
        })

if len(follow_up_content_blocks) > 0:
    follow_up_message = {
        "role": "user",
        "content": follow_up_content_blocks,
    }

    message_list.append(follow_up_message)

    response = bedrock.converse(
        modelId="anthropic.claude-3-sonnet-20240229-v1:0",
        messages=message_list,
        inferenceConfig={
            "maxTokens": 2000,
            "temperature": 0
        },
        toolConfig={
            "tools": tool_list
        },
        system=[{"text":"You must only do math by using a tool."}]
    )

    response_message = response['output']['message']
    print(json.dumps(response_message, indent=4))
    message_list.append(response_message)

この処理によって、以下のような応答が得られます。

{
  "role": "assistant",
  "content": [
    {
      "text": "The cosine of 7 is 0.7539022543433046."
    }
  ]
}

素晴らしいですね!ここまではうまくいきました。しかし、ツールの利用が失敗した場合はどうなるのでしょうか?

エラーハンドリング ― Claudeにツール利用の失敗を知らせる

ここでは、一歩戻ってエラーを作り出し、LLMに返す方法を説明します。status属性をerrorに設定することで、Claudeが次にどうするかを判断できるようにします。

del message_list[-2:]  # 最後のリクエストとレスポンスメッセージを削除

content_block = next((block for block in response_content_blocks if 'toolUse' in block), None)

if content_block:
    tool_use_block = content_block['toolUse']

    error_tool_result = {
        "toolResult": {
            "toolUseId": tool_use_block['toolUseId'],
            "content": [
                {
                    "text": "invalid function: cosine"
                }
            ],
            "status": "error"
        }
    }

    follow_up_message = {
        "role": "user",
        "content": [error_tool_result],
    }

    message_list.append(follow_up_message)

    response = bedrock.converse(
        modelId="anthropic.claude-3-sonnet-20240229-v1:0",
        messages=message_list,
        inferenceConfig={
            "maxTokens": 2000,
            "temperature": 0
        },
        toolConfig={
            "tools": tool_list
        },
        system=[{"text":"You must only do math by using a tool."}]
    )

    response_message = response['output']['message']
    print(json.dumps(response_message, indent=4))
    message_list.append(response_message)

この処理によって、以下のようなレスポンスが得られます。

{
  "role": "assistant",
  "content": [
    {
      "text": "申し訳ありませんが、この環境では「cosine」ツールが利用できないようです。数学関数にアクセスできないため、7のコサインを直接計算することはできません。AIアシスタントとして組み込みの数学機能がないため、三角関数のような計算を一から行うことはできません。利用可能なツールの制限について、事前にお伝えすべきでした。他にお手伝いできることがあればお知らせください。"
    }
  ]
}

この場合、Claudeは選択肢がなくなり、ツールリクエストを諦めることになります。

結論

今回はツール利用のごく簡単な例でしたが、ツール利用の仕組みを基本的に理解していただけたかと思います。今後の記事では、複数ツールのオーケストレーション(Zenn)より複雑なJSON応答の生成(Zenn)など、より高度な例を紹介していきます。

さらに学ぶ

Continue reading articles in this series about tool use / function calling:

記事一覧

AWS Bedrock の converse API の使い方

AWS Bedrock の converse API の使い方

Bedrock Converse API の Tool Use とはなんぞや?

Bedrock Converse API の Tool Use とはなんぞや?

Bedrock Converse API のJSON生成を理解したい

Bedrock Converse API のJSON生成を理解したい

Bedrock で Tool-use を組み込んだエージェントループを作りたい

Bedrock で Tool-use を組み込んだエージェントループを作りたい

Discussion