Powered by AppSignal & Oban Pro

AIP-C01 04: エージェントとツール統合

04_agents_and_tools.livemd

AIP-C01 04: エージェントとツール統合

Mix.install([
  {:aws, "~> 1.0.15"},
  {:hackney, "~> 4.7"},
  {:kino, "~> 0.19"},
  {:kino_vega_lite, "~> 0.1"}
])

概要

最終確認日: 2026-09-22

対応範囲: 2.1, 2.3, 2.5, 3.1, 4.3, 5.1

このノートで身につけること

  • client-side tool use の制御ループを実装する
  • ツール入力を信頼せず、スキーマと業務権限で検証する
  • Bedrock Agents、AgentCore、Strands Agents、Step Functions の役割を比較する
  • 停止条件、タイムアウト、人間承認、監査を設計する

最新仕様の要点

Amazon Bedrock の tool use では、通常モデル自身が外部処理を実行するのではなく、モデルがツール呼び出しを要求し、アプリケーションが検証・実行して結果を返します。Bedrock は client-side tool use に加え、Responses API の server-side tool use、Anthropic 固有ツールも提供します。API とモデルごとの対応を確認してください。

標準版 AIP-C01 には Amazon Bedrock AgentCore が追加されています。AgentCore は Runtime、Gateway、Memory、Identity、Observability、Policy に加え、Harness、Browser、Code Interpreter、Payments、Evaluations、Optimization、Registry などを含む本番エージェント向けプラットフォームです。機能追加が速いため、名称の暗記より「実行・ツール接続・状態・認証認可・監視評価」の要件へ対応付けます。フレームワークや FM は Strands Agents や Bedrock 内のモデルに限定されません。

2026-09-22 現在、従来の Amazon Bedrock Agents は Agents Classic とされ、新規顧客向け提供は終了しています。既存顧客は継続利用できます。AIP-C01 の出題範囲と既存環境の保守のため Action Groups、Knowledge Bases、InvokeAgent、session state は学びつつ、新規の本番設計では AgentCore や client-side tool use も比較してください。

Kino.Mermaid.new("""
sequenceDiagram
  actor User
  participant App as Application
  participant FM as Bedrock FM
  participant Policy as Auth / Policy
  participant Tool as Tool / Lambda

  User->>App: request
  App->>FM: messages + tool schema
  FM-->>App: toolUse(name, input)
  App->>Policy: validate identity, tool, arguments
  alt approval required
    Policy-->>App: human review
  else allowed
    Policy-->>App: allow
    App->>Tool: idempotent invocation
    Tool-->>App: validated result
    App->>FM: toolResult
    FM-->>App: final answer
    App-->>User: response
  end
""")

接続

access_key_input = Kino.Input.password("AWS_ACCESS_KEY_ID")
secret_key_input = Kino.Input.password("AWS_SECRET_ACCESS_KEY")
session_token_input = Kino.Input.password("AWS_SESSION_TOKEN(一時認証の場合)")
region_input = Kino.Input.text("AWS_REGION", default: "ap-northeast-1")
model_id_input = Kino.Input.text("Tool use 対応 MODEL_ID", default: "amazon.nova-lite-v1:0")

Kino.Layout.grid([access_key_input, secret_key_input, session_token_input, region_input, model_id_input], columns: 2)
access_key = Kino.Input.read(access_key_input)
secret_key = Kino.Input.read(secret_key_input)
session_token = Kino.Input.read(session_token_input)
region = Kino.Input.read(region_input)

client =
  if session_token in [nil, ""] do
    AWS.Client.create(access_key, secret_key, region)
  else
    AWS.Client.create(access_key, secret_key, session_token, region)
  end

ハンズオン1: ツールを最小権限で定義する

tool_spec = %{
  "toolSpec" => %{
    "name" => "get_order_status",
    "description" => "注文IDから配送状態を取得する。状態変更は行わない。",
    "inputSchema" => %{
      "json" => %{
        "type" => "object",
        "properties" => %{
          "order_id" => %{"type" => "string", "description" => "ORD-で始まる注文ID"}
        },
        "required" => ["order_id"],
        "additionalProperties" => false
      }
    }
  }
}

