AWS

Bedrock Agentsのカスタムオーケストレーション|Lambda実装・権限設定とAgentCore移行

Bedrock Agentsのカスタムオーケストレーション|Lambda実装・権限設定とAgentCore移行

Amazon Bedrock Agentsのカスタムオーケストレーションは、エージェントが「いつモデルを呼び、いつツールを実行し、いつ回答を確定するか」という判断を、自前のAWS Lambda関数に任せる機能です。2024年11月27日に提供が始まりました。2026年7月30日にBedrock AgentsはAmazon Bedrock Agents Classicへ改称されてメンテナンスモードに入ったため、今から使えるかどうかはAWSアカウントの利用実績で決まります。この記事では、Lambdaが受け取るイベントの中身と返すべき値、権限設定、そしてAgentCoreへ移すときにどこまで書き直しが要るかを、AWS公式ドキュメントと公式サンプルに沿って説明します。

まとめ

カスタムオーケストレーションでは、エージェントがSTART・MODEL_INVOKED・TOOL_INVOKED・APPLY_GUARDRAIL_INVOKEDのいずれかのstateをLambdaへ送り、LambdaはINVOKE_MODEL・INVOKE_TOOL・APPLY_GUARDRAIL・FINISHのいずれかをactionEventとして返します。INVOKE_MODELで返すのはConverse APIのリクエストそのものです。有効化はUpdateAgentでorchestrationTypeをCUSTOM_ORCHESTRATIONにし、customOrchestration.executor.lambdaにLambdaのARNを渡したうえでPrepareAgentを実行します。Lambda側にはbedrock.amazonaws.comからの呼び出しを許可するリソースベースポリシーが要ります。

過去12か月にBedrock Agentsを使っていないアカウントは、CreateAgentがHTTP 403になり、例外申請もできません。AgentCoreへの移行では、ツール呼び出しの却下や呼び出しの停止だけが目的なら、ハーネスのライフサイクルフック(2026年9月追加)で代わりが利きます(挙動は同じではありません)。ReWOOのようにループの形そのものを変えている構成は、AgentCore Runtimeにコードとして移すしかありません。

カスタムオーケストレーションで制御できる範囲と状態遷移

STARTからFINISHまでのstateとactionEvent

AWSのユーザーガイドは、会話の最初の状態は常にSTARTだと明記しています。以降は、Lambdaが返したactionEventをエージェントが実行し、その結果を次のstateとしてLambdaへ戻す、という往復がFINISHまで続きます。ユーザー定義の値をstateとactionEventの両方に使えるため、独自の段階を挟むこともできます。

Lambdaが受け取るstate 直前にエージェントがしたこと 公式サンプルが返すactionEvent
START InvokeAgentを受けた INVOKE_MODEL
MODEL_INVOKED モデルを呼んだ tool_useならINVOKE_TOOL/end_turnならFINISH
TOOL_INVOKED ツールを実行した INVOKE_MODEL
APPLY_GUARDRAIL_INVOKED ガードレールを適用した (サンプルに分岐なし)

この表の右列は、AWSが公開しているaws-samplesのReAct実装(lambda_react.py)の分岐です。標準のReActと同じ動きをLambdaで再現したもので、ここに「このツールは呼ばせない」「3回呼んだら打ち切る」といった条件を足していくのがカスタムオーケストレーションの使い方になります。

標準オーケストレーション(DEFAULT)から切り替える条件

orchestrationTypeを省略するとDEFAULTになり、モデルが推論と行動を繰り返しながら次の一手を選びます。カスタムに切り替えると、停止理由の解釈、無限ループの防止、失敗時の返答まで全部Lambda側の責任です。切り替えてよいのは、次のように標準のループでは書けない制約が要件にある場合だけです。

  • 最終回答の前に、外部システムでの検証を必ず通す
  • モデルが提案したツール呼び出しを、業務ルールで却下する
  • 計画を最初に1回だけ立て、途中でモデルに再評価させずに実行する(ReWOO)

