Strands AgentsとAmazon Bedrockで構築したAIエージェントをMicrosoft 製 agent-sreでSRE運用してみる

Strands AgentsとAmazon Bedrockで構築したAIエージェントをMicrosoft 製 agent-sreでSRE運用してみる

2026.05.29

はじめに

データ事業本部のkobayashiです。

前回の記事では、LiteLLM + LangGraph で構築した ReAct エージェントに Microsoft 製の agent-sre パッケージを統合して、SLO・サーキットブレーカー・コストガード等のSRE機能を実装する例を紹介しました。

今回は同じ構成をAWSネイティブの組み合わせ、つまり Strands Agents SDK + Amazon Bedrock で構築したエージェントに対して試してみます。Strands AgentsはAWSが開発した軽量・モデル駆動のAIエージェントフレームワークで、BedrockModelを通じてClaude / Nova等のBedrockモデルを直接呼び出せます。

agent-sreにはLangGraph・CrewAI・AutoGen・OpenAI Agents・SemanticKernel・Difyのアダプタは標準提供されていますが、Strands用のアダプタは未提供です。BaseAdapterは公開クラスとして提供されているので、それを継承して自作する形で連携します。

Strands Agents SDK とは

Strands Agentsは2025年にAWSが公開したオープンソースのAIエージェントSDKで、以下の特徴があります。

  • モデル駆動: 数行のコードでReActエージェントを構築できる
  • Bedrock のネイティブサポート: BedrockModelmodel_idregion_nameguardrail_id等を直接指定可能
  • Hooks システム: BeforeInvocationEvent / AfterInvocationEvent / BeforeModelCallEvent / AfterModelCallEvent / BeforeToolCallEvent / AfterToolCallEvent 等のライフサイクルイベントにフックを差し込める(他に AgentInitializedEvent / MessageAddedEvent 等も存在)
  • Hook 経由のリトライ機構: AfterModelCallEvent.retry = True を Hook 内でセットすると、SDK が同じ入力でモデル呼び出しを再試行する。ModelThrottledException のようなエラーも Hook で受けて能動的にリトライ可能
  • 観測性: OpenTelemetry ベースで計装でき、ADOT 経由で CloudWatch Application Signals 等にも接続可能
  • Bedrock Guardrails 統合: BedrockModel(guardrail_id=...)でコンテンツフィルタ・PII検出を直接組み込める

@toolデコレータでツールを定義し、Agent(model=..., tools=[...])でエージェントを作るだけで動作するため、LangGraphよりも記述量が少ない印象です。

agent-sre側の対応状況

agent-sre 3.5.0 時点では、agent_sre.adapters配下に以下のアダプタが提供されています。

アダプタ 対象フレームワーク
LangGraphAdapter LangGraph
CrewAIAdapter CrewAI
AutoGenAdapter AutoGen
OpenAIAgentsAdapter OpenAI Agents SDK
SemanticKernelAdapter Microsoft Semantic Kernel
DifyAdapter Dify

Strandsのアダプタは未提供ですが、BaseAdapterという抽象基底クラスが公開されており、これを継承すれば任意のフレームワーク向けのアダプタを自作できます。BaseAdapterは以下の機能を提供します。

  • TaskRecord(タスク単位の実行記録)の管理
  • SLI スナップショット(task_success_rate / total_cost_usd / avg_duration_ms / tool_accuracy等)
  • agent-sre 本体の SLO エンジンとのデータ連携

今回はStrands向けに StrandsAdapter を自作し、Strands HooksからBaseAdapterのメソッドを呼び出す形で連携します。

全体のアーキテクチャ

Strands Hooks がエージェントのライフサイクルイベントを受け取り、自作のSREHookを経由してStrandsAdapterRogueAgentDetectorにデータを流し込みます。CircuitBreakerSLOCostGuardはラッパー関数側で呼び出すパターンは前回と同じです。

環境構築

環境

Python 3.13.9
strands-agents 1.39.0
strands-agents-tools 0.5.2
agent-sre 3.5.0
boto3 1.42.10

インストール

uv pip でインストールします。バージョン固定で再現性を確保しています。

