♨️

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

に公開

この記事は、AWS CommunityのBedrock Converse APIの記事を日本語訳したものです。一連の翻訳記事の最後の投稿となります。原文はこちら | Build a Tool Use-Based Agent Loop with Amazon Bedrockから確認できます。なお、実際は記事の公開順に差異がありますが、理解の順番として容易性の高い順序を優先しています。


はじめに

この記事は、Amazon Bedrockを使ったツール利用に関するシリーズの一部です。パート1では、Amazon Bedrock Converse APIのクイックチュートリアルを紹介しました。パート2では、Converse APIでのツール利用方法を解説しました。本記事では、ユーザーリクエストを完了するために複数のツールをオーケストレーションするシンプルなエージェントループの構築方法を説明します。モデルに対して変化球も投げてみて、エラー処理や計画も実演します。これはあくまで簡単な例ですが、エージェントループがより重要なユースケースにどう応用できるかを示すことを目的としています。

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

ツール利用とは、大規模言語モデルが呼び出しアプリケーションに対して、モデルが提供するパラメータで関数を呼び出すよう指示できる手法です。利用可能な関数とサポートされるパラメータはプロンプトと一緒にモデルに渡されます。大規模言語モデル自体が関数を呼び出すわけではなく、JSONを返し、呼び出しアプリケーションがその後の処理を行うという点に注意が必要です。

エージェントループは、生成AIの設計パターン(design pattern)の一つで、いくつかのステップを踏むことで解決できる問題に対し、最終目標を達成するまで、LLMがアプリケーションに一連のアクションを繰り返します。関数を呼び出して、その結果を解釈するという、一連の行動のことです。

なぜエージェント機能が重要なのでしょうか?それは、エージェント機能を使えば、生成AIを使ってより複雑な問題を解決し、より高度なタスクを実行できるからです。ツール利用やエージェントパターンは、LLMの基本的なテキスト処理能力を超えた豊かな有用性をもたらすと私(原文における筆者。AWS SA Jason Stehle氏)は考えています。