3つ目について、AWSのブログはレストラン予約の例で、複雑な問い合わせのレイテンシがReActより50〜70%下がったと報告しています。ただしこれは1つの事例の数字で、ReWOOは途中経過を検証せずに計画を実行するため、前の手順の結果で次の手順を変える必要がある業務には向きません。「エージェントが思いどおりに動かない」程度の理由なら、カスタムへ切り替える前に指示文とアクショングループのスキーマを見直すほうが早く片付きます。

オーケストレーションLambdaの入出力仕様と実装

受け取るイベントとinput.textの中身

Lambdaが受け取るイベントは、version("1.0")、state、input、contextで構成されます。context.agentConfigurationには指示文・既定モデルID・ツール定義・ガードレール設定が、context.sessionにはそれまでのintermediarySteps(各ステップのorchestrationInputとorchestrationOutputの組)が入ります。

実装で迷うのはinput.textです。ユーザーガイドは「ユーザーの入力、またはConverse形式のツール結果」としか書いていません。公式サンプルはinput.textを毎回JSONとして読み、状態ごとに次のように扱っています。

// START:ユーザーの発話
{"text": "明日19時に2名で予約したい"}

// MODEL_INVOKED:モデルの応答(outputはそのままmessagesへ積める形)
{"output": {"role": "assistant",
            "content": [{"toolUse": {"toolUseId": "t1", "name": "create_booking", "input": {"people": 2}}}]},
 "stopReason": "tool_use"}

// TOOL_INVOKED:ツールの実行結果
{"toolResult": {"toolUseId": "t1", "content": [{"text": "予約番号 123"}]}}

値は説明用に置き換えています。ユーザーガイドが示すのは構造例までで、全フィールドの必須・任意を定めたスキーマは公開されていません。公式サンプルと同じく、届いたイベントを最初にprint(json.dumps(event))でCloudWatch Logsへ出し、実物で確かめてから分岐を固めてください。イベントには会話の全文やツール結果、セッション属性が入るので、全文を出すのは機密を含まない検証データのときだけにし、本番では記録する項目を絞ってマスキングします。

INVOKE_MODELで返すConverseリクエスト

INVOKE_MODELを返すとき、output.textにはConverse APIのリクエストをJSON文字列で入れます。公式サンプルはmodelId・指示文・ツール定義をagentConfigurationから取り、messagesをintermediaryStepsから組み立てています。書式は通常のConverse APIと同じなので、Amazon Bedrock Converse APIの使い方|boto3実装・ConverseStream・Tool Use・IAM権限のTool Useの章がそのまま参考になります。FINISHのときだけは、output.textにユーザーへ返す回答の文字列を入れます。

output.trace.event.textに入れた文字列は、enableTraceを有効にしてInvokeAgentを呼んだとき、レスポンスにトレースとして流れます(トレースは既定で無効です)。どのstateで何を返したかをここに書いておくと、本番の調査をCloudWatch Logsだけに頼らずに済みます。

ツールの許可リストと回数上限を入れたハンドラ(Python)

公式サンプルの入出力の扱い方に沿って、許可していないツールの呼び出しを打ち切る処理と、1ターン内のツール呼び出しを5回までに抑える処理を足したハンドラです。converse_requestとbuild_messagesも含めた全体像で、使うときはALLOWED_TOOLSを自分のアクショングループの関数名に合わせ、検証用アカウントで実際のイベントに当てて確かめてください。モデルが1回の応答で複数のツールを同時に要求した場合は、対応を簡単にするため終了させています。

import json

MAX_TOOL_CALLS = 5
ALLOWED_TOOLS = {"get_booking", "create_booking"}