orders = %{
  "ORD-100" => %{status: "shipped", eta: "2026-09-23"},
  "ORD-200" => %{status: "processing", eta: nil}
}
execute_tool = fn
  "get_order_status", %{"order_id" => "ORD-" <> _ = order_id} ->
    case Map.fetch(orders, order_id) do
      {:ok, order} -> {:ok, Map.put(order, :order_id, order_id)}
      :error -> {:error, "order_not_found"}
    end

  "get_order_status", _ ->
    {:error, "invalid_order_id"}

  _, _ ->
    {:error, "tool_not_allowed"}
end

ツールの description と schema はモデル向けの説明です。セキュリティ境界ではありません。実行時に、ツール名、引数、呼び出し主体、対象リソース、レート、重複実行を検証します。

ハンズオン2: client-side tool use ループ

user_message = %{
  "role" => "user",
  "content" => [%{"text" => "注文 ORD-100 はいつ届きますか?"}]
}

payload = %{
  "messages" => [user_message],
  "toolConfig" => %{
    "tools" => [tool_spec],
    "toolChoice" => %{"auto" => %{}}
  },
  "inferenceConfig" => %{"maxTokens" => 300, "temperature" => 0.0}
}

{:ok, first_response, _} =
  AWS.BedrockRuntime.converse(
    client,
    Kino.Input.read(model_id_input),
    payload,
    recv_timeout: 60_000
  )
assistant_message = get_in(first_response, ["output", "message"])

tool_use =
  assistant_message["content"]
  |> Enum.find_value(& &1["toolUse"])

tool_result_content =
  case execute_tool.(tool_use["name"], tool_use["input"]) do
    {:ok, result} ->
      %{
        "toolResult" => %{
          "toolUseId" => tool_use["toolUseId"],
          "content" => [%{"json" => result}],
          "status" => "success"
        }
      }

    {:error, reason} ->
      %{
        "toolResult" => %{
          "toolUseId" => tool_use["toolUseId"],
          "content" => [%{"text" => reason}],
          "status" => "error"
        }
      }
  end
second_payload = %{
  "messages" => [
    user_message,
    assistant_message,
    %{"role" => "user", "content" => [tool_result_content]}
  ],
  "toolConfig" => %{"tools" => [tool_spec]},
  "inferenceConfig" => %{"maxTokens" => 300, "temperature" => 0.0}
}

{:ok, final_response, _} =
  AWS.BedrockRuntime.converse(
    client,
    Kino.Input.read(model_id_input),
    second_payload,
    recv_timeout: 60_000
  )

final_response
|> get_in(["output", "message", "content", Access.at(0), "text"])
|> Kino.Markdown.new()

ハンズオン3: 停止条件と人間承認

policy = %{
  max_steps: 6,
  max_tool_calls: 3,
  timeout_ms: 15_000,
  approval_required: ["refund_order", "delete_account", "send_email"],
  allowed_tools: ["get_order_status", "search_faq", "refund_order"]
}

authorize = fn tool_name, step, tool_calls ->
  cond do
    step >= policy.max_steps -> {:deny, :max_steps}
    tool_calls >= policy.max_tool_calls -> {:deny, :max_tool_calls}
    tool_name not in policy.allowed_tools -> {:deny, :not_allowed}
    tool_name in policy.approval_required -> {:review, :human_approval}
    true -> :allow
  end
end

[
  authorize.("get_order_status", 1, 0),
  authorize.("refund_order", 2, 1),
  authorize.("unknown_tool", 2, 1),
  authorize.("search_faq", 7, 2)
]

エージェントは「賢い Lambda」ではありません。非決定的な判断を含むため、リソース境界、冪等性キー、タイムアウト、サーキットブレーカー、ステップ上限、承認、監査証跡を明示します。

ハンズオン4: session state と業務データを分離する

Agents Classic の sessionAttributes は同じ sessionId の間で保持され、promptSessionAttributes は1回の InvokeAgent だけに適用されます。どちらも認可の正本にはせず、注文所有者や返金可否はツール側のデータストアで再確認します。

