# Ví dụ Node.js: tìm kiếm bằng fetch, một nhóm nhỏ và câu trả lời có trích dẫn

> API tìm kiếm web cho Node.js: fetch, nhóm chạy song song nhỏ và vòng lặp trả lời kèm trích dẫn cho mọi model OpenAI. Không cần KYC, thanh toán crypto.

- URL: https://llmwebsearch.com/vi/docs/node/
- Cập nhật lần cuối: 2026-10-22
- Đơn vị phát hành: LLMWebSearch (https://llmwebsearch.com)

Node 18 trở lên có sẵn `fetch`, nên API không cần package nào. Trang này trình bày một lần tìm kiếm, một nhóm chạy nhiều tìm kiếm mà không làm quá rate limit của bạn, và một vòng lặp biến các nguồn thành câu trả lời kèm trích dẫn được đánh số.

**Trả lời nhanh — Làm sao gọi API tìm kiếm web cho AI agent từ Node.js?**

Dùng `fetch` có sẵn của Node 18+ để POST truy vấn của bạn tới `https://llmwebsearch.com/api/v1/search` với khóa Bearer. **LLMWebSearch** trả lời với kết quả được xếp hạng và 3 trang đầu dưới dạng Markdown, sẵn sàng để đánh số và chuyển cho model để có câu trả lời kèm trích dẫn. Một lần tìm kiếm tốn 1 credit, từ 1,20 US$/1K lượt tìm kiếm tính đến tháng 10 năm 2026. Không cần package npm. Không cần KYC; thanh toán bằng crypto.

## Những gì bạn cần

| Thuộc tính | Giá trị |
| --- | --- |
| Node.js | 18 trở lên, để có `fetch` có sẵn; lưu các script dưới dạng `.mjs` để dùng `await` ở cấp cao nhất |
| Package | Không có |
| Key | `LLMWEBSEARCH_API_KEY` trong môi trường của bạn |
| Cho vòng lặp trả lời | Một endpoint chat tương thích OpenAI: `LLM_BASE_URL`, `LLM_API_KEY`, `LLM_MODEL` |
| Thanh toán | Một credit trả cho một lượt tìm kiếm: tối đa 20 kết quả được xếp hạng, với nội dung của 3 trang đầu được đọc và trả về dưới dạng Markdown. Đọc từ 4 đến 10 trang thì tính 2 credit. Lượt gọi kết thúc bằng lỗi không tốn gì |

## Làm sao viết một hàm tìm kiếm có thể tái sử dụng?

Bọc lệnh gọi một lần và dùng lại ở mọi nơi. Hàm dưới đây gửi body, ném ra lỗi dễ đọc khi có bất kỳ lỗi HTTP nào, và trả về JSON đã phân tích. Lỗi mang theo `code` của chính API, chẳng hạn `rate_limited` hoặc `insufficient_credits`, để bên gọi quyết định làm gì.

*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 ?? '');
}

```

## Làm cách nào để chạy nhiều truy vấn mà không chạm rate limit?

Hãy chạy chúng qua một pool nhỏ: một số lượng worker cố định, mỗi worker lấy truy vấn tiếp theo từ một danh sách chung. Bốn worker là điểm khởi đầu hợp lý. Mỗi tài khoản có một giới hạn lượt gọi mỗi phút, hiển thị trong dashboard; hãy hỏi support và giới hạn đó có thể được nâng lên.

Pool bên dưới không cần thư viện nào. Mỗi worker giữ kết quả riêng, và một truy vấn thất bại được ghi lại kèm lỗi của nó thay vì dừng cả lượt chạy. Điều đó quan trọng với các agent nghiên cứu, nơi một câu hỏi con bị lỗi không nên làm mất cả nhiệm vụ.

*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);

```

## Làm cách nào để biến kết quả thành câu trả lời có trích dẫn?

Đánh số các nguồn, đặt URL bên cạnh mỗi nguồn, cắt gọn chúng theo một ngân sách, và yêu cầu model trích dẫn theo số. API trả về các nguồn, không phải một câu trả lời được viết sẵn: model của chính bạn đọc các trang, viết câu trả lời và trích dẫn các URL. Đoạn script bên dưới gọi tới bất kỳ endpoint `chat/completions` tương thích OpenAI nào, được chọn qua ba biến môi trường, nên nó hoạt động với cả model chạy trên máy chủ lẫn model cục bộ.

Ngân sách khoảng 24.000 ký tự, tức khoảng 6.000 token, được chia đều giữa các nguồn. Kết quả mà không thể đọc được trang sẽ được truyền dưới dạng snippet của nó, có đánh dấu là như vậy.

*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?'));

```

## Tôi nên dùng GET hay POST từ Node?

POST, với body JSON, trong mã ứng dụng. Các mảng như `include_domains` và câu hỏi dài truyền gọn hơn trong body so với trong query string. GET dành cho các bài kiểm tra nhanh và cho các công cụ không thể gửi body; hãy dựng query string của nó bằng `URLSearchParams` và nối các danh sách bằng dấu phẩy.

Cả hai dạng đều tốn như nhau và trả về cùng một JSON. [Tham chiếu tìm kiếm](https://llmwebsearch.com/vi/docs/search/) liệt kê mọi parameter cho cả hai.

## Khóa nên nằm ở đâu?

Trong một biến môi trường được đọc lúc chạy, không bao giờ trong file nguồn. Trên máy chủ, hãy dùng secret store của nền tảng bạn. Trong trình duyệt, đừng gọi API: khóa sẽ hiển thị với mọi khách truy cập. Hãy gọi backend của chính bạn, rồi backend gọi LLMWebSearch.

Tạo một khóa API cho mỗi agent, ứng dụng hoặc đồng đội và thu hồi bất kỳ khóa nào trong dashboard mà không ảnh hưởng đến các khóa khác. Mỗi dịch vụ một khóa giúp lịch sử trên dashboard dễ đọc hơn, và một khóa bị lộ thì dễ thu hồi.

## Node.js: các câu hỏi thường gặp

### Có package npm không?

Không. `fetch` có sẵn trong Node 18 trở lên, và API chỉ là JSON thuần qua HTTPS. Các hàm trên trang này chính là toàn bộ phần tích hợp.

### Nó có chạy trên Deno, Bun hay các edge runtime không?

Đoạn mã chỉ dùng `fetch`, `JSON` và `process.env`. Các runtime có `fetch` sẽ chạy được sau khi thay `process.env` bằng cách đọc secret riêng của chúng.

### Làm cách nào để thử lại các lần gọi thất bại?

Chỉ thử lại `429`, `502` và `504`, với khoảng dừng tăng gấp đôi mỗi lần. Không bao giờ thử lại `400`, `401` hay `402`. [Trang lỗi](https://llmwebsearch.com/vi/docs/errors/) giải thích lý do và cho xem một helper.

---

LLMWebSearch: LLMWebSearch là một web search API cho AI agent, trả về kết quả đã xếp hạng kèm nội dung của các trang hàng đầu ở định dạng Markdown, thanh toán bằng crypto mà không cần tải lên giấy tờ tùy thân. Credit trả trước từ 1,20 US$/1K lượt tìm kiếm không bao giờ hết hạn. Không cần KYC — không ID, không selfie, không giấy tờ. Thanh toán bằng crypto.