$ uv pip install \
    "strands-agents==1.39.0" \
    "strands-agents-tools==0.5.2" \
    "agent-sre==3.5.0" \
    "boto3>=1.40.0"

Bedrock モデルアクセスの有効化

ap-northeast-1 (東京リージョン) で Claude Sonnet 4.5 のクロスリージョン推論プロファイル jp.anthropic.claude-sonnet-4-5-20250929-v1:0 を使用します。事前に Bedrock コンソールから対象モデルのアクセスを有効化しておいてください。

AWSのクレデンシャルは標準的な AWS_PROFILE などの仕組みで設定します。

$ export AWS_PROFILE="your-profile"
$ export AWS_DEFAULT_REGION="ap-northeast-1"

aws bedrock list-inference-profiles で対象のプロファイルが ACTIVE になっていることを確認します(出力例、Account ID はマスク済み)。

$ aws bedrock list-inference-profiles --region ap-northeast-1 \
    --query "inferenceProfileSummaries[?inferenceProfileId=='jp.anthropic.claude-sonnet-4-5-20250929-v1:0']"
[
    {
        "inferenceProfileName": "JP Anthropic Claude Sonnet 4.5",
        "inferenceProfileId": "jp.anthropic.claude-sonnet-4-5-20250929-v1:0",
        "inferenceProfileArn": "arn:aws:bedrock:ap-northeast-1:111122223333:inference-profile/jp.anthropic.claude-sonnet-4-5-20250929-v1:0",
        "status": "ACTIVE",
        "type": "SYSTEM_DEFINED"
    }
]

簡単な疎通確認を行います。

from strands import Agent
from strands.models import BedrockModel

model = BedrockModel(
    model_id="jp.anthropic.claude-sonnet-4-5-20250929-v1:0",
    region_name="ap-northeast-1",
    temperature=0,
)
agent = Agent(model=model, system_prompt="短く一言で答えてください。")
print(agent("東京タワーの高さは何メートルですか?"))
333メートルです。

これで Bedrock 経由で Claude Sonnet 4.5 が呼び出せることを確認できました。

ツールの定義

エージェントが使用するツールを Strands の@toolデコレータで定義します。デモ用のモックレスポンスを返します。

demo.py
from strands import tool

@tool
def web_search(query: str) -> str:
    """Web で情報を検索する."""
    data = {
        "東京タワー": "東京タワーの高さは333メートルです。1958年に完成しました。",
        "富士山": "富士山の標高は3,776メートルで、日本最高峰です。",
    }
    for key, val in data.items():
        if key in query:
            return val
    return f"'{query}' の検索結果: 関連情報が見つかりました。"

@tool
def calculator(expression: str) -> str:
    """数式を計算する(四則演算のみ対応)."""
    allowed = set("0123456789+-*/.() ")
    if not all(c in allowed for c in expression):
        return "エラー: サポートされない式です"
    try:
        return str(eval(expression))  # noqa: S307 デモ用途のみ
    except Exception:
        return "計算エラー"

@tool
def get_weather(location: str) -> str:
    """指定した場所の現在の天気を取得する."""
    data = {
        "東京": "晴れ、気温 25°C、湿度 60%",
        "大阪": "曇り、気温 23°C、湿度 70%",
    }
    return f"{location}: {data.get(location, '晴れ、気温 20°C')}"

LangGraphの@toolと書き味は同じで、関数のdocstringがそのままツール説明になります。

StrandsAdapter の自作

agent-sreBaseAdapterを継承して、Strands Hooks から呼び出される薄いメソッド群を実装します。

demo.py
from agent_sre.adapters import BaseAdapter, TaskRecord

class StrandsAdapter(BaseAdapter):
    """agent-sre の BaseAdapter を Strands 用に拡張したアダプタ."""

    def __init__(self) -> None:
        super().__init__("strands")

    def on_invocation_start(self, agent_name: str = "") -> TaskRecord:
        return self._start_task({"agent_name": agent_name})

    def on_model_call(
        self,
        input_tokens: int = 0,
        output_tokens: int = 0,
        cost_usd: float = 0.0,
    ) -> None:
        if self._current:
            self._current.input_tokens += input_tokens
            self._current.output_tokens += output_tokens
            self._current.cost_usd += cost_usd

    def on_tool_call(self, tool_name: str, error: str = "") -> None:
        if self._current:
            self._current.tool_calls += 1
            if error:
                self._current.tool_errors += 1

    def on_invocation_end(self, success: bool = True, error: str = "") -> TaskRecord:
        return self._finish_task(success=success, error=error)

