Bedrock Agentsのシングル vs マルチエージェント、速度・設計・運用を徹底比較
はじめに
Amazon Bedrock Agentsでは、1つのエージェントに複数のAction Groupを持たせる「シングルエージェント」構成と、
専門エージェントをオーケストレーターが束ねる「マルチエージェント(Supervisor)」構成の2つのアプローチが取れます。
「マルチエージェントはオーバーヘッドが大きいのでは?」「どちらをいつ使うべきか?」という疑問を検証するため、
同じタスクを両構成で実行してレスポンスタイムを比較しました。
また、実際に構築してみて遭遇したハマりポイントと、本番運用での考慮点も合わせて紹介します。
アーキテクチャ
シングルエージェント
User
└── SingleAgent
├── WeatherActionGroup → Lambda
├── NewsActionGroup → Lambda
└── StockActionGroup → Lambda
1つのエージェントがWeather / News / Stockの3つのAction Groupを持ち、すべてのタスクを自分で処理します。
エージェントはユーザーの質問を解釈し、必要なAction Groupを自分で選択・呼び出します。
マルチエージェント(Supervisor構成)
User
└── OrchestratorAgent (Supervisor)
├── WeatherAgent → WeatherActionGroup → Lambda
├── NewsAgent → NewsActionGroup → Lambda
└── StockAgent → StockActionGroup → Lambda
オーケストレーターが質問を解釈し、適切なサブエージェントに委譲します。
各サブエージェントは専門領域のAction Groupを1つだけ持ちます。
Bedrock Agentsの内部動作
シングルエージェントの処理フロー
- ユーザーの入力をLLMに渡します
- LLMがどのAction Groupを呼ぶか判断し、パラメータを生成します(ReAct形式)
- BedrockがAction GroupのOpenAPIスキーマに従いLambdaを呼び出します
- Lambdaのレスポンスを受け取り、LLMが最終回答を生成します
- 複数のAction Groupが必要な場合は2〜4を繰り返します(逐次)
マルチエージェントの処理フロー
- ユーザーの入力をオーケストレーターLLMに渡します
- オーケストレーターがどのサブエージェントに委譲するか判断します
- サブエージェントのAliasを
InvokeAgentAPIで呼び出します - サブエージェントが自分のAction Groupを呼び出し結果を返します
- オーケストレーターが全サブエージェントの結果を統合して最終回答を生成します
重要な点: Supervisorモードでは、オーケストレーターはサブエージェントを
デフォルトで逐次呼び出します。並列呼び出しはオーケストレーターのLLMが
「これらは独立したタスクだ」と判断した場合に自動で行われますが、
Instructionの設計によって挙動が変わります。
BedrockがOpenAPIスキーマを使う仕組み
BedrockはAction GroupのOpenAPIスキーマをLLMのコンテキストに含めます。
LLMはスキーマを読んで「どのエンドポイントを、どのパラメータで呼ぶか」を判断し、
BedrockがそれをLambda呼び出しに変換します。
つまりOpenAPIスキーマはLLMへの「説明書」であり、
summary / description / operationId の記述品質が
エージェントの判断精度に直結します。
計測環境
- モデル: Amazon Nova Lite (
amazon.nova-lite-v1:0) - リージョン: ap-northeast-1(東京)
- Action Group: モックLambda(固定レスポンスを返す)
- 計測プロンプト:
"Tell me the weather in Tokyo, the latest technology news, and the stock price for AMZN." - 計測回数: 5回(各実行間に2秒のインターバル)
モックLambdaを使うことでLambdaの処理時間を排除し、
エージェントのLLM処理・オーケストレーション時間のみを計測対象としました。
Lambdaのレスポンス形式はBedrock Agent専用の形式が必要で、以下のように実装しました:
def lambda_handler(event, context):
action_group = event.get("actionGroup", "")
api_path = event.get("apiPath", "")
parameters = event.get("parameters", [])
city = next((p["value"] for p in parameters if p["name"] == "city"), "Tokyo")
body = {"city": city, "temperature": "22C", "condition": "Sunny", "humidity": "55%"}
return {
"messageVersion": "1.0",
"response": {
"actionGroup": action_group,
"apiPath": api_path,
"httpMethod": event.get("httpMethod", "GET"),
"httpStatusCode": 200,
"responseBody": {
"application/json": {
"body": json.dumps(body)
}
}
}
}
messageVersion: "1.0" と responseBody の形式はBedrock Agent固有の仕様で、
通常のAPI GatewayバックエンドのLambdaとは異なる点に注意が必要です。
計測結果(5回)
シングルエージェント
| Run | 時間 (sec) |
|---|---|
| 1 | 6.556 |
| 2 | 5.414 |
| 3 | 5.173 |
| 4 | 5.132 |
| 5 | 5.227 |
| 指標 | 値 (sec) |
|---|---|
| 最小 | 5.132 |
| 最大 | 6.556 |
| 平均 | 5.500 |
| 中央値 | 5.227 |
| 標準偏差 | 0.600 |
マルチエージェント
| Run | 時間 (sec) |
|---|---|
| 1 | 6.207 |
| 2 | 5.899 |
| 3 | 6.110 |
| 4 | 6.108 |
| 5 | 1.843 |
| 指標 | 値 (sec) |
|---|---|
| 最小 | 1.843 |
| 最大 | 6.207 |
| 平均 | 5.233 |
| 中央値 | 6.108 |
| 標準偏差 | 1.899 |
考察
平均レスポンスタイムはほぼ同等
平均値はMultiが5.233秒、Singleが5.500秒と差は0.267秒にとどまりました。
「マルチエージェントはオーバーヘッドが大きい」という先入観に反し、
今回の構成では速度差はほぼ見られませんでした。
これはオーケストレーターのLLM呼び出しコストが、
サブエージェントへの委譲によって各エージェントのコンテキストが
シンプルになることで相殺されているためと考えられます。
マルチエージェントはばらつきが大きい
Multiの標準偏差は1.899秒とSingleの0.600秒に比べて3倍以上大きくなっています。
5回目の計測が1.843秒と突出して速かったのはBedrockのレスポンスキャッシュの影響と推測されます。
本番運用ではこのばらつきを考慮したタイムアウト設計が必要になります。
逐次 vs 並列呼び出しの理論値
今回の計測ではオーケストレーターがサブエージェントを逐次呼び出していました。
仮に3つのサブエージェントを並列呼び出しできた場合、
理論上のレスポンスタイムは以下のように変わります:
逐次: T_orch + T_weather + T_news + T_stock + T_final
並列: T_orch + max(T_weather, T_news, T_stock) + T_final
今回の計測では各サブエージェントの呼び出しが約1〜2秒と推定されるため、
並列化できれば2〜4秒の短縮が期待できます。
並列化の実現にはオーケストレーターのInstructionに
「これらのタスクは独立しているため並列で実行せよ」と明示する必要があります。
シングル vs マルチの使い分け基準
| 観点 | Single Agent | Multi Agent |
|---|---|---|
| Action Group数 | 〜5個程度まで | 6個以上、または増加が見込まれる場合 |
| タスクの独立性 | 依存関係がある | 独立したタスクが多い(並列化の恩恵大) |
| 開発体制 | 1チームで管理 | 複数チームで分担開発 |
| Instructionの複雑さ | シンプルに保てる | 複雑になってきたら分割を検討 |
| 拡張性 | 低い(全体に影響) | 高い(サブエージェント単位で差し替え可) |
| デバッグのしやすさ | ◎(呼び出しが1段) | △(委譲の追跡が必要) |
推奨: まずシングルエージェントで始め、Action Groupが5〜6個を超えてきたり、
Instructionが複雑になってきたタイミングでマルチエージェントへの移行を検討してください。
本番運用での考慮点
コスト
マルチエージェントはオーケストレーターとサブエージェントの両方でLLMを呼び出すため、
トークン消費量がシングルエージェントより多くなります。
オーケストレーターに高性能モデル(Nova Pro等)、
サブエージェントに軽量モデル(Nova Lite等)を使い分けることでコストを最適化できます。
タイムアウト設計
マルチエージェントは呼び出しが多段になるため、
Lambda・API Gatewayのタイムアウト設定を余裕を持って設定する必要があります。
今回の計測でも最大6.2秒かかっており、デフォルトの29秒制限には余裕がありますが、
サブエージェントが増えるほどタイムアウトリスクが高まります。
エラーハンドリング
サブエージェントがエラーを返した場合、オーケストレーターがどう振る舞うかは
Instructionの設計に依存します。
「サブエージェントがエラーを返した場合はその旨をユーザーに伝えよ」など、
エラー時の挙動を明示的にInstructionに含めることを推奨します。
構築時のハマりポイント
1. OpenAPIスキーマにresponse bodyのcontentが必須
Action GroupのAPIスキーマで responses.200 に content と schema を省略すると
「Failed to create OpenAPI 3 model」エラーになります。
NG例:
"responses": { "200": { "description": "Weather information" } }
OK例:
"responses": {
"200": {
"description": "Weather information",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"city": { "type": "string" },
"temperature": { "type": "string" }
}
}
}
}
}
}
2. エージェントのバージョンとAliasの関係
CloudFormationでエージェントのモデルIDを変更して更新しても、
Aliasが指すバージョンは自動更新されません。
AutoPrepare: true はDRAFTをPrepareするだけで新バージョンは作成しません。
モデルIDを変更した場合はスタックを削除・再作成してバージョン1を作り直す必要があります。
3. マルチエージェントのCloudFormationサポートの制約
AWS::Bedrock::Agent の AgentCollaborators プロパティを使うと
AssociateAgentCollaboratorRequest で権限エラーが発生します。
CloudFormationがサブエージェントのAliasに対するリソースベースポリシーを
自動設定しないためです。マルチエージェントはコンソールまたはSDKで作成する必要があります。
4. エージェントがAction Groupを呼ばない問題
Instructionが曖昧だとエージェントがAction Groupを使わず
「Sorry, I am unable to assist」と返すことがあります。
NG例: You are a helpful assistant that can retrieve weather information.
OK例: You are a weather specialist. You MUST call WeatherActionGroup to get weather data. Always use the action group. Never refuse.
「MUST」「Never refuse」など強制的にAction Groupを使わせる指示が効果的でした。
5. LambdaのレスポンスはBedrock Agent専用の形式が必要
通常のLambdaと異なり、Bedrock AgentのAction Groupから呼ばれるLambdaは
messageVersion と responseBody を含む専用形式を返す必要があります(前述のコード参照)。
この形式を守らないとエージェントがLambdaの結果を正しく解釈できません。
6. Lambda PermissionのSourceArn
Lambda関数のリソースポリシーに SourceArn でエージェントARNを指定すると、
エージェントを再作成するたびにIDが変わるため更新が必要になります。
ワイルドカードを使うと管理が楽になります。
SourceArn: !Sub "arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:agent/*"
まとめ
| 観点 | Single Agent | Multi Agent |
|---|---|---|
| 平均レスポンス | 5.500 sec | 5.233 sec |
| ばらつき(標準偏差) | 0.600 sec | 1.899 sec |
| 構築の容易さ | ◎ | △(CFn制約あり) |
| 拡張性 | △ | ◎ |
| 関心の分離 | △ | ◎ |
| コスト | 低 | 高(LLM呼び出し増) |
今回の計測では速度差はほぼ出ませんでした。
マルチエージェントの真価は速度ではなく、関心の分離・拡張性・並列化による高速化にあります。
シンプルなユースケースではシングルエージェントで十分ですが、
Action Groupが増えてきたタイミングでマルチエージェントへの移行を検討する価値があります。
さいごに
本記事の内容は執筆時点の情報に基づいており、正確性・完全性を保証するものではありません。実際の構築・運用はご自身の責任のもとで行ってください。最後まで読んでいただきありがとうございました。