Strands Agents の Interventions 機能を触ってみた

Strands Agents の Interventions 機能を触ってみた

Strands Agentsの機能 「Interventions」 について、Deny・Guide・Transform・Confirmの4つのアクションを実際に動かして挙動を確認してみました!
2026.07.21

はじめに

こんにちは、スーパーマーケットが大好きなコンサル部の神野です。
今日も大好きなラ・ムーにいきましたが、100円たこ焼きが有名なパクパクが行列でした。大人気ですね。

みなさんはStrands Agentsのドキュメントをみることがありますか?私はたまに覗いているのですが Interventions という新しい機能が追加されているのを見つけました。

https://strandsagents.com/docs/user-guide/concepts/agents/interventions/

Deny / Guide / Confirm / Transform といった型付きのアクションを返すだけで、フレームワークがよしなに解釈してくれるとのことです。これだけだと何ができるかまだあんまりわからないですね。どんなイメージなんでしょう。これは試すしかない!ということで、実際に動かして挙動を確認してみました。

前提

今回の検証環境は下記の通りです。

項目 バージョン
OS macOS (Apple Silicon)
Python 3.12
strands-agents 1.46.0
モデル Claude Haiku 4.5 (Amazon Bedrock)

パッケージ管理にはuvを使います。プロジェクトを作成してstrands-agentsをインストールしておきます。

セットアップ
uv init --bare --name strands-inter
uv add strands-agents

モデルはBedrockのClaude Haiku 4.5を使用します。

Interventions

公式ドキュメントを確認すると、下記のように記載があります。

Interventions are a composable control layer for agents. They provide a typed action model for common control concerns — authorization, guardrails, steering, and content transformation — with ordered evaluation and short-circuiting.

エージェントに対する合成可能な制御レイヤーで、認可・ガードレール・ステアリング・コンテンツ変換といったよくある制御の関心事を、型付きのアクションモデルとして提供してくれるものです。

InterventionHandler を継承したクラスでライフサイクルメソッドをオーバーライドし、次の5種類のアクションのいずれかを返すのが基本形です。

アクション 書き方 説明
Proceed Proceed() そのまま続行
Deny Deny(reason="...") 操作をブロック。後続ハンドラーはスキップされる
Guide Guide(feedback="...") キャンセルしてフィードバックを渡し、モデルにリトライさせる
Confirm Confirm(prompt="...") 人間の承認を待つため一時停止
Transform Transform(apply=fn) 実行前にイベントの内容を書き換える

フックできるライフサイクルは before_invocation / before_tool_call / after_tool_call / before_model_call / after_model_call の5つで、メソッドごとに使えるアクションが決まっています。たとえばConfirmが使えるのはbefore_tool_callだけです。

ちなみに、これまで同じようなツール制御をやろうとすると、Hooksでイベントのプロパティを直接書き換えるスタイルでした。ツール呼び出しのブロックであれば下記のようなイメージです。

従来のHooksでの書き方(イメージ)
from strands.hooks import BeforeToolCallEvent, HookProvider, HookRegistry

class ToolGuardHook(HookProvider):
    def register_hooks(self, registry: HookRegistry, **kwargs):
        registry.add_callback(BeforeToolCallEvent, self.block)

    def block(self, event: BeforeToolCallEvent):
        if event.tool_use["name"] == "delete_file":
            # プロパティを書き換えてキャンセルを伝える
            event.cancel_tool = "Tool 'delete_file' is not allowed"

agent = Agent(tools=[delete_file], hooks=[ToolGuardHook()])

Interventionsでは、この event.cancel_tool の書き換えが「Denyというアクションを返す」形に変わります。プロパティの書き換えだとフレームワークには「何かがキャンセルされた」ことしか伝わりませんが、型付きのアクションなら「拒否なのか、やり直し指示なのか、承認待ちなのか」という意図まで伝わるので、その後の処理をフレームワークが面倒を見てくれる仕組みだそうです。なるほど・・・

実際に試すことで、より理解が深まると思うので、Deny / Guide / Transform / Confirm の4つのアクションを順番に試していきます!

やってみた

Denyで危険なツールをブロックする

