# Python 示例：搜索、并行运行查询、带引用回答

> Python 网页搜索 API 示例：requests、带 semaphore 的异步 httpx，以及一个在 token 预算内为任意 LLM 提供带引用回答的循环。无需 KYC。

- URL: https://llmwebsearch.com/zh/docs/python/
- 最后更新: 2026-10-22
- 发布者: LLMWebSearch (https://llmwebsearch.com)

三个脚本覆盖了大多数 Python agent 的需求：用 `requests` 执行一次搜索，用 `httpx` 和 semaphore 同时执行多次搜索，以及一个把来源交给语言模型并取回带编号引用回答的简短循环。

**快速回答 — 如何从 Python 为 AI agent 调用网页搜索 API？**

将查询 POST 到 `https://llmwebsearch.com/api/v1/search`，带上 `requests` 或 `httpx` 和 Bearer 密钥。**LLMWebSearch** 返回排序结果，其中前 3 个页面为 Markdown 格式，你可以给它们编号并传给任意 LLM 以带引用作答。每次搜索消耗 1 个积分，截至 2026 年 10 月低至 US$1.20/千次搜索。Python 3.9+，无需 SDK。无需 KYC；用加密货币支付。

## 你需要什么

| 属性 | 值 |
| --- | --- |
| Python | 3.9 或更高版本 |
| 软件包 | `requests` 用于简单脚本，`httpx` 用于异步脚本 |
| 密钥 | 在你的环境中的 `LLMWEBSEARCH_API_KEY` |
| 用于回答循环 | 任意兼容 OpenAI 的 chat endpoint：`LLM_BASE_URL`、`LLM_API_KEY`、`LLM_MODEL` |
| 计费 | 1 积分支付一次查询：最多 20 条排名结果，其中前 3 条的页面会被读取并以 Markdown 返回。读取 4 到 10 个页面为 2 积分。以错误结束的调用不收费 |

## 如何用 requests 执行一次搜索？

用 `q` 和你需要的选项构建 JSON 请求体，POST 它，然后遍历 `results`。在访问 `content` 之前先检查 `content_status`：只有标记为 `ok` 的结果才带有页面文本。下面的脚本会将每个已读取的页面保存为以其位置命名的 Markdown 文件，第一行为来源 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`。semaphore 限制同时进行的调用数，使你保持在账户的每分钟限额内并分散负载。四到八个并发调用适合大多数研究型 agent。每个账户都有每分钟调用上限，显示在控制面板中；联系支持即可提高。

该示例为每个子问题发送一次搜索（研究型 agent 的典型步骤），并收集已读取页面的 URL。每次调用相互独立：一次失败不会阻止其他调用。

*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` 格式的 chat endpoint 配合使用；在三个环境变量中设置其地址、密钥和模型。

控制好上下文。脚本会裁剪每个来源，使总量保持在约 6,000 token 的预算内，按每 token 约四个字符计算。没有内容的结果以其 snippet 传入并标注为 snippet，这样模型就不会假装读过该页面。

*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 token 的页面通常胜过十个完整页面：模型更专注，每次回答成本也更低。当你确定会裁剪时，在搜索本身中降低 `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 页面](https://llmwebsearch.com/zh/docs/errors/) 提供了现成的重试 helper。

## 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 智能体的网页搜索 API，返回排序后的搜索结果，并附带热门页面的 Markdown 格式内容，采用加密货币支付，无需上传证件。预付费积分低至 US$1.20/千次搜索，永不过期。无 KYC——无需证件、无需自拍、无需提交任何文件。加密货币支付。
