目次を表示する

Bedrockで Production Ready な AI 機能を作る ── 設計・運用・現場の知恵

Bedrock Agents で多段ステップタスクを実行する ── Action Groups と Trace

Bedrock Agents で多段ステップタスクを実行する ── Action Groups と Trace

対象読者:ch07 の Tool Use ループを書いたが、本番運用の難しさ(リトライ・観測・並列性)に直面しはじめた読者 難易度:★★★☆☆(実装ハンズオン) 対象バージョン:@aws-sdk/client-bedrock-agent-runtime v3 系 / @aws-sdk/client-bedrock-agent v3 系 想定読了時間:約 45 分 関連章:第 7 章(Tool Use)/ 第 10 章(AgentCore への移行)

手書き Tool Use ループの限界

ch07 で書いた Tool Use ループを思い出してほしい。

// ch07 で書いた素朴な Tool Use ループ(抜粋)
for (let i = 0; i < 5; i++) {
  const res = await client.send(new ConverseCommand({ /* ... */ }));
  if (res.stopReason !== "tool_use") return messages;
  // assistant.content の tool_use ブロックを集めて、自前で実行して、結果を push して、また回す …
}

「東京の今の気温は?」のような 単発の関数呼び出し にはこれで十分だった。1 回ツールを叩いて、結果を整形して返す。シンプルで読みやすい。

ところが、helpdesk-ai で社員からこう聞かれたらどうなるか。

先月の私のリモートワーク勤怠を集計して、有給を 3 日(来週の月・水・金で)申請して。

これを 1 つのリクエストで処理しようとすると、最低でも次のステップが必要になる。

  1. get_employee_id(slack_user_id) ── 社員 ID を引く
  2. get_attendance(employee_id, month="2026-05") ── 先月の勤怠を集計
  3. get_pto_balance(employee_id) ── 有給残数を確認
  4. submit_pto_request(employee_id, dates=[...]) ── 3 件まとめて申請
  5. 結果を要約して社員に返す

5 ステップ。途中で「有給残数が足りなかったら?」のような分岐も入ってくる。手書きの for ループでこれを書こうとした瞬間に、コードはあっという間に肥大化する。中間結果(observation)の蓄積、暴走防止のループ上限、session ID と会話履歴の管理、終了判定、リトライ方針、各 step の rationale をどう残すか ── これらは「LLM エージェント」を作るたびに毎回現れる共通課題だ。AWS マネージドな ReAct ループ がほしくなる。それが Bedrock Agents の入口になる。

本章のゴール:helpdesk-ai に Bedrock Agents を導入し、社員の自然言語リクエストを「人事 API への複数ステップ呼び出し」に落とし込む。Action Groups で関数群を定義し、Trace で実行履歴を観測できる状態にする。

対象バージョン:AWS SDK v3(@aws-sdk/client-bedrock-agent v3、@aws-sdk/client-bedrock-agent-runtime v3)、Bedrock Agents 2026 年 6 月時点


Bedrock Agents の動作原理

Bedrock Agents は、内部で 3 段階のフロー を回している。AWS 公式ドキュメントの整理に沿って読み解こう。

3 段階フロー

flowchart TD
    UserInput[User Input] --> Pre[1\. Pre-processing]
    Pre -->|Sanitized prompt| Orch[2\. Orchestration<br/>ReAct ループ]
    Orch -->|Final answer| Post[3\. Post-processing]
    Post --> Response[User Response]

    subgraph Orch[2\. Orchestration ReAct ループ]
        direction TB
        Rationale[FM が rationale 生成<br/>'次に何をすべきか'] --> Decide{呼ぶべき<br/>ものは?}
        Decide -->|Action Group| Action[Lambda / RoC<br/>を呼ぶ]
        Decide -->|Knowledge Base| KB[KB.Retrieve<br/>を呼ぶ]
        Decide -->|終了| End[最終応答]
        Action --> Obs[Observation<br/>を蓄積]
        KB --> Obs
        Obs --> Rationale
    end
  1. Pre-processing:入力の文脈化、分類、バリデーション。プロンプトインジェクションっぽい入力をここで弾く。
  2. Orchestration:ReAct ループの本体。
    1. FM が現在の状況を解釈し、rationale(次の手順の根拠)を生成
    2. Action Group の関数呼び出しか、Knowledge Base クエリかを判断
    3. 結果(observation)を得て、プロンプトに足してまた FM を回す
    4. 終了条件まで繰り返す
  3. Post-processing:最終応答の整形(デフォルトでは無効)