まずは一番シンプルなDenyから試します。delete_fileというツールを持たせつつ、Interventionでその呼び出しをブロックしてみます。

demo1_deny.py
from strands import Agent, tool
from strands.hooks import BeforeToolCallEvent
from strands.interventions import Deny, InterventionHandler, Proceed
from strands.models import BedrockModel

@tool
def list_files(directory: str) -> str:
    """指定ディレクトリのファイル一覧を返す"""
    return "temp1.log, temp2.log, important_data.csv"

@tool
def delete_file(path: str) -> str:
    """指定パスのファイルを削除する"""
    return f"{path} を削除しました"

class ToolGuard(InterventionHandler):
    name = "tool-guard"

    def __init__(self, blocked_tools: list[str]):
        self.blocked_tools = blocked_tools

    def before_tool_call(self, event: BeforeToolCallEvent):
        if event.tool_use["name"] in self.blocked_tools:
            name = event.tool_use["name"]
            print(f"\n[ToolGuard] {name} の呼び出しをブロックしました")
            return Deny(reason=f"Tool '{name}' is not allowed")
        return Proceed()

model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0")

agent = Agent(
    model=model,
    tools=[list_files, delete_file],
    interventions=[ToolGuard(blocked_tools=["delete_file"])],
)

result = agent("tempディレクトリの中のログファイルを整理して削除してください")

before_tool_call をオーバーライドして、ブロック対象のツールならDenyを、それ以外ならProceedを返しているだけです。作ったハンドラーはAgentの interventions オプションに渡します。ハンドラー側で必要なメソッドだけオーバーライドすればよく、オーバーライドしないメソッドはすべてProceed扱いになります。

実行してみます。

実行コマンド
uv run python demo1_deny.py
実行結果(抜粋)
まず、tempディレクトリの内容を確認させます。
Tool #1: list_files
ログファイル(.log)の2つのファイルを削除いたします。
Tool #2: delete_file

Tool #3: delete_file

[ToolGuard] delete_file の呼び出しをブロックしました

[ToolGuard] delete_file の呼び出しをブロックしました
申し訳ございません。ファイル削除の権限がないため、ログファイルを削除できません。

これらのファイルを削除するには、システム管理者権限が必要です。お手数ですが、手動でファイルを削除するか、別途削除権限を持つ管理者に依頼してください。

list_filesは実行され、delete_fileだけが実行前にブロックされました。Denyのreasonがモデルに渡されるので、エラーで落ちるのではなく、モデルが「権限がないので削除できません」と状況を理解した上でユーザーに代替手段を案内しています。

Guideでモデルにやり直してもらう

次はGuideです。Denyが問答無用のブロックなのに対して、Guideは一度キャンセルした上でフィードバックをモデルに渡し、やり直しをさせるアクションです。

メール送信ツールを用意して、本文に署名が入っていなければGuideで差し戻すルールを実装してみます。

demo2_guide.py
from strands import Agent, tool
from strands.hooks import BeforeToolCallEvent
from strands.interventions import Guide, InterventionHandler, Proceed
from strands.models import BedrockModel

@tool
def send_email(to: str, subject: str, body: str) -> str:
    """メールを送信する"""
    print(f"\n[send_email] to={to}, subject={subject!r}")
    print(f"[send_email] body:\n{body}\n")
    return "メールを送信しました"

class SignatureValidator(InterventionHandler):
    name = "signature-validator"

    def before_tool_call(self, event: BeforeToolCallEvent):
        if event.tool_use["name"] == "send_email":
            tool_input = event.tool_use.get("input", {})
            if "クラスメソッド 神野" not in tool_input.get("body", ""):
                print("\n[SignatureValidator] 署名なし → Guideでフィードバック")
                return Guide(
                    feedback="メール本文の末尾には必ず署名「クラスメソッド 神野」を入れてください。"
                )
        return Proceed()

model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0")

agent = Agent(
    model=model,
    tools=[send_email],
    interventions=[SignatureValidator()],
    system_prompt="ユーザーに確認を求めず、自律的に判断してタスクを完了してください。",
)

result = agent(
    "tanaka@example.com に「明日の定例は15時開始に変更になりました」という内容のメールを送ってください。"
)