この記事で紹介するツール利用ベースのエージェントループは、以下の手順で動作します:

  1. 呼び出しアプリケーションが(A)ツール定義と(B)呼び出しメッセージ(トリガーメッセージ)をLLMに渡す
  2. モデルがツール利用リクエストを生成し、ツールに渡すパラメータを含める
  3. 呼び出しアプリケーションがモデルのツール利用リクエストからパラメータを抽出し、対応するアプリケーションの関数に渡して結果を取得(必要なら外部サービスを呼び出す)
  4. 呼び出しアプリケーションがツール結果をモデルに渡してさらに応答を取得
  5. モデルが最終応答を返すか、別のツールを要求(#3に戻る)
  6. ループが多すぎる場合は、解決せずに手順を終了

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

進める前に、最新のAWS SDKとAmazon Bedrockモデルアクセスを設定してください:


免責事項

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

コードウォークスルー:エージェントループ内で複数のツールを使う

コマンドラインから実行できるPythonスクリプトを書いてみましょう。ツール定義、関数呼び出し、エラー処理、そして解決または最大ループ制限に達するまでのループ実行を示します。


依存関係とツールエラークラスの定義

ツール利用で発生しうる問題を処理するため、ToolErrorクラスを定義します。

import boto3, json, math

class ToolError(Exception):
    pass

Amazon Bedrockを呼び出して応答を返す関数の定義

Anthropic Claude 3 Sonnetをconverseメソッドで呼び出します。メッセージのリストとツールのリストを渡し、出力トークン制限や温度0(temperature)も設定します(開発・テスト時はtemperatureを高くして、色々なレスポンスを確認するのも良いでしょう)。

def call_bedrock(message_list, tool_list):
    session = boto3.Session()

    bedrock = session.client(service_name='bedrock-runtime')
    
    response = bedrock.converse(
        modelId="anthropic.claude-3-sonnet-20240229-v1:0",
        messages=message_list,
        inferenceConfig={
            "maxTokens": 2000,
            "temperature": 0
        },
        toolConfig={ "tools": tool_list }
    )
    
    return response

Converseメソッドの詳細はこちら


ツール利用メソッド呼び出しを処理する関数を追加

基本的な数学の関数を呼び出すため、単純なif/elif文で実装します。マジックのタネを仕込むため、tangentツールは意図的に除外しています。

def get_tool_result(tool_use_block):
    
    tool_use_name = tool_use_block['name']
            
    print(f"Using tool {tool_use_name}")
    
    # Note: We're deliberately excluding tangent so something magical can happen
    if tool_use_name == 'cosine':
        return math.cos(tool_use_block['input']['x'])
    elif tool_use_name == 'sine':
        return math.sin(tool_use_block['input']['x'])
    elif tool_use_name == 'divide_numbers':
        return tool_use_block['input']['x'] / tool_use_block['input']['y'] 
    else:
        raise ToolError(f"Invalid function name: {tool_use_name}")

LLM応答を処理し、フォローアップツール呼び出しが必要か判断する関数

LLMはテキストとtoolUseを含むcontentブロックの組み合わせを返します。toolUseブロックを探し、ツールを実行し、実行できたら、toolResultブロックを含むメッセージを返します。

def handle_response(response_message):
    
    response_content_blocks = response_message['content']
    
    follow_up_content_blocks = []
    
    for content_block in response_content_blocks:
        if 'toolUse' in content_block:
            tool_use_block = content_block['toolUse']
            
            try:
                tool_result_value = get_tool_result(tool_use_block)
                
                if tool_result_value is not None:
                    follow_up_content_blocks.append({
                        "toolResult": {
                            "toolUseId": tool_use_block['toolUseId'],
                            "content": [
                                { "json": { "result": tool_result_value } }
                            ]
                        }
                    })
                
            except ToolError as e:
                follow_up_content_blocks.append({ 
                    "toolResult": {
                        "toolUseId": tool_use_block['toolUseId'],
                        "content": [  { "text": repr(e) } ],
                        "status": "error"
                    }
                })
        
    
    if len(follow_up_content_blocks) > 0:
        
        follow_up_message = {
            "role": "user",
            "content": follow_up_content_blocks,
        }
        
        return follow_up_message
    else:
        return None

リクエスト/レスポンスループを実行する関数

LLMがツール利用を要求しなくなるか、最大ループ数に達するまでループします。

def run_loop(prompt, tool_list):
    MAX_LOOPS = 6
    loop_count = 0
    continue_loop = True
    
    message_list = [
        {
            "role": "user",
            "content": [ { "text": prompt } ]
        }
    ]
    
    while continue_loop:
        response = call_bedrock(message_list, tool_list)
        
        response_message = response['output']['message']
        message_list.append(response_message)
        
        loop_count = loop_count + 1
        
        if loop_count >= MAX_LOOPS:
            print(f"Hit loop limit: {loop_count}")
            break
        
        follow_up_message = handle_response(response_message)
        
        if follow_up_message is None:
            # No remaining work to do, return final response to user
            continue_loop = False 
        else:
            message_list.append(follow_up_message)
            
    return message_list

ツールの定義

基本的な三角関数と除算関数のために4つのツールを定義します。ツール定義フォーマットの詳細は前の記事(翻訳済)で解説しています。

tools = [
    {
        "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"]
                }
            }
        }
    },
    {
        "toolSpec": {
            "name": "sine",
            "description": "Calculate the sine of x.",
            "inputSchema": {
                "json": {
                    "type": "object",
                    "properties": {
                        "x": {
                            "type": "number",
                            "description": "The number to pass to the function."
                        }
                    },
                    "required": ["x"]
                }
            }
        }
    },
    {
        "toolSpec": {
            "name": "tangent",
            "description": "Calculate the tangent of x.",
            "inputSchema": {
                "json": {
                    "type": "object",
                    "properties": {
                        "x": {
                            "type": "number",
                            "description": "The number to pass to the function."
                        }
                    },
                    "required": ["x"]
                }
            }
        }
    },
    {
        "toolSpec": {
            "name": "divide_numbers",
            "description": "Divide x by y.",
            "inputSchema": {
                "json": {
                    "type": "object",
                    "properties": {
                        "x": {
                            "type": "number",
                            "description": "The numerator."
                        },
                        "y": {
                            "type": "number",
                            "description": "The denominator."
                        }
                    },
                    "required": ["x", "y"]
                }
            }
        }
    }
]

ループを開始するプロンプトを渡す

Anthropic Claudeに「7のタンジェントを計算して」と依頼し、やりとりされたメッセージを表示します。

messages = run_loop("What is the tangent of 7?", tools)

print("\nMESSAGES:\n")
print(json.dumps(messages, indent=4))