各ステップの プロンプトテンプレート は Bedrock コンソールの「Advanced prompts」から編集でき、Lambda parser で FM の出力を解釈し直すこともできる。実務では、最初はデフォルトで動かすのが定石。

「手書きと何が違うのか」

ch07 の for ループは上の Orchestration を アプリ側で実装 していたものだ。Bedrock Agents は次を AWS 側に押し付ける。

関心事手書き Tool UseBedrock Agents
ループ管理自分で for を書くAgents が管理
rationale の保存自分で messages に積むTrace に自動記録
KB 連携自分で Retrieve を呼ぶAction Group と同列で扱える
会話履歴自分で配列を維持sessionId で AWS が保持
インジェクション対策自分で書くPre-processing で 1 段クッション

代わりに Agent をリソースとして作る(CreateAgent → PrepareAgent → CreateAgentAlias)という前準備が要る。


Action Groups ── Agent にツールを持たせる

Agent が実行できる関数(ツール)は、Action Group という単位で束ねる。Action Group は次の 2 つを定義する。

  1. スキーマ(どんな関数がどんな引数で呼べるか)
  2. Executor(実際にその関数を実行する場所)

スキーマの 2 種類

flowchart LR
    AG[Action Group] --> Schema{スキーマ}
    Schema --> Func[Function detail schema<br/>関数名・説明・パラメータを<br/>コンソール / JSON で定義]
    Schema --> OpenAPI[OpenAPI schema<br/>既存 API を OpenAPI 3.0 で記述<br/>インライン or S3 URI]

    AG --> Executor{Executor}
    Executor --> Lambda[Lambda function<br/>Bedrock が直接呼び出す<br/>resource-based policy 必須]
    Executor --> RoC[Return of Control<br/>InvokeAgent の応答に<br/>呼ぶべき関数を返す]
  • Function detail schema:「関数名 + 説明 + パラメータ」をコンソールまたは JSON で直接定義。最大 11 functions / action group5 parameters / functionrequireConfirmation: ENABLED でユーザー確認を必須化できる(インジェクション対策に有効)
  • OpenAPI schema:既存 API を OpenAPI 3.0 ドキュメントで与える。インライン or S3 URI。既存 REST API を Agent に生やしたい ときに便利

helpdesk-ai では人事 API を Lambda 経由で叩くので、本章は Function detail schema + Lambda Executor に絞る。

Executor の 2 種類

Executor動作向く用途
Lambda functionBedrock が Lambda を直接呼び出す。resource-based policy で bedrock.amazonaws.com からの呼び出しを許可する必要サーバーレスで完結する社内ツール
Return of Control(RoC)Lambda を経由せず、InvokeAgent の応答に「呼ぶべきアクション + パラメータ」が返ってくる。アプリ側で実行VPC 内の私的 API、Lambda 化しにくい既存システム

RoC の発想転換:Bedrock 側で実行せず、自分のアプリで実行した結果を次の InvokeAgentsessionState.returnControlInvocationResults として返す。オンプレ DB やレガシー業務システムを叩きたいときの逃げ道だ。


Lambda 関数の入出力契約

Action Group の Executor を Lambda にするとき、Lambda は Bedrock 規定の構造 で入力を受け、規定の構造で出力を返す。

受け取る入力(Function schema 時)

{
  "messageVersion": "1.0",
  "agent": {
    "name": "helpdesk-agent",
    "id": "AGENT123",
    "alias": "ALIAS456",
    "version": "1"
  },
  "sessionId": "session-...",
  "actionGroup": "hr-actions",
  "function": "get_attendance",
  "parameters": [
    { "name": "employee_id", "type": "string", "value": "E-0421" },
    { "name": "month", "type": "string", "value": "2026-05" }
  ],
  "sessionAttributes": {},
  "promptSessionAttributes": {}
}

parameters配列で 渡ってくる点に注意。{ employee_id: "E-0421", month: "2026-05" } のようなオブジェクトではない。

返す出力

{
  "messageVersion": "1.0",
  "response": {
    "actionGroup": "hr-actions",
    "function": "get_attendance",
    "functionResponse": {
      "responseBody": {
        "TEXT": { "body": "出勤 18 / リモート 4 / 有休 0 / 残業 12h" }
      }
    }
  },
  "sessionAttributes": {},
  "promptSessionAttributes": {}
}

