# Pythonの例：検索、クエリの並列実行、引用付きで回答

> Python Web検索APIの例：requests、セマフォ付きの非同期httpx、そしてトークン予算内で任意のLLM向けに引用付きで回答するループ。KYC不要。

- URL: https://llmwebsearch.com/ja/docs/python/
- 最終更新: 2026-10-22
- 発行者: LLMWebSearch (https://llmwebsearch.com)

3つのスクリプトで、ほとんどのPythonエージェントが必要とする内容をカバーします。`requests`による1回の検索、`httpx`とセマフォによる多数の同時検索、そしてソースを言語モデルに渡して番号付き引用付きで回答を得る短いループです。

**クイック回答 — PythonからAIエージェント向けWeb検索APIを呼び出すには？**

`requests`または`httpx`とBearerキーを指定して、検索クエリを`https://llmwebsearch.com/api/v1/search`にPOSTします。**LLMWebSearch**は上位3ページをMarkdownとして含むランク付けされた結果を返し、それを番号付けして任意のLLMに渡し、引用付きで回答させられます。検索1回のコストは1クレジット、2026年10月時点で$1.20/1K検索からです。Python 3.9以上、SDK不要。KYC不要、暗号資産で支払い。

## 必要なもの

| 属性 | 値 |
| --- | --- |
| Python | 3.9以上 |
| パッケージ | シンプルなスクリプトには`requests`、非同期のものには`httpx` |
| キー | 環境変数の`LLMWEBSEARCH_API_KEY` |
| 回答ループ用 | OpenAI互換の任意のチャットendpoint：`LLM_BASE_URL`、`LLM_API_KEY`、`LLM_MODEL` |
| 課金 | 1クレジットで検索1回分です。最大20件のランク付けされた結果を取得でき、上位3ページは本文抽出されてMarkdownで返されます。4〜10ページを読む場合は2クレジットです。エラーで終わった呼び出しは無料です |

## requestsで1回の検索を実行するには？

`q`と必要なオプションでJSONボディを構築し、それをpostし、`results`をループします。`content`に触れる前に`content_status`を確認してください。`ok`とマークされた結果のみがページ本文を含みます。以下のスクリプトは、読み取られた各ページをその位置にちなんだ名前のMarkdownファイルとして保存し、1行目にソースURLを記載します。

*search_once.py*

```python
import os
import pathlib

import requests

resp = requests.post(
    "https://llmwebsearch.com/api/v1/search",
    headers={"Authorization": "Bearer " + os.environ["LLMWEBSEARCH_API_KEY"]},
    json={"q": "how to size a postgres connection pool", "max_results": 5, "read": 3},
    timeout=60,
)
resp.raise_for_status()
data = resp.json()
out = pathlib.Path("sources")
out.mkdir(exist_ok=True)
for r in data["results"]:
    if r["content_status"] != "ok":
        print("skip", r["position"], r["content_status"], r["url"])
        continue
    path = out / f"{r['position']:02d}.md"
    path.write_text("Source: " + r["url"] + "\n\n" + r["content"], encoding="utf-8")
    print("saved", path, r["word_count"], "words")
print("credits used:", data["credits_used"])

```

## 多数の検索を一度に実行するには？

`httpx.AsyncClient`と`asyncio.Semaphore`を使用します。セマフォは同時に実行中の呼び出し数を制限し、アカウントの毎分制限内に収めて負荷を分散させます。4〜8の同時呼び出しがほとんどのリサーチエージェントに適しています。すべてのアカウントには1分あたりの呼び出し上限があり、ダッシュボードに表示されます。サポートに依頼すれば引き上げられます。

この例ではサブクエスチョンごとに1回の検索を送信し（リサーチエージェントの典型的なステップ）、読み取られたページのURLを収集します。各呼び出しは独立しており、1つの失敗が他を止めることはありません。

*many_searches.py*

```python
import asyncio
import os

import httpx

API = "https://llmwebsearch.com/api/v1/search"
HEADERS = {"Authorization": "Bearer " + os.environ["LLMWEBSEARCH_API_KEY"]}
QUESTIONS = [
    "what is connection pooling in postgres",
    "recommended pool size per cpu core",
    "pgbouncer transaction mode limitations",
    "connection pool exhaustion symptoms",
]


async def search(client, sem, q):
    async with sem:
        resp = await client.post(API, json={"q": q, "read": 2, "max_chars": 4000})
        if resp.status_code != 200:
            return q, resp.json().get("error", {}).get("code"), []
        data = resp.json()
        read = [r["url"] for r in data["results"] if r["content_status"] == "ok"]
        return q, "ok", read


async def main():
    sem = asyncio.Semaphore(4)
    async with httpx.AsyncClient(headers=HEADERS, timeout=60) as client:
        for q, status, urls in await asyncio.gather(*(search(client, sem, q) for q in QUESTIONS)):
            print(status, q, urls)


asyncio.run(main())

```

## 任意のLLMから引用付きの回答を得るには？

まず検索し、次にモデルに番号付きのソースを渡して、それらを引用するよう指示します。APIが返すのは情報源であり、書かれた回答ではありません。あなた自身のモデルがページを読み、返答を書き、URLを引用しますので、モデルと引用スタイルはあなたが選べます。以下のループは、OpenAI互換の`chat/completions`形式を扱う任意のチャットendpointで動作します。そのアドレス、キー、モデルを3つの環境変数で設定してください。

コンテキストを制御下に保ちます。スクリプトは各ソースをトリムし、合計が約6,000トークンの予算内に収まるようにします（おおよそ1トークンあたり4文字で計算）。コンテンツのない結果はスニペットとして渡され、そのようにラベル付けされるため、モデルがページを読んだふりをすることはありません。

*answer_with_citations.py*

```python
import os

import requests

SEARCH = "https://llmwebsearch.com/api/v1/search"
KEY = os.environ["LLMWEBSEARCH_API_KEY"]
BUDGET_CHARS = 6000 * 4  # about 6,000 tokens of sources


def sources_for(question):
    resp = requests.post(SEARCH, headers={"Authorization": "Bearer " + KEY},
                         json={"q": question, "max_results": 5, "read": 3}, timeout=60)
    resp.raise_for_status()
    results = resp.json()["results"]
    share = BUDGET_CHARS // max(len(results), 1)
    blocks = []
    for i, r in enumerate(results, start=1):
        if r["content_status"] == "ok":
            body = r["content"][:share]
        else:
            body = "(snippet only) " + r["snippet"]
        blocks.append(f"[{i}] {r['title']} - {r['url']}\n{body}")
    return "\n\n".join(blocks)


def answer(question):
    prompt = ("Answer the question using only the sources below. "
              "Cite them as [1], [2] and so on. Say so if the sources do not answer it.\n\n"
              + sources_for(question) + "\n\nQuestion: " + question)
    resp = requests.post(
        os.environ["LLM_BASE_URL"].rstrip("/") + "/chat/completions",
        headers={"Authorization": "Bearer " + os.environ["LLM_API_KEY"]},
        json={"model": os.environ["LLM_MODEL"], "messages": [{"role": "user", "content": prompt}]},
        timeout=120,
    )
    resp.raise_for_status()
    return resp.json()["choices"][0]["message"]["content"]


print(answer("How should I size a Postgres connection pool?"))

```

## モデルに送るテキストはどれくらいにすべきですか？

質問に答えるのに必要な最小限に抑えます。それぞれ2,000トークンにトリムした3ページは、通常、完全な10ページよりも優れています。モデルがより集中でき、各回答のコストも下がります。トリムする予定がある場合は、検索自体の`max_chars`を下げて、転送されるデータを減らしましょう。

ページの先頭からトリムすることはほとんどの記事で有効です。要点は先頭に来る傾向があるためです。長いリファレンスページでは、コンテンツを見出しで分割し、タイトルが質問に一致するセクションを保持します。

## ページが見つからない場合、コードはどうすべきですか？

他のソースで続行します。`blocked_by_robots`、`blocked_by_site_policy`、または`target_refused`とマークされたページは再試行しても読み取られないため、ループしないでください。`fetch_timeout`または`fetch_failed`のページは、後で`https://llmwebsearch.com/api/v1/extract`経由で動作する可能性があります。

HTTPエラーは別です。`429`、`502`、`504`は待機時間を増やしながら再試行する価値があります。[errors page](https://llmwebsearch.com/ja/docs/errors/)にはすぐに使える再試行ヘルパーがあります。

## Python：よくある質問

### LLMWebSearchのpipパッケージはありますか？

いいえ。APIはプレーンなHTTPSとJSONなので、必要なのは`requests`または`httpx`だけです。このページのスクリプトがそのまま連携用です。プロジェクトにコピーして調整してください。

### これらのスクリプトはローカルモデルで使えますか？

はい。OpenAI互換の`chat/completions`ルートを公開しているサーバーであれば、引用ループと組み合わせて使えます。`LLM_BASE_URL`にそのアドレスを、`LLM_MODEL`にそのサーバーが期待するモデル名を設定してください。

### Pythonでクレジットを数えるには？

各レスポンスの`credits_used`を合計するか、ジョブの前後に`GET https://llmwebsearch.com/api/v1/credits`を呼び出してください。その呼び出しは無料です。

---

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