_start_task() / _finish_task()BaseAdapterから継承した内部メソッドで、TaskRecordの生成・終了を抽象化してくれます。

Strands Hooks で agent-sre にブリッジする

HookProviderを実装して、Strandsの各ライフサイクルイベントをStrandsAdapterとRogueAgentDetectorに流し込みます。

demo.py
from strands.hooks import (
    AfterInvocationEvent,
    AfterModelCallEvent,
    AfterToolCallEvent,
    BeforeInvocationEvent,
    BeforeToolCallEvent,
    HookProvider,
    HookRegistry,
)

class SREHook(HookProvider):
    """Strands Hook を agent-sre の各コンポーネントに橋渡しする HookProvider."""

    def __init__(
        self,
        adapter: StrandsAdapter,
        detector: RogueAgentDetector,
    ) -> None:
        self.adapter = adapter
        self.detector = detector
        self._tool_names: list[str] = []

    def register_hooks(self, registry: HookRegistry) -> None:
        registry.add_callback(BeforeInvocationEvent, self._on_before_invocation)
        registry.add_callback(BeforeToolCallEvent, self._on_before_tool)
        registry.add_callback(AfterToolCallEvent, self._on_after_tool)
        registry.add_callback(AfterModelCallEvent, self._on_after_model)
        registry.add_callback(AfterInvocationEvent, self._on_after_invocation)

    def _on_before_invocation(self, event: BeforeInvocationEvent) -> None:
        self._tool_names.clear()
        self.adapter.on_invocation_start(agent_name=event.agent.name)

    def _on_before_tool(self, event: BeforeToolCallEvent) -> None:
        name = event.tool_use["name"]
        self._tool_names.append(name)
        # action にツール名を渡すことで ActionEntropyScorer がツール多様性を評価する
        # ("tool_call" 等の固定文字列だとエントロピーが常に最小値に張り付くため)
        self.detector.record_action(
            agent_id=AGENT_ID,
            action=name,
            tool_name=name,
            timestamp=time.time(),
        )

    def _on_after_tool(self, event: AfterToolCallEvent) -> None:
        name = event.tool_use["name"]
        error = ""
        # event.exception は Strands がツール実行で捕捉した例外(None なら正常)
        if event.exception is not None:
            error = str(event.exception)
        elif event.result and event.result.get("status") == "error":
            error = "tool_error"
        self.adapter.on_tool_call(name, error=error)

    def _on_after_model(self, event: AfterModelCallEvent) -> None:
        # トークン使用量は最終的に AgentResult.metrics に集約されるので
        # ここでは個別記録はせず、ラッパー側でまとめて処理する
        pass

    def _on_after_invocation(self, event: AfterInvocationEvent) -> None:
        # AfterInvocationEvent は try/finally の finally 側で発火するため例外時にも来る。
        # event.result が None なら途中で失敗したとみなす。最終的な成功/失敗判定は
        # ラッパー (run_with_sre) で行うので、ここではアダプタ側 TaskRecord に
        # 暫定ステータスを残す扱い。
        success = event.result is not None
        self.adapter.on_invocation_end(success=success)

    @property
    def last_tool_names(self) -> list[str]:
        return list(self._tool_names)

BeforeToolCallEventの時点でRogueAgentDetectorにツール使用を記録するので、許可外ツール(capability スコア)の判定は呼び出しごとに反映されます。一方、行動エントロピー(同一ツールの連続ループ検出)や呼び出し頻度(バースト検出)はある程度の呼び出し履歴が蓄積してから判定が安定します。なお event.agent.name はエージェントが任意の名前で構築されていれば取得できますが、Agent() 構築時に name=AGENT_ID を明示しないと既定値("Strands Agents" 等)になり agent-sre 側のラベルと一致しなくなるため、後述の build_agent() で必ず name=AGENT_ID を渡しています。

Strands Agent + BedrockModel の構築