返すボディは原則テキストTEXT.body)で、FM が次の rationale を組み立てる材料にする。JSON を返したいなら、文字列化して body に入れるのが安全。


helpdesk-ai の Action Group「hr-actions」を実装する

ここからコードに落とす。シナリオは次の 2 関数。

  • get_attendance(employee_id, month) ── 指定月の勤怠サマリを返す
  • submit_pto_request(employee_id, dates) ── 有給申請を送る

Lambda 関数本体(src/handlers/hr-actions.ts

import type { Handler } from "aws-lambda";

// Bedrock Agents から受け取る入力型(Function schema 用)
interface AgentEvent {
  messageVersion: "1.0";
  agent: { id: string; name: string; alias: string; version: string };
  sessionId: string;
  actionGroup: string;
  function: string;
  parameters: { name: string; type: string; value: string }[];
  sessionAttributes?: Record<string, string>;
  promptSessionAttributes?: Record<string, string>;
}

interface AgentResponse {
  messageVersion: "1.0";
  response: {
    actionGroup: string;
    function: string;
    functionResponse: {
      responseBody: { TEXT: { body: string } };
    };
  };
  sessionAttributes?: Record<string, string>;
  promptSessionAttributes?: Record<string, string>;
}

// parameters 配列 → オブジェクト変換ヘルパ
const toArgs = (params: AgentEvent["parameters"]): Record<string, string> =>
  Object.fromEntries(params.map((p) => [p.name, p.value]));

// 人事 API(架空)の呼び出し。実体は別 Lambda or 社内 API
async function fetchAttendance(employeeId: string, month: string) {
  // 実装は省略。社内 HR API を fetch する想定
  return {
    employee_id: employeeId,
    month,
    workday: 18,
    remote: 4,
    pto_used: 0,
    overtime_hours: 12,
  };
}

async function submitPto(employeeId: string, dates: string[]) {
  // 実装は省略
  return { request_id: "PTO-2026-0421-001", status: "submitted", dates };
}

export const handler: Handler<AgentEvent, AgentResponse> = async (event) => {
  const args = toArgs(event.parameters);
  let body: string;

  try {
    switch (event.function) {
      case "get_attendance": {
        const r = await fetchAttendance(args.employee_id, args.month);
        body = `${r.month} の勤怠サマリ: 出勤 ${r.workday} 日 / リモート ${r.remote} 日 / 有給消化 ${r.pto_used} 日 / 残業 ${r.overtime_hours}h`;
        break;
      }
      case "submit_pto_request": {
        // dates は "2026-06-08,2026-06-10,2026-06-12" のような CSV で渡ってくる前提
        const dates = (args.dates ?? "").split(",").map((s) => s.trim()).filter(Boolean);
        const r = await submitPto(args.employee_id, dates);
        body = `申請を受理しました(ID: ${r.request_id}, 日付: ${r.dates.join(", ")})`;
        break;
      }
      default:
        body = `unknown function: ${event.function}`;
    }
  } catch (e) {
    // Agent には「失敗した事実」をテキストで返す。スタックトレースは渡さない
    body = `関数 ${event.function} の実行に失敗: ${(e as Error).message}`;
  }

  return {
    messageVersion: "1.0",
    response: {
      actionGroup: event.actionGroup,
      function: event.function,
      functionResponse: { responseBody: { TEXT: { body } } },
    },
    sessionAttributes: event.sessionAttributes,
    promptSessionAttributes: event.promptSessionAttributes,
  };
};

ポイントを 3 つ。

  1. parameters は配列で来るので、必ず toArgs のような変換ヘルパで扱う
  2. 例外は呑む。投げると Agent 側で「Failure trace」になる。エラーをテキストで返した方が、FM は次の手を考えやすい
  3. 複数値は Function schema のパラメータ型に配列がないので、CSV 文字列で渡すのが定石(type: "string"

Lambda の resource-based policy

Bedrock が Lambda を呼び出すには、Lambda 側で bedrock.amazonaws.com からの lambda:InvokeFunction を許可する必要がある。CDK なら次の 1 行。

import * as lambda from "aws-cdk-lib/aws-lambda";
import * as iam from "aws-cdk-lib/aws-iam";

const hrFn = new lambda.Function(this, "HrActionsFn", { /* ... */ });

hrFn.addPermission("AllowBedrockInvoke", {
  principal: new iam.ServicePrincipal("bedrock.amazonaws.com"),
  action: "lambda:InvokeFunction",
  sourceArn: `arn:aws:bedrock:${this.region}:${this.account}:agent/*`,
});

sourceArnAgent ARN の prefix を絞れば、別 Agent からの誤呼び出しを防げる。この 1 行を忘れて「Lambda が呼ばれない」と詰まる人が非常に多い ので最初に書くこと。


Agent を作成する

Agent は control plane@aws-sdk/client-bedrock-agent)でリソースとして作る。CDK の CfnAgent を使う方法と、SDK で直接 CreateAgentCommand を叩く方法がある。ここでは SDK 版を示す。

SDK で Agent を作る(infra/create-agent.ts

import {
  BedrockAgentClient,
  CreateAgentCommand,
  CreateAgentActionGroupCommand,
  PrepareAgentCommand,
  CreateAgentAliasCommand,
} from "@aws-sdk/client-bedrock-agent";

const region = process.env.AWS_REGION ?? "us-east-1";
const bedrockMgmt = new BedrockAgentClient({
  region,
  maxAttempts: 5,
  retryMode: "adaptive",
});

const AGENT_ROLE_ARN = process.env.AGENT_ROLE_ARN!; // bedrock:InvokeModel 等を許可した IAM Role
const HR_LAMBDA_ARN = process.env.HR_LAMBDA_ARN!;

// 1) Agent を作る
const createRes = await bedrockMgmt.send(
  new CreateAgentCommand({
    agentName: "helpdesk-agent",
    agentResourceRoleArn: AGENT_ROLE_ARN,
    foundationModel: "us.anthropic.claude-sonnet-4-5",
    instruction: [
      "あなたは社内ヘルプデスクのアシスタントです。",
      "勤怠・有給に関する質問は hr-actions の関数を使って答えてください。",
      "申請を行う前に、必ず社員に確認文を提示してから実行してください。",
      "回答は簡潔に、箇条書きで。",
    ].join("\n"),
    idleSessionTTLInSeconds: 1800,
  }),
);
const agentId = createRes.agent!.agentId!;

// 2) Action Group を紐付ける(Function detail schema + Lambda Executor)
await bedrockMgmt.send(
  new CreateAgentActionGroupCommand({
    agentId,
    agentVersion: "DRAFT",
    actionGroupName: "hr-actions",
    actionGroupExecutor: { lambda: HR_LAMBDA_ARN },
    functionSchema: {
      functions: [
        {
          name: "get_attendance",
          description:
            "指定した社員 ID と月(YYYY-MM)の勤怠サマリを返します。出勤日数・リモート日数・有給消化・残業時間を含みます。",
          parameters: {
            employee_id: { type: "string", required: true, description: "社員 ID(例: E-0421)" },
            month: { type: "string", required: true, description: "対象月 YYYY-MM 形式" },
          },
          requireConfirmation: "DISABLED",
        },
        {
          name: "submit_pto_request",
          description:
            "指定日付の有給休暇申請を送ります。複数日付は CSV 文字列で指定してください(例: 2026-06-08,2026-06-10)。",
          parameters: {
            employee_id: { type: "string", required: true, description: "社員 ID" },
            dates: {
              type: "string",
              required: true,
              description: "申請日。複数日は CSV(YYYY-MM-DD,YYYY-MM-DD,...)",
            },
          },
          // 申請系は人間に確認を取る
          requireConfirmation: "ENABLED",
        },
      ],
    },
  }),
);

// 3) Prepare(編集中の DRAFT を実行可能にビルド)
await bedrockMgmt.send(new PrepareAgentCommand({ agentId }));

// 4) Alias を切る(本番呼び出しはこの aliasId を使う)
const aliasRes = await bedrockMgmt.send(
  new CreateAgentAliasCommand({
    agentId,
    agentAliasName: "prod",
  }),
);
const agentAliasId = aliasRes.agentAlias!.agentAliasId!;

console.log({ agentId, agentAliasId });

ポイントを 2 つ。

  • PrepareAgent が必須。DRAFT への変更は Prepare して初めて反映される。忘れると古い設定で動き続ける
  • 書き込み系の関数には requireConfirmation: ENABLED を付ける。「全社員に有給 365 日を申請して」とインジェクションで仕込まれても、確認なしで実行されなくなる

IAM Role(AGENT_ROLE_ARN の中身)

Agent が引き受ける IAM Role には、最低限こうしたポリシーが要る。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "InvokeFoundationModel",
      "Effect": "Allow",
      "Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
      "Resource": "arn:aws:bedrock:*::foundation-model/anthropic.claude-sonnet-4-5*"
    }
  ]
}