def lambda_handler(event, context):
    state = event["state"]
    ctx = event["context"]

    if state in ("START", "TOOL_INVOKED"):
        action, text = "INVOKE_MODEL", json.dumps(converse_request(event))
    elif state == "MODEL_INVOKED":
        result = json.loads(event["input"]["text"])
        stop_reason = result.get("stopReason")
        content = result.get("output", {}).get("content", [])
        tool_uses = [c for c in content if "toolUse" in c]
        if stop_reason == "tool_use" and tool_uses:
            name = tool_uses[0]["toolUse"]["name"]
            if len(tool_uses) > 1:
                action, text = "FINISH", "複数ツールの同時呼び出しには対応していません。"
            elif name not in ALLOWED_TOOLS:
                action, text = "FINISH", "この操作は受け付けられません。"
            elif count_tool_calls(ctx) >= MAX_TOOL_CALLS:
                action, text = "FINISH", "処理が規定回数を超えたため中断しました。"
            else:
                action, text = "INVOKE_TOOL", json.dumps(tool_uses[0])
        elif stop_reason == "end_turn":
            texts = [c["text"] for c in content if "text" in c]
            action, text = "FINISH", "".join(texts) or "回答を生成できませんでした。"
        else:
            raise ValueError("unhandled stopReason: " + str(stop_reason))
    else:
        raise ValueError("unhandled state: " + state)

    return {
        "version": "1.0",
        "actionEvent": action,
        "output": {
            "text": text,
            "trace": {"event": {"text": "state=" + state + " event=" + action}},
        },
        "context": {
            "sessionAttributes": ctx.get("sessionAttributes", {}),
            "promptSessionAttributes": ctx.get("promptSessionAttributes", {}),
        },
    }


def count_tool_calls(ctx):
    turn = (ctx.get("session") or [{}])[-1]
    return sum(
        1 for step in turn.get("intermediarySteps", [])
        if step.get("orchestrationOutput", {}).get("event") == "INVOKE_TOOL"
    )


def converse_request(event):
    ctx = event["context"]
    conf = ctx["agentConfiguration"]
    request = {
        "modelId": conf["defaultModelId"],
        "system": [{"text": conf["instruction"]}],
        "messages": build_messages(ctx, event["input"]),
        "inferenceConfig": {"maxTokens": 1024},
    }
    if conf.get("tools"):
        request["toolConfig"] = {"tools": conf["tools"]}
    return request


def build_messages(ctx, current_input):
    messages = []
    for turn in ctx.get("session", []):
        for step in turn.get("intermediarySteps", []):
            src = step.get("orchestrationInput", {})
            if src.get("state") == "START":
                messages.append({"role": "user", "content": [{"text": src["text"]}]})
            elif src.get("state") == "MODEL_INVOKED":
                messages.append(json.loads(src["text"])["output"])
            elif src.get("state") == "TOOL_INVOKED":
                messages.append({"role": "user", "content": [json.loads(src["text"])]})
    messages.append({"role": "user", "content": [json.loads(current_input["text"])]})
    return messages

返り値のcontextでsessionAttributesを返し直しているのは、公式サンプルと同じく、ターンをまたいで値を持ち越すためです。ガードレールを使うときは、LambdaがAPPLY_GUARDRAILを返し、その結果がAPPLY_GUARDRAIL_INVOKEDとして戻ってくる流れになります。このコードには分岐が無く例外になるので、拒否されたときの終了処理も含めて足してください。ガードレール側の設定はAmazon Bedrock Guardrails(AWSのガードレール)とは|日本語で効かせる設定・料金・実装で扱っています。

エージェントへの設定とLambdaの呼び出し権限

bedrock.amazonaws.comによるLambda呼び出しの許可設定

オーケストレーションLambdaはエージェント(Bedrockのサービス)から呼ばれるため、Lambdaのリソースベースポリシーでbedrock.amazonaws.comにlambda:InvokeFunctionを許可します。公式サンプルは呼び出し元をエージェントのARNに絞っています。

import boto3

region, account_id, agent_id = "ap-northeast-1", "123456789012", "ABCDEFGHIJ"

boto3.client("lambda", region_name=region).add_permission(
    FunctionName="my-orchestrator",
    StatementId="allow-bedrock-agent",
    Action="lambda:InvokeFunction",
    Principal="bedrock.amazonaws.com",
    SourceArn=f"arn:aws:bedrock:{region}:{account_id}:agent/{agent_id}",
)

アクショングループのLambda向けにAWSが掲載しているポリシー例は、AWS:SourceArnに加えてAWS:SourceAccountでも呼び出し元を絞っています。上のコードもエージェントARNを固定しているので呼び出し元は特定のエージェントに絞れていますが、オーケストレーションLambda専用のポリシー要件は公式に明記されていないため、アクショングループと同じくSourceAccountも併せて指定しておくと条件の書き方を揃えられます。