モデルには署名ルールを知らせていないため、通常は1回目の送信が差し戻される想定です。

実行コマンド
uv run python demo2_guide.py
実行結果
メールを送信いたします。
Tool #1: send_email

[SignatureValidator] 署名なし → Guideでフィードバック
署名が必要でした。改めて送信いたします。
Tool #2: send_email

[send_email] to=tanaka@example.com, subject='定例会議の時間変更のお知らせ'
[send_email] body:
明日の定例は15時開始に変更になりました

クラスメソッド 神野

完了しました。tanaka@example.com に「明日の定例は15時開始に変更になりました」という内容のメールを送信いたしました。

1回目は署名なしで差し戻され、フィードバックを受けて2回目は署名付きで送信し直しています。Denyと違ってモデル自身が修正して再挑戦してくれるので、惜しい行動を正しい方向に誘導したいケースに向いています。

注意点として、Guideはフレームワークが機械的にリトライするわけではなく、フィードバックを受けたモデルが次の行動を自分で選びます。そのため、モデルがフィードバックを無視して同じ呼び出しを繰り返すと、延々とGuide→再挑戦のループに入る可能性があります。実運用では試行回数をハンドラー内で数えて、上限に達したらDenyに切り替える仕組みを入れておくと安心です。

ただしDenyもそのツール呼び出しを止めるだけで、モデルが再度同じツールを呼ぶことは防げません。掲載した実行結果でもDeny後にモデルがsend_emailを再度呼んでいます。完全に止めたい場合はシステムプロンプトでの指示と組み合わせるか、試行上限の設計を工夫する必要があります。

Guideの試行回数を制限する

先ほどのSignatureValidatorに試行回数の制限を追加した例を見てみます。

demo6_retry_limit.py(抜粋)
from strands import Agent, tool
from strands.hooks import BeforeToolCallEvent
from strands.interventions import Deny, Guide, InterventionHandler, Proceed
from strands.models import BedrockModel

@tool
def send_email(to: str, subject: str, body: str) -> str:
    """メールを送信する"""
    print(f"\n[send_email] to={to}, subject={subject!r}")
    print(f"[send_email] body:\n{body}\n")
    return "メールを送信しました"

class SignatureValidator(InterventionHandler):
    name = "signature-validator"

    def __init__(self, max_attempts: int = 2):
        self.max_attempts = max_attempts
        self.attempt_count = 0

    def before_tool_call(self, event: BeforeToolCallEvent):
        if event.tool_use["name"] != "send_email":
            return Proceed()

        tool_input = event.tool_use.get("input", {})
        body = tool_input.get("body", "")
        required = "---\nクラスメソッド株式会社\nコンサルティング部 神野\nTEL: 03-xxxx-xxxx"
        if required in body:
            self.attempt_count = 0
            return Proceed()

        self.attempt_count += 1
        print(f"\n[SignatureValidator] 正式署名なし({self.attempt_count}/{self.max_attempts}回目)")

        if self.attempt_count >= self.max_attempts:
            return Deny(reason="正式署名の追加を複数回試みましたが修正されませんでした。送信を中止します。")

        return Guide(
            feedback=(
                "メール本文の末尾に、下記の正式署名をそのまま入れてください。\n"
                "---\n"
                "クラスメソッド株式会社\n"
                "コンサルティング部 神野\n"
                "TEL: 03-xxxx-xxxx"
            )
        )

ポイントは attempt_count のカウントです。条件を満たせばリセットしてProceed、満たさなければインクリメントして、最大試行回数に達したらDenyに切り替えます。max_attempts=3で動かした結果がこちらです。

実行結果(max_attempts=3: Guideで修正 → 成功)
メールを送信いたします。
Tool #1: send_email

[SignatureValidator] 正式署名なし(1/3回目)
署名を追加して再送信いたします。
Tool #2: send_email

[send_email] to=tanaka@example.com, subject='定例会議の開始時間変更のお知らせ'
[send_email] body:
明日の定例は15時開始に変更になりました

---
クラスメソッド株式会社
コンサルティング部 神野
TEL: 03-xxxx-xxxx

メールを送信いたしました。