KB を紐付ける場合は bedrock:Retrieve、Guardrail なら bedrock:ApplyGuardrail も足す。IAM 設計の本格化は ch14 に譲り、ここでは最小限で進む。


InvokeAgent で呼び出す

リソースが揃ったら、ランタイム(@aws-sdk/client-bedrock-agent-runtime)で InvokeAgent を叩く。レスポンスは EventStream(async iterable)で、completion の各イベントに chunk・trace・returnControl のいずれかが入ってくる。

src/handlers/agent.ts

import {
  BedrockAgentRuntimeClient,
  InvokeAgentCommand,
} from "@aws-sdk/client-bedrock-agent-runtime";
import { randomUUID } from "node:crypto";

const region = process.env.AWS_REGION ?? "us-east-1";
const bedrockAgent = new BedrockAgentRuntimeClient({
  region,
  maxAttempts: 5,
  retryMode: "adaptive",
});

const AGENT_ID = process.env.AGENT_ID!;
const AGENT_ALIAS_ID = process.env.AGENT_ALIAS_ID!;

export async function askHelpdesk(
  inputText: string,
  sessionId = `session-${randomUUID()}`,
) {
  const res = await bedrockAgent.send(
    new InvokeAgentCommand({
      agentId: AGENT_ID,
      agentAliasId: AGENT_ALIAS_ID,
      sessionId,
      inputText,
      enableTrace: true, // Trace を観測したいので必ず true
    }),
  );

  let answer = "";
  const traces: unknown[] = [];

  for await (const event of res.completion ?? []) {
    if (event.chunk?.bytes) {
      answer += new TextDecoder().decode(event.chunk.bytes);
    }
    if (event.trace) {
      traces.push(event.trace.trace);
    }
    if (event.returnControl) {
      // RoC を使う場合はここでアプリ側が関数を実行し、
      // 次の InvokeAgent で sessionState.returnControlInvocationResults を返す
      throw new Error("RoC は本章のスコープ外(hr-actions は Lambda Executor)");
    }
  }

  return { answer, traces, sessionId };
}

