AWS BedrockのAIエージェントは25KB以上扱えない？ToolUse/ConverseAPIで突破する方法

生成AIを活用する機会が増えてきており、私もAWSのBedrockを活用する機会が多くなってきました。
しかし、Bedrockをはじめとする生成AIを活用している中でLLMが学習をしていない情報について質問をしても当たり障りのないことが返却されることは少なくありません。

こういったLLMが学習していない情報を具体的に回答できる方法として「AIエージェント」を活用することがあげられます。
BedrockでAIエージェントを用いる場合、LLMが学習していない外部情報をLambdaを活用して取得することができます。
しかし、LambdaからBedrockが受け取れるレスポンスサイズは25KB[1]と決して大きくはなく、取り扱う外部情報が大きい場合に、AIエージェントでは難しい場合があります。

今回はBedrockのToolUse/ConverseAPIを活用し、25KB以上の外部情報が取り扱える方法をご紹介します。

[1]LambdaからBedrockが受け取れるレスポンスサイズは25KBまで
https://docs.aws.amazon.com/bedrock/latest/userguide/agents-lambda.html#agents-lambda-response


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790709/bfdfd3bc-2145-bb48-e687-f36aa63262d8.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790709/9713a1bb-6ba2-0dbf-55ed-f364f1de5ee7.png)


# ソリューションの概要

AWS BedrockのAIエージェントでは、25KBまでの外部情報しか扱えません。
この制限を克服するために、「ToolUse」と「ConverseAPI」を活用します。

## ToolUse

ToolUse は、LLMが外部ツールやAPIを利用できるようにする機能です。
この仕組みを使うことで、LLMがプロンプトから「外部情報が必要」と判断した場合、カスタムツールを通じて情報を取得し、より高度で柔軟な応答を生成できます。

## ConveseAPI

ConverseAPI は、AWS Bedrockの「プレイグラウンド → Chat / Text」で使用されているAPIで、プログラムから直接プロンプトを送信し、生成AIモデルの応答を取得するためのものです。
このAPIを利用することで、プレイグラウンド上と同じようにモデルを操作できます。

## ソリューションの流れ

ToolUse/ConverseAPIを下記のような流れで使用することで、25KBの制限を超えた外部情報を扱えるようにできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790709/37ae9404-020a-433f-b4ae-9c0c842d10f1.png)

# ToolUse/ConverseAPI実装

ConverseAPI/ToolUseを組み合わせたものが下記のコードになります。

※ 参考ドキュメント
[2]ConverseAPI/ConverseStreamAPIの使用例
https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/tool-use-examples.html