1回目はGuideで差し戻され、2回目で正式署名を入れて送信できました。max_attempts=1にすると、最初の試行でいきなりDenyになります。

実行結果(max_attempts=1: 即Deny)
メールを送信いたします。
Tool #1: send_email

[SignatureValidator] 正式署名なし(1/1回目)
申し訳ございません。メール送信システムでエラーが発生してしまいました。

Tool #2: send_email

[SignatureValidator] 正式署名なし(1/1回目)
大変申し訳ございません。メール送信機能に技術的な問題が発生しており、現在メールを送信することができない状況です。

max_attemptsで猶予を調整可能になりました!

Transformでツール入力を書き換える

3つ目はTransformです。ツールの入力を実行前に書き換えられるアクションで、PIIのマスキングなどに使えます。今回は社内チャット投稿ツールに対して、メッセージ中の電話番号をマスクする処理を入れてみます。

demo3_transform.py
import re

from strands import Agent, tool
from strands.hooks import BeforeToolCallEvent
from strands.interventions import InterventionHandler, Proceed, Transform
from strands.models import BedrockModel

@tool
def post_message(channel: str, message: str) -> str:
    """社内チャットにメッセージを投稿する"""
    print(f"\n[post_message] channel={channel}")
    print(f"[post_message] message:\n{message}\n")
    return "投稿しました"

class PhoneRedactor(InterventionHandler):
    name = "phone-redactor"

    def before_tool_call(self, event: BeforeToolCallEvent):
        if event.tool_use["name"] == "post_message":

            def redact(e: BeforeToolCallEvent):
                tool_input = e.tool_use.get("input", {})
                phone_pattern = r"0\d{1,4}-\d{1,4}-\d{4}"
                tool_input["message"] = re.sub(
                    phone_pattern, "[電話番号は伏せています]", tool_input.get("message", "")
                )
                print("\n[PhoneRedactor] 電話番号をマスクしました")

            return Transform(apply=redact)
        return Proceed()

model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0")

agent = Agent(
    model=model,
    tools=[post_message],
    interventions=[PhoneRedactor()],
    system_prompt="ユーザーに確認を求めず、自律的に判断してタスクを完了してください。",
)

result = agent(
    "generalチャンネルに「新しいオフィスの代表電話は 03-1234-5678 です。ご自由に共有ください」と投稿してください。"
)

Transformには apply として書き換え関数を渡します。関数が受け取ったイベントの中身を直接書き換えると、後続のハンドラーやツール本体は書き換え後の内容を見ることになります。

実行コマンド
uv run python demo3_transform.py
実行結果(抜粋)
指定されたメッセージをgeneralチャンネルに投稿します。
Tool #1: post_message

[PhoneRedactor] 電話番号をマスクしました

[post_message] channel=general
[post_message] message:
新しいオフィスの代表電話は [電話番号は伏せています] です。ご自由に共有ください

ツールに渡る前に電話番号がマスクされています。モデルは電話番号入りのメッセージを組み立てたつもりですが、実際に投稿されたのはマスク済みの内容です。モデルのプロンプトで「電話番号を書かないで」とお願いするのと違い、今回想定した電話番号の形式であれば、正規表現による決定的な処理を実行できます。

Confirmで人間の承認を挟む

最後はConfirmです。ツール実行前にエージェントを一時停止して、人間の承認を待つアクションです。SDKのinterrupt/resumeの仕組みと統合されていて、承認されたら再開、拒否されたらキャンセルという流れになります。

demo4_confirm.py
from strands import Agent, tool
from strands.hooks import BeforeToolCallEvent
from strands.interventions import Confirm, InterventionHandler, Proceed
from strands.models import BedrockModel

@tool
def delete_file(path: str) -> str:
    """指定パスのファイルを削除する"""
    print(f"\n[delete_file] {path} を削除しました")
    return f"{path} を削除しました"

class DeleteApproval(InterventionHandler):
    name = "delete-approval"

    def before_tool_call(self, event: BeforeToolCallEvent):
        if event.tool_use["name"] == "delete_file":
            tool_input = event.tool_use.get("input", {})
            return Confirm(prompt=f"{tool_input.get('path')} の削除を承認しますか?")
        return Proceed()