// 使い方
const { answer, sessionId } = await askHelpdesk(
  "社員 ID E-0421 の先月の勤怠を集計して、来週月・水・金(2026-06-08, 06-10, 06-12)に有給を申請して。",
);
console.log(answer);

// 同じ sessionId を渡せば会話が継続する
const next = await askHelpdesk("申請の状況を確認して。", sessionId);
console.log(next.answer);
  • sessionId を保持 すれば、会話履歴を AWS 側が orchestration プロンプトに自動 augment する
  • enableTrace: true はデバッグ・監視に必須。本番では Trace を CloudWatch Logs か OpenTelemetry に流す(ch13 で詳述)

Trace ── 実行履歴を観測する

enableTrace: true でレスポンスに付随する trace フィールドは、Agent が内部で何をしたかを ステップ単位で全部 教えてくれる。デバッグでも本番監視でも、これが唯一の窓になる。

Trace の 5 種類

Trace 種別含まれる情報
PreProcessingTrace前処理(入力カテゴリ判定)の入出力、FM への送信プロンプトと生応答
OrchestrationTracerationale / invocation input(Lambda or KB への引数)/ observation(戻り値) の流れ。最重要
PostProcessingTrace後処理(応答整形)の入出力。デフォルト無効なので空のことが多い
GuardrailTraceGuardrail が介入した場合の判定結果。ch11 で活用
FailureTrace失敗理由。Lambda の TimeoutError、resource-based policy 不足など

OrchestrationTrace の中身(抜粋)

{
  "orchestrationTrace": {
    "rationale": {
      "text": "ユーザーは先月の勤怠集計と有給申請を求めている。まず get_attendance を呼ぶ。"
    },
    "invocationInput": {
      "actionGroupInvocationInput": {
        "actionGroupName": "hr-actions",
        "function": "get_attendance",
        "parameters": [
          { "name": "employee_id", "value": "E-0421" },
          { "name": "month", "value": "2026-05" }
        ]
      }
    },
    "observation": {
      "actionGroupInvocationOutput": {
        "text": "2026-05 の勤怠サマリ: 出勤 18 日 / リモート 4 日 / 有給消化 0 日 / 残業 12h"
      }
    }
  }
}