UpdateAgentによるCUSTOM_ORCHESTRATIONの有効化

APIでは、ビルド時エンドポイント(bedrock-agent)へUpdateAgentを送ります。orchestrationTypeに指定できるのはDEFAULTとCUSTOM_ORCHESTRATIONの2つで、customOrchestration.executorに指定できるのはlambda(関数ARN)だけです。CreateAgentでも同じ2項目を渡せます。

import boto3

import time

client = boto3.client("bedrock-agent", region_name="ap-northeast-1")
agent = client.get_agent(agentId="ABCDEFGHIJ")["agent"]
optional = {"instruction": agent["instruction"]} if agent.get("instruction") else {}

client.update_agent(
    agentId=agent["agentId"],
    agentName=agent["agentName"],
    agentResourceRoleArn=agent["agentResourceRoleArn"],
    foundationModel=agent["foundationModel"],
    orchestrationType="CUSTOM_ORCHESTRATION",
    customOrchestration={
        "executor": {
            "lambda": "arn:aws:lambda:ap-northeast-1:123456789012:function:my-orchestrator"
        }
    },
    **optional,
)


def wait_status(agent_id, done, timeout=300):
    for _ in range(0, timeout, 5):
        status = client.get_agent(agentId=agent_id)["agent"]["agentStatus"]
        if status in done:
            return status
        if status == "FAILED":
            raise RuntimeError("agent status FAILED")
        time.sleep(5)
    raise TimeoutError(agent_id)


wait_status(agent["agentId"], {"NOT_PREPARED", "PREPARED"})
client.prepare_agent(agentId=agent["agentId"])
wait_status(agent["agentId"], {"PREPARED"})

UpdateAgentはagentName・agentResourceRoleArn・foundationModelが必須で、差分だけを送る作りではありません。上の例のようにGetAgentで読んだ現在の値を渡し直し、指示文などの任意項目も含めて設定を落とさないようにします。指示文は指定するなら40〜4,000文字で空文字は送れないため、既存値があるときだけ渡しています。UpdateAgentとPrepareAgentはどちらも非同期(HTTP 202)なので、agentStatusがPREPAREDになるのを待ってから、DRAFTを指すテスト用エイリアスTSTALIASIDにInvokeAgentを送って確認します(AWSのテスト手順)。

コンソールでは、Amazon Bedrockの「Agents」から対象エージェントを開き、「Working draft」の「Orchestration strategy」で「Edit」を押し、「Custom orchestration」を選んでLambda関数とバージョンを指定します。保存後、テストウィンドウの「Prepare」で反映されます。

Agents Classicのメンテナンスモードで変わった前提

AWSの案内によると、2023年11月に始まったAmazon Bedrock Agentsは2026年7月30日からAgents Classicとしてメンテナンスモードに入りました(告知はユーザーガイドの変更履歴で2026年6月30日)。Amazon Bedrock本体、モデル推論、ナレッジベース、ガードレールは対象外です。

項目 2026年7月30日以降
既存エージェントの稼働 継続
CreateAgent・InvokeInlineAgent 利用実績のないアカウントは403
UpdateAgent・PrepareAgent・InvokeAgentほか 全アカウントで利用可
モデルカタログ 7月30日時点で凍結
新機能・移行期限・終了日 いずれも予定なし
Agents Classic自体の料金 無料(モデル推論・Lambda等は課金)

使えるかどうかはアカウント単位で、過去12か月にBedrock Agentsの利用があれば自動で許可リストに入ります。利用のないアカウントがCreateAgentを呼ぶとAccessDeniedException(HTTP 403)で「Bedrock Agents is in Maintenance Mode. New agent creation is not available for accounts without prior service usage.」と返り、例外申請の窓口はありません。カスタムオーケストレーションを新しく試すには、利用実績のある検証用アカウントを使うしかないということです。

モデルカタログの凍結により、7月30日より後に出たモデルはAgents Classicのオーケストレーション層では選べません。新しいモデルへ上げ続けたいなら、それ自体がAgentCoreへ移す理由になります。名称変更はラベルだけで、bedrock-agentのAPIネームスペース、SDK、CloudFormationのリソースタイプ、IAMアクションのプレフィックスは変わっていません。