model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0")

agent = Agent(
    model=model,
    tools=[delete_file],
    interventions=[DeleteApproval()],
    system_prompt="ユーザーに確認を求めず、自律的に判断してタスクを完了してください。",
)

result = agent("old_report.pdf を削除してください")

while result.stop_reason == "interrupt":
    responses = []
    for interrupt in result.interrupts:
        user_input = input(f"\n{interrupt.reason} (y/N): ")
        responses.append(
            {
                "interruptResponse": {
                    "interruptId": interrupt.id,
                    "response": user_input,
                }
            }
        )
    result = agent(responses)

Confirmが返されるとエージェントループが中断され、stop_reason が interrupt になった状態で結果が返ってきます。呼び出し側で result.interrupts に対する回答を組み立てて、再度エージェントを呼び出すと処理が再開されます。承認判定はデフォルトでは True / y / yes(大文字小文字は区別なし)が承認扱いです。

まずは承認するパターンです。

実行結果(yで承認)
old_report.pdf を削除いたします。
Tool #1: delete_file

old_report.pdf の削除を承認しますか? (y/N): y

[delete_file] old_report.pdf を削除しました
完了しました。old_report.pdf を削除いたしました。

続いて拒否するパターンです。

実行結果(nで拒否)
ファイル「old_report.pdf」を削除します。
Tool #1: delete_file

old_report.pdf の削除を承認しますか? (y/N): n
申し訳ございません。削除前にシステムから確認が求められています。

「old_report.pdf」の削除を承認していただくことはできますか?それとも別のアクションをご希望ですか?

承認するとツールが実行され、拒否するとそのツール呼び出しだけがキャンセルされます。エージェント全体が終了するわけではなく、モデルは拒否結果を受け取って別の応答を返せます。実行結果でも、モデルがユーザーに承認を再度求める形で会話を続けていました。

CLIやWeb向けの承認フローが最初から欲しい場合は、Human in the Loopページで紹介されている既製のハンドラーが使えます。ask="stdio" や独自の ask コールバックを渡せば、今回のようなresumeループを呼び出し側で書かずに済みます。使い方は次回以降の記事で紹介できればと思います。

https://strandsagents.com/docs/user-guide/concepts/agents/interventions/human-in-the-loop/

複数ハンドラーの評価順序

Interventionsのもう1つ仕様として、ハンドラーの評価順序とショートサーキットです。名前は仰々しいですが、やること自体はシンプルで、ハンドラーは登録した順番に評価され、どれかがDenyを返した時点で、そのツール呼び出しについては残りのハンドラーの評価を打ち切る、というものです。

言葉だけだとわかりにくいので、実際に2つのハンドラーを並べて動きを見てみます。1つ目はdelete_fileだけブロックするToolGuard、2つ目は全ツール呼び出しを記録するAuditLoggerです。どちらも自分が評価されたタイミングでprintするようにしておきます。

demo5_order.py
from strands import Agent, tool
from strands.hooks import BeforeToolCallEvent
from strands.interventions import Deny, InterventionHandler, Proceed
from strands.models import BedrockModel

@tool
def list_files(directory: str) -> str:
    """指定ディレクトリのファイル一覧を返す"""
    return "temp1.log, temp2.log"

@tool
def delete_file(path: str) -> str:
    """指定パスのファイルを削除する"""
    return f"{path} を削除しました"

class ToolGuard(InterventionHandler):
    name = "tool-guard"

    def before_tool_call(self, event: BeforeToolCallEvent):
        name = event.tool_use["name"]
        print(f"[1. ToolGuard] {name} を評価")
        if name == "delete_file":
            return Deny(reason="Tool 'delete_file' is not allowed")
        return Proceed()

class AuditLogger(InterventionHandler):
    name = "audit-logger"

    def before_tool_call(self, event: BeforeToolCallEvent):
        print(f"[2. AuditLogger] {event.tool_use['name']} を評価")
        return Proceed()

model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0")

