
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
Environment:
AgentCoreRuntimeEnvironment:
LifecycleConfiguration:
IdleRuntimeSessionTimeout: 300
MaxLifetime: 3600
NetworkConfiguration:
NetworkMode: PUBLIC
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が拒否します。
LifecycleConfiguration(ランタイム課金制御)
CFnテンプレートの Environment.AgentCoreRuntimeEnvironment.LifecycleConfiguration でランタイムの生存期間を制御します。
Environment:
AgentCoreRuntimeEnvironment:
LifecycleConfiguration:
IdleRuntimeSessionTimeout: 300 # アイドル5分でセッションタイムアウト
MaxLifetime: 3600 # ランタイムの最大生存時間: 1時間
NetworkConfiguration:
NetworkMode: PUBLIC
| パラメータ | デフォルト | 推奨値 | 意味 |
|---|---|---|---|
IdleRuntimeSessionTimeout |
900(15分) | 300(5分) | セッションがアイドルになってからタイムアウトするまでの秒数 |
MaxLifetime |
28800(8時間) | 3600(1時間) | ランタイムインスタンスの最大生存時間 |
この設定を省略するとデフォルト値が適用されます。 特に MaxLifetime のデフォルト8時間は、短時間バッチ実行の場合に想定外のコストにつながるため、明示的に指定することを推奨します。詳細は後述の「ランタイム課金の注意点」を参照してください。
7モデル逐次呼び出し
ハーネスのinvoke_harness APIで、per-invocation overrideを使ってモデルを切り替えます。呼び出し時のポイントは2つです。
runtimeSessionId: 必須パラメータ。ハーネスはセッション単位で会話履歴を管理するため、呼び出しごとに一意の値を渡すmodel.bedrockModelConfig: デフォルトモデルを上書きするper-invocation override。modelIdとapiFormatの組み合わせを指定する
この2つを組み合わせた呼び出しコードの全文を以下に示します。
import boto3
import uuid
client = boto3.client("bedrock-agentcore", region_name="us-west-2")
HARNESS_ARN = "arn:aws:bedrock-agentcore:us-west-2:123456789012:harness/multi_model_review-xxxxx" # CFnスタックのOutputs(HarnessArn)から取得
MODELS = [
{"modelId": "global.anthropic.claude-haiku-4-5-20251001-v1:0", "apiFormat": "converse_stream"},
{"modelId": "us.anthropic.claude-sonnet-5", "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_harness(
harnessArn=HARNESS_ARN,
runtimeSessionId=session_id,
messages=[
{
"role": "user",
"content": [{"text": "日本の首都はどこですか?一言で答えてください。"}]
}
],
model={"bedrockModelConfig": model_config},
)
# レスポンスはEventStream形式。contentBlockDelta の delta.text にテキストが分割されて届く
text = ""
for event in response["stream"]:
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を使った直接呼び出しでは両モデルとも正常に応答しました。公式ドキュメントにもこれらのモデルのサポートに関する記述はなく、現時点でハーネス経由ではサポート外であると判断しました。
ランタイム課金の注意点
ハーネス自体の作成・保持は無料ですが、InvokeHarness を実行するとAgentCore Runtimeが起動し、ランタイムの生存中はMemory(GB-Hour)とvCPU(vCPU-Hour)が課金されます。
課金の仕組み
Harness(制御プレーン) → 無料
└→ AgentCore Runtime(データプレーン) → 有料
├── Memory: $0.00945/GB-Hour(起動中ずっと課金)
├── vCPU: I/O wait中はゼロ(実際のCPU使用時のみ)
└── 生存期間: MaxLifetime まで(デフォルト8時間)
vCPUはLLM応答待ち中に課金されませんが、Memoryはランタイムが起動している限り課金され続けます。 ランタイム1インスタンスあたり約27GBのメモリが割り当てられるため、意図せず長時間起動すると高額になります。
実際に発生したコスト
今回の検証で8並列実行した際の実コストです。
| 項目 | 金額 | 内容 |
|---|---|---|
| Runtime Memory | $16.17 | 27GB × 8並列 × 約8時間 = 1,711 GB-Hours |
| Runtime vCPU | $1.26 | 14.1 vCPU-Hours |
| Short-Term Memory | $1.11 | 4,443 Events |
| Long-Term Memory Retrieval | $0.73 | 1,456 Retrievals |
| 合計 | $19.40 | — |
LifecycleConfiguration を省略していたため MaxLifetime がデフォルトの8時間となり、実処理が完了してもランタイムが8時間走り続けました。
Lambda との比較
同じ処理を Lambda(256MB)で実行した場合の見積もりです。
| 方式 | 8並列 × 1時間の実行コスト |
|---|---|
| AgentCore Runtime(MaxLifetime=8h) | $16.17 |
| AgentCore Runtime(MaxLifetime=1h) | $2.04 |
| Lambda 256MB | $0.12 |
LLM API を呼び出して応答を受け取るだけのワークロード(ランタイム上でモデル推論を行わない場合)では、Lambdaの方がコスト効率が高いです。
ハーネスが有利なケース
以下のようなケースでは、ランタイムの課金を含めてもハーネスを使う価値があります。
- マルチターン会話: Memory機能で会話履歴を自動管理。DynamoDB等で自前実装する開発コストが不要
- Tool Useオーケストレーション: MaxIterationsで自動ループ。Lambda側でループ制御を書く必要がない
- 新モデル追加時の変更最小化: per-invocation overrideでモデルIDを変えるだけ。Lambdaコードの修正・デプロイが不要
1ショットのバッチ実行(レビュー結果を1回取得して終了)がメインの場合は、Lambda直接呼び出しの方が適しています。
今回の構成での使い分け
今回の検証結果から、モデルの安定性とハーネス対応状況で使い分けることにしました。
| モデル | 実行方式 | 理由 |
|---|---|---|
| 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 エラー) |
ただし、1ショットのバッチ実行がメインであれば、コスト面ではすべてLambdaで実行する方が有利です。ハーネス経由にする場合は LifecycleConfiguration の MaxLifetime を処理時間に合わせて短く設定してください。
まとめ
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はハーネス経由では呼び出せませんでした。
コスト面では、ハーネスのvCPU課金はI/O wait中にゼロになりますが、Memoryはランタイム生存中ずっと課金されます。 LifecycleConfiguration の MaxLifetime を明示的に短く設定しないと、デフォルトの8時間分が課金されるため注意が必要です。マルチターン会話やTool Useが不要な1ショット実行では、Lambdaの方がコスト効率は高くなります。
ハーネスを採用する場合は、以下を推奨します。
MaxLifetimeを実際の処理時間 + バッファ(例: 3600秒)に設定するIdleRuntimeSessionTimeoutを短め(例: 300秒)に設定する- 並列実行数を制御する(Step Functions の MaxConcurrency 等)









