
AgentCore ハーネスでBedrock Mantleを含む7モデルを呼び分けてみた
はじめに
Amazon Bedrock AgentCoreのハーネスは、呼び出し時のパラメータ(per-invocation override)でモデルを切り替えられます。単一エンドポイントでAPIフォーマットの異なる複数モデルを呼び分けられるため、マルチモデル構成の実行基盤として有力な選択肢です。
本記事では、ハイエンドのLLMに頼らず、複数のLLMを併用した日本語テキストのマルチレビューの効率的な実現を目指し、以下を検証しました。
- CFnでハーネスを複数モデルに対応したフルアクセスポリシーを使わないIAMとともに構築する
- 7モデルをper-invocation overrideで逐次呼び出しする
- 発生した問題をハーネスのCloudWatch Logsで特定する
- 検証結果をもとに、ハーネスでカバーできる範囲とできない範囲を整理する
利用可能なモデルとAPIフォーマットの対応は、公式ドキュメントに記載されています。
CFnでハーネスを構築する
AWS::BedrockAgentCore::Harnessリソースタイプで、ハーネスをテンプレートから作成できます。以下が今回使用したテンプレート全文です。
CFnテンプレート全文(クリックで展開)
AWSTemplateFormatVersion: "2010-09-09"
Description: "AgentCore Harness for multi-model invocation (7 models via per-invocation override)"
Resources:
HarnessExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: multi-model-harness-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: bedrock-agentcore.amazonaws.com
Action: sts:AssumeRole
Policies:
# === Base: Model Invocation ===
- PolicyName: BedrockConverseAllowedModels
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: ConverseAllowedModels
Effect: Allow
Action:
- bedrock:InvokeModel
- bedrock:InvokeModelWithResponseStream
Resource:
- "arn:aws:bedrock:*::foundation-model/anthropic.claude-haiku-4-5*"
- "arn:aws:bedrock:*::foundation-model/anthropic.claude-sonnet-5*"
- "arn:aws:bedrock:*::foundation-model/google.gemma-3-27b-it"
- !Sub "arn:aws:bedrock:*:${AWS::AccountId}:inference-profile/*"
- PolicyName: MantleAllowedModels
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: MantleCreateInference
Effect: Allow
Action:
- bedrock-mantle:CreateInference
- bedrock-mantle:GetInference
- bedrock-mantle:CancelInference
Resource: !Sub "arn:aws:bedrock-mantle:*:${AWS::AccountId}:project/*"
Condition:
StringEquals:
"bedrock-mantle:Model":
- "openai.gpt-5.4"
- "deepseek.v3.2"
- "zai.glm-5"
- "openai.gpt-oss-120b"
- Sid: MantleBearerToken
Effect: Allow
Action:
- bedrock-mantle:CallWithBearerToken
Resource: "*"
# === Base: AgentCore Runtime ===
- PolicyName: AgentCoreRuntime
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: EcrPublicTokenAccess
Effect: Allow
Action:
- ecr-public:GetAuthorizationToken
Resource: "*"
- Sid: StsForEcrPublicPull
Effect: Allow
Action:
- sts:GetServiceBearerToken
Resource: "*"
- Sid: XRayTracingAccess
Effect: Allow
Action:
- xray:PutTraceSegments
- xray:PutTelemetryRecords
- xray:GetSamplingRules
- xray:GetSamplingTargets
Resource: "*"
- Sid: CloudWatchLogsGroup
Effect: Allow
Action:
- logs:CreateLogGroup
- logs:DescribeLogStreams
Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/bedrock-agentcore/runtimes/*"
- Sid: CloudWatchLogsDescribeGroups
Effect: Allow
Action:
- logs:DescribeLogGroups
Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:*"
- Sid: CloudWatchLogsStream
Effect: Allow
Action:
- logs:CreateLogStream
- logs:PutLogEvents
Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*"
- Sid: CloudWatchMetricsPublish
Effect: Allow
Action:
- cloudwatch:PutMetricData
Resource: "*"
Condition:
StringEquals:
"cloudwatch:namespace": "bedrock-agentcore"
- Sid: AgentCoreWorkloadIdentity
Effect: Allow
Action:
- bedrock-agentcore:GetWorkloadAccessToken
- bedrock-agentcore:GetWorkloadAccessTokenForJWT
Resource:
- !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default"
- !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:workload-identity-directory/default/workload-identity/harness_multi_model_review-*"
# === Optional: AgentCore Memory ===
- PolicyName: AgentCoreMemory
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: MemoryAccess
Effect: Allow
Action:
- bedrock-agentcore:CreateEvent
- bedrock-agentcore:DeleteEvent
- bedrock-agentcore:GetEvent
- bedrock-agentcore:ListEvents
- bedrock-agentcore:RetrieveMemoryRecords
Resource: !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:memory/*"
# === Deny: Expensive Models (failsafe) ===
- PolicyName: DenyExpensiveModels
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: DenyExpensiveConverse
Effect: Deny
Action:
- bedrock:InvokeModel
- bedrock:InvokeModelWithResponseStream
- bedrock:Converse
- bedrock:ConverseStream
Resource:
- "arn:aws:bedrock:*::foundation-model/anthropic.claude-fable-5*"
- "arn:aws:bedrock:*::foundation-model/anthropic.claude-opus-4*"
- !Sub "arn:aws:bedrock:*:${AWS::AccountId}:inference-profile/anthropic.claude-fable-5*"
- !Sub "arn:aws:bedrock:*:${AWS::AccountId}:inference-profile/anthropic.claude-opus-4*"
- Sid: DenyExpensiveMantle
Effect: Deny
Action:
- bedrock-mantle:CreateInference
Resource: "*"
Condition:
StringEquals:
"bedrock-mantle:Model":
- "anthropic.claude-fable-5"
- "anthropic.claude-opus-4-8"
- "openai.gpt-5.5"
ReviewHarness:
Type: AWS::BedrockAgentCore::Harness
DependsOn: HarnessExecutionRole
Properties:
HarnessName: multi_model_review
ExecutionRoleArn: !GetAtt HarnessExecutionRole.Arn
Model:
BedrockModelConfig:
ModelId: global.anthropic.claude-haiku-4-5-20251001-v1:0
ApiFormat: converse_stream
SystemPrompt:
- Text: "あなたは多言語対応のAIアシスタントです。ユーザーの質問に簡潔に回答してください。"
MaxIterations: 5
AllowedTools: []
TimeoutSeconds: 120
Outputs:
HarnessArn:
Value: !GetAtt ReviewHarness.Arn
HarnessId:
Value: !GetAtt ReviewHarness.HarnessId
ExecutionRoleArn:
Value: !GetAtt HarnessExecutionRole.Arn
IAMポリシーの設計
フルアクセス管理ポリシー(AmazonBedrockFullAccess、AmazonBedrockMantleFullAccess等)は使わず、許可モデルをResource ARNと条件キーで限定しました。
ランタイム基盤の権限は公式ドキュメントのポリシー例に準拠しています。
| カテゴリ | 許可アクション | 制御方法 |
|---|---|---|
| Converse API (Claude, Gemma 3) | bedrock:InvokeModel, InvokeModelWithResponseStream |
モデル ARN で Resource 制限 |
| Mantle (GPT-5.4, DeepSeek, GLM-5, gpt-oss-120b) | bedrock-mantle:CreateInference 等 |
bedrock-mantle:Model 条件キーで制限 |
| AgentCore ランタイム基盤 | ECR Public / STS / X-Ray / CloudWatch | 公式ドキュメント準拠 |
| AgentCore Memory | CreateEvent / DeleteEvent / GetEvent / ListEvents / RetrieveMemoryRecords | memory リソース ARN で制限 |
| WorkloadIdentity | GetWorkloadAccessToken / GetWorkloadAccessTokenForJWT | ディレクトリ + ワークロード ARN で制限 |
| 高額モデル Deny (フェイルセーフ) | Fable 5, Opus 4, GPT-5.5 を明示 Deny | 誤指定時の課金防止 |
Converse API系のモデルはResource ARNでモデル単位の制御が効きます。一方、Mantle経由のモデル(GPT-5.4やDeepSeek V3.2など)はResourceがproject ARNになります。そのため、bedrock-mantle:Model条件キーでモデルを制限します。
高額モデルのDenyポリシーはフェイルセーフです。実行ロール側でモデルを絞ることで、per-invocation overrideで誤って高額モデルを指定してもIAMが拒否します。
7モデル逐次呼び出し
ハーネスのinvoke_runtime APIで、per-invocation overrideを使ってモデルを切り替えます。呼び出し時のポイントは2つです。
runtimeSessionId: 必須パラメータ。ハーネスはセッション単位で会話履歴を管理するため、呼び出しごとに一意の値を渡すmodel.bedrockModelConfig: デフォルトモデルを上書きするper-invocation override。modelIdとapiFormatの組み合わせを指定する
この2つを組み合わせた呼び出しコードの全文を以下に示します。
import boto3
import uuid
import json
client = boto3.client("bedrock-agentcore-runtime", region_name="us-west-2")
HARNESS_ID = "multi_model_review-dhcTb3Kjzm" # CFnスタックのOutputs(HarnessId)から取得
MODELS = [
{"modelId": "global.anthropic.claude-haiku-4-5-20251001-v1:0", "apiFormat": "converse_stream"},
{"modelId": "global.anthropic.claude-sonnet-5-v2-20250514-v1:0", "apiFormat": "converse_stream"},
{"modelId": "openai.gpt-5.4", "apiFormat": "responses"},
{"modelId": "zai.glm-5", "apiFormat": "chat_completions"},
{"modelId": "deepseek.v3.2", "apiFormat": "chat_completions"},
{"modelId": "openai.gpt-oss-120b", "apiFormat": "chat_completions"},
{"modelId": "google.gemma-3-27b-it", "apiFormat": "converse_stream"},
]
def invoke_model(model_config: dict) -> str:
session_id = str(uuid.uuid4())
response = client.invoke_runtime(
harnessId=HARNESS_ID,
runtimeSessionId=session_id,
input={"text": "日本の首都はどこですか?一言で答えてください。"},
model={"bedrockModelConfig": model_config},
)
# EventStream からテキストを連結
# レスポンスはEventStream形式。contentBlockDelta の delta.text にテキストが分割されて届く
text = ""
for event in response["output"]:
if "contentBlockDelta" in event:
delta = event["contentBlockDelta"].get("delta", {})
text += delta.get("text", "")
return text
for model in MODELS:
result = invoke_model(model)
print(f"{model['modelId']}: {result}")
結果
| モデル | apiFormat | 応答時間 | 回答 | 安定度 |
|---|---|---|---|---|
| Claude Haiku 4.5 | converse_stream | 3-4s | 東京です。 | ✅ 安定 |
| Claude Sonnet 5 | converse_stream | 3-4s | 東京です。 | ✅ 安定 |
| GPT-5.4 | responses | 3-30s | 東京 | ⚠️ 不安定 |
| GLM-5 | chat_completions | 4-8s | 東京。 | ✅ 安定 |
| DeepSeek V3.2 | chat_completions | 3-4s | 東京 | ✅ 安定 |
| gpt-oss-120b | chat_completions | 3-4s | 東京 | ✅ 安定 |
| Gemma 3 27B | converse_stream | 2-3s | 東京。 | ✅ 安定 |
GPT-5.4のみ不安定だったため、次章で詳しく確認します。
GPT-5.4の空レスポンス問題
GPT-5.4の不安定さは2パターンに分かれました。
- 空レスポンス → リトライで成功(約30秒): ハーネス内部でリトライが発生し、最終的にモデルが応答を返す。応答時間が30秒近くかかる
- 空レスポンス → リトライしても全部空で終了: リトライを繰り返しても空のまま終了し、クライアントにテキストなしが返る
結果テーブルの「3-30s」はパターン1の幅で、即座に成功する場合は3秒、リトライを経由すると30秒です。
CloudWatch Logsで原因を特定する
ハーネスの実行ログはCloudWatch Logsに自動出力されます。ロググループは/aws/bedrock-agentcore/runtimes/harness_<ハーネス名>-DEFAULTです。
ハーネスがモデルとの入出力をテレメトリとして記録しているため、実際にモデルが何を返したかを確認できます。
成功時のログ:
{
"role": "assistant",
"message": "東京\n"
}
失敗時のログ:
{
"role": "assistant",
"message": ""
}
ログを見ると、モデルから受け取った時点で既に空文字列であり、ハーネス側の処理ではなくGPT-5.4モデル側の問題です。
関連情報
この問題は今回の検証固有ではありません。
- DevelopersIOの検証でもBedrock経由のGPT-5.x系で同様の空レスポンス・重複出力が確認されています
ハーネス経由で呼び出せなかったモデル
Gemma 4 31BとGrok 4.3は、ハーネス経由で呼び出すと以下のエラーが返りました。
Berm is not enabled for this account
一方、MantleのBearer Tokenを使った直接呼び出しでは両モデルとも正常に応答しました。公式ドキュメントにもこれらのモデルのサポートに関する記述はなく、現時点でハーネス経由ではサポート外であると判断しました。
今回の構成での使い分け
今回の検証結果から、モデルの安定性とハーネス対応状況で使い分けることにしました。
| モデル | 実行方式 | 理由 |
|---|---|---|
| Claude Haiku 4.5 / Sonnet 5 | ハーネス | 安定、converse_stream |
| GLM-5 / DeepSeek V3.2 / gpt-oss-120b / Gemma 3 27B | ハーネス | 安定、chat_completions / converse_stream |
| GPT-5.4 | Lambda | 空レスポンス問題(モデル側)、リトライ必須 |
| Gemma 4 31B / Grok 4.3 | Lambda | ハーネス経由で呼び出せない(Berm エラー) |
まとめ
AgentCoreハーネスをCFnで構築し、7モデルをper-invocation overrideで呼び出しました。モデルごとにLambdaを用意する構成と比べると、IAMをハーネスの実行ロールに集約でき、ログも単一のロググループにまとまります。
今回の検証では、Claude、GLM-5、DeepSeek、gpt-oss-120b、Gemma 3 27Bはハーネス経由で安定して応答しました。一方、GPT-5.4は空レスポンスが発生し、Gemma 4 31BとGrok 4.3はハーネス経由では呼び出せませんでした。
コスト面でも、ハーネスのランタイムはI/O wait中のCPU課金がゼロであり、LLM応答待ちが大半を占めるワークロードではLambdaより有利です。
そのため、この構成ではハーネスをマルチモデル呼び出しの標準経路とし、不安定なモデルやハーネス非対応のモデルだけをLambdaに切り出す方針がシンプルです。









