AgentCore ハーネスでBedrock Mantleを含む7モデルを呼び分けてみた

AgentCore ハーネスでBedrock Mantleを含む7モデルを呼び分けてみた

AgentCoreハーネスをCFnで構築し、Claude・GPT-5.4・DeepSeekなど7モデルをper-invocation overrideで呼び出しました。フルアクセスポリシーを使わないIAM設計、モデルごとの安定性、CloudWatch Logsでの問題切り分けまでをまとめています。
2026.07.04

はじめに

Amazon Bedrock AgentCoreのハーネスは、呼び出し時のパラメータ(per-invocation override)でモデルを切り替えられます。単一エンドポイントでAPIフォーマットの異なる複数モデルを呼び分けられるため、マルチモデル構成の実行基盤として有力な選択肢です。

本記事では、ハイエンドのLLMに頼らず、複数のLLMを併用した日本語テキストのマルチレビューの効率的な実現を目指し、以下を検証しました。

  • CFnでハーネスを複数モデルに対応したフルアクセスポリシーを使わないIAMとともに構築する
  • 7モデルをper-invocation overrideで逐次呼び出しする
  • 発生した問題をハーネスのCloudWatch Logsで特定する
  • 検証結果をもとに、ハーネスでカバーできる範囲とできない範囲を整理する

利用可能なモデルとAPIフォーマットの対応は、公式ドキュメントに記載されています。

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-config-and-models.html

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ポリシーの設計

フルアクセス管理ポリシー(AmazonBedrockFullAccessAmazonBedrockMantleFullAccess等)は使わず、許可モデルをResource ARNと条件キーで限定しました。

ランタイム基盤の権限は公式ドキュメントのポリシー例に準拠しています。

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-security.html

カテゴリ 許可アクション 制御方法
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パターンに分かれました。

  1. 空レスポンス → リトライで成功(約30秒): ハーネス内部でリトライが発生し、最終的にモデルが応答を返す。応答時間が30秒近くかかる
  2. 空レスポンス → リトライしても全部空で終了: リトライを繰り返しても空のまま終了し、クライアントにテキストなしが返る

結果テーブルの「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系で同様の空レスポンス・重複出力が確認されています

https://dev.classmethod.jp/articles/amazon-bedrock-gpt-5-5-integration-insights/

https://dev.classmethod.jp/articles/bedrock-gpt55-high-effort-duplicate-json-corruption-workaround/

ハーネス経由で呼び出せなかったモデル

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に切り出す方針がシンプルです。

参考リンク

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事