状態をDynamoDBに置くかの判断基準

ループを回すだけなら外部ストアは要りません。経過はintermediaryStepsとして毎回渡され、ターンをまたぐ小さな値はsessionAttributesで運べます。公式サンプルでもDynamoDBを使うのは予約データを保存するアクショングループ側で、オーケストレーションLambdaは何も保存していません。

外部ストアが要るかどうかは、セッションより長く残したいデータがあるかで決まります。idleSessionTTLInSecondsに指定できるのは60〜5,400秒(90分)で、会話が途切れてこの時間が過ぎるとセッションの情報は消えます。申込の途中状態や監査ログはアクショングループのLambdaからDynamoDBなどへ書き出し、オーケストレーションLambdaは状態を持たない作りにしておくと、後で戦略を差し替えても業務データを巻き込みません。

AgentCoreへ移すときの判断:ハーネスのフックか、Runtimeでの書き直しか

AWSの推奨移行先はAmazon Bedrock AgentCoreで、モデル・ツール・指示を宣言するだけの「ハーネス」と、任意のフレームワークで書いたコードを載せる「コード定義エージェント(AgentCore Runtime)」の2つの道があります。AWSの機能対応表は、カスタムオーケストレーターを「AgentCore Runtimeで独自コードをデプロイすればサポート、ハーネスでは利用不可」としています。ただ、カスタムにしていた理由によってはハーネスで足ります。

ツール却下と呼び出し停止に使うハーネスのライフサイクルフック

2026年9月、ハーネスにbefore_invocation・before_tool_call・after_tool_call・after_invocationの4つのライフサイクルフックが加わりました(AgentCoreのフックの解説)。Lambdaを宛先にすると同期で呼ばれ、{"decision": "deny"}を返せば次のように動きます。

  • before_tool_call:そのツール呼び出しだけを飛ばし、ループは続く。LambdaにはtoolName・toolType・toolInput・toolUseIdが渡る
  • after_tool_call:ツールの結果は残したまま、呼び出し全体を止める(stopReasonはhook_stopped)
  • before_invocation:エージェントが動き出す前に止める

上のハンドラの「許可していないツールは実行させない」部分は、before_tool_callでtoolNameを見てdenyを返せば移せます。ただし、上のハンドラは未許可ツールが出た時点で会話を終えますが、before_tool_callのdenyはそのツールを飛ばしてループを続けます。打ち切りたいならafter_tool_callのdenyで止める形になり、その場合は直前のツールが実行された後に止まります。フックのcontextには呼び出し回数が入らないため、回数上限もLambda側で数える必要があります。sessionIdは複数ターンで共有されるので、それだけをキーにするとセッション全体の上限になります。1ターン単位にするには、before_invocationでカウンタを初期化するなど、ターンの区切りを自分で持たなければなりません。LambdaのtimeoutSecondsは既定60秒(1〜900秒)で、タイムアウトや不正な応答のときはfailureModeの既定どおりdeny扱いになります。フックを設定できるのはCreateHarnessとUpdateHarnessだけで、UpdateHarnessにhooksを渡すと既存のフック設定は丸ごと置き換わります。ハーネスそのものの仕組みと料金はBedrock AgentCoreハーネスGAとは?仕組み・料金・導入判断を解説にまとめています。

独自ループを持つ構成のAgentCore Runtimeへのコード移行

入力やツール呼び出し・ツール結果の検証は、AWSもフックの用途に挙げています。一方、ReWOOのように計画を先に立てて一括実行する、検証に失敗したらモデルに再生成させる、独自のstateで段階を増やす、といった「次に何をするかを自分で選ぶ」構成はフックでは表現できません。フックが返せるのは特定の時点での許可・拒否だけで、次にモデルを呼ぶかツールを呼ぶかという分岐そのものは変えられないからです。この場合はStrands AgentsやLangGraphなどでループを書き、AgentCore Runtimeにデプロイします。