Bedrock Claude Sonnet 4.5 を呼び出すエージェントを構築します。hooks引数でSREHookを渡すだけで、上で定義したライフサイクル連携が有効になります。

demo.py
from strands import Agent
from strands.models import BedrockModel

AGENT_ID = "research-agent"
MODEL_ID = "jp.anthropic.claude-sonnet-4-5-20250929-v1:0"
REGION = "ap-northeast-1"

def build_agent(hook: SREHook) -> Agent:
    model = BedrockModel(
        model_id=MODEL_ID,
        region_name=REGION,
        temperature=0,
        # 本番では guardrail_id を指定して Bedrock Guardrails を有効化する
        # guardrail_id="xxxxxxxx",
        # guardrail_version="DRAFT",
        # guardrail_trace="enabled",
    )
    return Agent(
        name=AGENT_ID,
        model=model,
        tools=[web_search, calculator, get_weather],
        system_prompt=(
            "あなたはツールを使ってユーザーの質問に回答するアシスタントです。"
            "必要に応じてツールを呼び出して、簡潔に答えてください。"
        ),
        hooks=[hook],
    )

LangGraphの場合はStateGraphを組み立てる必要がありましたが、StrandsはAgent()tools=を渡すだけでReActループを内部で組み立ててくれるため、コード量が少なめです。

agent-sre コンポーネントの初期化

CircuitBreaker / SLO / RogueAgentDetector / CostGuardの初期化は前回の記事と同じパターンです。TaskSuccessRateSLIも同じ実装を流用します。

demo.py
from agent_sre.anomaly import RiskLevel, RogueAgentDetector, RogueDetectorConfig
from agent_sre.cascade.circuit_breaker import (
    CircuitBreaker,
    CircuitBreakerConfig,
    CircuitOpenError,
)
from agent_sre.cost import CostGuard
from agent_sre.slo import SLI, SLO, ErrorBudget, SLIValue
from agent_sre.slo.objectives import ExhaustionAction

class TaskSuccessRateSLI(SLI):
    def collect(self) -> SLIValue:
        values = self.values_in_window()
        if not values:
            return self.record(1.0)
        good = sum(1 for v in values if v.is_good)
        return self.record(good / len(values))

def init_sre_components(
    failure_threshold: int = 3,
    recovery_timeout: float = 10.0,
) -> tuple[CircuitBreaker, SLO, RogueAgentDetector, CostGuard]:
    breaker = CircuitBreaker(
        agent_id=AGENT_ID,
        config=CircuitBreakerConfig(
            failure_threshold=failure_threshold,
            recovery_timeout_seconds=recovery_timeout,
            half_open_max_calls=1,
        ),
    )
    slo = SLO(
        name=f"{AGENT_ID}-reliability",
        indicators=[
            TaskSuccessRateSLI(name="task_success_rate", target=0.95, window="1h"),
        ],
        error_budget=ErrorBudget(
            total=0.05,
            window_seconds=3600,
            burn_rate_alert=2.0,
            burn_rate_critical=10.0,
            exhaustion_action=ExhaustionAction.THROTTLE,
        ),
        agent_id=AGENT_ID,
    )
    detector = RogueAgentDetector(
        config=RogueDetectorConfig(
            frequency_z_threshold=2.0,
            quarantine_risk_level=RiskLevel.HIGH,
        ),
    )
    detector.register_capability_profile(
        agent_id=AGENT_ID,
        allowed_tools=["web_search", "calculator", "get_weather"],
    )
    cost_guard = CostGuard(
        per_task_limit=0.10,
        per_agent_daily_limit=1.00,
        org_monthly_budget=100.00,
        auto_throttle=True,
        kill_switch_threshold=0.95,
    )
    return breaker, slo, detector, cost_guard

SRE ラッパー

エージェント呼び出しを各コンポーネントで保護するラッパーです。Strands ではagent(question)の戻り値がAgentResultオブジェクトで、result.metrics.latest_agent_invocation.usageに直近の呼び出し分のトークン使用量が入っているため、それを取り出してコスト計算に使います(accumulated_usageは同一 Agent インスタンスの全リクエスト累積なので、個別タスクのコスト計上には使えない点に注意)。

demo.py
import time
from typing import Any