「FM がなぜそのツールを選んだのか(rationale)」と「実際に返ってきた値(observation)」が ステップ単位でログに残る。ch07 の手書きループでは自分で蓄積する必要があった情報が、Trace に全部流れてくる。

デバッグと本番監視

const { traces } = await askHelpdesk(/* ... */);
// 開発時はファイルに出して目視
import { writeFile } from "node:fs/promises";
await writeFile("./trace.json", JSON.stringify(traces, null, 2));

期待した関数が呼ばれない」「毎回 KB を引いてしまう」「ループが 3 回で打ち切られる」── こうした不可解な挙動は、ほぼ Trace を読むと原因が見える。console.log で追うのではなく、Trace を読む文化 を最初から作るのが正解だ。

Production では Trace を 構造化したまま CloudWatch Logs / OpenTelemetry に流す。平均ステップ数、Action Group / KB のレイテンシ、FailureTrace 発生率、GuardrailTrace 介入率 ── 派生できるメトリクスは多い。Trace を流し続けないと Agent の振る舞いは可視化できない。具体実装は ch13「観測を設計する」で扱う。


Multi-Agent Collaboration ── helpdesk-ai を 3 つに分ける

ここまでは Action Group「hr-actions」だけだった。だが現実の helpdesk-ai には、人事だけでなく IT サポート経費精算 も来る。これを 1 つの Agent に詰め込むと、instruction が肥大化して破綻する。

2024-12 GA の Multi-Agent Collaboration は、Supervisor + Collaborators の階層モデルで分割を可能にする。

flowchart TD
    User[社員からの質問] --> Sup[Supervisor Agent<br/>helpdesk-supervisor]

    Sup -->|勤怠・有給・人事| HR[Sub-agent: hr-agent<br/>Action Group: hr-actions]
    Sup -->|VPN・端末・SaaS| IT[Sub-agent: it-agent<br/>Action Group: it-actions<br/>+ KB: it-troubleshoot]
    Sup -->|精算・申請・規程| Exp[Sub-agent: expense-agent<br/>Action Group: expense-actions<br/>+ KB: expense-policy]

    HR --> Sup
    IT --> Sup
    Exp --> Sup
    Sup --> Reply[最終応答]
  • Supervisor agent がユーザー入力を解釈し、適切な collaborator にルーティングする
  • 各 collaborator は 独自に action group / knowledge base / guardrail を持つ
  • Collaborator は並列に走らせられる

helpdesk-ai 想定の運用フロー:「先月の勤怠を集計して有給を申請して、ついでに新しい MacBook の交換も申請したい」のような 複合質問 を Supervisor が分解 → 勤怠は hr-agent、MacBook 交換は it-agent に振り分け → 並列実行して結果を統合。

注意点

  • Supervisor を 先に保存 してから collaborator を関連付ける(順序が決まっている)
  • 役割の重複は最小化する(IT と人事で同じツールを持たない)
  • Supervisor の instruction には「ルーティングだけ」「業務処理はしない」と明示する

ch10 で AgentCore に移行するとき、この階層モデルは AgentCore Runtime + Strands Agents SDK のような他フレームワークでも組み直せる。Multi-Agent はモデル化の道具であって、Bedrock Agents 専売ではない


Inline Agents ── ランタイムで Agent を動的構築する

2024-11 GA の Inline Agents は、CreateAgent でリソース化せずに、InvokeInlineAgent API で その場で Agent を組み立てて即実行 する。

import {
  BedrockAgentRuntimeClient,
  InvokeInlineAgentCommand,
} from "@aws-sdk/client-bedrock-agent-runtime";
import { randomUUID } from "node:crypto";

const bedrockAgent = new BedrockAgentRuntimeClient({ region: "us-east-1" });

