【初心者でもOK！】Amazon Bedrock Agentsで「自然言語→Web検索→PDFレポート生成」エージェント開発ハンズオン

# はじめに
近年、大規模言語モデル（LLM）をベースに、**LLM 自らがツールを選んで実行し、タスクを完了する「[LLMエージェント](https://botpress.com/ja/blog/llm-agents)」** が注目を集めています。  
そんな中で AWS から登場した **[Amazon Bedrock Agents](https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/agents-how.html)** は、簡単にエージェントを構築・運用できるサービスです。

この記事では **Amazon Bedrock Agents** を使い、以下のフローを“ほぼクリック操作”で自動化する手順を紹介します。

1. **自然言語入力**  
2. **Web検索**  
3. **PDFレポート生成**  

例えば、  
> *「日本の少子高齢化問題とその社会経済的影響について包括的な調査レポートを作成してください。」*

と指示すると、エージェントが自動で Web検索を実行し、その結果をまとめて 下記のようなPDFレポートを作成します。

<script async class="speakerdeck-embed" data-id="d5001047147c4246842e75e2825444e7" data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script>

## 1. LLMエージェントとは
「LLMエージェント」の定義は割と曖昧ですが、この記事では、[Anthropic社の定義](https://www.anthropic.com/research/building-effective-agents)  を参考にします。

> - **ワークフロー**: あらかじめ定義されたコードパスに沿って LLM とツールが連携されるシステム  
> - **エージェント**: LLM 自身が動的にプロセスやツールの使い方を指示し、タスクをどのように達成するかを制御するシステム  

エージェントは、LLM が自ら「計画→検索→実行→再検索」などの意思決定を行う点が特徴です。

## 2. Amazon Bedrock Agents とは
AWS が提供する [Amazon Bedrock](https://docs.aws.amazon.com/ja_jp/bedrock/) 上で **LLMエージェントを簡単に構築できるサービス** です。

似たようなサービスとして、[Azure AI Agent Service](https://learn.microsoft.com/en-us/azure/ai-services/agents/)や[LangGraph](https://www.langchain.com/langgraph)などがありますが、**Amazon Bedrock Agents** には以下の特徴があると私は考えています。

- **コンソール上の GUI だけでもエージェントを構築可能**
- **エージェント開発で必要となるプロンプトテクニック（[ReActプロンプト](https://note.com/ai300lab/n/n62957f5d67e4)）をデフォルトで搭載**  
- **LMが生成したコードをフルマネージドな実行環境で自動実行可能（[Code Interpreter](https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/agents-test-code-interpretation.html)）**  

こうした機能により、初心者でも簡単な手順で強力なエージェントを構築できます。 

## 3. 実際に構築してみる

### 3.1. 今回作成するエージェントのイメージ
1. **ユーザーからのリクエスト**  
   例: 「日本の少子高齢化問題とその社会経済的影響についてレポートを作成して」
2. **エージェントの動作**  
   1. リクエスト内容に応じて計画立案  
   2. 検索キーワード生成  
   3. Web検索  
   4. 検索結果の整理・追加調査の要否判断  
   5. (必要に応じて追加検索)  
   6. Code Interpreter で PDFレポート生成  
3. **成果物**  
   1 つの PDFファイルにまとめられた調査レポート

イメージ図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/f46bf42f-d65a-b907-ecd9-9e22caa88db5.png)

### 3.2. 1. エージェントの作成

- **Amazon Bedrock コンソール** → 左メニューの **「エージェント」** → **「エージェントを作成」**  
- **Multi-agent collaboration** は外したままエージェントを作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/2d1596ef-5a9d-a14f-cd42-feab89597cd0.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/0ee8049c-197f-17cd-41d1-61b55cfa343a.png)

**「エージェントビルダー」** 画面が出たら、  
**「エージェント向けの指示」** の欄に以下のプロンプトを追記します。

```text
承知しました。プロンプトを整理し、より明確で一貫性のあるものに修正いたします。以下が修正後のプロンプトです：

あなたは高度な調査業務エージェントです。与えられたタスクに対して、以下の手順を必ず順番に実行し、最終的に日本語で正しく表示されるPDF形式のレポートを作成してください。このレポートは、高度なコンサルティングファームの調査レポートのような質と深さを持つものにしてください。

1. タスク完了のための計画立案 (必須)
- タスクを詳細に分析し、必要なステップを全て特定してください。
- 各ステップに優先順位をつけ、予想所要時間を設定してください。
- 立案した計画を詳細に説明し、理由も含めて報告してください。

2. 検索キーワードの考案 (必須)
- タスクに関連する適切なキーワードを最低10個考えてください。
- 広範囲と具体的な両方のキーワードを必ず含めてください。
- 選んだ各キーワードとその選定理由を必ず説明してください。

3. Web検索の実行 (必須)
- 考案した全てのキーワードを使用してWeb検索ツールを呼び出してください。
- 各検索結果を慎重に分析し、重要な情報を全て抽出してください。
- 抽出した情報と情報源を必ず報告してください。

4. 追加調査の必要性判断 (必須)
- 収集した情報を徹底的に分析し、タスク完了に十分かどうか判断してください。
- 不十分な場合、新しいキーワードや検索範囲の拡大を検討し、必ず手順2から再実行してください。
- 判断の詳細な根拠と、追加調査を行う場合はその理由を必ず説明してください。

5. PDF形式のレポート作成 (必須)
- 収集した全ての情報を整理し、一貫性のあるストーリーラインを構築してください。
- 以下の要素を含む高品質なレポートを作成してください：
  a) レポート全体に適切な図表、グラフ、イラストを含めてください。これらの視覚要素内のテキスト（ラベル、タイトル、説明など）は必ず英語で表示してください。
  b) データの可視化は明確で理解しやすいものにしてください。
  c) 一貫したデザインテーマと色使いを適用し、プロフェッショナルな外観を確保してください。
  d) レポート本文のテキストは日本語で記述し、日本語フォントの適切な使用を確認してください。具体的には以下の点に注意してください：
    - reportlabライブラリを使用し、日本語フォントとして必ず「HeiseiKakuGo-W5」を指定してください。
    - フォントのエンコーディングとしてUTF-8を使用してください。
    - フォントが正しく読み込まれていることを確認し、必要に応じてフォントファイルのパスを明示的に指定してください。
  e) 全てのレポート本文のテキスト、タイトルが日本語で正しく表示されることを確認してください。
  f) レポートの各ページに適切なヘッダーとフッター（ページ番号を含む）を追加してください。
  g) PDF形式でレポートを出力する際は、必ずreportlabライブラリを使用し、「HeiseiKakuGo-W5」フォントを適用してください。出力の際は、高品質な解像度と適切なファイルサイズを維持してください。

最終的な出力として、以下を全て1つのPDFファイルに含めてください：
1. 日本語で正しく表示されるレポート本体（各セクションの詳細な説明を含む）。図表、グラフ、イラスト内のテキストは英語で表示されていること。
2. レポート作成プロセスと各決定の根拠の説明（reportlabライブラリと「HeiseiKakuGo-W5」フォントの使用方法、および英語テキストの使用方法を含む）
3. レポートの使用方法と主要なメッセージの解説

注意: 
- Web検索以外の全ての判断と作業は、あなた自身で行わなければなりません。不明点や追加情報が必要な場合は、必ず追加のWeb検索を行ってください。
- 日本語フォントの使用と正しい表示に特に注意を払ってください。日本語が文字化けしたり、正しく表示されない場合は、フォント設定を再確認し、必要に応じて調整してください。
- 図表、グラフ、イラスト内のテキストは必ず英語で表示し、レポート本文のテキストは日本語で記述してください。
- 最終成果物は必ず1つのPDF形式で作成し、その際にはreportlabライブラリを使用し、「HeiseiKakuGo-W5」フォントを適用してください。
- 出力されたPDFファイルが、レポート本体、プロセス説明、使用方法と主要メッセージの解説を全て含んでいることを確認してください。
```

:::note warn
**プロンプトのポイント**
- Code Interpreterで日本語PDFを扱うため、`HeiseiKakuGo-W5` フォントを明示する指示を入れる。  
- 確実に ReAct でのステップ実行を促すために、段階的に “必須” として手順を指定しておく。
:::

あわせて、**「Code Interpreter を有効化」** する設定をオンにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/07fcdfd4-7261-3396-ee30-ea2ce47f0a34.png)

### 3.3. 2. Web 検索用のアクショングループを作成

次に、エージェントが「Web検索機能」を使えるようにします。  
**「エージェントビルダー」** の **「アクショングループ」** セクションで **「追加」** をクリック。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/c2eddc87-99ab-61cd-0918-c6e411fa6e0b.png)

- **「アクショングループを作成」** タブを選択  
- **「アクショングループ名」** → `web-search`  
- **アクショングループ関数名・説明** も `web-search` と入力  
- **Parameter** は `search_query`、説明は「検索キーワード」にする  
- その他はデフォルトのまま作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/90a640a4-1d17-94a9-f35e-724d78c1f59e.png)