from strands.types.exceptions import ModelThrottledException

# Claude Sonnet 4.5 の単価($/token)
INPUT_COST_PER_TOKEN = 3.0 / 1_000_000
OUTPUT_COST_PER_TOKEN = 15.0 / 1_000_000

def run_with_sre(
    question: str,
    *,
    breaker: CircuitBreaker,
    slo: SLO,
    detector: RogueAgentDetector,
    cost_guard: CostGuard,
    adapter: StrandsAdapter,
    hook: SREHook,
    agent: Agent,
    estimated_cost: float = 0.005,
    simulate_failure: bool = False,
) -> dict[str, Any]:
    # 1. コストチェック(プリフライト)
    allowed, reason = cost_guard.check_task(AGENT_ID, estimated_cost=estimated_cost)
    if not allowed:
        return {"status": "cost_blocked", "reason": reason}

    # 2. サーキットブレーカーチェック + エージェント実行
    def _invoke():
        if simulate_failure:
            raise RuntimeError("simulated_fault")
        return agent(question)

    start = time.time()
    try:
        result = breaker.call(_invoke)
    except CircuitOpenError as e:
        slo.indicators[0].record(0.0)
        slo.record_event(good=False)
        return {"status": "circuit_open", "retry_after": round(e.retry_after, 1),
                "cb_state": breaker.state}
    except ModelThrottledException as e:
        slo.indicators[0].record(0.0)
        slo.record_event(good=False)
        return {"status": "throttled", "error": str(e), "cb_state": breaker.state}
    except Exception as e:  # CircuitBreaker.call() は任意の例外を failure として再送出する
        slo.indicators[0].record(0.0)
        slo.record_event(good=False)
        return {"status": "failed", "error": str(e), "cb_state": breaker.state}

    latency = time.time() - start

    # 3. 不正エージェント検出(Hookで record_action 済みなので assess のみ)
    assessment = detector.assess(AGENT_ID)

    # 4. SLO 記録
    slo.indicators[0].record(1.0)
    slo.record_event(good=True)

    # 5. コスト記録(直近の Agent 呼び出し分のトークン使用量を取得)
    # ※ accumulated_usage は同一 Agent インスタンスの「全リクエスト累積」のため、
    #   個別タスクのコスト計上には latest_agent_invocation.usage を使う
    latest = result.metrics.latest_agent_invocation
    usage = latest.usage if latest else {"inputTokens": 0, "outputTokens": 0}
    input_tokens = usage.get("inputTokens", 0)
    output_tokens = usage.get("outputTokens", 0)
    prompt_cost = input_tokens * INPUT_COST_PER_TOKEN
    completion_cost = output_tokens * OUTPUT_COST_PER_TOKEN
    actual_cost = prompt_cost + completion_cost
    cost_guard.record_cost(
        agent_id=AGENT_ID,
        task_id=f"task-{int(time.time() * 1000)}",
        cost_usd=actual_cost,
        breakdown={"input": prompt_cost, "output": completion_cost},
    )

    return {
        "status": "success",
        "response": str(result),
        "latency": round(latency, 2),
        "tool_calls": hook.last_tool_names,
        "risk_level": assessment.risk_level.value,
        "cost": round(actual_cost, 6),
        "input_tokens": input_tokens,
        "output_tokens": output_tokens,
    }

LangGraphのときは AIMessage.usage_metadata 経由でトークン数を取り出していましたが、Strands では直近の Agent 呼び出し分が AgentResult.metrics.latest_agent_invocation.usage に、Agent インスタンス全体の累積が accumulated_usage に分かれて格納されています。個別タスクのコスト計上には前者を使い、エージェント単位の累計確認には後者を使う、という使い分けになります。Claude Sonnet 4.5 のトークン単価は2026-05時点で input: $3.00 / 1M、output: $15.00 / 1M です。

エージェントの動作確認

実装したコードを実行して動作を確認していきます。

$ python demo.py

正常系の動作確認

