Agentic AI 開発実践 - Strands Agents 版
ホーム
演習ガイド
ホーム
演習ガイド
  • 演習1: 最小構成エージェント
  • 演習2: 複数ツールを持つ単一エージェント
  • 演習3: Graphによる一次切り分け
  • 演習4: 永続化・Hooks・HITL
  • 演習5: MCPサーバーへの接続
  • 演習6: Guardrails設定と評価結果の確認

演習2: 複数ツールを持つ単一エージェント

所要時間の目安: 25分

この演習でやること

演習1で作成したエージェントに、get_ticket_status(チケット対応状況確認)とget_password_reset_guide(パスワードリセット手順案内)の2つのツールを追加し、3つのツールを適切に使い分けられるエージェントに拡張します。

ベースコードファイル: code/ex2_multi_tool/agent.py

このファイルには、演習1の完成形(search_faqツール・SupportAnswerによる構造化出力)がすでに組み込まれています。TODO 1、TODO 2、…と番号が振られた場所に、以下の手順のコードを追加・書き換えしながら完成させてください(番号はファイル上の位置ではなく、追加する順番を表しています)。

手順

手順1: チケットデータを用意する

agent.pyのTODO 1に、以下のチケットデータを貼り付けます。

agent.py(TODO 1)
TICKET_DATA = {
    "TCK-1001": {"requester": "山田太郎", "category": "アカウント", "status": "対応中",
                 "summary": "VPN接続不可の問い合わせ", "updated_at": "2026-09-18 10:30"},
    "TCK-1002": {"requester": "佐藤花子", "category": "ハードウェア", "status": "未対応",
                 "summary": "貸与PCのバッテリー不良", "updated_at": "2026-09-19 09:00"},
    "TCK-1003": {"requester": "鈴木一郎", "category": "ソフトウェア", "status": "対応完了",
                 "summary": "業務システムのインストール申請", "updated_at": "2026-09-15 14:20"},
    "TCK-1004": {"requester": "高橋美咲", "category": "アカウント", "status": "対応中",
                 "summary": "メール送受信エラーの調査", "updated_at": "2026-09-19 11:05"},
    "TCK-1005": {"requester": "田中健太", "category": "ネットワーク", "status": "未対応",
                 "summary": "会議室の無線LANが不安定", "updated_at": "2026-09-20 08:45"},
    "TCK-1006": {"requester": "伊藤さくら", "category": "ハードウェア", "status": "対応完了",
                 "summary": "モニターの交換対応", "updated_at": "2026-09-14 16:10"},
}

手順2: get_ticket_status・get_password_reset_guideツールを定義する

TODO 2に、2つのツールの定義を貼り付けます。

agent.py(TODO 2)
@tool
def get_ticket_status(ticket_id: str) -> str:
    """指定されたチケット番号の対応状況を確認する。

    「チケットはどうなっていますか」「対応状況を教えてください」といった、
    既に起票済みの問い合わせの進捗確認に使う。新規の質問への回答には使わない。

    Args:
        ticket_id: チケット番号(例: TCK-1001)
    """
    ticket = TICKET_DATA.get(ticket_id)
    if not ticket:
        return f"チケット番号 {ticket_id} は見つかりませんでした。"
    return f"[{ticket_id}] ステータス: {ticket['status']} / 概要: {ticket['summary']}"


@tool
def get_password_reset_guide() -> str:
    """パスワードを忘れた、またはロックされた場合のリセット手順を案内する。

    「パスワードを忘れた」「ログインできない」といった質問に使う。
    """
    return ("社内ポータルの「パスワード再設定」から手続きできます。"
            "アカウントがロックされている場合は、情報システム部での本人確認が必要です。")

ここでも、docstringの書き方が重要です。get_ticket_statusのdocstringに「新規の質問への回答には使わない」という一文が入っているのは、search_faqとの役割の境界をLLMに明確に伝えるためです。ツールが増えるほど、「どんな時に使うか」だけでなく「どんな時に使わないか」まで書くことが、ツール選択の精度に直結します。

手順3: Agentの定義を3ツール対応に書き換える

TODO 3のagent = Agent(...)を、次のように書き換えます。

agent.py(TODO 3・書き換え)
agent = Agent(
    model=model,
    tools=[search_faq, get_ticket_status, get_password_reset_guide],
    system_prompt="あなたは社内ITヘルプデスクの担当者です。質問内容に応じて適切なツールを使ってください。",
)

手順4: 動作確認する

TODO 4(if __name__ == "__main__":ブロック)を、3つのツールをそれぞれ試せる内容に書き換えます。TODO 4のコメント行から、その下に最初から書かれているコード(print(answer.needs_escalation)の行まで)を、行頭の空白も含めてまとめて選択し、その上から以下のコードを貼り付けてください。コードはif __name__ == "__main__":の中に入るため、半角スペース4つでインデント済みです(インデントの途中や後ろにカーソルを置いて貼ると、1行目だけインデントがずれてエラーになります)。

