ลอง สร้าง Knowledge Based Chatbot แบบ Full offline บนเว็บ ด้วย On-device AI และ Chrome Built-in AI

TLDR
– สร้างแชทบอทที่ทำงาน 100% บนเบราว์เซอร์ของผู้ใช้ ไม่ต้องมีเซิร์ฟเวอร์
-ใช้ Transformers.js เพื่อรันโมเดล AI สำหรับแปลงข้อความเป็น Vector (Embeddings) ในฝั่ง Client
– ข้อมูลผู้ใช้เป็นส่วนตัวทั้งหมดเพราะไม่ได้ส่งไปไหน และตอบสนองได้ทันทีเพราะไม่มี Latency ของเน็ตเวิร์ค
-ใช้เทคนิคคล้าย RAG (Retrieval-Augmented Generation) โดยดึงข้อมูลจาก Knowledge Base ในเครื่องมาช่วยให้ AI ตอบได้แม่นยำขึ้น

ช่วงนี้ ผมอินกับ On-Device / AI Built In มาก งาน Google IO Extended ผมก็พูดเรื่องนี้ คราวนี้ก็เลยมาคิดว่า ปกติแล้วพวก AI ที่เป็น Server เนี่ยเค้าทำอะไรได้บ้างนะ แล้วมันจะเอามาทำบน Browser แทนได้มั้ยนะ ก็เลยคิดว่า เออมันมี chatbot นี่..ที่น่าจะใช้แค่ embeding model แล้วก็ Prompt API ก็น่าจะจบเลยนี่ ก็เลยลองเขียน code ออกมาเป็นแบบ prototype ดูว่าถ้าเราทำ chatbot แบบ LLM Based มันจะทำได้มั้ย แล้วถ้าจะทำต้องทำยังไง

AI ส่วนตัวที่ไม่ต้องง้อเซิร์ฟเวอร์

ก่อนอื่นเลยหลายๆคนอาจจะสงสัยว่าจะใช้ On-device AI ไปทำไม แล้ว On-device AI มันดียังไง ไอเดียหลักของโปรเจกต์นี้คือการสร้างแชทบอทที่ทำงานได้ด้วยตัวเองทั้งหมดบนอุปกรณ์ของ User โดยไม่ต้องพึ่งพาเซิร์ฟเวอร์เลย ซึ่งพอเราทำแบบนี้ได้ เราก็ได้ข้อดีมาเต็มๆ คือ

  • เป็นส่วนตัวสุดๆ: ข้อมูลและคำถามที่คุยกับบอทจะไม่มีวันหลุดออกจากเครื่องของผู้ใช้
  • เร็วทันใจ: เพราะไม่มีการส่งข้อมูลไป-กลับผ่านอินเทอร์เน็ต การตอบสนองจึงเกิดขึ้นแทบทันที
  • ประหยัด: ไม่ต้องมีค่าใช้จ่ายเรื่องเซิร์ฟเวอร์เลย โฮสต์บน static web hosting ธรรมดาๆ ก็พอ

ซึ่งหลักการทำงานของ Project นี้มันคือการดึงข้อมูลที่เกี่ยวข้องที่สุดจากชุดความรู้ (Knowledge Base) ที่เราเตรียมไว้ แล้วส่งให้ AI ช่วยเรียบเรียงเป็นคำตอบอีกทีหนึ่ง เหมือนการทำ RAG ทั่วๆไปแหละแค่ทำให้มันจบ บน browser

มันทำได้อย่างไร?