============================================================
デモ 1: Strands エージェント実行と Agent SRE 監視
============================================================

  質問 1: 東京タワーの高さは何メートルですか?
  回答: 東京タワーの高さは**333メートル**です。1958年に完成した日本を代表する電波塔で、現在も東京のシンボルとして親しまれています。
  レイテンシ=4.6s  ツール=['web_search']  リスク=low  トークン=1939/121
  SLO バジェット残=100.0%  累積コスト=$0.0076

  質問 2: 123 * 456 を計算してください。
  回答: 123 × 456 = **56,088** です。
  レイテンシ=2.73s  ツール=['calculator']  リスク=low  トークン=2256/72
  SLO バジェット残=100.0%  累積コスト=$0.0155

  質問 3: 東京の天気を教えてください。
  回答: 東京の現在の天気は晴れで、気温は25°C、湿度は60%です。
  レイテンシ=3.32s  ツール=['get_weather']  リスク=low  トークン=2479/104
  SLO バジェット残=100.0%  累積コスト=$0.0245

3つの質問それぞれで適切なツールが選択され、リスクレベルlow、SLOバジェット100%、累積コスト$0.0245でタスクを完了しています。前回記事の同等デモ(gpt-4o-mini)では累積コスト$0.0002だったので、おおよそ 100倍超 のコスト差です。これは Claude Sonnet 4.5 のトークン単価(input $3.00/1M、output $15.00/1M)が gpt-4o-mini(input $0.15/1M、output $0.60/1M)の20倍以上であることに加え、Claude Sonnet 4.5 はより詳細な応答を返す傾向があるため出力トークン数も増えていることに起因します。

サーキットブレーカーの動作確認

待ち時間短縮のためrecovery_timeout=2.0で再初期化します。

demo.py
breaker, slo, detector, cost_guard = init_sre_components(
    failure_threshold=3, recovery_timeout=2.0,
)

3回連続失敗を注入してサーキットブレーカーの動作を確認します。

============================================================
デモ 2: サーキットブレーカー (失敗シミュレーション)
============================================================
  リクエスト 1: failed          CB=CLOSED
  リクエスト 2: failed          CB=CLOSED
  リクエスト 3: failed          CB=OPEN
  リクエスト 4: circuit_open    CB=OPEN
  リクエスト 5: circuit_open    CB=OPEN

  (recovery_timeout=2秒 待機...)
  リクエスト 6 (復帰): success         CB=CLOSED

3回連続失敗でCLOSED → OPENに遷移し、以降はCircuitOpenErrorが投げられてBedrock呼び出しが行われません。recovery_timeout=2秒経過後にHALF_OPENへ遷移し、試行が成功してCLOSEDに復帰しています。

SLO とエラーバジェット

成功と失敗が混在するシナリオです。

============================================================
デモ 3: SLO トラッキングとエラーバジェット
============================================================
  SLO: タスク成功率 >= 95%  エラーバジェット: 5%

  [1] success         バジェット残=100.0%  バーンレート= 0.00  SLO=healthy    アラート=[]
  [2] success         バジェット残=100.0%  バーンレート= 0.00  SLO=healthy    アラート=[]
  [3] failed          バジェット残=  0.0%  バーンレート=24000.00  SLO=exhausted  アラート=['burn_rate_warning', 'burn_rate_critical']
  [4] success         バジェット残=  0.0%  バーンレート=18000.00  SLO=exhausted  アラート=['burn_rate_warning', 'burn_rate_critical']
  [5] failed          バジェット残=  0.0%  バーンレート=28800.00  SLO=exhausted  アラート=['burn_rate_warning', 'burn_rate_critical']
  [6] success         バジェット残=  0.0%  バーンレート=24000.00  SLO=exhausted  アラート=['burn_rate_warning', 'burn_rate_critical']
  [7] success         バジェット残=  0.0%  バーンレート=20571.43  SLO=exhausted  アラート=['burn_rate_warning', 'burn_rate_critical']
  [8] success         バジェット残=  0.0%  バーンレート=18000.00  SLO=exhausted  アラート=['burn_rate_warning', 'burn_rate_critical']

