# Web検索APIでPythonのディープリサーチエージェントを構築する

> 約60行のPythonでディープリサーチエージェントを構築：検索クエリを計画し、ページ本文込みで検索し、重複ソースを除外し、任意のLLMで引用付きの回答を書きます。

- URL: https://llmwebsearch.com/ja/guides/build-a-deep-research-agent/
- 最終更新: 2026-10-22
- 発行者: LLMWebSearch (https://llmwebsearch.com)

ディープリサーチはモデルではなくループです。このガイドでは数分で読めるものを構築します：検索クエリを計画し、ページ本文込みでWebを検索し、各ソースを1回だけ保持し、不足を探し、番号付き引用付きの回答を書きます。

**クイック回答 — Pythonでディープリサーチエージェントを構築するには？**

LLMに検索クエリを尋ね、それぞれをページ本文を返すWeb検索APIで実行し、各ソースを1回だけ保持し、追加の検索クエリを求め、最後に引用付きの回答を書くループを組んでください。**LLMWebSearch**では、各検索が最初の3ページをMarkdownで返し、1クレジットです。以下のスクリプトは1つの質問につき6回検索します。$1.20/1K検索から、KYC不要、暗号資産で支払い。

## エージェントは何をしますか？

1. **プラン** — モデルに、質問を合わせてカバーする4つの短い検索クエリを尋ねます。
2. **検索** — 各検索クエリをread=3で実行し、最初の3ページをMarkdownで取得します。
3. **ソースを1回だけ保持する** — 結果をURLで保存し、すでに収集したページや本文のないページはスキップします。
4. **不足を探す** — これまでに見つかったタイトルをモデルに示し、不足している内容について2つの検索クエリを尋ねます。
5. **書く** — 最大12件のソースに番号を付け、[1]、[2]のように引用する回答を求めます。

## 始める前に何が必要ですか？

3つあります：

- **LLMWebSearchのAPIキー**を環境変数`LLMWEBSEARCH_API_KEY`に設定。APIキーは最初のパック購入後にダッシュボードに表示されます。
- **OpenAI互換のチャットendpoint**をどれでも。`LLM_BASE_URL`、`LLM_API_KEY`、`LLM_MODEL`で設定します。ホスト型モデルもローカルサーバーも使えます。
- **Python 3.9以降**と`requests`パッケージ。

スクリプトは両方のサービスにプレーンなHTTPを使うので、検索コードを変えずにモデルを差し替えられます。

*Python：deep_research.py*

```python
import os

import requests

SEARCH = "https://llmwebsearch.com/api/v1/search"
SEARCH_HEADERS = {"Authorization": "Bearer " + os.environ["LLMWEBSEARCH_API_KEY"]}
# Any OpenAI-compatible chat endpoint works here: set your own base URL, key and model name.
LLM_URL = os.environ.get("LLM_BASE_URL", "https://llm.example.com/v1") + "/chat/completions"
LLM_HEADERS = {"Authorization": "Bearer " + os.environ.get("LLM_API_KEY", "")}
MODEL = os.environ.get("LLM_MODEL", "your-model-name")


def ask_llm(prompt):
    body = {"model": MODEL, "messages": [{"role": "user", "content": prompt}]}
    resp = requests.post(LLM_URL, headers=LLM_HEADERS, json=body, timeout=120)
    resp.raise_for_status()
    return resp.json()["choices"][0]["message"]["content"]


def plan_queries(question, n=4):
    text = ask_llm(f"Write {n} short web search queries, one per line, to answer: {question}")
    queries = [line.strip("-*0123456789. ").strip() for line in text.splitlines()]
    return [q for q in queries if q][:n]


def search(query):
    body = {"q": query, "max_results": 5, "read": 3, "max_chars": 5000}
    resp = requests.post(SEARCH, headers=SEARCH_HEADERS, json=body, timeout=60)
    resp.raise_for_status()
    return resp.json()["results"]


def research(question, rounds=2):
    sources = {}  # url -> result, so the same page is never cited twice
    queries = plan_queries(question)
    for _ in range(rounds):
        for query in queries:
            for r in search(query):
                if r.get("content_status") == "ok" and r["url"] not in sources:
                    sources[r["url"]] = r
        notes = "\n".join(f"- {r['title']}" for r in sources.values())
        queries = plan_queries(f"{question}\nAlready found:\n{notes}\nWhat is still missing?", n=2)
    numbered = list(sources.values())[:12]
    context = "\n\n".join(
        f"[{i}] {r['title']} ({r['url']})\n{r['content']}" for i, r in enumerate(numbered, 1)
    )
    prompt = (
        "Answer the question using only the sources below. Cite them as [1], [2]. "
        "Say so when the sources disagree or do not cover a point.\n\n"
        f"Question: {question}\n\nSources:\n{context}"
    )
    return ask_llm(prompt), [r["url"] for r in numbered]


if __name__ == "__main__":
    answer, urls = research("What changed in the EU rules for general-purpose AI models in 2025?")
    print(answer)
    for i, url in enumerate(urls, 1):
        print(f"[{i}] {url}")
```

## 各部分はどう動きますか？

`plan_queries`はモデルに短い検索クエリを1行ずつ尋ね、リストの記号を取り除きます。短い検索クエリは、完全な質問よりも検索エンジンがページをランク付けする方法に合っています。

`search`は`POST https://llmwebsearch.com/api/v1/search`を`max_results=5`、`read=3`、`max_chars=5000`付きで呼び出します。レスポンスには5件の結果が並び、最初の3件は`content`にページ本文が含まれます。

`research`はラウンドを実行します。ソースはURLをキーとする辞書に保持するので、2つの検索クエリで見つかったページも1回だけ数えます。`content_status`が`ok`に等しい結果だけを保持します。回答はモデルが読める本文に基づく必要があるからです。

各ラウンドの後、モデルはこれまでに集めたタイトルを見て、不足している内容について2つの新しい検索クエリを提案します。最終プロンプトでは最大12件のソースに番号を付け、引用のルールを設定します。

## 1件のレポートのコストはいくらですか？

デフォルト設定で1つの質問につき6回検索します：1ラウンド目に4回、2ラウンド目に2回。`read`が3なら、6クレジットです。したがって25,000検索を$41.25で購入すると、4,000件以上のレポートをカバーできます。

モデルの呼び出しはLLMの請求に別途かかります：1レポートにつき計画呼び出し3回と長い回答呼び出し1回です。回答呼び出しは5,000文字のページを最大12件扱うので、モデルが高価なら`max_chars`を小さくしてください。

## どうすればより深く調査できますか？

数字を変え、構造は変えないでください：

- **ラウンド数を増やす。** `rounds=4` でモデルはさらに2組のギャップを追えます。
- **1ラウンドあたりの検索クエリを増やす。** ギャップ検出の呼び出しで `n` を2から4に上げます。
- **ソースを絞り込む。** 検索ボディに `include_domains` を追加して公式サイトや学術サイトに限定します。
- **最近の資料のみ。** `"time_range": "month"` または `"topic": "news"` を追加します。
- **1検索クエリあたりのページ数を増やす。** `read=5` は5ページを読み取り、検索1回につき2クレジットです。

上限を上げる前に停止ルールを追加してください：1ラウンドで新しいソースが2件未満しか得られなかったらループを終了します。

## エージェントを安全かつ礼儀正しく保つには？

すべてのページを信頼できないテキストとして扱ってください。最終プロンプトでモデルに、ソースは証拠としてのみ使うよう指示し、ソース内の指示は無視しなければならないという一文を明示的に加えてください。ソースがツール呼び出しやファイル書き込みを引き起こすことは絶対に許さないでください。

Web側では、当社のリーダーはrobots.txtを尊重します。LLMWebSearchとして名乗り、サイトが自身またはすべてのロボット向けに定めたルールに従い、読み取りを許可されていないページのコンテンツは除外します。アクセスを拒否するページはコンテンツなしで返り、スクリプトは単にそれをスキップします。サイトが当社のリーダーを拒否した場合、結果はコンテンツなしで、サイトのステータスとともに返されます。当社がCAPTCHAを解いたり、アカウントにサインインしたり、ブロックやペイウォールを回避したりすることは一切ありません。

## 何がうまくいかない可能性がありますか？

テストで3つの問題が現れます：

- **曖昧な検索クエリ。** 計画が弱いと、毎ラウンド同じページが見つかります。モデルに具体的な名前や日付を使った検索クエリを求めてください。
- **テキスト付きのソースが少なすぎる。** 一部のトピックはペイウォールの向こう側にあります。検索クエリを広げるか、結果数の上限を増やしてください。
- **プロンプトが長すぎる。** 長いページ12件は小型モデルのコンテキストを超えることがあります。`max_chars` を下げるか、ソースを8件に保ってください。

各検索クエリとそれが返したURLをログに記録してください。そのログを読むのが、計画プロンプトを改善する最速の方法です。

## ディープリサーチエージェント：よくある質問

### ローカルモデルで実行できますか？

はい、ローカルサーバーがOpenAIのchat-completions形式に対応していれば可能です。`LLM_BASE_URL` をそのアドレスに、`LLM_MODEL` をモデル名に設定してください。

### MCPツールにできますか？

はい。[MCPページ](https://llmwebsearch.com/ja/docs/mcp/) にあるプレーンな検索ツールを `research()` でラップするか、エージェントにそのツールを渡してループ自体を実行させてください。

### なぜ検索APIに直接答えを尋ねないのですか？

LLMWebSearch は回答を書かないからです。APIが返すのは情報源であり、書かれた回答ではありません。あなた自身のモデルがページを読み、返答を書き、URLを引用します。

### 各検索のコストはどうやって確認できますか？

すべてのレスポンスに `credits_used` が含まれ、`GET https://llmwebsearch.com/api/v1/credits` は無料で残りの残高を表示します。

---

LLMWebSearch: LLMWebSearchは、AIエージェント向けのWeb検索APIです。ランキング済みの検索結果を、上位ページの本文をMarkdownに変換した内容とともに返し、暗号資産で支払えます。本人確認書類のアップロードは不要です。プリペイドのクレジットは$1.20/1K検索から購入でき、有効期限はありません。KYC不要 — 身分証も自撮りも書類も不要。暗号資産で支払い。