agent.py(TODO 4・書き換え)
    result1 = agent(
        "TCK-1001のチケットは今どうなっていますか?",
        structured_output_model=SupportAnswer,
    )
    print(result1.structured_output.answer)

    result2 = agent(
        "パスワードを忘れてしまいました。どうすればいいですか?",
        structured_output_model=SupportAnswer,
    )
    print(result2.structured_output.answer)

    result3 = agent(
        "TCK-9999のチケットの状況を教えてください。",
        structured_output_model=SupportAnswer,
    )
    print(result3.structured_output.answer)

ターミナルで実行します。

python agent.py

1つ目はget_ticket_status、2つ目はget_password_reset_guide、3つ目は存在しないチケット番号に対するget_ticket_statusの応答(「見つかりませんでした」)が、それぞれ正しく呼ばれることを確認してください。

模範解答(agent.py 全体)を表示
agent.py(完成形)
from strands import Agent, tool
from strands.models import BedrockModel
from pydantic import BaseModel, Field


model = BedrockModel(
    model_id="jp.amazon.nova-2-lite-v1:0",
    region_name="ap-northeast-1",
    max_tokens=1024,
)


FAQ_DATA = {
    "VPN": "VPNに接続できない場合は、まず社内ポータルから最新のVPNクライアントを"
           "再インストールし、二要素認証アプリの時刻同期を確認してください。",
    "パスワード": "パスワードを忘れた場合は、社内ポータルの『パスワード再設定』から"
                 "手続きできます。ロックされた場合の解除は情報システム部での対応が必要です。",
    "Wi-Fi": "社内Wi-Fiに接続できない場合は、端末のWi-Fi設定を一度削除し、"
             "社内ポータルに掲載のパスワードで再接続してください。",
    "プリンター": "プリンターが認識されない場合は、社内ポータルの『プリンタードライバー』"
                 "ページから該当機種のドライバーを再インストールしてください。",
    "メール": "社用メールが送受信できない場合は、Webメール版で状況を確認し、"
             "改善しない場合は情報システム部に問い合わせてください。",
    "会議室予約": "会議室予約システムにログインできない場合は、社内SSOのパスワードを"
                 "確認し、改善しない場合は総務部に問い合わせてください。",
    "ソフトウェア": "新しいソフトウェアのインストールには、情報システム部への"
                   "利用申請(社内ポータルの『ソフトウェア利用申請』)が必要です。",
    "PC持ち出し": "社外へのPC持ち出しには、上長承認を得たうえで『PC持ち出し申請』を"
                 "情報システム部に提出してください。",
}


TICKET_DATA = {
    "TCK-1001": {"requester": "山田太郎", "category": "アカウント", "status": "対応中",
                 "summary": "VPN接続不可の問い合わせ", "updated_at": "2026-09-18 10:30"},
    "TCK-1002": {"requester": "佐藤花子", "category": "ハードウェア", "status": "未対応",
                 "summary": "貸与PCのバッテリー不良", "updated_at": "2026-09-19 09:00"},
    "TCK-1003": {"requester": "鈴木一郎", "category": "ソフトウェア", "status": "対応完了",
                 "summary": "業務システムのインストール申請", "updated_at": "2026-09-15 14:20"},
    "TCK-1004": {"requester": "高橋美咲", "category": "アカウント", "status": "対応中",
                 "summary": "メール送受信エラーの調査", "updated_at": "2026-09-19 11:05"},
    "TCK-1005": {"requester": "田中健太", "category": "ネットワーク", "status": "未対応",
                 "summary": "会議室の無線LANが不安定", "updated_at": "2026-09-20 08:45"},
    "TCK-1006": {"requester": "伊藤さくら", "category": "ハードウェア", "status": "対応完了",
                 "summary": "モニターの交換対応", "updated_at": "2026-09-14 16:10"},
}


@tool
def search_faq(keyword: str) -> str:
    """社内FAQをキーワードで検索し、よくある質問への回答を返す。

    VPN・パスワード・Wi-Fiなど、社内の手続きややり方に関する
    「どうすればよいか」という問い合わせに使う。

    Args:
        keyword: 検索キーワード(例: VPN, パスワード, Wi-Fi)
    """
    hits = [text for key, text in FAQ_DATA.items() if keyword in key or key in keyword]
    if hits:
        return "\n".join(hits)
    return "該当するFAQが見つかりませんでした。"


@tool
def get_ticket_status(ticket_id: str) -> str:
    """指定されたチケット番号の対応状況を確認する。

    「チケットはどうなっていますか」「対応状況を教えてください」といった、
    既に起票済みの問い合わせの進捗確認に使う。新規の質問への回答には使わない。

    Args:
        ticket_id: チケット番号(例: TCK-1001)
    """
    ticket = TICKET_DATA.get(ticket_id)
    if not ticket:
        return f"チケット番号 {ticket_id} は見つかりませんでした。"
    return f"[{ticket_id}] ステータス: {ticket['status']} / 概要: {ticket['summary']}"


@tool
def get_password_reset_guide() -> str:
    """パスワードを忘れた、またはロックされた場合のリセット手順を案内する。

    「パスワードを忘れた」「ログインできない」といった質問に使う。
    """
    return ("社内ポータルの「パスワード再設定」から手続きできます。"
            "アカウントがロックされている場合は、情報システム部での本人確認が必要です。")


