🔎

Strands AgentsのToolに"Tavily"を活用して高度なWEB検索を行う

に公開

はじめに

こんにちはFusicの大宮です🫡🙇💪
前回、「Googleのカスタム検索エンジンを使用してWEB検索を行う」という内容で記事を書きました。
今回は StrandsAgentsにWEB検索を行わせるための手段として"Tavily"を活用してみました。
結論として、GoogleCSMよりもLLMとの相性が良く、高機能で使い勝手が良かったです。
結論に行き着いた調査・検証結果をまとめました。

Tavilyとは

  • AIエージェント向けの安全でモジュール化されたWebインテリジェンス

Tavilyは、LLMエージェント向けに特別に設計されたAPIファーストのWebインテリジェンスレイヤーで、リアルタイム検索、高忠実度コンテンツ抽出、構造化Webクロールを実現します。AIベースシステムを構築する開発者向けに構築されたTavilyは、精度、速度、モジュール性を重視して設計されています。Strands Agentsなどのエージェントフレームワークとのシームレスな統合エクスペリエンスを提供します。TavilyのAPIは、大手AI企業から信頼されているエンタープライズグレードのインフラストラクチャレイヤーです。堅牢な機能と、本番環境レベルの運用保証を兼ね備えています。
引用:https://aws.amazon.com/jp/blogs/machine-learning/build-dynamic-web-research-agents-with-the-strands-agents-sdk-and-tavily/

StrandsのToolにTavilyを採用する理由

上に引用した内容がそのままですが、下記のような内容が挙げられます。

  • LLMエージェント向けに特別に設計されたAPIファーストのWebレイヤーである点。
  • 多様な機能(リアルタイム検索、高忠実度コンテンツ抽出、構造化Webクロールを実現)
  • AIシステムに適合した設計(精度、速度、モジュール性を重視して設計)

※正直、単純にAgentに安定したWEB検索をさせるための外部APIが思いつかないから汗
※最近になってやっとTavilyを知りました。

AWSはTavilyを採用する理由について以下のように記述してます。

  • 共通のビジョン– Tavily と AWS はどちらも、エンタープライズ対応、セキュリティ、プライバシーを重視し、次世代の AI ベースのビルダーにサービスを提供しています。
  • マーケットプレイス統合– Tavily はAWS マーケットプレイスで利用可能であり、エンタープライズ顧客にとってシームレスな統合と調達を実現します。
  • ウェブアクセスの頼れるパートナー– AWS は、Strands Agents SDK 内でリアルタイム検索を統合するための最高のツールとして Tavily を選択し、エージェント開発者に最高のウェブアクセス エクスペリエンスを提供しました。
  • Amazon Bedrock – Amazon Bedrock は、Meta、Anthropic、AI21、Amazon などの大手 AI 企業が提供する高性能 FM の選択肢を提供する、完全に管理された安全なサービスです。

実装

概要を理解したところで、実装に移っていきましょう。
空のStrands Agentsに対してTavilyを使用したToolを定義していきます。

シンプルなエージェントを構築

Strands Agentsのインストール

pip install strands-agents

さっそくAgent開発

# agent.py
from strands import Agent, tool
agent = Agent(tools=[]) # toolsはこのあと追加
input = input('質問を入力:')
agent(input)

⚠️注意点⚠️
aws configureでアカウント情報を設定
・Bedrockのモデルアクセスから,"claude 4 sonnet"をリクエストしたアカウント&権限があることを確認
・リクエストしたリージョンと同一であることを確認

即実行💨

python agent.py
$python3 agent.py
質問を入力:こんにちは
こんにちは!お手伝いできることがありましたら、お気軽にお尋ねください。インターネット検索や特定の情報を調べたいことなど、何でもお聞きください。

これで基盤ができました。
次にTavilyの環境変数を設定します。

環境変数の設定

Tavily api keyの取得
公式サイトにログインして作成します。

環境変数に設定
.envファイルを作成し、取得したAPIキーを設定

TAVILY_API_KEY=<取得したAPIキー>

Toolの作成

やっと本題です!
@tool イテレーションを使用して独自にToolを作成し、Tavily APIにアクセスすることでWEB検索を実行します。

agent部分とTool部分を分けたいので、agent.pyと同じディレクトリにtools/ ディレクトリを作成し、web_search.py を作成しましょう。