最初の失敗(タスク3)の時点でバジェット残0.0%・SLO=exhaustedに遷移し、burn_rate_warningburn_rate_criticalの両アラートが発火しています。挙動は前回記事と同じで、agent-sre 3.5.0 の ErrorBudget 実装が失敗1件でconsumed += 1.0カウントされる仕様によるものです。total=0.05のような小さな値だと最初の1件で即枯渇するので、実運用では想定イベント数に応じた絶対値(例: 100イベントあたり5件まで許容なら total=5.0)で設計するほうが直感的です(詳細は前回記事の SLO/ErrorBudget セクションを参照)。バーンレートが秒単位の異常値になっているのは、本来1時間〜30日スケールの消費を秒単位デモで計測しているためです。

不正エージェント検出

RogueAgentDetectorのシナリオ別動作を確認します(LLM呼び出しなし、ローカル検証のみ)。

============================================================
デモ 4: 不正エージェント検出(3シグナル別)
============================================================
  A) 正常              リスク=low       スコア=0.00  隔離推奨=False
    capability=0.00  frequency=0.00  entropy=0.00
  B) 許可外ツール多用  リスク=low       スコア=0.50  隔離推奨=False
    capability=0.50  frequency=0.00  entropy=0.00
  C) ループ (低entropy)  リスク=medium    スコア=1.00  隔離推奨=False
    capability=0.00  frequency=0.00  entropy=1.00
  D) バースト攻撃     リスク=critical  スコア=18.30  隔離推奨=True
    capability=0.00  frequency=18.30  entropy=0.00

シナリオBでは検証用に allowed_tools=["web_search", "get_weather"]calculator を意図的にホワイトリストから外した状態にしておき、その上で calculator を 50% の割合で呼ばせて Capability スコア = 0.50 を再現しています。Strands エージェント側ではBeforeToolCallEventの時点でdetector.record_action()が呼ばれているので、暴走ループや許可外ツール使用が起きた瞬間にスコアが反映されます。

コストガード

============================================================
デモ 5: コストガード
============================================================
  タスク1(通常): allowed=True  支出=$0.05/0.50  利用率=10%
  タスク2(通常): allowed=True  支出=$0.10/0.50  利用率=20%
  タスク3(通常): allowed=True  支出=$0.15/0.50  利用率=30%

  大型タスク: allowed=False  reason=Estimated cost $0.50 exceeds per-task limit $0.10
  タスク1: 支出=$0.20  利用率= 40%  状態=ok  アラート=[]
  タスク2: 支出=$0.25  利用率= 50%  状態=ok  アラート=['warning']
  タスク3: 支出=$0.30  利用率= 60%  状態=ok  アラート=[]
  タスク4: 支出=$0.35  利用率= 70%  状態=ok  アラート=[]
  タスク5: 支出=$0.40  利用率= 80%  状態=ok  アラート=['warning']
  タスク6: 支出=$0.45  利用率= 90%  状態=throttled  アラート=['warning']
  タスク7: 支出=$0.50  利用率=100%  状態=throttled,killed  アラート=['critical', 'critical', 'critical']
  タスク8: 支出=$0.55  利用率=110%  状態=throttled,killed  アラート=['critical']

大型タスクはcheck_task()で事前ブロックされ、利用率90%でスロットル、kill_switch_threshold=0.95超過でキルスイッチが発動しています。

AWS 純正サービスとの責務マッピング

執筆時点でAWSのフルマネージドサービスだけでもagent-sre の各機能に対応するサービスを揃えつつあり、組み合わせるとかなりの領域を補完できます。

領域 AWS 純正 agent-sre 備考
ポリシー / 不正検出 Bedrock AgentCore Policy (Cedarベース、Gateway での決定論的ブロック) RogueAgentDetector (Capability/Frequency/Entropy 統計検出) AgentCore Policy は実際にブロックする事前防御、agent-sre は事後検出
SLO 監視 CloudWatch Application Signals SLO (Bedrock の CloudWatch メトリクスを SLI として活用可能、Burn Rate Alarm 対応) SLO / ErrorBudget どちらでも可、agent-sre は Bedrock 以外のプロバイダでも使える
コスト管理 AWS Budgets + Cost Anomaly Detection + Application Inference Profiles CostGuard リアルタイム性なら CostGuard、組織レベル運用なら Budgets。Application Inference Profiles はコスト配賦・可視化寄りで、即時遮断は Budgets Actions 等と組み合わせる
サーキットブレーカー 直接対応サービスなし CircuitBreaker AWS 純正の直接対応サービスは薄く、アプリ側実装か外部ライブラリで補う形になる
コンテンツ安全性 Bedrock Guardrails (PII / フィルタ / プロンプトインジェクション検出) (カバー外) 主たる責務が異なるため、組み合わせて使いやすい
観測性 AgentCore Observability + CloudWatch Bedrock メトリクス (TimeToFirstTokenEstimatedTPMQuotaUsage 等) BaseAdapter の SLI スナップショット どちらも併用が現実的

