🧠

Strands AgentsのSteeringについて調べた

に公開

v1.19.0でリリースされたStrands AgentsのSteeringについて調べたので、その内容を書きます。
まだ、Experimental機能なので、本番環境での使用は推奨されていません。

https://strandsagents.com/latest/documentation/docs/user-guide/concepts/experimental/steering/
https://github.com/strands-agents/sdk-python/releases/tag/v1.19.0

Steeringとは

AIエージェントで複数ステップのタスクを実行する際に、従来だとプロンプトに全ての手順を記載していました。例えば、メールを送るエージェントの場合、以下のようなプロンプトを使用していました。

あなたはメール送信エージェントです。
以下の手順とルールを厳守してメールを送信してください。

## 手順
1. メールの宛先を取得する
2. メールの件名を取得する...(省略)

## 禁止事項とルール
- 社外秘の情報(APIキーやパスワード)は絶対に含めないこと
- 相手が「重要顧客」リストにある場合は、敬語を「尊敬語」レベルに引き上げること
- 深夜(22時以降)の場合は、送信せずにドラフト保存にとどめること
- 件名には必ず【重要】などの隅付き括弧を含めること
- ...(他20行続く)

このくらいの手順や禁止事項であれば特に問題はないのですが、ここから新しい禁止事項や手順を追加したい場合、プロンプトが非常に長くなってしまいます。当然、プロンプトが長くなるとエージェントが指示を無視したり、ハルシネーションが発生したりするリスクが高まります。

この問題を解決するために、ワークフローを構築する手段もあるのですが、これだとエージェント特有の柔軟性が失われます。

今回リリースされたSteeringは、プロンプトに手順やルールを直接記載するのではなく、適切なタイミングでフィードバックを行うことで、エージェントの挙動を制御します。

仕組み

公式ドキュメントによると、Steeringは下記のように使用します。

from strands import Agent, tool
from strands.experimental.steering import LLMSteeringHandler

@tool
def send_email(recipient: str, subject: str, message: str) -> str:
    """メールを送信する"""
    return f"{recipient}にメールを送信しました"

# 明るいトーンを確保するためのSteeringハンドラーを作成
handler = LLMSteeringHandler(
    system_prompt="""
    あなたはメールが明るくポジティブなトーンを維持するようにガイダンスを提供します。

    ガイダンス:
    - メールの内容のトーンと感情を確認する
    - メッセージがネガティブまたは中立的な場合は、より明るい表現を提案する
    - ポジティブな言葉遣いとフレンドリーな挨拶の使用を促す

    エージェントがメールを送信しようとしたとき、メッセージのトーンが
    適切に明るいかどうかを確認し、改善が必要な場合はフィードバックを提供してください。
    """
)

agent = Agent(
    tools=[send_email],
    hooks=[handler]  # Steeringハンドラーをhooksとして統合
)

# エージェントがメールのトーンについてガイダンスを受ける
response = agent("重要な会議を何度も直前にリスケする顧客のtom@example.comに、イライラしたメールを送って")
print(agent.messages)  # "Tool call cancelled given new guidance..."(新しいガイダンスによりツール呼び出しがキャンセルされました)と表示

LLMSteeringHandlerクラスでSteeringを定義して、Agenthooksに渡すことで、エージェントの挙動を制御できます。

使用されているLLMSteeringHandlerの実装を詳しく見てみましょう。

https://github.com/strands-agents/sdk-python/blob/main/src/strands/experimental/steering/handlers/llm/llm_handler.py

まず、LLMSteeringHandlerでは下記のパラメータが存在します。

パラメータ 説明 デフォルト値
system_prompt Steeringの判断基準となるシステムプロンプト。ガイダンスのルールを自然言語で記述 -
prompt_mapper ツール呼び出し情報(ツール名、引数など)をSteering用のプロンプトに変換する DefaultPromptMapper
model Steering判断用のLLMモデル エージェントと同じモデル
context_providers Steering判断時に参照する追加情報を収集する [LedgerProvider()](ツール呼び出しの履歴、タイミング、結果を追跡)