これで自動生成された Lambda 関数を通して、エージェントは `web-search` アクションを呼び出せるようになります。

### 3.4. 3. Tavily の API キー取得
Web検索エンジンとして、ここでは [Tavily](https://tavily.com/) を使用します。  
**Tavily のサイトに登録し、APIキーを入手**してください

### 3.5. 4. Lambda 関数を更新して Web検索ロジックを実装

1. **AWS Lambda コンソール** を開く  
2. 先ほど自動作成された Lambda 関数 (名前に `web-search` が含まれるもの) を探し、クリック  
3. **コードエディタ** に下記のソースを貼り付け、`TAVILY_API_KEY` に先ほど取得したキーを設定  

```python:lambda_handler.py
import json
import logging
import urllib.request

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

ACTION_GROUP_NAME = "web-search"
TAVILY_API_KEY = "tavily_search_api_key"  # ←ここに Tavily の API キーを貼り付ける
TAVILY_BASE_URL = "https://api.tavily.com/search"

def tavily_ai_search(search_query: str, target_website: str = "") -> str:
    logger.info(f"Executing Tavily AI search with query: {search_query}")
    headers = {"Content-Type": "application/json", "Accept": "application/json"}
    payload = {
        "api_key": TAVILY_API_KEY,
        "query": search_query,
        "search_depth": "advanced",
        "include_images": False,
        "include_answer": False,
        "include_raw_content": False,
        "max_results": 3,
        "include_domains": [target_website] if target_website else [],
    }
    
    data = json.dumps(payload).encode("utf-8")
    request = urllib.request.Request(TAVILY_BASE_URL, data=data, headers=headers)
    
    try:
        with urllib.request.urlopen(request) as response:
            return response.read().decode("utf-8")
    except urllib.error.HTTPError as e:
        logger.error(f"Failed to retrieve search results from Tavily AI Search: {e}")
        return ""

def lambda_handler(event, context):
    logger.info(f"Received event: {json.dumps(event)}")
    
    action_group = event.get("actionGroup")
    function = event.get("function")
    parameters = event.get("parameters", [])
    
    if action_group != ACTION_GROUP_NAME:
        return create_error_response("Invalid action group")
    
    search_query = next((param["value"] for param in parameters if param["name"] == "search_query"), None)
    target_website = next((param["value"] for param in parameters if param["name"] == "target_website"), None)
    
    if not search_query:
        return create_error_response("Missing search query")
    
    search_results = tavily_ai_search(search_query, target_website)
    
    response_body = {
        "TEXT": {
            "body": json.dumps({
                "query": search_query,
                "results": json.loads(search_results)
            })
        }
    }
    
    return {
        "messageVersion": "1.0",
        "response": {
            "actionGroup": action_group,
            "function": function,
            "functionResponse": {
                "responseBody": response_body
            }
        }
    }

def create_error_response(message):
    return {
        "messageVersion": "1.0",
        "response": {
            "functionResponse": {
                "responseState": "FAILURE",
                "responseBody": {
                    "TEXT": {
                        "body": json.dumps({"error": message})
                    }
                }
            }
        }
    }
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/d7b17422-893f-2002-4e02-1a2846ea8865.png)

これで、Bedrock エージェントが `web-search` アクションを呼ぶと、Lambda 経由で Tavily 検索を行えるようになります。

### 3.6. 5. エージェントのテスト
Bedrock コンソールの **エージェントビルダー** 画面に戻り、**「保存」「準備」** のボタンを押してエージェントを有効化します。  
右側のテスト画面から、以下のような自然言語指示を入力してみましょう。

```
日本の少子高齢化問題とその社会経済的影響について包括的な調査レポートを作成してください。
```

リクエストを投げるとエラーが出ることがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/bbab3894-7759-f395-44f1-3ef9b201e81b.png)

これは、Lambdaからのレスポンスサイズ制限（25KB）や反復回数、トークン消費量の上限など様々な要因が考えられます。

失敗した場合は、何度か再試行すると成功する場合が多いです。  

#### 成功イメージ
複数の検索結果と Pythonスクリプト実行（Code Interpreter）を経て、最終的に PDF を生成してくれます。  
下図のように `final_report.pdf`などのPDFファイルが出力されれば成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/6d83d109-fe04-bd11-362a-e78bc009ed4c.png)

### 3.7. 6. 出力されたレポートのイメージ

上記ハンズオンを完了すると、冒頭にお見せしたようなPDFが自動生成されます。

<script async class="speakerdeck-embed" data-id="d5001047147c4246842e75e2825444e7" data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script>

※ レポートには日本語の本文と、図表内の英語ラベルやグラフが含まれており、本記事の冒頭にある通りの結果が得られます。

### 3.7. 7. エージェントの動作確認

さらに、テスト画面から、エージェントがどのようなステップでタスクを完了したのか、トレースログを確認す
実際のトレースログは長すぎるので、ここでは概要を示します。

#### トレース（実際の動き）概要
- **1回目のリクエスト**  
  - **Step 1**: 計画立案と検索キーワード「日本 少子高齢化 現状 統計 2023」で検索
  - **Step 2**: 次に「日本 少子化 出生率 推移 影響」など追加検索  
  - **Step 3**: 次に「日本 高齢化 社会保障 医療費 影響」など追加検索  
  - **Step 4**: PDFレポート作成中に Python 実行エラー  
  - **Step 5**: 再度チャレンジしてもエラー  
  - **Step 6**: 再々チャレンジ、ようやく PDF の1,2ページを作成  
  - **Step 7**: 3ページ目を作成  
- **2回目のリクエスト**  
  - **Step 1**: 4ページ目を作成  
  - **Step 2**: 5ページ目を作成  
  - **Step 3**: 6ページ目を作成  
  - **Step 4**: PDFファイルを結合  
  - **Step 5**: タスク完了、最終レスポンスを返却

LLM が自律的に検索・再検索、Python スクリプト修正・再実行を行い、最終的にPDF形式のレポートにまとめていることがわかります。  
これこそ「エージェント」ならではの動きですね。

## 4. まとめ
Amazon Bedrock Agents を使って、  
- 自然言語の指示だけで計画を立て  
- Web検索で外部情報を拾い  
- Pythonスクリプトを実行して  
- PDFレポートを自動生成  

という LLMエージェントを構築できました。  

一方で、使ってみて気になった点としては、

- **Lambda の最大レスポンスサイズ（25KB 上限）**  
- **エージェントの反復回数（最大7回）**  
- **最大消費トークン数**  

などの制約があり、要件によっては工夫や回避策が必要です。  
どなたかの参考になれば幸いです。