# ตัวอย่าง Node.js: ค้นหาด้วย fetch, pool เล็ก ๆ และคำตอบพร้อมการอ้างอิง

> ตัวอย่าง Node.js web search API: fetch, concurrency pool เล็ก ๆ และลูปตอบพร้อมการอ้างอิงสำหรับโมเดลที่เข้ากันได้กับ OpenAI ทุกรุ่น ไม่ต้อง KYC จ่ายด้วยคริปโต

- URL: https://llmwebsearch.com/th/docs/node/
- อัปเดตล่าสุด: 2026-10-22
- ผู้เผยแพร่: LLMWebSearch (https://llmwebsearch.com)

Node 18 ขึ้นไปมี `fetch` ในตัว ดังนั้น API ไม่ต้องใช้แพ็กเกจใดเลย หน้านี้แสดงการค้นหาหนึ่งครั้ง, pool ที่รันหลายการค้นหาโดยไม่ทำให้ rate limit ท่วม และลูปที่แปลงแหล่งข้อมูลเป็นคำตอบพร้อมการอ้างอิงแบบมีหมายเลข

**คำตอบสั้น — จะเรียก web search API สำหรับ AI agent จาก Node.js ได้อย่างไร?**

ใช้ `fetch` ที่มีในตัวของ Node 18+ ส่ง POST คำค้นหาของคุณไปยัง `https://llmwebsearch.com/api/v1/search` พร้อม Bearer key **LLMWebSearch** ตอบกลับด้วยผลลัพธ์ที่จัดอันดับแล้วและหน้าเว็บ 3 อันดับแรกในรูปแบบ Markdown พร้อมใส่หมายเลขและส่งให้โมเดลเพื่อสร้างคำตอบพร้อมการอ้างอิง การค้นหาหนึ่งครั้งใช้ 1 เครดิต เริ่มต้นที่ US$1.20/1K การค้นหา ณ ตุลาคม 2026 ไม่ต้องใช้แพ็กเกจ npm ไม่ต้อง KYC จ่ายด้วยคริปโต

## สิ่งที่คุณต้องมี

| คุณลักษณะ | ค่า |
| --- | --- |
| Node.js | 18 ขึ้นไป สำหรับ `fetch` ที่มีในตัว; บันทึกสคริปต์เป็น `.mjs` เพื่อใช้ `await` ระดับบนสุด |
| แพ็กเกจ | ไม่มี |
| Key | `LLMWEBSEARCH_API_KEY` ใน environment ของคุณ |
| สำหรับลูปตอบ | chat endpoint ที่เข้ากันได้กับ OpenAI: `LLM_BASE_URL`, `LLM_API_KEY`, `LLM_MODEL` |
| การเรียกเก็บเงิน | หนึ่งเครดิตจ่ายสำหรับการค้นหาหนึ่งครั้ง: ผลลัพธ์แบบจัดอันดับสูงสุด 20 รายการ โดยอ่านหน้าเว็บของ 3 รายการแรกและส่งกลับเป็น Markdown การอ่าน 4 ถึง 10 หน้าใช้ 2 เครดิต การเรียกที่จบลงด้วยข้อผิดพลาดไม่มีค่าใช้จ่าย |

## จะเขียนฟังก์ชันค้นหาที่ใช้ซ้ำได้อย่างไร?

ห่อการเรียกครั้งเดียวแล้วใช้ซ้ำได้ทุกที่ ฟังก์ชันด้านล่างส่ง body, โยนข้อผิดพลาดที่อ่านได้เมื่อ HTTP ล้มเหลว และคืน JSON ที่ parse แล้ว ข้อผิดพลาดมี `code` ของ API เอง เช่น `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 ?? '');
}

```

## ฉันจะรันหลายการค้นหาโดยไม่ชน rate limit ได้อย่างไร

รันผ่านพูลเล็กๆ: จำนวน worker คงที่ที่แต่ละตัวหยิบคำค้นหาถัดไปจากรายการที่แชร์กัน สี่ worker ถือเป็นจุดเริ่มต้นที่สมเหตุสมผล ทุกบัญชีมีขีดจำกัดการเรียกต่อนาที แสดงในแดชบอร์ด; แจ้งฝ่ายสนับสนุนและสามารถเพิ่มได้

พูลด้านล่างไม่ต้องใช้ไลบรารี Worker แต่ละตัวเก็บผลลัพธ์ของตัวเอง และการค้นหาที่ล้มเหลวจะถูกบันทึกพร้อม error แทนที่จะหยุดทั้งรอบ ซึ่งสำคัญสำหรับ research 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);