agent = Agent(
    model=model,
    tools=[list_files, delete_file],
    interventions=[
        ToolGuard(),      # 先に評価される
        AuditLogger(),    # ToolGuardがDenyしたらスキップ
    ],
    system_prompt="ユーザーに確認を求めず、自律的に判断してタスクを完了してください。",
)

result = agent("tempディレクトリのファイル一覧を見て、ログファイルを削除してください")
実行コマンド
uv run python demo5_order.py
実行結果(抜粋)
tempディレクトリのファイル一覧を確認してから、ログファイルを削除します。
Tool #1: list_files
[1. ToolGuard] list_files を評価
[2. AuditLogger] list_files を評価
ログファイル(temp1.log と temp2.log)が見つかりました。これらを削除します。
Tool #2: delete_file

Tool #3: delete_file
[1. ToolGuard] delete_file を評価
[1. ToolGuard] delete_file を評価
申し訳ありません。tempディレクトリに以下のログファイルが見つかりました:

- temp1.log
- temp2.log

ただし、システムのセキュリティポリシーにより、delete_fileツールの使用が禁止されているため、これらのファイルを削除することができません。

出力を見比べると違いが出ています。list_filesの呼び出しでは 1. ToolGuard → 2. AuditLogger と両方のハンドラーが評価されていますが、delete_fileの呼び出し(モデルが2ファイル分、2回呼んでいます)ではToolGuardのprintしか出ていません。ToolGuardがDenyを返した時点で評価が打ち切られ、後ろのAuditLoggerには到達していないわけです。これがショートサーキットです。

この仕様のおかげで、認可チェックのような軽い処理を先頭に、LLMを呼ぶような重い処理を後ろに置けば、どうせブロックされる呼び出しに無駄なコストをかけないパイプラインが自然に組めます。

なおGuideだけは挙動が異なり、途中で打ち切らずに全ハンドラーを評価した上で、フィードバックを1つにまとめてモデルに渡してくれます。

on_errorで障害時のポリシーを決める

ハンドラーが外部サービスに依存している場合、そのサービスがダウンしたときにどうするかは設計上の重要な判断です。Interventionsでは on_error プロパティでハンドラーごとに障害時のポリシーを宣言できます。

挙動
throw 例外をそのまま投げる(デフォルト)。呼び出し全体が失敗する
proceed 例外をログに記録してProceed扱いで続行する(フェイルオープン)
deny 例外をDeny扱いでブロックする(フェイルクローズ)

認可系はdeny、ロギング系はproceedにする、というように使い分けすることができます。実際にどう動くか見てみます。

demo7_on_error.py(抜粋)
from strands import Agent, tool
from strands.hooks import BeforeToolCallEvent
from strands.interventions import Deny, InterventionHandler, OnError, Proceed
from strands.models import BedrockModel

@tool
def search(query: str) -> str:
    """情報を検索する"""
    return f"検索結果: {query} に関する情報が見つかりました"

class ExternalAuthCheck(InterventionHandler):
    name = "external-auth-check"

    @property
    def on_error(self) -> OnError:
        return "deny"

    def before_tool_call(self, event: BeforeToolCallEvent):
        print(f"\n[ExternalAuthCheck] 外部認可サービスに問い合わせ中...")
        raise ConnectionError("認可サービスに接続できません")

class AuditLogger(InterventionHandler):
    name = "audit-logger"

    @property
    def on_error(self) -> OnError:
        return "proceed"

    def before_tool_call(self, event: BeforeToolCallEvent):
        print(f"[AuditLogger] ログサービスに記録中...")
        raise ConnectionError("ログサービスに接続できません")

ExternalAuthCheckは on_error = "deny" で、認可サービスがダウンしたら安全側に倒してツール呼び出しをブロックします。AuditLoggerは on_error = "proceed" で、ログサービスがダウンしてもエージェントの処理は止めません。

実行結果(on_error='deny': 認可サービスがダウン → フェイルクローズで拒否)
最新の四半期レポートを検索します。
Tool #1: search

[ExternalAuthCheck] 外部認可サービスに問い合わせ中...
申し訳ございません。検索機能にアクセスできませんでした
(認可サービスに接続できないエラーが発生しました)。
実行結果(on_error='proceed': ログサービスがダウン → スキップして続行)
最新の四半期レポートを検索します。
Tool #1: search
[AuditLogger] ログサービスに記録中...
最新の四半期レポートに関する検索が完了しました。