```
import logging
import json
import boto3
import requests
from bs4 import BeautifulSoup
import time

from botocore.exceptions import ClientError

class WeatherInfoNotFoundError(Exception):
    """天気情報が見つからない場合のカスタム例外"""
    pass

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

# ★★★ ToolUseが使用するカスタムツール ★★★ 
def get_weather_info():
    """指定されたURLから天気情報を取得します。
    戻り値:
        list: 天気情報に関連するテキスト内容を抽出したリスト。
    """
    
    print("get_weather_info start")
    
    weather_info_contents = ""
    call_url = "{特定のサイトのURL}"
    
    # 指定されたURLにGETリクエストを送信してコンテンツを取得
    response = requests.get(call_url)
    soup = BeautifulSoup(response.content, "html.parser")
    
    texts = []
    
    # クラス"honbun"を持つ<p>タグ内の内容を抽出
    for tmp_content in soup.find_all("p", class_="honbun"):
        # HTMLタグを削除して純粋なテキストを取得
        text = tmp_content.get_text(strip=True)
        if text:
            texts.append(text)
        
        weather_info_contents = texts

    logger.info("weather_info_contents %s", weather_info_contents)

    return weather_info_contents

def generate_text(bedrock_client, model_id, tool_config, input_text):
    """Amazon Bedrockを使用してToolUse機能を活用してテキストを生成します。
    引数:
        bedrock_client: Boto3のBedrockランタイムクライアント。
        model_id (str): Amazon BedrockのモデルID。
        tool_config (dict): ToolUseの設定。
        input_text (str): 応答を生成するためのユーザー入力。
    """
    
    logger.info("Generating text with model %s", model_id)
    
    # ユーザー入力メッセージを準備
    messages = [{
        "role": "user",
        "content": [{"text": input_text}]
    }]

    # ★★★ ToolUseを組み込んだConverseAPIの実行 ★★★ 
    # 入力をBedrockに送信して応答を取得
    response = bedrock_client.converse(
        modelId=model_id,
        messages=messages,
        toolConfig=tool_config
    )
    
    output_message = response['output']['message']
    messages.append(output_message)
    stop_reason = response['stopReason']
    
    print("stop_reason:" + stop_reason)
    
    if stop_reason == 'tool_use':
        # ToolUseが要求された場合、ツールとのやり取りを処理
        tool_requests = response['output']['message']['content']
        for tool_request in tool_requests:
            if 'toolUse' in tool_request:
                tool = tool_request['toolUse']
                logger.info("Requesting tool %s. Request: %s", tool['name'], tool['toolUseId'])

                text = "weather_info"
                if text in tool['name']:
                    tool_result = {}
                    try:
                        # ★★★ ToolUseの実行 ★★★ 
                        # カスタム関数を使用して外部情報をを取得 (例で天候情報を取得する関数を使用)
                        weather_info_contents = get_weather_info()
                        
                        # Bedrockに返すツール結果を準備
                        tool_result = {
                            "toolUseId": tool['toolUseId'],
                            "content": [{"json": {"weather_info_contents": weather_info_contents}}]
                        }
                    except WeatherInfoNotFoundError as err:
                        # 天気情報が見つからない場合の処理
                        tool_result = {
                           "toolUseId": tool['toolUseId'],
                           "content": [{"text": err.args[0]}],
                            "status": 'error'
                        }
                    
                    tool_result_message = {
                        "role": "user",
                        "content": [
                            {
                                "toolResult": tool_result 
                            }
                        ]
                    }
                    
                    messages.append(tool_result_message)
                    
                    # レート制限を回避するための遅延を追加
                    time.sleep(60)
                    
                    # ツール結果をBedrockに再送信してさらに処理
                    response = bedrock_client.converse(
                        modelId=model_id,
                        messages=messages,
                        toolConfig=tool_config
                    )
                    
                    output_message = response['output']['message']      

        # モデルからの最終応答を出力
        for content in output_message['content']:
            print(json.dumps(content, indent=4, ensure_ascii=False))
    else: 
        # ToolUseを使用しない場合、初期応答を出力
        for content in output_message['content']:
            print(json.dumps(content, indent=4, ensure_ascii=False))

def main():  
    logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
    
    model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0"
    input_text = "7月の天気について教えて"
   
    # ToolUseの設定を定義
    tool_config = {
        "tools": [
            {
                "toolSpec": {
                    "name": "weather_info",
                    "description": "2024年7月の天気に関する情報",
                    "inputSchema": {
                        "json": {
                            "type": "object",
                            "properties": {},
                            "required": []
                        }
                    }
                }
            }
        ]
    }
    
    bedrock_client = boto3.client(service_name='bedrock-runtime')
    
    try:
        print(f"Question: {input_text}")
        # 設定されたToolUseを使用してBedrockでテキストを生成
        generate_text(bedrock_client, model_id, tool_config, input_text)
        
    except ClientError as err:
        # AWSクライアントエラーを処理
        message = err.response['Error']['Message']
        logger.error("A client error occurred: %s", message)
        print(f"A client error occurred: {message}")
    
    else:
        print(f"Finished generating text with model {model_id}.")
    
if __name__ == "__main__":
    main()

```

# 結論

ToolUse/ConverseAPI を活用することで、従来の AI エージェントでは対応できなかった 25KB を超える外部情報の取り込みが可能になりました。
これにより、より幅広い外部データを扱えるようになり、LLM の応答の多様性と精度が向上しています。
AI エージェントで取り扱えない外部情報が必要な場合は、ぜひ ToolUse/ConverseAPI を活用する方法 もご検討ください。
| 取り扱えるデータ量| AIエージェント | ConverseAPI/ToolUse |
| ----| ---- | ---- |
| 25KBまで| 〇 | 〇 |
| 25KB以上| × | 〇 |