クイック回答
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を記載します。
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つの失敗が他を止めることはありません。
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文字で計算)。コンテンツのない結果はスニペットとして渡され、そのようにラベル付けされるため、モデルがページを読んだふりをすることはありません。
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にはすぐに使える再試行ヘルパーがあります。
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を呼び出してください。その呼び出しは無料です。