// テナント A は人事 API のみ、テナント B は人事 + 経費 という構成を動的に切り替え
async function askForTenant(tenantId: string, inputText: string) {
  const actionGroups = [
    {
      actionGroupName: "hr-actions",
      actionGroupExecutor: { lambda: process.env.HR_LAMBDA_ARN! },
      functionSchema: { /* ... 前述と同じ ... */ },
    },
  ];

  if (tenantId === "tenant-b") {
    actionGroups.push({
      actionGroupName: "expense-actions",
      actionGroupExecutor: { lambda: process.env.EXPENSE_LAMBDA_ARN! },
      functionSchema: { /* ... */ },
    });
  }

  const res = await bedrockAgent.send(
    new InvokeInlineAgentCommand({
      foundationModel: "us.anthropic.claude-sonnet-4-5",
      instruction: `あなたは ${tenantId} 向けのヘルプデスクアシスタントです。…`,
      sessionId: `session-${randomUUID()}`,
      inputText,
      actionGroups,
    }),
  );

  for await (const event of res.completion ?? []) {
    if (event.chunk?.bytes) {
      process.stdout.write(new TextDecoder().decode(event.chunk.bytes));
    }
  }
}

使いどころ

ユースケースなぜ Inline が向く
マルチテナント SaaSテナント別に有効ツール・KB・guardrail を変える。リソースを 1000 個作らずに済む
A/B 実験instruction や FM を動的に切り替えて差分を観測
ロールベース管理者と一般ユーザーで使える関数を変える

helpdesk-ai は リソース化した Agent で始めて、テナント別に instruction を変えたくなった段階で Inline に切り替える、という二段構えがおすすめ。


Bedrock Agents の限界 ── 次章への接続

ここまで読むと「Bedrock Agents 便利だな」と感じるはずだ。実際、PoC とプロトタイプまでは Bedrock Agents で十分 に到達できる。

だが、Production 運用に持ち込もうとした瞬間、足りないものが見えてくる。

本番要件Bedrock Agents 単体での状態
Memory(cross-session の長期記憶)session ID ベースの短期会話履歴のみ。「先月この社員はこう申請した」のような蓄積はない
Identity(誰がこの Agent を叩いたか)IAM ロールベース。Slack ユーザーや Okta ユーザーとの紐付けは自前
Observability(メトリクス・ダッシュボード)Trace は出るが、可視化・アラートは自前で組む
Gateway(ツール群の集中管理)Action Group 単位の管理にとどまる。MCP エンドポイントとして公開はできない
Code Interpreter / Browserサンドボックスで Python を走らせる、Web をクロールするは含まれない
実行ウィンドウリクエストごとの短時間呼び出し前提。長時間タスクには向かない
EvaluationsLLM-as-a-Judge は別途 Model Evaluation を組む必要(ch12)

これらを埋めるのが ch10 で扱う AgentCore(2025-10 GA)だ。Bedrock Agents の「マネージドな ReAct ループ」から、Production 向けインフラへの進化 として読み解いていく。

なお、Action Group + KB + Guardrails の組み合わせは 評価が難しい(ステップ単位の正しさ、ツール選択の妥当性、最終応答の品質を分離する必要がある)。これは ch12「Day 1 から評価を組み込む」で AgentCore Evaluations を使って解く。Trace を Production の観測にどう繋ぐかは ch13 で具体実装を示す。本章は Agent を動かすところまで、次章以降で本番化していく という分担になる。


章末まとめ

  • 手書きの Tool Use ループは単発タスクには良いが、多段ステップになると ループ管理・rationale 保存・会話履歴・終了判定 が肥大化する。Bedrock Agents はこれをマネージドな ReAct ループとして抽象化する
  • Agent は Pre-processing → Orchestration(rationale → action → observation のループ)→ Post-processing の 3 段階で動く
  • Action Group は「Function detail schema / OpenAPI schema」× 「Lambda / Return of Control」の組合せ。Lambda には必ず bedrock.amazonaws.com 向けの resource-based policy を付ける(最頻出の詰まりポイント)
  • 書き込み系の関数には requireConfirmation: ENABLED を付けて、プロンプトインジェクションでの暴発を防ぐ
  • enableTrace: true + Trace の読書 が Agent デバッグの基本。OrchestrationTrace の rationale / invocationInput / observation を読めば、ほぼ全ての不可解な挙動の原因がわかる
  • Multi-Agent Collaboration で Supervisor + Collaborators に分割すると、helpdesk-ai のような複合ドメインを綺麗に切り出せる
  • Inline Agents はマルチテナント SaaS や A/B 実験に向く。リソース化せずランタイムで構築できる
  • 本番運用には Memory・Identity・Observability・実行ウィンドウ が足りない。次章では AgentCore でこれらを埋める