LiteLLMとLangGraphでLinkup・Tavily・Parallel・Exa・Firecrawlの5つのWeb検索APIを共通インターフェースで呼び分けてみる
はじめに
データ事業本部のkobayashiです。
前回の記事ではLangGraphのSend APIを使ったDeep Researchエージェントを実装しましたが、検索ツールはオフラインで動かすための擬似実装で済ませていました。今回はその検索部分を実物のWeb検索APIに置き換えます。
取り上げるのはAIエージェント/RAG用途で広く使われている Linkup・Tavily・Parallel・Exa・Firecrawl の5サービスです。SDKの設計思想がそれぞれ異なるため、LangGraphの@toolで共通インターフェースに揃え、ReActエージェントからバックエンドを差し替えられる構成にします。
5サービスの比較
各サービスの位置付けを最初に整理します。無料枠は5社ともカード登録不要で、サインアップ後すぐに利用できます。
| サービス | アプローチ | 強み | 想定用途 |
|---|---|---|---|
| Linkup | キーワード検索+深掘り抽出 | MCP/LangChain/LlamaIndexにネイティブ対応 | 一般的なRAG・エージェント |
| Tavily | LLM向けに最適化された検索+要約 | LangChain公式統合、include_answerで要約まで一発取得 |
一般的なRAG・コーディングエージェント |
| Parallel | エージェント最適化(objective駆動) | LLM向けに事前圧縮された引用付きexcerpt | Deep Research、複数クエリ並列 |
| Exa | ニューラル埋め込み | 概念的に近いコンテンツの発見、企業/人/コード検索 | セマンティック探索、類似ページ |
| Firecrawl | 検索+クロール/スクレイピング統合 | /searchに加え本文をMarkdown化、OSSも公開 |
検索結果のページ本文を丸ごとLLMに渡したい用途 |
公式Python SDKのGitHubスター数(執筆時点)を見てみるとTavilyが一番多く、次いでExa・FirecrawlとなりLinkup・Parallelは現時点ではコンパクトになっています。
| サービス | 公式Python SDK | ★スター数 |
|---|---|---|
| Linkup | LinkupPlatform/linkup-python-sdk | 41 |
| Tavily | tavily-ai/tavily-python | 1,205 |
| Parallel | parallel-web/parallel-sdk-python | 22 |
| Exa | exa-labs/exa-py | 211 |
| Firecrawl | firecrawl/firecrawl-py | 87 |
環境
今回使用した環境は以下の通りです。
Python 3.13
litellm 1.83.14
langgraph 1.1.10
langchain-litellm 0.6.4
langchain-core 1.3.2
linkup-sdk 0.13.0
tavily-python 0.7.24
parallel-web 0.5.1
exa-py 2.12.1
firecrawl-py 4.24.0
$ uv pip install litellm langgraph langchain-litellm langchain-core
$ uv pip install linkup-sdk tavily-python parallel-web exa-py firecrawl-py
$ export OPENAI_API_KEY="sk-..."
$ export LINKUP_API_KEY="..."
$ export TAVILY_API_KEY="..."
$ export PARALLEL_API_KEY="..."
$ export EXA_API_KEY="..."
$ export FIRECRAWL_API_KEY="..."
共通インターフェースの設計
3社のSDKはシグネチャがバラバラなので、LangGraphから扱える同じ@tool関数の形に揃える必要があります。すべてquery: str -> strのシグネチャに統一し、結果はLLMに渡しやすいプレーンテキスト(タイトル + URL + 要約)として返すようにします。
""" LangGraph から共通インターフェースで呼ぶ"""
from __future__ import annotations
from langchain_core.tools import tool
def _format_results(items: list[dict]) -> str:
"""検索結果をLLM向けに整形する共通フォーマッタ。"""
if not items:
return "検索結果が得られませんでした。"
lines = []
for i, item in enumerate(items, 1):
lines.append(f"[{i}] {item['title']}")
lines.append(f" URL: {item['url']}")
if item.get("snippet"):
lines.append(f" {item['snippet'][:300]}")
return "\n".join(lines)
このフォーマッタを通すことで、ReActエージェント側からは中身を意識せずバックエンドを差し替え可能になります。
Linkupツール
Linkupはdepthとoutput_typeの2軸で挙動を変えるシンプルな設計です。output_type="searchResults"を指定すると「タイトル + URL + スニペット」のリストが返るので、共通フォーマッタにそのまま流せます。
from linkup import LinkupClient
_linkup = LinkupClient() # LINKUP_API_KEY を環境変数から読み込む
@tool
def linkup_search(query: str) -> str:
"""Linkup でWeb検索を実行し、上位の結果を返します。
ニュースや一般的な情報を素早く調べたいときに使います。
"""
response = _linkup.search(
query=query,
depth="standard", # コスト重視。深掘りしたいときは "deep"
output_type="searchResults",
)
items = [
{"title": r.name, "url": r.url, "snippet": r.content}
for r in response.results
]
return _format_results(items)
公式docsによると 毎月€5まで自動補充される仕組みで、depth="standard"は €0.005/req(€5で月1,000回相当)、depth="deep"は €0.05/req(同100回相当)です。さらに重い深掘り用途は同期Searchとは別の /research エンドポイント(€0.8/req)が用意されているので、エージェントから大量に呼ぶ並列リサーチでは standard を使い、最終確認だけ deep か /research という使い分けが現実的です。
Tavilyツール
TavilyはLLM向けに最適化された検索APIで、LangChain公式統合(langchain-tavily)が用意されているのが大きな強みです。search_depthを"basic"/"advanced"、include_answer=Trueを指定すると検索結果に加えてLLMによる短い要約も同時に返ってきます。
from tavily import TavilyClient
_tavily = TavilyClient() # TAVILY_API_KEY を環境変数から読み込む
@tool
def tavily_search(query: str) -> str:
"""Tavily でWeb検索を実行し、LLM向けに整形された結果を返します。
一般的な質問応答やコーディング系のリサーチに広く使えます。
"""
response = _tavily.search(
query=query,
search_depth="basic", # コスト重視。深掘りしたいときは "advanced"
max_results=5,
)
items = [
{"title": r["title"], "url": r["url"], "snippet": r["content"]}
for r in response["results"]
]
return _format_results(items)
search_depth="basic"は1クエリ1クレジット、"advanced"は2クレジットです。月1,000クレジットの無料枠でbasicなら1,000回/advancedなら500回呼び出せます。LangChain公式サンプルや各種MCP実装でデファクト的に使われている分、エコシステムの厚さではもっとも安心感があります。
Parallelツール
Parallelは1リクエストでobjective(自然言語の調査目的)とsearch_queries(2〜3個のクエリ)を渡す独特の設計です。シンプルなReActツール化するときは、エージェントから渡された1つのqueryをobjectiveに展開して呼び出します。
from parallel import Parallel
_parallel = Parallel() # PARALLEL_API_KEY を環境変数から読み込む
@tool
def parallel_search(query: str) -> str:
"""Parallel でWeb調査を実行し、引用付きの抜粋を返します。
複数の観点から深掘りが必要な調査タスクに向いています。
"""
response = _parallel.search(
objective=f"次の質問に答えるための情報を収集する: {query}",
search_queries=[query],
)
items = [
{
"title": r.title,
"url": r.url,
"snippet": " / ".join(r.excerpts) if r.excerpts else "",
}
for r in response.results
]
return _format_results(items)
公式pricingによると 無料で20,000リクエストされ、料金は $0.004 - $0.009/req、レート制限は 600 req/min です。LangGraphのSendで並列化しないと逐次では遅いので、Deep Research型のフローと組み合わせるのが合理的です。
Exaツール
Exaはtypeに"neural"/"keyword"/"auto"を指定できる点が独自で、search_and_contentsを使えば検索とページ本文取得を1リクエストで済ませられます。
from exa_py import Exa
_exa = Exa() # EXA_API_KEY を環境変数から読み込む
@tool
def exa_search(query: str) -> str:
"""Exa でセマンティックWeb検索を実行し、本文要約を返します。
概念的に近いページを探したいときや、類似ドキュメントを集めたいときに使います。
"""
response = _exa.search_and_contents(
query,
num_results=5,
type="auto", # neural / keyword をクエリに応じて自動選択
text={"maxCharacters": 800},
)
items = [
{"title": r.title, "url": r.url, "snippet": r.text or ""}
for r in response.results
]
return _format_results(items)
2026年3月3日の価格改定で text / highlights がSearch本体料金に含まれるようになり、無料枠は 月1,000リクエスト、num_results=5 + text の構成までは追加課金なしでカバーできます。summary を使う場合のみ $1/1,000 summaries が別途かかる程度です。
Firecrawlツール
Firecrawlは元々Webスクレイピング/クロールに強みを持つサービスですが、/searchエンドポイントが追加されてから「検索+本文取得」を1リクエストで完結できるようになりました。scrape_optionsでformats=["markdown"]を渡すと、検索結果の各ページ本文をMarkdown化して返してくれます。
from firecrawl import Firecrawl
_firecrawl = Firecrawl() # FIRECRAWL_API_KEY を環境変数から読み込む
@tool
def firecrawl_search(query: str) -> str:
"""Firecrawl でWeb検索を実行し、ページ本文をMarkdown化して返します。
検索結果の本文をそのままLLMに読ませたいときに使います。
"""
response = _firecrawl.search(
query=query,
limit=5,
scrape_options={"formats": ["markdown"]},
)
items = []
for doc in response.web or []:
# scrape_options 指定時は Document 型でメタデータが metadata に格納される
meta = getattr(doc, "metadata", None)
title = (meta.title if meta else None) or getattr(doc, "title", "") or ""
url = (meta.url if meta else None) or getattr(doc, "url", "") or ""
snippet = (
getattr(doc, "markdown", None)
or (meta.description if meta else None)
or ""
)
items.append({"title": title, "url": url, "snippet": snippet[:800]})
return _format_results(items)
無料枠は月1,000クレジットで公式pricingによると/searchは10件あたり2クレジット、/scrapeは1ページ1クレジットなので、scrape_options={"formats":["markdown"]}をlimit=5で付けた場合は 検索1 + scrape 5 = 6クレジット 程度を想定しておくのが安全です。検索結果のページ本文をMarkdown化して返してくれる代わりにレイテンシが大きくなる点もトレードオフです。検索+クロール+スクレイプを1つのAPIキーで賄えるのがFirecrawlの強みで、リサーチエージェントが「検索→気になったURLを深堀り」する用途と相性が良いです。
LangGraphから共通呼び出し
5つのツールが揃ったので、LangGraphのcreate_react_agentに渡すだけでバックエンドを差し替え可能なエージェントになります。
"""LiteLLM + LangGraph + (Linkup | Tavily | Parallel | Exa | Firecrawl) のReActエージェント。
実行時引数で検索バックエンドを切り替えられる。
"""
from __future__ import annotations
import sys
from langchain_core.messages import HumanMessage
from langchain_litellm import ChatLiteLLM
from langgraph.prebuilt import create_react_agent
from search_tools import (
exa_search,
firecrawl_search,
linkup_search,
parallel_search,
tavily_search,
)
TOOL_MAP = {
"linkup": linkup_search,
"tavily": tavily_search,
"parallel": parallel_search,
"exa": exa_search,
"firecrawl": firecrawl_search,
}
SYSTEM_PROMPT = """\
あなたはWebリサーチエージェントです。
ユーザーの質問に答えるために、与えられた検索ツールを必ず1回以上呼び出してください。
検索結果のURLを引用しつつ、3〜5行で簡潔に回答してください。
"""
def build_agent(backend: str):
if backend not in TOOL_MAP:
raise ValueError(f"unknown backend: {backend}")
llm = ChatLiteLLM(model="openai/gpt-5-mini")
return create_react_agent(
llm,
tools=[TOOL_MAP[backend]],
prompt=SYSTEM_PROMPT,
)
def main():
backend = sys.argv[1] if len(sys.argv) > 1 else "linkup"
question = (
sys.argv[2]
if len(sys.argv) > 2
else "2026年に発表されたAIエージェント向けWeb検索APIの動向を教えて"
)
agent = build_agent(backend)
result = agent.invoke({"messages": [HumanMessage(content=question)]})
print(f"=== backend={backend} ===")
print(result["messages"][-1].content)
if __name__ == "__main__":
main()
バックエンドを順に切り替えて実行します。エージェント本体のコードは1行も変えずに、検索基盤だけが差し替わります。
$ python agent_runner.py linkup "Claude Code 1.0の主な機能は?"
=== backend=linkup ===
Claude Code 1.0 は端末内で動く「エージェント型」開発ツールで、自然言語でコードベースを理解・編集し、ファイル操作やコマンド実行、テスト実行・修正、Gitワークフローの自動化などを行います(CI監視や自動コミット機能も含む)。詳細は公式説明とリポジトリを参照してください: https://www.anthropic.com/product/claude-code 、https://github.com/anthropic/claude-code
$ python agent_runner.py parallel "Claude Code 1.0の主な機能は?"
=== backend=parallel ===
Claude Code 1.0は、コードベースを読み取り・編集し、コマンド実行やテスト・CI連携までこなす「エージェント型」コーディングツールです(公式概要): https://code.claude.com/docs/en/overview
ターミナル/IDE/デスクトップ/ブラウザで動作し、リアルタイム対話・ファイル編集・スキル(hooks/メモリ/サブエージェント)・自動フォーマットなど開発支援機能を備えます: https://qiita.com/...
PRレビューやヘッドレスCI実行などワークフロー自動化に強く、フックや権限でチーム運用に組み込みやすいのも特徴です: https://qiita.com/...
$ python agent_runner.py exa "Claude Code 1.0の主な機能は?"
=== backend=exa ===
Claude Code 1.0は「agentic coding」ツールで、プロジェクトを読み取ってファイル編集・コマンド実行・開発ツールと統合して自動的に開発作業を行えます。主な機能はコードベースの解析、ファイル編集とパッチ適用、ターミナルコマンド実行、IDE/デスクトップとの連携(組み込みツール群によるワークフロー自動化)です。詳しい概要や仕組みは公式ドキュメントを参照してください。
https://code.claude.com/docs/ja/overview
https://code.claude.com/docs/ja/how-claude-code-works
$ python agent_runner.py tavily "Claude Code 1.0の主な機能は?"
=== backend=tavily ===
Claude Code 1.0は「ターミナル特化のAIコーディングアシスタント」で、リポジトリ全体の理解をもとに機能実装、バグ修正、テスト実行、コードレビューや開発タスクの自動化を支援します(GitHub連携やカスタム/スラッシュコマンドも利用可能): https://code.claude.com/docs/ja/overview
導入・使い方や実例まとめはコミュニティ記事が参考になります(概要や操作感の紹介): https://sogyotecho.jp/claude-code/ 、https://zenn.dev/kg_motors_mibot/articles/f46c6927c409fc
$ python agent_runner.py firecrawl "Claude Code 1.0の主な機能は?"
=== backend=firecrawl ===
Claude Code 1.0はターミナル上で動くエージェント型コーディングツールで、コードベースを理解して自然言語で指示してファイル編集、テスト実行、リファクタリング、コード生成や説明、そしてGitワークフロー(コミット/PR作成など)を自動化します(公式概要): https://code.claude.com/docs/en/overview
またオープンソースのリポジトリで導入手順や機能詳細(CLI操作・実行権限の扱い等)を確認できます: https://github.com/anthropics/claude-code
5つのバックエンドの傾向の違いが実機で確認できます。
- Linkup: 簡潔な1段落 + URL 2件。Anthropic公式ページとGitHubリポジトリにヒット
- Parallel: 引用付きで構造化された3段落。Qiita等の解説記事も含めて多角的に拾う
- Exa: セマンティック検索の効きで
code.claude.comの 日本語ドキュメント がヒット(言語圏を意識した結果が返る) - Tavily: 公式日本語ドキュメント+日本語コミュニティ記事を簡潔にまとめる。出力長が短めで応答整形が速い
- Firecrawl: 公式概要+GitHubリポジトリにヒット。
scrape_optionsでMarkdown化までしている分、レイテンシは大きい
TOOL_MAPに新しいバックエンドを追加するだけで他社(Brave、Perplexity Sonar、Serperなど)にも拡張できます。@tool関数のシグネチャをquery: str -> strに揃えておけば、エージェント側のロジックには一切手を入れる必要がなくなります。
実行時間の比較
「どの検索APIが速いか」は実用上重要なポイントなので、5パターンで実測しました。同じクエリ("Claude Code 1.0の主な機能は?")に対して以下の3層で時間を測ります。
- 検索API単独: ツールを直接
.invoke()した素の応答時間(LLMなし) - agent.invoke 全体: LLM 1回目(tool_call決定)+ 検索 + LLM 2回目(応答整形)の合計
- real time: Python起動 + ライブラリインポート + agent.invoke の実時間
"""5つの検索バックエンドの単独応答時間を計測するベンチマーク。"""
from __future__ import annotations
import sys
import time
from search_tools import (
exa_search,
firecrawl_search,
linkup_search,
parallel_search,
tavily_search,
)
DEFAULT_QUERY = "Claude Code 1.0の主な機能は?"
BACKENDS = [
("linkup", linkup_search),
("tavily", tavily_search),
("parallel", parallel_search),
("exa", exa_search),
("firecrawl", firecrawl_search),
]
def main() -> None:
query = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_QUERY
print(f"query: {query}")
print("検索API単独の応答時間:")
for name, tool in BACKENDS:
t0 = time.perf_counter()
result = tool.invoke({"query": query})
elapsed = time.perf_counter() - t0
print(f" {name:10s}: {elapsed:6.2f}s ({len(result):>5} chars)")
if __name__ == "__main__":
main()
計測結果
検索API単独の応答時間:
linkup : 2.85s (7686 chars)
tavily : 1.68s (1375 chars)
parallel : 3.59s (3453 chars)
exa : 0.86s (2052 chars)
firecrawl : 6.85s (1891 chars)
| バックエンド | 検索API単独 | agent.invoke全体 | real(Python起動込) |
|---|---|---|---|
| Exa | 0.86s | 14.50s | 16.75s |
| Tavily | 1.68s | 12.45s | 15.14s |
| Linkup | 2.85s | 23.18s | 26.12s |
| Parallel | 3.59s | 15.38s | 17.63s |
| Firecrawl | 6.85s | 50.16s | 53.44s |
この計測から読み取れる傾向
1. 検索API単独ではExaが圧倒的に速い
Exaは0.86秒で応答し、他社の2〜8倍の速度です。セマンティック検索のインデックスが事前計算されているため、リクエスト時の処理が軽いと推測できます。次点はTavily(1.68秒)で、LLM向けに最小限の情報量に整形されているぶん安定して速い印象です。
2. エージェント全体ではTavilyが最速、Firecrawlが圧倒的に遅い
agent.invoke の全体時間で見ると Tavily(12.45秒) が最速で、Exa(14.50秒)よりわずかに速い結果になりました。Tavilyは検索結果が短く整形されているため、LLMの2回目呼出(応答整形)が軽いことが効いています。一方 Firecrawl(50.16秒) は突出して遅く、これは scrape_options={"formats":["markdown"]} を指定して検索結果5件すべての本文をMarkdown化しているためです。「検索→ヒットページの本文取得」を1リクエストで済ませている分の対価として時間がかかります。本文が不要なら scrape_options を外すと大幅に速くなります。
3. Linkupは検索は速いがエージェント全体では遅い
検索単独では2.85秒のLinkupが、エージェント全体では23秒と長くなります。これは Linkupが返すスニペットの量が多い(7,686 chars)ため、それを受け取るLLMの2回目呼出(応答整形)に時間がかかるためです。Linkupのoutput_type="searchResults"は情報量重視で結果を返すので、LLM側で要約コストが上乗せされます。
4. Parallel は1リクエスト3〜4秒、レート制限の余裕も大きい
Parallelは「objective駆動」「LLM向けに事前圧縮されたexcerptを返す」という設計思想からエージェント特化APIの中ではリッチな処理をしている印象がありましたが、今回の計測では3.59秒で安定して返ってきました。公式ドキュメントには明確なレイテンシ目安は記載されていない一方、レート制限が 600 req/min と非常に余裕があるため、asyncio.gatherやSendで並列化したときに律速になりにくいのが利点です。
結論
- レイテンシ重視: Tavily / Exa(agent全体ではTavilyが最速、検索単独ではExaが最速)
- 情報量重視: Linkup(スニペットが充実、ただしLLM処理時間が伸びる)
- 多角的調査: Parallel(引用付きexcerptは深掘りに向く、ただし速度のばらつきあり)
- 検索+本文取得を1発で: Firecrawl(
scrape_optionsでMarkdown化まで一括。トレードオフでレイテンシは大きい)
@toolの共通インターフェースで束ねているおかげで、こうした計測もバックエンドを切り替えるだけで均等条件で実行できます。本番投入前にこの3層計測を回し、SLA要件と照らし合わせて選定するのがおすすめです。
Deep Researchへの組み込み
前回のDeep Research記事で実装したfake_searchを、上で作ったツールにそのまま差し替えられます。
# Before:
# from fake_search import search
# After: 任意のバックエンドに切替可能
from search_tools import linkup_search as search # or tavily_search / parallel_search / exa_search / firecrawl_search
def researcher_node(state):
subtopic = state["subtopic"]
# search.invoke({"query": subtopic}) のシグネチャはそのまま
search_result = search.invoke({"query": subtopic})
...
@toolデコレータ経由なら.invoke({"query": ...})の呼び出し規約が共通なので、researcher_nodeに1行差し替えるだけで実Web検索版に変更できます。
Deep ResearchのSend並列パターンと組み合わせると、1リクエストでresearcherがN回起動します。
まとめ
LiteLLM + LangGraphの共通コードからLinkup・Tavily・Parallel・Exa・Firecrawlの5つの検索APIを呼び分ける方法を紹介しました。
@toolデコレータでquery: str -> strのシグネチャに揃えておけば、ReActエージェントの本体には一切手を入れずバックエンドだけ差し替えられます。Deep Research記事の擬似検索ツールも1行で実Web検索版に格上げでき、無料枠もカード登録不要でPoC範囲なら5社いずれも完結する手軽さです。用途と特性に合わせて使い分けるのがおすすめです。
最後まで読んでいただきありがとうございました。