เพื่อให้เห็นภาพมากขึ้น ผมจะแยกการทำงานออกมาเป็น 4 ขั้นตอนหลักๆ ที่เกิดขึ้นทั้งหมดในเบราว์เซอร์ครับ

  1. แปลง Knowladge เป็น Vector: ต้องบอกว่าขั้นตอนนี้ตอนแรกผมก็อยากที่จะจบด้วย built-in AI นั่นแหละแต่ตัว model ที่มีมาไม่ไม่ support Embedding ก็เลยต้องไปไล่หา model Embedding ที่เอามาใช้บน browser ได้ ซึ่ง size กลายเป็นเรื่องที่ผมกังวลมากๆเพราะว่า ถ้า model ใหญ่เกินไป ความดูเป็นไปได้ของ project นี้จะล้มเลย คราวนี้พอ research ไปๆมาๆก็ไปเจอ Model all-MiniLM-L6-v2 ที่ Support Transformer.js และขนาดน่ารักมากๆแค่ 20MB เองซึ่งพอทำงานจริงๆก็โหลดไม่กี่วินาทีก็เสร็จ คราวนี้ผมก็เอา JSON ของ FAQ ที่ mock เอาไว้ มาแปลงเป็น vector แล้วก็ยัดลงไปใน LocalStorage จริงๆพยายามหา localDB ที่ support vector ไม่เจอ ก็เลย save มันไปแบบโง่ๆแบบนี้แหละ สุดท้ายถ้าจะทำจริงๆคงต้อง implement index-db ที่ suport vector ขึ้นมา (จริงแล้วมีนะ แต่ไม่มีใคร maintain มานานแล้วผมเลยข้ามไป https://github.com/PaulKinlan/idb-vector)
import { pipeline, env } from '@huggingface/transformers';
import faqs from "@/faq.json";
const extractor = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
const embeddings = [];
for (const faq of faqs) {
const embedding = await extractor(faq.question + " " + faq.answer, {
pooling: "mean",
normalize: true,
});
const embeddingArray = embedding.tolist()[0];
embeddings.push({
embedding: embeddingArray,
text: faq.question,
metadata: { question: faq.question, answer: faq.answer },
});
}
view raw extract.js hosted with ❤ by GitHub

2. คราวนี้เมื่อผู้ใช้พิมพ์คำถามเข้ามา ผมใช้ไลบรารี Transformers.js เพื่อโหลดโมเดล all-MiniLM-L6-v2 (เหมือน process เดิมที่โหลด FAQ )ขึ้นมาทำงานในเบราว์เซอร์ โมเดลนี้จะแปลงประโยคคำถามให้กลายเป็นชุดตัวเลข (Vector) ที่สื่อถึงความหมายของประโยคนั้นๆ คราวนี้พอเราได้ vector เราก็เอาไปหา cosine similarity กับ doc ที่เก็บใน LocalStorage แล้วก็เอา top 10 ที่มี Similarity สูงที่สุดมา สำหรับ function cosineSimilarity ผมก็ search หาทั่วๆไปเลยครับใครจะใช้ Lib ก็ได้นะ

const resultsWithScores = storedEmbeddings.map((item, index) => {
const similarity = cosineSimilarity(queryEmbedding.tolist()[0], item.embedding);
return {
…item,
similarity
};
});
// Sort by similarity score (descending) and limit results
const topResults = resultsWithScores
.sort((a, b) => b.similarity – a.similarity)
.slice(0, 10);
view raw search.js hosted with ❤ by GitHub

3. ประกอบร่าง Prompt: แทนที่จะโยนคำถามดิบๆ ไปให้ AI, ผมเอา “ข้อมูลที่เกี่ยวข้อง” ที่หาเจอในข้อ 2 มาประกอบเข้าไปใน Prompt ด้วย วิธีนี้ทำให้ AI มีบริบทและข้อมูลที่ถูกต้องสำหรับใช้ในการสร้างคำตอบ ผมยัด intial prompt เข้าไปด้วยเพื่อให้ตัวมันเข้าใจมากขึ้น

const initialPrompts = [
{
role: 'system',
content: `You are a knowledgeable Thai food and cuisine specialist. Your role is to help customers learn about authentic Thai dishes, ingredients, cooking methods, and food culture using accurate information from the FAQ data. Be enthusiastic about Thai cuisine and highlight its unique flavors and traditions.
Key guidelines:
– Use the provided FAQ information to give accurate, helpful answers about Thai food
– Emphasize the authentic flavors, spices, and cooking techniques of Thai cuisine
– Mention popular dishes like Pad Thai, Tom Yum, Green Curry, and Som Tam
– Highlight the balance of sweet, sour, spicy, and savory flavors in Thai cooking
– Use markdown formatting to make responses visually appealing with **bold** for key dishes and ingredients, bullet points for features, and clear structure
– Be encouraging about exploring Thai cuisine and trying new dishes
– If asked about spice levels, explain the different heat options and how to adjust them
– Always be helpful and supportive of the customer\'s interest in Thai food
– **IMPORTANT: Keep all responses to 20 words or less**
– Remember and reference previous conversation context when appropriate`
}
];
const faqContext = faqResults.map((result, index) =>
`FAQ ${index + 1}:\nQuestion: ${result.metadata.question}\nAnswer: ${result.metadata.answer}`
).join('\n\n');
const prompt = `A customer is asking about Thai food and cuisine. Here's their question: "${question}"
Based on the following Thai food FAQ information, please provide a helpful, enthusiastic response:
FAQ Information:
${faqContext}
Please give a friendly, informative answer that helps them learn about Thai cuisine. Use markdown formatting to make your response visually appealing and highlight the authentic flavors and traditions of Thai food. **Keep your response to exactly 20 words or less.**`;
view raw prompt.js hosted with ❤ by GitHub

4. สร้างคำตอบด้วย Prompt API: สุดท้าย ผมส่ง Prompt ที่สมบูรณ์แล้วไปให้ Prompt API ซึ่งเป็น Built-in AI ของ Chrome / Edge ช่วยสร้างคำตอบที่สอดคล้องกับข้อมูลที่เราป้อนให้ หน้าตาของโค้ดตอนเรียกใช้ Prompt API ก็จะประมาณนี้ครับ:

// Check availability first
const availability = await LanguageModel.availability({
})
// Create session with monitoring
const session = await LanguageModel.create({
temperature: 0.7,
monitor(m) {
m.addEventListener('downloadprogress', (e) => {
console.log(`Downloaded ${e.loaded * 100}%`)
})
}
})
// Use regular API for complete response
const result = await session.prompt(prompt)
// Clean up when done
session.destroy()
view raw prompt-api.js hosted with ❤ by GitHub

ข้อจำกัด

  • ขอบเขตความรู้: แชทบอทจะรู้แค่เรื่องที่มีอยู่ในฐานข้อมูลที่เราเตรียมไว้ให้เท่านั้น ไม่สามารถตอบเรื่องอื่นๆ นอกเหนือจากนี้ได้เพราะตัว model ที่รันอยู่ข้างหลังเล็กมากๆ
  • ⚠️⚠️⚠️ Browser Compatibility: ตอนนี้ฟีเจอร์หลักยังต้องพึ่งพา Prompt API ซึ่งเป็นฟีเจอร์ทดลองยังใช้ไม่ได้บนเว็บต้องเปิด Flag หรือรันบน origin trial และยังมีแค่บน Chrome และ Microsoft Edge ถ้าใครอยากลองก็ให้ไปเปิด feature flag ก่อน
    chrome://flags/#prompt-api-for-gemini-nano
    edge://flags/#edge-llm-prompt-api-for-phi-mini
  • ทรัพยากรเครื่อง: ตอนนี้ฟีเจอร์ Built-in AI ยังรองรับแค่บน Desktop ที่มี RAM 4 GB และมีพื้นที่ว่าง 22 GB ขึ้นไปได้เท่านั้น

ยังไม่สุดเลยขออัดต่อ

พอทำๆไปก็มาคิดว่า ถ้าไม่มี Prompt API หละ จะทำยังไง ก็เลยคิดถึงบทความก่อนที่เราเอา Gemma 3 1B มารันบน Browser ก็เลยคิดว่า ลองเอาสองงานมารวมกันดู ก็เลยลองเขียน code แบบเดียวกับบทความก่อนเพื่อรัน Gemma 3 1B ดู

import {
FilesetResolver,
LlmInference,
} from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-genai";
const genai = await FilesetResolver.forGenAiTasks(
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-genai@latest/wasm"
);
llmInference = await LlmInference.createFromOptions(genai, {
baseOptions: {
modelAssetPath: "/assets/gemma3-1B-it-int4.task",
},
maxTokens: 2048
});
const responseText = await llmInference.generateResponse("Hello, nice to meet you");
view raw gemma-on-web.js hosted with ❤ by GitHub

สิ่งที่เจอไม่ค่อยประทับใจเท่าไหร่ เพราะ model ต่อให้เล็กขนาดไหน ก็ยังกินเนื้อที่ 500MB อยู่ดี ไม่พอ model ขนาด 1B เทียบไม่ได้เยก็ Prompt API เพราะว่า model ที่เป็น prompt API ถ้าจำไม่ผิด คือ Gemma E4B ที่ขนาดใหญ่กว่าเยอะมากๆ แต่ถ้าถามว่าพอใช้ได้มั้ย ก็ตอบว่า พอใช้ได้อยู่

สรุป

ถ้าถามว่าพอจะทำ Chatbot บน browser ได้แล้วมั้ย ก็ตอบว่าพอทำได้แหละ อาจจะเป็น Chatbot ง่ายๆหรืออาจจะเป็นพวก Fix response ก็พอไหวอยู่ แต่ถ้าทำแบบ Gemerative 100% อาจจะต้องจัดการเรื่อง local DB หรือ เรื่อง context window ให้ดีพอ ยังไม่รวมพวก เรื่อง browser compatibility ด้วย เอาจริงๆแล้ว วิธีนี้อาจจะเอาไปใช้กับ Application ที่เป็น app android หรือ ios ด้วยก็ได้ไม่จำเป็นต้องทำบนเว็บอย่างเดียว

ถ้าใครอยากดู code ที่ผมสร้างเอาไว้ก็ไปดูที่ Github https://github.com/thangman22/ondevice-chatbot ได้เลยแต่อย่าคาดหวังว่ามันจะดีมากนะ ฮ่าๆ


Discover more from Thangman22's

Subscribe to get the latest posts sent to your email.

Leave a Reply

Discover more from Thangman22's

Subscribe now to keep reading and get access to the full archive.

Continue reading