コード全体

まずはコード全体を示して、その後に各部分の説明を行います。

# tools/web_search.py
import os
import logging
import threading
from typing import List, Dict, Any
from tavily import TavilyClient
from strands import tool

logger = logging.getLogger(__name__)
ALLOWED_TIME_RANGES = {"d", "w", "m", "y"}

_client: TavilyClient | None = None
_client_lock = threading.Lock()

def init() -> None:
    _ensure_client()

def _ensure_client() -> TavilyClient:
    global _client
    if _client is not None:
        return _client
    with _client_lock:
        if _client is None:
            api_key = os.getenv("TAVILY_API_KEY")
            if not api_key:
                raise RuntimeError("環境変数 TAVILY_API_KEY が設定されていません")
            _client = TavilyClient(api_key=api_key)
            logger.info("TavilyClient initialized")
    return _client

def _format_results(resp: Dict[str, Any]) -> str:
    results = resp.get("results", [])
    if not results:
        return "検索結果はありません。"
    lines: List[str] = []
    for i, r in enumerate(results, 1):
        title = r.get("title") or "(no title)"
        url = r.get("url") or ""
        content = (r.get("content") or "").strip()
        if len(content) > 300:
            content = content[:300] + "..."
        lines.append(f"[{i}] {title}\nURL: {url}\n{content}\n")
    return "\n".join(lines).rstrip()

@tool
def web_search(
    query: str,
    time_range: str | None = None,
    include_domains: list[str] | None = None,
):
    """Tavily を用いたウェブ検索ツール。

    Args:
        query: 検索クエリ
        time_range: 'd','w','m','y' のいずれかで期間フィルタ
        include_domains: 絞り込みたいドメインのリスト
    Returns:
        Strands Agent 互換の辞書 (status/content)
    """
    if not query or not query.strip():
        return {"status": "error", "content": [{"text": "query は必須です"}]}
    if time_range and time_range not in ALLOWED_TIME_RANGES:
        return {"status": "error", "content": [{"text": f"time_range は {ALLOWED_TIME_RANGES} のいずれか"}]}

    try:
        client = _ensure_client()
        resp = client.search(
            query=query,
            max_results=10,
            time_range=time_range,
            include_domains=include_domains,
        )
        formatted = _format_results(resp)
        return {"status": "success", "content": [{"text": formatted}, {"json": resp}]}
    except Exception as e:
        return {"status": "error", "content": [{"text": f"検索失敗: {type(e).__name__}: {e}"}]}

各コード解説

Tavily Clientの確保(準備)

  • 明示的な初期化処理(agent.pyから呼び出す)
  • 取得したキーを設定、存在しなければエラーを返す。
def init() -> None:
    _ensure_client()

def _ensure_client() -> TavilyClient:
    global _client
    if _client is not None:
        return _client
    with _client_lock:
        if _client is None:
            api_key = os.getenv("TAVILY_API_KEY")
            if not api_key:
                raise RuntimeError("環境変数 TAVILY_API_KEY が設定されていません")
            _client = TavilyClient(api_key=api_key)
            logger.info("TavilyClient initialized")
    return _client

出力のフォーマッターを作成(必須でない)

  • レスポンスを整形
def _format_results(resp: Dict[str, Any]) -> str:
    results = resp.get("results", [])
    if not results:
        return "検索結果はありません。"
    lines: List[str] = []
    for i, r in enumerate(results, 1):
        title   = r.get("title") or "(no title)"
        url     = r.get("url") or ""
        content = (r.get("content") or "").strip()
        if len(content) > 300:
            content = content[:300] + "..."
        lines.append(f"[{i}] {title}\nURL: {url}\n{content}\n")
    return "\n".join(lines).rstrip()

Tool自体の作成

  • _ensure_client()でClient確保
  • クエリ、期間フィルタ、ドメインを引数にとる(Agentが判断)
resp = client.search(
    query=query,
    max_results=10,
    time_range=time_range,
    include_domains=include_domains,
)
  • ツールをいつ使用すべきか、その機能を明確に説明。
  • 関数シグネチャで型ヒントを使用して、パラメータ、戻り値の型、およびデフォルト値を記述。
  • 各パラメータの詳細と、受け入れられる形式の例を示す。

