# Node.js 示例：用 fetch 搜索、小型池和带引用的回答

> Node.js 网页搜索 API 示例：fetch、一个小型并发池，以及面向任何 OpenAI 兼容模型的带引用回答循环。无需 KYC，加密货币支付。

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

Node 18 及更新版本自带 `fetch`，所以 API 完全不需要任何包。本页展示一次搜索、一个可以并行运行多次搜索而不超出 rate limit 的池，以及一个把来源变成带编号引用回答的循环。

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

使用 Node 18+ 内置的 `fetch`，用 Bearer 密钥把你的查询 POST 到 `https://llmwebsearch.com/api/v1/search`。**LLMWebSearch** 返回排序后的结果，以及前 3 个页面的 Markdown，可直接编号并传给模型生成带引用的回答。一次搜索花费 1 积分，低至 US$1.20/千次搜索（截至 2026 年 10 月）。无需 npm 包。无需 KYC；加密货币支付。

## 你需要什么

| 属性 | 值 |
| --- | --- |
| Node.js | 18 或更新版本，以使用内置的 `fetch`；把脚本保存为 `.mjs` 以使用顶层 `await` |
| 软件包 | 无 |
| 密钥 | 在你的环境中的 `LLMWEBSEARCH_API_KEY` |
| 用于回答循环 | 一个 OpenAI 兼容的聊天 endpoint：`LLM_BASE_URL`、`LLM_API_KEY`、`LLM_MODEL` |
| 计费 | 1 积分支付一次查询：最多 20 条排名结果，其中前 3 条的页面会被读取并以 Markdown 返回。读取 4 到 10 个页面为 2 积分。以错误结束的调用不收费 |

## 如何编写可复用的搜索函数？

把调用封装一次，到处复用。下面的函数发送 body，在任何 HTTP 失败时抛出可读的错误，并返回解析后的 JSON。错误会带上 API 自己的 `code`，例如 `rate_limited` 或 `insufficient_credits`，让调用方决定如何处理。

*search.mjs*

```javascript
const API = 'https://llmwebsearch.com/api/v1/search';

export async function search(body) {
  const res = await fetch(API, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.LLMWEBSEARCH_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });
  const data = await res.json();
  if (!res.ok) {
    const err = new Error(`${res.status} ${data.error?.code}: ${data.error?.message}`);
    err.status = res.status;
    throw err;
  }
  return data;
}

const data = await search({ q: 'how to size a postgres connection pool', read: 3 });
for (const r of data.results) {
  console.log(r.position, r.content_status, r.url, r.word_count ?? '');
}

```

## 如何在不触发速率限制的情况下运行多次搜索？

通过一个小型池来运行：固定数量的 worker，每个 worker 从共享列表中取出下一个查询。四个 worker 是一个合理的起点。每个账户都有每分钟调用上限，显示在控制面板中；联系支持即可提高。

下面的池不需要任何库。每个 worker 保留自己的结果，失败的搜索会连同错误一起记录，而不会中断整个运行。这对研究型 agent 很重要，因为一个有问题的子问题不应让整个任务失败。

*pool.mjs*

```javascript
const API = 'https://llmwebsearch.com/api/v1/search';
const questions = [
  'what is connection pooling in postgres',
  'recommended pool size per cpu core',
  'pgbouncer transaction mode limitations',
  'connection pool exhaustion symptoms',
  'pool size for serverless functions',
];

async function search(q) {
  const res = await fetch(API, {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.LLMWEBSEARCH_API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ q, read: 2, max_chars: 4000 }),
  });
  const data = await res.json();
  if (!res.ok) return { q, error: data.error?.code ?? String(res.status) };
  return { q, urls: data.results.filter((r) => r.content_status === 'ok').map((r) => r.url) };
}

async function runPool(items, size, task) {
  const out = [];
  let next = 0;
  const worker = async () => {
    while (next < items.length) {
      const i = next++;
      out[i] = await task(items[i]);
    }
  };
  await Promise.all(Array.from({ length: size }, worker));
  return out;
}

for (const row of await runPool(questions, 4, search)) console.log(row);

```

## 如何把结果变成带引用的答案？

给来源编号，在每个来源旁附上 URL，按预算裁剪内容，然后让模型按编号引用。API 返回的是来源，而不是写好的答案：由你自己的模型读取页面、撰写回复并引用 URL。下面的脚本可以与任何 OpenAI 兼容的 `chat/completions` endpoint 通信，通过三个环境变量选择，因此托管模型和本地模型都能用。

约 24,000 个字符（大致 6,000 个 token）的预算在各来源之间平均分配。页面无法读取的结果会以摘要形式传入，并加以标注。

*answer.mjs*

```javascript
const SEARCH = 'https://llmwebsearch.com/api/v1/search';
const BUDGET = 24000; // characters, about 6,000 tokens

async function sources(question) {
  const res = await fetch(SEARCH, {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.LLMWEBSEARCH_API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ q: question, max_results: 5, read: 3 }),
  });
  const { results } = await res.json();
  const share = Math.floor(BUDGET / Math.max(results.length, 1));
  return results
    .map((r, i) => {
      const body = r.content_status === 'ok' ? r.content.slice(0, share) : `(snippet only) ${r.snippet}`;
      return `[${i + 1}] ${r.title} - ${r.url}\n${body}`;
    })
    .join('\n\n');
}

async function answer(question) {
  const prompt = 'Answer using only the sources below. Cite them as [1], [2] and so on. '
    + 'Say so if they do not answer the question.\n\n' + (await sources(question)) + '\n\nQuestion: ' + question;
  const res = await fetch(`${process.env.LLM_BASE_URL.replace(/\/$/, '')}/chat/completions`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.LLM_API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ model: process.env.LLM_MODEL, messages: [{ role: 'user', content: prompt }] }),
  });
  const data = await res.json();
  return data.choices[0].message.content;
}

console.log(await answer('How should I size a Postgres connection pool?'));

```

## 从 Node 调用时该用 GET 还是 POST？

在应用代码中用 POST，带 JSON body。像 `include_domains` 这样的数组和较长的查询放在 body 里比放在 query string 里更干净。GET 用于快速测试和无法发送 body 的工具；用 `URLSearchParams` 构建它的 query string，并用逗号连接列表。

两种方式的费用相同，返回的 JSON 也相同。[搜索参考](https://llmwebsearch.com/zh/docs/search/) 列出了两种方式的全部参数。

## 密钥应该放在哪里？

放在运行时读取的环境变量里，绝不要放在源文件中。在服务器上，使用你平台的 secret store。在浏览器中，根本不要调用 API：密钥会暴露给每位访客。改为调用你自己的后端，再由后端调用 LLMWebSearch。

为每个 agent、应用或团队成员各建一个 API 密钥，并可在控制面板中吊销其中任意一个，而不影响其他密钥。每个服务用一个密钥，仪表盘历史更易读，泄露的密钥也容易吊销。

## Node.js：常见问题

### 有 npm 包吗？

没有。`fetch` 已内置于 Node 18 及更新版本，API 就是通过 HTTPS 传输的纯 JSON。本页这些函数就是全部集成代码。

### 它能在 Deno、Bun 或 edge 运行时上使用吗？

代码只用到 `fetch`、`JSON` 和 `process.env`。具有 `fetch` 的运行时，把 `process.env` 换成其读取 secret 的方式后即可使用。

### 如何重试失败的调用？

只重试 `429`、`502` 和 `504`，每次暂停时间翻倍。绝不重试 `400`、`401` 或 `402`。[错误页面](https://llmwebsearch.com/zh/docs/errors/) 解释了原因，并给出了一个辅助函数。

---

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