移行ツールはこの区別に合わせて振る舞いが分かれます。AWSのagent toolkit for AWSのamazon-bedrockスキルはハーネスへの移行を案内しますが、マルチエージェント連携・マルチモーダル入力・カスタムオーケストレーションを使うエージェントは適格性判定で止まります。一方、2026年10月28日に一般提供となったAgentCore CLI 1.0(npmの@aws/agentcore、11月3日時点の最新は1.0.1)では、agentcore add runtimeに--agent-id・--agent-alias-id・--framework strands|langgraphのオプションがあり、既存のBedrock AgentをRuntime用のコードへ変換できます。エイリアスはDRAFTではなく準備済みのバージョンを指している必要があります。

ただし、このインポートでオーケストレーションLambdaまで変換されるとは考えないでください。1.0.1の配布コードを読むと、変換時の注記はアクショングループのスタブやAMAZON.UserInputなどが対象で、orchestrationTypeを読む処理は見当たりません。Lambdaに書いていた状態分岐は、生成されたコードのループへ手で移すことになります。CLIの導入とデプロイの手順はAgentCore APIの使い方:SDK・CLI 1.0・InvokeAgentRuntimeでエージェントを本番に載せる手順を参照してください。ループを自前で書くときの代表的な型はマルチエージェントオーケストレーションとは?LLMエージェントを束ねる5パターンと実装コードで比べています。業務システムとの接続や権限設計まで含めて移行を進める場合は、AIエージェント開発の支援も相談先になります。

処理の順番を完全に固定したいだけで、モデルに次の一手を選ばせる必要がないなら、エージェントではなくAmazon Bedrock Flowsとは|ノード14種・料金・上限とboto3実装のようなワークフローで組むほうが素直です。ただしFlowsのエージェントノードが呼ぶのはAgents Classicのエージェントなので、利用実績のないアカウントではノードに指定するエージェント自体を新しく作れません。

よくある質問

Bedrock Agentsのカスタムオーケストレーションとは何ですか?

エージェントがいつモデルを呼び、いつツールを実行し、いつ回答を確定するかを、AWS Lambda関数のコードで決める方式です。orchestrationTypeを既定のDEFAULTからCUSTOM_ORCHESTRATIONに切り替えると有効になります。

新しいAWSアカウントでカスタムオーケストレーションを試せますか?

過去12か月にBedrock Agentsの利用がないアカウントでは試せません。CreateAgentがHTTP 403になり、例外申請の仕組みもないため、新規に作るならAgentCoreを使います。

カスタムオーケストレーションに追加料金はかかりますか?

Agents Classic自体は無料で、支払うのはモデル推論と関連リソースの利用料です。カスタムオーケストレーションでは、1回のInvokeAgentの中でもオーケストレーションLambdaが状態の数だけ呼ばれます。公式サンプルの流れでツールを2回使う会話なら、STARTで1回、モデル呼び出し後に3回、ツール実行後に2回の計6回です。ループの回数はそのままLambdaの課金に響きます。

AgentCoreのハーネスにカスタムオーケストレーションを移せますか?

そのままでは移せません。AWSの対応表はハーネスでは利用不可としています。ツールの却下や呼び出しの停止が目的ならライフサイクルフックで代わりが利き、ループの形を変えている構成はAgentCore Runtimeにコードとして移します。

カスタムオーケストレーションはどのリージョンで使えますか?

Amazon Bedrock Agentsが提供されている全リージョンで使えます(2024年11月27日の発表)。カスタムオーケストレーション単独のリージョン制限はありません。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.09 テックブログ エディオン不正アクセス第2報:カード番号の対象確認とログ・決済画面の改ざん対策
  2. 2026.11.09 テックブログ LANSCOPE不正アクセス第2報:製品への影響と偽連絡の確認、停止中のライセンス認証の窓口
  3. 2026.11.09 テックブログ ステップゴルフ不正アクセス:口座情報の対象確認と偽連絡・引き落としの注意点
  4. 2026.11.10 テックブログ Claude Fable 5.2とは:料金・破壊的変更4点とFable 5.1からの移行手順【2026年11月】
  5. 2026.11.10 テックブログ ニトリの情報漏洩:最大3,762万件の対象確認となりすまし連絡の見分け方

RELATED POSTS 関連記事

目次