Steeringは、エージェントがツールを呼び出そうとしたタイミング(BeforeToolCallEvent)で判断を行い、以下の3つのアクションのいずれかを返します。
(公式ドキュメントの画像を拝借しました)

Steeringの動作

画像をまとめると以下の通りです。

Action 動作
Proceed そのままツール実行を許可する
Guide ツール呼び出しをキャンセルし、エージェントにフィードバックを返す。エージェントはこのフィードバックを受けて別のアプローチを試みる
Interrupt ツール実行を一時停止し、人間の入力を待つ。承認されれば実行、拒否されればキャンセル

このように、Steeringを使用することで、プロンプトに長々とルールを書かなくても、エージェントの挙動を柔軟に制御できるようになります。

使ってみた

「仕組み」セクションで紹介したコードを実際に実行してみました。実行すると、以下のようにトレースされました。

トレース

流れを整理すると以下の通りです。

  1. 人間がエージェントに「イライラしたメールを送って」と指示する
  2. エージェントがネガティブなトーンのメール文章を作成する
  3. send_emailツールを呼び出そうとする
  4. Steeringがツール呼び出しを検出し、内容を評価 → 修正するためにGuideアクションを返す
  5. ツール呼び出しがキャンセルされ、「ポジティブなトーンに修正してください」とフィードバックが返る
  6. エージェントがフィードバックを受けて、ポジティブなトーンのメール文章に修正する
  7. 再度send_emailツールを呼び出そうとする
  8. Steeringがツール呼び出しを検出し、内容を評価 → 問題なしと判断しProceedアクションを返す
  9. ツール実行が許可され、メールが送信される

1つ目のツール呼び出しの中身は以下のようになっています。

1回目のツール呼び出し

「非常に困惑」や「大きな支障」といったかなりネガティブなトーンでメールが生成されているので、Steeringが Guideアクションを返しています。このレスポンスを受けて、エージェントは再度メールを生成し、send_emailツール呼び出しを行っています。

トレース

2回目のツール呼び出しでは、statusがProceedとなっていますね。そのままツール実行が許可されたことを意味します。このように、Steeringを使用することで、エージェントの挙動を動的に制御できました。

次に、Interruptアクションも試してみました。LLMSteeringHandlersystem_promptを以下のように変更します。

handler = LLMSteeringHandler(
    system_prompt="""
    あなたはメール送信の安全性を確認するガイダンスを提供します。

    ガイダンス:
    - メールの宛先が社外ドメインの場合は、人間の確認を求める
    - 添付ファイルがある場合は、人間の確認を求める
    - 重要な内容(金額、契約、機密情報など)の場合は、人間の確認を求める
    - 人間の確認を求める際は理由を含めたメッセージを表示する
    - 上記以外で内容に問題がなければ、そのまま送信を許可する
    """
)

また、エージェントには「tom@external-company.com(社外)に契約更新の見積もり100万円についてメールを送って」と指示します。正しくSteeringが働くと、ツール呼び出しが一時停止され、人間に確認を求めるはずです。

実行結果は以下の通りです。

Interruptのトレース

実行結果を見ると、Interruptアクションが返され、ツール呼び出しが一時停止されていることが確認できました。

Interruptを継続する場合は、下記のように記載するとツール実行が続行されます。

# interrupt_idの取得
interrupt_id = result.interrupts[0].id

# 実際にツール実行を続行する場合
responses = [
    {
        "interruptResponse": {
            "interruptId": interrupt_id,
            "response": "承認します。",
        },
    },
]
result = agent(responses)

langfuse上のinputはnullになっていますが、画像のようにきちんとツールが実行され、メールが送信されました。

Interrupt続行のトレース

まとめ

Strands AgentsのSteeringについて調べた内容を書きました。プロンプトに長々とルールを書かなくても、適切なタイミングでフィードバックを与えることで、エージェントの挙動を柔軟に制御できました。

しかし、毎回Tool呼び出しにSteering用のLLMが介入するので、もしかしたらレスポンスが遅くなったり、コストが増えたりする可能性があります。実際の運用で使用する場合は、このあたりも考慮する必要がありそうです。

Discussion