整理すると、選択の指針は以下になります。

  • Bedrock 専業 + AWS 完結を目指す: AgentCore Policy + Application Signals SLO + Budgets + Guardrails の純正スタックなら AWS 内で完結させやすく、運用も AWS 側に寄せられます
  • マルチクラウド / マルチプロバイダ前提: agent-sre のようなプロバイダー非依存の抽象化レイヤを採用し、Bedrock 以外(OpenAI / Anthropic API / Azure OpenAI 等)にも同じガードを掛けたい
  • 混在(本記事のアプローチ): 純正と agent-sre で役割を分けて組み合わせる。たとえば「ポリシー判定は AgentCore Policy、サーキットブレーカーは agent-sre、コンテンツ安全性は Bedrock Guardrails」のように切り分けると、各層の強みを活かせます

なお、CircuitBreaker の領域だけは AWS 純正に直接対応するサービスが薄いため、Bedrock 専業構成でも agent-sre を採用する動機になります。「Bedrock 呼び出しが連続失敗したら一定時間 API 呼び出しを止めてコスト浪費を防ぐ」というユースケースで有用です。

また EstimatedTPMQuotaUsage メトリクスについては、AWS公式ドキュメントでも推定値であり、スロットリング判定の唯一の根拠としては使わないよう注意喚起されています(Monitor Amazon Bedrock with Amazon CloudWatch)。傾向把握や補助的なシグナルとして扱うのが適切です。

各サービスの公式資料は以下を参照してください。

Bedrock Guardrails との組み合わせ

Strands のBedrockModelは Bedrock Guardrails をサポートしています。guardrail_id等を渡すだけで、入力プロンプトとモデル出力をBedrock Guardrails のフィルタに通せます。

demo.py
model = BedrockModel(
    model_id=MODEL_ID,
    region_name=REGION,
    guardrail_id="your-guardrail-id",
    guardrail_version="DRAFT",
    guardrail_trace="enabled",          # トレース情報を有効化
    guardrail_stream_processing_mode="sync",
    guardrail_redact_input=True,
    guardrail_redact_output=False,
    guardrail_latest_message=True,      # 直近のユーザーメッセージのみ評価
)

agent-sre の責務は信頼性管理(SLO・サーキットブレーカー・暴走検出・コスト管理)で、コンテンツ安全性は対象外です。一方 Bedrock Guardrails は PII 検出・コンテンツフィルタ・ハルシネーション検出・プロンプトインジェクション検出といった「コンテンツ安全性」を担います。両者は主たる責務が異なるため、組み合わせて使うことでより対AIエージェントSREとして堅牢になります。

レイヤー 担当
Bedrock Guardrails コンテンツ安全性 PII漏洩防止、有害コンテンツフィルタ、プロンプトインジェクション検出
agent-sre エージェント信頼性 SLO、サーキットブレーカー、暴走検出、コスト超過防止
Strands Hooks 連携ハーネス ライフサイクルイベントを上記2層に流す

まとめ

Strands Agents + Amazon Bedrock で構築したエージェントに、Microsoft の agent-sre パッケージを統合してみました。BaseAdapter を継承した StrandsAdapter を30行程度で自作するだけで、標準アダプタが未提供のフレームワークでも agent-sre を載せられることが確認できました。

最後まで読んで頂いてありがとうございました。


生成AI活用はクラスメソッドにお任せ

過去に支援してきた生成AIの支援実績100+を元にホワイトペーパーを作成しました。御社が抱えている課題のうち、どれが解決できて、どのようなサービスが受けられるのか?4つのフェーズに分けてまとめています。どうぞお気軽にご覧ください。

生成AI資料イメージ

無料でダウンロードする

この記事をシェアする

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

関連記事