build_session_state = fn user_context, request_context ->
  %{
    "sessionAttributes" => %{
      "locale" => user_context.locale,
      "preferred_unit" => user_context.preferred_unit
    },
    "promptSessionAttributes" => %{
      "request_id" => request_context.request_id,
      "time_zone" => request_context.time_zone
    }
  }
end

session_state =
  build_session_state.(
    %{locale: "ja-JP", preferred_unit: "metric"},
    %{request_id: "req-20260922-001", time_zone: "Asia/Tokyo"}
  )

Kino.Tree.new(session_state)

セッションへクレジットカード番号、アクセスキー、恒久的な顧客属性を入れません。sessionId は会話の相関用であり、ユーザー認証や tenant 境界の代替ではありません。

ハンズオン5: 副作用ツールを exactly-once に近づける

ネットワーク再試行とモデルの再計画で、同じ返金ツールが複数回要求されることがあります。toolUseId だけでなく、業務操作を一意にする idempotency key と永続ストアを使います。

{:ok, idempotency_store} = Agent.start_link(fn -> %{} end)

refund = fn order_id, amount, idempotency_key ->
  Agent.get_and_update(idempotency_store, fn state ->
    case Map.fetch(state, idempotency_key) do
      {:ok, previous} ->
        {{:replayed, previous}, state}

      :error ->
        result = %{refund_id: "RF-#{order_id}", order_id: order_id, amount: amount}
        {{:created, result}, Map.put(state, idempotency_key, result)}
    end
  end)
end

key = "tenant-a:ORD-100:refund-v1"

[
  refund.("ORD-100", 1_200, key),
  refund.("ORD-100", 1_200, key)
]

この Agent は教材用です。本番は DynamoDB の条件付き書き込みなどを使い、tenant + operation + business entity + version をキーにします。引数が同じキーと矛盾した場合は replay せず、監査イベントとして拒否します。

ハンズオン6: trace から失敗境界を特定する

trace_events = [
  %{step: 1, type: "orchestration", target: "FM", latency_ms: 740, status: "ok"},
  %{step: 2, type: "tool", target: "get_order_status", latency_ms: 120, status: "ok"},
  %{step: 3, type: "tool", target: "refund_order", latency_ms: 2_900, status: "timeout"},
  %{step: 4, type: "orchestration", target: "FM retry", latency_ms: 810, status: "ok"}
]

latency_chart =
  VegaLite.new(width: 650, height: 240, title: "Agent trace のステップ別レイテンシー")
  |> VegaLite.data_from_values(trace_events)
  |> VegaLite.mark(:bar, tooltip: true)
  |> VegaLite.encode_field(:x, "step", type: :ordinal, title: "step")
  |> VegaLite.encode_field(:y, "latency_ms", type: :quantitative, title: "latency (ms)")
  |> VegaLite.encode_field(:color, "status", type: :nominal, title: "status")
  |> Kino.VegaLite.new()

Kino.Layout.tabs([
  {"レイテンシー", latency_chart},
  {"trace", Kino.DataTable.new(trace_events)}
])

最終応答だけでは、計画、FM、Knowledge Base、Action Group / tool、権限、下流 API のどこで失敗したか判別できません。trace は機密情報を含み得るため、全文を無期限保存せず、request ID・step・resource・duration・outcome を中心に記録します。

構成の使い分け

選択 適する場面
アプリ内 client-side tool use 小さな制御ループ、独自の厳密な実行制御
Step Functions 可視な決定的ワークフロー、待機・承認・再試行
Bedrock Agents Classic 既存環境の Bedrock 管理オーケストレーション、Action Groups、Knowledge Bases
AgentCore 任意フレームワークの本番実行、Gateway、Memory、Identity、Policy、Observability
Lambda 上の MCP server 軽量・ステートレスなツール公開
ECS 上の MCP server 長時間・複雑・状態を伴うツール

判断問題

  1. モデルが生成したツール引数を、そのまま Lambda に渡してはいけない理由は何ですか。
  2. 返金処理で人間承認と冪等性キーの両方が必要なのはなぜですか。
  3. AgentCore Gateway と各ツールの IAM 権限は、どのように責務を分けますか。
  4. multi-agent が single-agent より妥当になる条件と、不要な複雑性になる条件を挙げてください。

公式資料