บทที่ 5: AI Integration (Google Gemini API)
สารบัญเนื้อหา
บทที่ 5: AI Integration (Google Gemini API)
🎯 เป้าหมายการเรียนรู้
หลังจากเรียนบทนี้แล้ว คุณจะสามารถ:
- ใช้ Google Gemini API สำหรับ AI Content Generation
- ออกแบบ Fallback Strategy (AI → Google Search → Local)
- จัดการ API Rate Limiting และ Cooldown
- ใช้ Caching เพื่อประหยัด API calls
- Parse และ Validate AI Responses
- จัดการ Errors และ Retry Logic
📚 ทฤษฎีและแนวคิด
AI Content Generation คืออะไร?
AI Content Generation คือการใช้ AI สร้างเนื้อหาอัตโนมัติ เช่น:
- 📝 สรุปข้อความ (Summarization)
- 💬 สร้างคำคม (Quote Generation)
- 🔤 แปลภาษา (Translation)
- 🎨 Paraphrasing (เขียนใหม่)
Google Gemini API
Gemini คือ Large Language Model (LLM) จาก Google
ข้อดี:
- ✅ รองรับภาษาไทยได้ดี
- ✅ มี Free Tier (60 requests/minute)
- ✅ Response เร็ว
- ✅ รองรับ JSON output
ข้อจำกัด:
- ⚠️ มี Rate Limit (429 Error)
- ⚠️ ต้องจัดการ Quota
- ⚠️ Response อาจไม่ consistent
Fallback Strategy (3 Layers)
Layer 1: Google Gemini AI
↓ (ถ้า fail หรือ quota เต็ม)
Layer 2: Google Search API
↓ (ถ้า fail หรือไม่เจอ)
Layer 3: Local Quotes (400 items)
↓
Always Return Something!
ทำไมต้องมี Fallback:
- 🛡️ Reliability: แอปไม่พังถ้า AI fail
- 💰 Cost Control: ประหยัด API costs
- ⚡ Performance: ใช้ cache ลด latency
💻 โค้ดจริงจาก QUOM App
1. Setup และ Configuration
ไฟล์: backend/src/services/ai.js
import { GoogleGenerativeAI } from "@google/generative-ai";
import dotenv from "dotenv";
import crypto from "crypto";
dotenv.config();
// Gemini initialization
const GEMINI_KEY = process.env.GEMINI_API_KEY || "";
const genAI = GEMINI_KEY ? new GoogleGenerativeAI(GEMINI_KEY) : null;
// Cache + Cooldown (in-memory)
const GEN_CACHE = new Map(); // key -> { ts, out }
const CACHE_TTL_MS = 1000 * 60 * 30; // 30 minutes
let geminiCooldownUntil = 0;
อธิบายทีละบรรทัด (Imports):
-
import { GoogleGenerativeAI } from "@google/generative-ai"- Library หลักของ Google Gemini (ต้องติดตั้งเพิ่มด้วย npm)
-
import dotenv from "dotenv"- ใช้โหลด API Key จากไฟล์
.env
- ใช้โหลด API Key จากไฟล์
-
import crypto from "crypto"- Module มาตรฐานของ Node.js ใช้ทำ Hash (SHA-1) สร้าง Cache Key
อธิบาย:
-
Import Gemini SDK
import { GoogleGenerativeAI } from "@google/generative-ai";- Official SDK จาก Google
-
Initialize Client
const GEMINI_KEY = process.env.GEMINI_API_KEY || ""; const genAI = GEMINI_KEY ? new GoogleGenerativeAI(GEMINI_KEY) : null;- ถ้าไม่มี API key ให้
genAI = null - ป้องกัน crash ถ้าลืมตั้งค่า
- ถ้าไม่มี API key ให้
-
In-Memory Cache
const GEN_CACHE = new Map(); const CACHE_TTL_MS = 1000 * 60 * 30; // 30 minutes- เก็บ response ไว้ 30 นาที
- ลด API calls สำหรับ input เดิม
-
Cooldown Mechanism
let geminiCooldownUntil = 0;- เก็บ timestamp ที่ต้องรอก่อนเรียก API อีกครั้ง
- ใช้เมื่อเจอ 429 (Rate Limit)
2. Cache Functions
function now() {
return Date.now();
}
function makeKey({ transcript, style, language }) {
const raw = `${language}::${style}::${transcript}`;
return crypto.createHash("sha1").update(raw).digest("hex");
}
function cacheGet(key) {
const hit = GEN_CACHE.get(key);
if (!hit) return null;
// Check if expired
if (now() - hit.ts > CACHE_TTL_MS) {
GEN_CACHE.delete(key);
return null;
}
return hit.out;
}
function cacheSet(key, out) {
GEN_CACHE.set(key, { ts: now(), out });
}
อธิบาย:
-
makeKey()
const raw = `${language}::${style}::${transcript}`; return crypto.createHash("sha1").update(raw).digest("hex");- สร้าง unique key จาก input parameters
- ใช้ SHA-1 hash เพื่อความเร็ว
- ตัวอย่าง key:
a3f5b2c1d4e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0
-
cacheGet()
- ตรวจสอบว่ามี cache หรือไม่
- ตรวจสอบว่า cache หมดอายุหรือยัง
- ถ้าหมดอายุ ลบออก
-
cacheSet()
- เก็บ response พร้อม timestamp
3. Main Generation Function
export async function generateSummaryAndQuote({
transcript,
style = "inspire",
language = "th"
}) {
const input = {
transcript: String(transcript || "").trim(),
style: String(style || "inspire"),
language: String(language || "th"),
};
// Normalize language: auto -> th
if (input.language === "auto") input.language = "th";
if (!input.transcript) {
return {
lines: [],
bullets: [],
keywords: [],
quote: "",
};
}
// ---- Step 1: Check Cache ----
const key = makeKey(input);
const cached = cacheGet(key);
if (cached) {
return { ...cached, _source: "cache" };
}
// ---- Step 2: Try Gemini ----
console.log("[SELECTION] Step 1: Trying AI...");
const geminiOk = await tryGeminiOnce(input);
if (geminiOk) {
cacheSet(key, geminiOk);
console.log("✅ AI generation successful");
return { ...geminiOk, _source: "gemini" };
}
// ---- Step 3: Try Google Search ----
console.log("[SELECTION] Step 2: Falling back to Google Search...");
try {
const googleResult = await tryGoogleSearch(
input.transcript,
input.style,
input.language
);
if (googleResult) {
cacheSet(key, googleResult);
return { ...googleResult, _source: "google" };
}
throw new Error("Google Search returned null");
} catch (googleErr) {
console.warn("⚠️ Google Search failed:", googleErr?.message);
}
// ---- Step 4: Local Quotes ----
console.log("[SELECTION] Step 3: Falling back to Local Quotes...");
const local = tryRandomQuotes(input.transcript, input.style);
cacheSet(key, local);
return { ...local, _source: "local" };
}
อธิบาย Flow:
-
Input Validation
- Normalize input
- แปลง
autoเป็นth - ตรวจสอบ transcript ไม่ว่าง
-
Layer 1: Cache
- เช็ค cache ก่อนเสมอ
- ถ้ามี return ทันที (เร็วที่สุด)
-
Layer 2: Gemini AI
- เรียก
tryGeminiOnce() - ถ้าสำเร็จ cache และ return
- เรียก
-
Layer 3: Google Search
- ถ้า AI fail ลอง Google Search
- ใช้
try-catchเพราะอาจ fail
-
Layer 4: Local Fallback
- ถ้าทุกอย่าง fail ใช้ local quotes
- Always return something!
4. Gemini AI Implementation
async function tryGeminiOnce({ transcript, style, language }) {
if (!genAI) {
console.warn("⚠️ GEMINI_API_KEY missing -> skip AI");
return null;
}
// Check cooldown
if (now() < geminiCooldownUntil) {
const left = Math.ceil((geminiCooldownUntil - now()) / 1000);
console.warn(`⚠️ Gemini cooldown active (${left}s) -> skip AI`);
return null;
}
try {
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash-exp" });
const langName = language === "th" ? "ไทย" : "English";
const prompt = `
คุณคือผู้ช่วยสร้างคำคมที่สร้างแรงบันดาลใจ
ให้ตอบเป็นภาษา: ${langName}
สไตล์คำคม: ${style}
**สำคัญมาก:**
- คำคม (quote) ต้อง**แตกต่าง**จากข้อความต้นฉบับ
- อย่าคัดลอกข้อความต้นฉบับมาเป็นคำคม
- สร้างคำคมใหม่ที่สร้างแรงบันดาลใจจากแนวคิดของข้อความต้นฉบับ
- คำคมควรสั้น กระชับ และมีพลัง
ตอบเป็น JSON เท่านั้น (ห้ามมีข้อความอื่น):
{
"lines": ["สรุป 3-5 บรรทัด"],
"bullets": ["หัวข้อย่อย 3 ข้อ"],
"keywords": ["คีย์เวิร์ด 3-5 คำ"],
"quote": "คำคมใหม่ที่สร้างแรงบันดาลใจ 1-2 ประโยค",
"translation": "แปลคำคม (quote) เป็นภาษาไทย"
}
ข้อความต้นฉบับ:
"${transcript}"
`.trim();
const result = await model.generateContent(prompt);
const text = result.response.text();
const parsed = extractJson(text);
return {
lines: Array.isArray(parsed.lines) ? parsed.lines : [],
bullets: Array.isArray(parsed.bullets) ? parsed.bullets : [],
keywords: Array.isArray(parsed.keywords) ? parsed.keywords : [],
quote: String(parsed.quote || "").trim(),
translation: String(parsed.translation || "").trim(),
};
} catch (err) {
// Handle 429 (Rate Limit)
if (err?.status === 429) {
const retryMs = parseRetryDelayMs(err) ?? 30_000;
geminiCooldownUntil = now() + retryMs;
console.warn(`⚠️ Gemini 429 -> cooldown ${Math.ceil(retryMs / 1000)}s`);
}
console.error("Gemini API Error:", err?.message || err);
return null;
}
}
อธิบาย:
-
Cooldown Check
if (now() < geminiCooldownUntil) { const left = Math.ceil((geminiCooldownUntil - now()) / 1000); console.warn(`⚠️ Gemini cooldown active (${left}s)`); return null; }- ถ้ายังอยู่ใน cooldown period ข้ามไป
- แสดงเวลาที่เหลือ
-
Get Model
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash-exp" });- ใช้ model
gemini-2.0-flash-exp(เร็วและถูก)
- ใช้ model
-
Prompt Engineering
const prompt = ` คุณคือผู้ช่วยสร้างคำคมที่สร้างแรงบันดาลใจ ให้ตอบเป็นภาษา: ${langName} สไตล์คำคม: ${style} ... `;- สำคัญมาก: Prompt ต้องชัดเจน
- บอกให้ตอบเป็น JSON
- บอกให้คำคมแตกต่างจากต้นฉบับ
-
Generate Content
const result = await model.generateContent(prompt); const text = result.response.text();- เรียก API
- ได้ text response
-
Parse JSON
const parsed = extractJson(text);- แปลง text เป็น JSON object
- จัดการกรณีที่มี ```json fences
-
Error Handling - 429
if (err?.status === 429) { const retryMs = parseRetryDelayMs(err) ?? 30_000; geminiCooldownUntil = now() + retryMs; }- ถ้าเจอ 429 (Rate Limit Exceeded)
- อ่าน retry delay จาก error
- ตั้ง cooldown
5. JSON Extraction
function extractJson(text) {
const s = String(text || "").trim();
// Remove ```json fences
const cleaned = s
.replace(/```json/gi, "")
.replace(/```/g, "")
.trim();
// Find JSON object
const start = cleaned.indexOf("{");
const end = cleaned.lastIndexOf("}");
if (start >= 0 && end > start) {
return JSON.parse(cleaned.slice(start, end + 1));
}
return JSON.parse(cleaned);
}
อธิบาย:
-
Remove Markdown Fences
- AI อาจ return
json ... - ต้องตัดออก
- AI อาจ return
-
Extract JSON
- หา
{แรกและ}สุดท้าย - ตัดเฉพาะส่วนนั้น
- หา
-
Parse
- ใช้
JSON.parse() - ถ้า fail จะ throw error
- ใช้
6. Rate Limit Handling
function parseRetryDelayMs(err) {
try {
const details = err?.errorDetails || [];
const retry = details.find(d =>
String(d["@type"] || "").includes("RetryInfo")
);
const s = String(retry?.retryDelay || "").trim(); // e.g. "44s"
const m = /^(\d+)\s*s$/.exec(s);
if (m) return Number(m[1]) * 1000;
} catch { }
return null;
}
อธิบาย:
- Google API บอก retry delay ใน error
- ตัวอย่าง:
"44s"= 44 วินาที - แปลงเป็น milliseconds
7. Local Fallback
function tryRandomQuotes(transcript, style) {
console.log("📚 Using Local Quotes (Fallback Layer 3)...");
const localQuotes = [
"ความล้มเหลวไม่ใช่จุดจบ แต่เป็นจุดเริ่มต้นของความสำเร็จ",
"อย่ารอให้โอกาสเข้ามาหา แต่จงออกไปสร้างโอกาสด้วยตัวเอง",
"ทุกความพยายามอาจไม่นำมาซึ่งความสำเร็จ แต่ทุกความสำเร็จล้วนเกิดจากความพยายาม",
// ... 400+ quotes
];
const randomQuote = localQuotes[
Math.floor(Math.random() * localQuotes.length)
];
return {
lines: [
`เกี่ยวกับ: ${transcript.substring(0, 30)}...`,
"ระบบใช้คำคมจากคลังข้อมูลสำรอง",
"เนื่องจากโควต้า AI เต็มหรือค้นหาไม่พบ",
],
bullets: ["สร้างแรงบันดาลใจ", "ข้อคิดเตือนใจ", "มุมมองชีวิต"],
keywords: ["แรงบันดาลใจ", "ชีวิต", "ความสำเร็จ", "พลังบวก"],
quote: randomQuote,
};
}
อธิบาย:
- มี local quotes 400+ ข้อ
- สุ่มเลือก 1 ข้อ
- Always return something
- ไม่มีทางที่ function นี้จะ fail
🔄 Flow Diagram
User Request
↓
generateSummaryAndQuote()
↓
┌─────────────────────────────────┐
│ 1. Check Cache │
│ ✅ Hit → Return immediately │
│ ❌ Miss → Continue │
└────────────┬────────────────────┘
↓
┌─────────────────────────────────┐
│ 2. Try Gemini AI │
│ ✅ Success → Cache & Return │
│ ❌ Fail (429/Error) → Next │
└────────────┬────────────────────┘
↓
┌─────────────────────────────────┐
│ 3. Try Google Search │
│ ✅ Found → Cache & Return │
│ ❌ Not Found → Next │
└────────────┬────────────────────┘
↓
┌─────────────────────────────────┐
│ 4. Local Quotes │
│ ✅ Always Success │
└────────────┬────────────────────┘
↓
Return Result
🎯 Best Practices
1. Always Have Fallback
// ✅ ดี - มี fallback
const result = await tryAI() || await trySearch() || getLocal();
// ❌ ไม่ดี - อาจ return null
const result = await tryAI();
2. Cache Aggressively
// ✅ ดี - cache ก่อนเรียก API
const cached = getCache(key);
if (cached) return cached;
const result = await callAPI();
setCache(key, result);
3. Handle Rate Limits
// ✅ ดี - ตั้ง cooldown
if (err.status === 429) {
cooldownUntil = now() + retryDelay;
}
// ❌ ไม่ดี - เรียก API ซ้ำทันที
if (err.status === 429) {
return await callAPI(); // จะโดน 429 อีก!
}
📝 แบบฝึกหัด
คำถามทบทวน
- ทำไมต้องมี Fallback Strategy?
- Cache ช่วยอะไร?
- 429 Error คืออะไร?
- Cooldown Mechanism ทำงานอย่างไร?
- ทำไม Local Fallback ต้องอยู่ท้ายสุด?
โจทย์ปฏิบัติ
โจทย์ที่ 1: เพิ่ม Retry Logic
// TODO: เพิ่ม retry 3 ครั้งสำหรับ Gemini API
// - ถ้า fail ครั้งแรก รอ 1 วินาที แล้วลองใหม่
// - ถ้า fail ครั้งที่ 2 รอ 2 วินาที
// - ถ้า fail ครั้งที่ 3 ไปใช้ fallback
โจทย์ที่ 2: Implement Redis Cache
// TODO: แทนที่ in-memory cache ด้วย Redis
// - ใช้ redis.get() และ redis.set()
// - ตั้ง TTL ด้วย EXPIRE
โจทย์ที่ 3: Add Metrics
// TODO: เพิ่ม metrics tracking
// - นับจำนวน cache hits/misses
// - นับจำนวน API calls ต่อ source
// - คำนวณ cost savings จาก cache
📌 สรุป
ในบทนี้เราได้เรียนรู้:
✅ Google Gemini API สำหรับ AI generation
✅ 3-Layer Fallback (AI → Search → Local)
✅ Caching เพื่อประหยัด API calls
✅ Rate Limiting และ Cooldown handling
✅ Prompt Engineering สำหรับ JSON output
✅ Error Handling และ Retry logic
ไฟล์ที่เกี่ยวข้อง
backend/src/services/ai.js- AI servicebackend/src/services/google-search.js- Google Search fallback
🔜 บทถัดไป
บทที่ 6: File Upload System (Multer)
เราจะเรียนรู้การจัดการ file uploads, validation, และ storage