引用:https://aws.amazon.com/jp/blogs/machine-learning/build-dynamic-web-research-agents-with-the-strands-agents-sdk-and-tavily/

@tool
def web_search(
    query: str,
    time_range: str | None = None,
    include_domains: list[str] | None = None,
):
    """Tavily を用いたウェブ検索ツール。

    Args:
        query: 検索クエリ
        time_range: 'd','w','m','y' のいずれかで期間フィルタ
        include_domains: 絞り込みたいドメインのリスト
    Returns:
        Strands Agent 互換の辞書 (status/content)
    """
    if not query or not query.strip():
        return {"status": "error", "content": [{"text": "query は必須です"}]}
    if time_range and time_range not in ALLOWED_TIME_RANGES:
        return {"status": "error", "content": [{"text": f"time_range は {ALLOWED_TIME_RANGES} のいずれか"}]}

    try:
        client = _ensure_client()
        resp = client.search(
            query=query,
            max_results=10, # 環境変数,定数で管理しましょう笑
            time_range=time_range,
            include_domains=include_domains,
        )
        formatted = _format_results(resp)
        return {"status": "success", "content": [{"text": formatted}, {"json": resp}]}
    except Exception as e:
        return {"status": "error", "content": [{"text": f"検索失敗: {type(e).__name__}: {e}"}]}

Agent部分の更新

  • 上で作成したweb_searchツールと初期化関数をImport
  • Agent() にImportしたToolを与える。(Agentは"WEB検索"のアクションが実行可能になる)
# agent.py
from strands import Agent
from dotenv import load_dotenv
load_dotenv()
from tools.web_search import web_search, init as init_web_search

init_web_search()

agent = Agent(tools=[web_search])
user_input = input('質問を入力:')
agent(user_input)

検証

GoogleCSEを使用したAgentとTavilyを使用したAgentで同じ質問に答えてみる。
質問1

最近のFusicの生成AIに関連する開発事例について3つ教えてください。

質問2

FusicのAWS関連の開発事例を4つ教えてください。

システムプロンプト
Tool名以外は同一にする。

あなたはWEB検索アシスタントです。[web_search / get_fusic_solutions] Toolを使用して検索結果を150文字以内に要約して質問に答えてください。

1. GoogleCSE(開発事例を検索するToolのみ与えたStrandsAgents)

回答1出力

時間:29秒

回答2出力

時間:18秒

===

2. Tavilyを使用した今回のAgent

回答1出力

時間:15秒

回答2出力

時間:13秒

結果

  • 応答速度:Tavily側Agentの方が早い。
  • 再現性:CSEが高い。
  • 内容:どちらも間違った情報は出力していない。
  • 出力結果が異なるのは、CSE側にはURL制限がかかっているためだと推測

まとめ

今回の調査・検証では、Strands AgentsにWeb検索機能を統合する手段としてTavilyが非常に有効であることが明らかになりました。

Tavilyを採用するメリット

LLMとの高い親和性: TavilyはAIエージェント向けに特別に設計されており、LLMが検索結果をより効果的に活用できるよう、精度、速度、モジュール性を重視しています。

豊富な機能と使いやすさ: リアルタイム検索、コンテンツ抽出、Webクロールといった多様な機能を提供し、シンプルなAPIを通じてエージェントフレームワークにシームレスに統合できます。

パフォーマンスの優位性: 応答速度の比較検証では、Tavilyを使用したエージェントがGoogleカスタム検索エンジン(CSE)を使用したエージェントよりも高速な応答を返す結果となり、LLMエージェントのパフォーマンス向上に寄与することが示されました。

今後

https://github.com/strands-agents/tools

  • Tavily が Strands Agentsの組み込みToolとして実行可能になりそうです。
  • 現状,実装例はREADMEにありますが、Module Not Found になってしまう状態でした。
    数行で高度なWEB検索を行うAgent構築が可能になる!楽しみです☺️

おわりに

長々した記事になりましたが、丁寧に実装について説明できました。
様々な検索手段を自律的に使い分ける検索ゴテゴテAgentを作ってみようとワクワクしているのですが、
WEB検索枠は"Tavily"一択だと思いました!
今後の組み込みToolも楽しみですね。

では!

Fusic 技術ブログ

Discussion