```

## ฉันจะแปลงผลลัพธ์เป็นคำตอบพร้อม citation ได้อย่างไร

ใส่หมายเลขให้แหล่งอ้างอิง วาง URL ไว้ข้างๆ แต่ละแหล่ง ตัดให้พอดีงบประมาณ แล้วขอให้โมเดลอ้างอิงตามหมายเลข API ส่งคืนแหล่งข้อมูล ไม่ใช่คำตอบที่เขียนไว้: โมเดลของคุณเองอ่านหน้าเว็บ เขียนคำตอบ และอ้างอิง URL สคริปต์ด้านล่างคุยกับ endpoint ที่รองรับ OpenAI ได้ทุกตัว `chat/completions` ซึ่งเลือกด้วย environment variable สามตัว จึงใช้ได้ทั้งกับโมเดลแบบ hosted และ local

งบประมาณราว 24,000 ตัวอักษร หรือประมาณ 6,000 token ถูกแบ่งเท่าๆ กันระหว่างแหล่งอ้างอิง ผลลัพธ์ที่อ่านหน้าเว็บไม่ได้จะถูกส่งเป็น snippet พร้อมระบุว่าเป็นเช่นนั้น

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

```

## ควรใช้ GET หรือ POST จาก Node?

POST พร้อม JSON body ในโค้ดแอปพลิเคชัน อาร์เรย์อย่าง `include_domains` และคำถามยาวๆ เดินทางใน body ได้สะอาดกว่าใน query string GET มีไว้สำหรับการทดสอบเร็วๆ และสำหรับเครื่องมือที่ไม่สามารถส่ง body ได้ สร้าง query string ด้วย `URLSearchParams` และเชื่อมรายการด้วยเครื่องหมายจุลภาค

ทั้งสองแบบมีค่าใช้จ่ายเท่ากันและคืน JSON เหมือนกัน [search reference](https://llmwebsearch.com/th/docs/search/) แสดงรายการทุกพารามิเตอร์สำหรับทั้งสองแบบ

## ควรเก็บ key ไว้ที่ไหน?

ใน environment variable ที่อ่านตอนรัน ไม่เคยอยู่ในไฟล์ซอร์ส บนเซิร์ฟเวอร์ ให้ใช้ secret store ของแพลตฟอร์มคุณ ในเบราว์เซอร์ อย่าเรียก API เลย: key จะมองเห็นได้โดยผู้เข้าชมทุกคน ให้เรียก backend ของคุณเอง ซึ่งจะเรียก LLMWebSearch ต่อ

สร้าง API key หนึ่งอันต่อเอเจนต์ แอป หรือเพื่อนร่วมทีม และเพิกถอนอันใดก็ได้จากแดชบอร์ดโดยไม่กระทบอันอื่น หนึ่ง key ต่อหนึ่งบริการทำให้ประวัติใน dashboard อ่านง่ายขึ้น และเพิกถอน key ที่รั่วไหลได้ง่าย

## Node.js: คำถามที่พบบ่อย

### มีแพ็กเกจ npm ไหม?

ไม่มี `fetch` มีมาในตัวใน Node 18 ขึ้นไป และ API เป็น JSON ธรรมดาผ่าน HTTPS ฟังก์ชันในหน้านี้คือการเชื่อมต่อทั้งหมด

### ใช้ได้บน Deno, Bun หรือ edge runtime ไหม?

โค้ดใช้แค่ `fetch`, `JSON` และ `process.env` รันไทม์ที่มี `fetch` ใช้ได้หลังจากแทน `process.env` ด้วยวิธีอ่าน secret ของตัวเอง

### ฉันจะ retry การเรียกที่ล้มเหลวได้อย่างไร?

Retry เฉพาะ `429`, `502` และ `504` โดยหยุดพักที่เพิ่มเป็นสองเท่าในแต่ละครั้ง อย่า retry `400`, `401` หรือ `402` เด็ดขาด [errors page](https://llmwebsearch.com/th/docs/errors/) อธิบายเหตุผลและแสดง helper

---

LLMWebSearch: LLMWebSearch คือ web search API สำหรับ AI agents ที่คืนผลการค้นหาที่จัดอันดับแล้วพร้อมเนื้อหาของหน้าอันดับต้นๆ ในรูปแบบ Markdown จ่ายด้วยคริปโตโดยไม่ต้องอัปโหลดเอกสารยืนยันตัวตน เครดิตแบบเติมล่วงหน้าตั้งแต่ US$1.20/1K การค้นหา ไม่มีวันหมดอายุ ไม่ต้อง KYC — ไม่ต้องใช้บัตรประชาชน ไม่ต้องเซลฟี่ ไม่ต้องใช้เอกสาร จ่ายด้วยคริปโต