class SupportAnswer(BaseModel):
    answer: str = Field(description="問い合わせへの回答本文")
    needs_escalation: bool = Field(description="情報システム部へのエスカレーションが必要か")


agent = Agent(
    model=model,
    tools=[search_faq, get_ticket_status, get_password_reset_guide],
    system_prompt="あなたは社内ITヘルプデスクの担当者です。質問内容に応じて適切なツールを使ってください。",
)


if __name__ == "__main__":
    result1 = agent(
        "TCK-1001のチケットは今どうなっていますか?",
        structured_output_model=SupportAnswer,
    )
    print(result1.structured_output.answer)

    result2 = agent(
        "パスワードを忘れてしまいました。どうすればいいですか?",
        structured_output_model=SupportAnswer,
    )
    print(result2.structured_output.answer)

    result3 = agent(
        "TCK-9999のチケットの状況を教えてください。",
        structured_output_model=SupportAnswer,
    )
    print(result3.structured_output.answer)

動作確認チェックリスト

この演習が完了すると、次のことができているはずです。

チャレンジ演習(任意)

SlidingWindowConversationManagerを追加し、window_sizeを小さい値(例: 10)に設定した場合と、大きい値(例: 100)に設定した場合とで、複数回のやり取りの後にエージェントの応答がどう変わるか比較してみましょう。

window_sizeは、小さくしすぎないでください。 window_sizeは「会話履歴に残すメッセージ数」です。ツールを使う問い合わせ1回あたり、ユーザーの質問・ツール呼び出し・ツールの結果・回答(構造化出力を使うとさらに数件)と、複数のメッセージが履歴に積まれます。1回分のやり取りより小さい値(例: 2)にすると、履歴の先頭がuserのメッセージではなくなり、A conversation must start with a user messageというValidationExceptionになることがあります。

チャレンジ演習の模範解答例を表示

まず、ファイル先頭のインポートに、次の1行を追加します(既存のfrom ... import ...の行の下に追記)。

from strands.agent.conversation_manager import SlidingWindowConversationManager

次に、TODO 3のAgentの定義を、conversation_manager付きに書き換えます。

agent = Agent(
    model=model,
    tools=[search_faq, get_ticket_status, get_password_reset_guide],
    conversation_manager=SlidingWindowConversationManager(window_size=10),
    system_prompt="あなたは社内ITヘルプデスクの担当者です。質問内容に応じて適切なツールを使ってください。",
)

最後に、TODO 4(if __name__ == "__main__":ブロック)を、複数の質問を連続で送り、最初の質問の内容を最後に尋ねるコードに書き換えます。履歴に残っているメッセージ数も表示して、window_sizeの効果を確認できるようにします。

    questions = [
        "TCK-1001のチケットは今どうなっていますか?",
        "パスワードを忘れてしまいました。どうすればいいですか?",
        "TCK-1002のチケットの状況を教えてください。",
        "VPNに接続できない場合はどうすればいいですか?",
        "TCK-1003のチケットの状況を教えてください。",
        "Wi-Fiに接続できない場合はどうすればいいですか?",
        "TCK-1004のチケットの状況を教えてください。",
        "プリンターが認識されない場合はどうすればいいですか?",
        "TCK-1005のチケットの状況を教えてください。",
        "最初にお尋ねしたチケットの番号と、その状況をもう一度教えてください。",
    ]
    for question in questions:
        result = agent(question, structured_output_model=SupportAnswer)
        print(f"Q: {question}")
        print(f"A: {result.structured_output.answer}")
        print(f"(履歴のメッセージ数: {len(agent.messages)})")
        print("-" * 40)

window_size=10で実行すると、履歴のメッセージ数が10前後で頭打ちになり、最後の質問の時点では最初のやり取りが履歴から外れています。そのため、最初のチケット(TCK-1001)ではなく、履歴に残っている直近のチケット(TCK-1005など)の状況を答えたり、「どのチケットですか?」と確認を求めたりする応答になります。忘れたことを自覚せずに、もっともらしい別の答えを返す点にも注目してください。window_size=100に変えて実行し直すと、メッセージ数が増え続け、最初のやり取りが履歴に残るため、「TCK-1001」とその状況(対応中)を答えられることを確認してください。

履歴に残るメッセージ数は、問い合わせの内容によって変わります。 ツールを呼ぶ問い合わせほど、1回あたりのメッセージ数が多くなります。質問が少ないと、window_size=10でも最初のやり取りが残ってしまうことがあるため、ここでは10回の質問を送っています。

次の演習へ

演習3では、社内ヘルプデスクに「社内システムの稼働状況確認」という、性質の異なる問い合わせが増えてきたことを受けて、1つのエージェントにツールを積み増すのではなく、役割ごとにエージェントを分割し、マルチエージェント構成(Graph)に拡張します。演習3のベースコード(code/ex3_graph/agent.py)には、この演習の完成形がすでに組み込まれています。

最終更新: 2026/10/20 1:23
Prev
演習1: 最小構成エージェント
Next
演習3: Graphによる一次切り分け