これでコードを実行し、結果を確認できます。異なるが類似した一連のイベントが観察される場合もあります。


出力

ループ中、スクリプトは呼び出されているツールを表示します。ただtangentツールを呼び出すだけでなく、他のツールも使っています。さらに見てみましょう。

Using tool tangent
Using tool sine
Using tool cosine
Using tool divide_numbers

これが最初のメッセージです。三角関数を計算しましょう:

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

Claudeはtangentツールが使えると判断し、ツールをリクエストします:

{
    "role": "assistant",
    "content": [
        {
            "text": "To calculate the tangent of 7, we can use the \"tangent\" tool:"
        },
        {
            "toolUse": {
                "toolUseId": "tooluse_WnWkxaxYS7-_5BnHvxpLmg",
                "name": "tangent",
                "input": {
                    "x": 7
                }
            }
        }
    ]
},

しかしtangentツールは正しく動作しません!Claudeに残念なお知らせを伝えましょう:

{
    "role": "user",
    "content": [
        {
            "toolResult": {
                "toolUseId": "tooluse_WnWkxaxYS7-_5BnHvxpLmg",
                "content": [
                    {
                        "text": "ToolError('Invalid function name: tangent')"
                    }
                ],
                "status": "error"
            }
        }
    ]
},

するとClaudeは三角関数の恒等式を使うことを思い出し、Plan B(代替案)を実行します。まずsineツールをリクエストします:

{
    "role": "assistant",
    "content": [
        {
            "text": "Apologies, it seems the \"tangent\" tool is not available in this environment. Let me calculate the tangent of 7 manually:\n\nThe tangent is defined as tan(x) = sin(x) / cos(x)\n\nTo find sin(7) and cos(7), I can use the sine and cosine tools:"
        },
        {
            "toolUse": {
                "toolUseId": "tooluse_mXWdxEyNTxui14s9vktNNw",
                "name": "sine",
                "input": {
                    "x": 7
                }
            }
        }
    ]
},

sineツールが正常に動作したので、その結果をClaudeに返します:

{
    "role": "user",
    "content": [
        {
            "toolResult": {
                "toolUseId": "tooluse_mXWdxEyNTxui14s9vktNNw",
                "content": [
                    {
                        "json": {
                            "result": 0.6569865987187891
                        }
                    }
                ]
            }
        }
    ]
},

Claudeはsin(7)を得たので、次にcos(7)をリクエストします:

{
    "role": "assistant",
    "content": [
        {
            "toolUse": {
                "toolUseId": "tooluse_b0uIDNT6Tnq9ZeV9g4d-5g",
                "name": "cosine",
                "input": {
                    "x": 7
                }
            }
        }
    ]
},

cosine島に旅行した感想もClaudeに返します:

{
    "role": "user",
    "content": [
        {
            "toolResult": {
                "toolUseId": "tooluse_b0uIDNT6Tnq9ZeV9g4d-5g",
                "content": [
                    {
                        "json": {
                            "result": 0.7539022543433046
                        }
                    }
                ]
            }
        }
    ]
},

そしていよいよ、Claudeはdivide_numbersツールをリクエストします:

{
    "role": "assistant",
    "content": [
        {
            "text": "Great, now I have sin(7) = 0.6569865987187891 and cos(7) = 0.7539022543433046.\n\nTo get tan(7), I divide sin(7) by cos(7):"
        },
        {
            "toolUse": {
                "toolUseId": "tooluse_Ss9CW9ldQvmlGijl31biow",
                "name": "divide_numbers",
                "input": {
                    "x": 0.6569865987187891,
                    "y": 0.7539022543433046
                }
            }
        }
    ]
},

このとても感動的な割り算の結果もClaudeに返します:

{
    "role": "user",
    "content": [
        {
            "toolResult": {
                "toolUseId": "tooluse_Ss9CW9ldQvmlGijl31biow",
                "content": [
                    {
                        "json": {
                            "result": 0.8714479827243188
                        }
                    }
                ]
            }
        }
    ]
},

そしてついに、Claudeが最終結果を返します!フィナーレですね!

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

(エンドロール)


結論

この例はトークン効率が良いとは言えませんが、ツールを組み合わせて多段階プロセスを解決できることを示しました。この記事が、ツール利用型エージェント設計のヒントやアイデアにつながれば幸いです。より高度なツール定義については前の記事|翻訳済で解説しています。


Learn more

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