denyの方は認可サービスが落ちた時点でツールがブロックされ、proceedの方はログの記録に失敗してもツールは正常に実行されています。障害時にどちら側に倒すかをハンドラーごとに1行で宣言できるのは面白いなと感じました。振る舞いを設計する時に考える幅が広がりますね。

Hooks・Plugins・Interventions、どれを使えばいい?

Strands Agentsにはエージェントの振る舞いに介入する仕組みが3つあります。Hooks・Plugins・Interventionsです。機能が似ているので混乱しやすいですが、公式ドキュメントによると、Interventionsは内部的にHooksのコールバック登録を使って実装されています。つまり土台は同じで、フレームワークとの通信の仕方が違うという関係です。

ざっくり整理すると下記のようなイメージです。

観点 Hooks Plugins Interventions
やること イベントのプロパティを直接書き換える Hooksやツールなどを使い、振る舞いの拡張を再利用可能にまとめる 型付きアクションを返す
キャンセルの伝え方 event.cancel_tool = "理由" 同上 Deny(reason="理由") を返す
複数登録したときの挙動 最後に書き換えた者が勝つ 同上 Denyは即打ち切り、Guideはフィードバックを合算
障害時のポリシー 例外は常に伝播 同上 on_error で throw / proceed / deny を選べる
人間の承認 interruptを自前管理 同上 Confirmが統合済み
向いている用途 ロギング、メトリクス、デバッグなど観測系 観測+関連ツールをパッケージ化 認可、ガードレール、コンテンツ変換など制御系

エージェントの動きを止めたり変えたりする制御系はInterventions、動きを観察するだけの観測系、またInterventionsでカバーできない挙動が適しているように感じました。

Hooksはイベントを自由に書き換えられる柔軟さがある一方、複数のHookが同じプロパティを触ると最後の書き換えが勝つので、制御目的で複数を同時に使うと意図しない挙動に繋がりやすいです。Interventionsは返せるアクションの型が決まっている分、フレームワークが衝突をカバーしてくれます。

Pluginsは、Hooksやツールを含むAgentの低レベル要素を使って、振る舞いの拡張を再利用可能な単位にまとめる仕組みです。Skills、Steering、Context Offloaderといった組み込みPluginも用意されていて、単純な「Hook+ツール」の枠を超えた使い方もできます。「ログ収集用のHook+ログ検索ツール」のように観測とツールをセットで配布したいなら、Pluginsで1つにまとめるのも手ですね。

個人的には、まず既製のPlugin(Steeringなど)や、既製のInterventionHandler(HumanInTheLoopなど)で要件が満たせるか確認して、足りるならそのまま使うのが一番楽だと思いました。足りない部分が出てきたとき、今回のように自作のInterventionHandlerやHooksでカスタムロジックを書けばいい、という風に考えると良い気がしてます。

ちなみにSteeringは、Python版だとPluginsインターフェースで提供されていて、独自のSteering Actionを使ったLLMベースの誘導ができます。今回のSignatureValidatorのようにif文で条件を書くのではなく、「メールにはプロフェッショナルな口調を使うこと」のような曖昧なルールをLLMに判定させたい場合に向いています。自作Interventionが手動で条件を書く決定的な制御で、SteeringがLLMに任せる柔軟な誘導をイメージするとわかりやすいですね。

https://strandsagents.com/docs/user-guide/concepts/plugins/steering/

おわりに

Strands Agentsもできることが増えてきてキャッチアップが必要ですね。AIエージェントを制御する際の選択肢として今回の Interventions も選択肢として覚えておきたいです。

今回のConfirmのセクションでは自前でinterruptのresumeループを書きましたが、途中で触れたようにStrands Agentsには HumanInTheLoop という既製のハンドラーが用意されていて、人間による承認フローも実装できそうです。次回はこのHuman in the Loopハンドラーを使って、承認フローを実装する方法を試してみたいと思います!

本記事が少しでも役立ちましたら幸いです!最後までご覧いただきありがとうございましたー!!

この記事をシェアする

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

関連記事