Bot WA AI untuk FAQ Dynamic
Cara membuat bot AI WhatsApp dengan FAQ dynamic. Jawab pertanyaan dari knowledge base, update real-time. Tutorial lengkap!
FAQ dynamic = Selalu up-to-date!
Berbeda dengan FAQ static yang harus update manual, AI FAQ bisa jawab dari knowledge base, handle variasi pertanyaan, dan update otomatis.
Static vs Dynamic FAQ
📊 PERBANDINGAN:
STATIC FAQ:
- Keyword exact match
- "harga" → response A
- Tidak paham variasi
- Update manual
DYNAMIC AI FAQ:
- Understand intent
- "brp duit", "mahal ga" → sama
- Jawab dari knowledge base
- Update real-timeArsitektur
🏗️ ARCHITECTURE:
┌─────────────┐
│ Customer │
│ Message │
└──────┬──────┘
│
▼
┌─────────────┐
│ Intent │
│ Detection │
└──────┬──────┘
│
▼
┌─────────────┐ ┌─────────────┐
│ Knowledge │◄────│ Admin │
│ Base │ │ Panel │
└──────┬──────┘ └─────────────┘
│
▼
┌─────────────┐
│ AI │
│ Response │
└──────┬──────┘
│
▼
┌─────────────┐
│ Answer │
└─────────────┘Knowledge Base Structure
Database Schema:
javascript
// FAQ Categories
const categorySchema = {
id: String,
name: String,
description: String,
priority: Number,
active: Boolean
};
// FAQ Items
const faqSchema = {
id: String,
categoryId: String,
question: String, // Main question
variations: [String], // Alternative phrasings
answer: String, // Answer template
keywords: [String], // Search keywords
metadata: {
lastUpdated: Date,
viewCount: Number,
helpfulCount: Number,
notHelpfulCount: Number
},
active: Boolean
};
// Example data
const faqData = [
{
id: 'faq-001',
categoryId: 'shipping',
question: 'Berapa lama pengiriman?',
variations: [
'kapan sampai',
'brp hari kirim',
'estimasi pengiriman',
'lama delivery'
],
answer: `Estimasi pengiriman:
📦 Jabodetabek: 1-2 hari
📦 Jawa: 2-3 hari
📦 Luar Jawa: 3-5 hari
📦 Indonesia Timur: 5-7 hari
Menggunakan kurir JNE, J&T, dan SiCepat.`,
keywords: ['kirim', 'pengiriman', 'sampai', 'hari', 'lama'],
active: true
},
{
id: 'faq-002',
categoryId: 'payment',
question: 'Metode pembayaran apa saja?',
variations: [
'bayar gimana',
'bisa transfer',
'terima gopay',
'payment method'
],
answer: `Metode pembayaran yang tersedia:
💳 Transfer Bank: BCA, Mandiri, BNI
📱 E-Wallet: GoPay, OVO, DANA, ShopeePay
🏪 Minimarket: Alfamart, Indomaret
💵 COD: Khusus area tertentu`,
keywords: ['bayar', 'payment', 'transfer', 'gopay', 'ovo'],
active: true
}
];Implementation
System Prompt dengan FAQ:
javascript
async function buildFAQPrompt() {
// Load active FAQs from database
const faqs = await db.faqs.find({ active: true }).toArray();
let faqContent = 'KNOWLEDGE BASE FAQ:\n\n';
for (const faq of faqs) {
faqContent += `Q: ${faq.question}\n`;
faqContent += `Keywords: ${faq.keywords.join(', ')}\n`;
faqContent += `A: ${faq.answer}\n\n`;
}
const systemPrompt = `Kamu adalah CS AI untuk [BRAND].
TUGAS:
1. Jawab pertanyaan customer berdasarkan knowledge base
2. Jika tidak ada di knowledge base, bilang akan cek ke tim
3. Jangan membuat informasi yang tidak ada
${faqContent}
ATURAN:
- Jawab dalam Bahasa Indonesia casual
- Gunakan info dari knowledge base
- Jika pertanyaan mirip tapi tidak exact, tetap jawab
- Jika tidak yakin, bilang "Saya cek dulu ke tim ya kak"
- Ramah dengan emoji secukupnya`;
return systemPrompt;
}Semantic Search untuk FAQ:
javascript
const OpenAI = require('openai');
const openai = new OpenAI();
// Generate embeddings untuk FAQ
async function generateFAQEmbeddings() {
const faqs = await db.faqs.find({ active: true }).toArray();
for (const faq of faqs) {
const textToEmbed = `${faq.question} ${faq.variations.join(' ')} ${faq.keywords.join(' ')}`;
const embedding = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: textToEmbed
});
await db.faqs.updateOne(
{ id: faq.id },
{ $set: { embedding: embedding.data[0].embedding } }
);
}
}
// Find relevant FAQs using semantic search
async function findRelevantFAQs(userQuestion, topK = 3) {
// Get embedding for user question
const questionEmbedding = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: userQuestion
});
const queryVector = questionEmbedding.data[0].embedding;
// Search in database (MongoDB with vector search)
const results = await db.faqs.aggregate([
{
$vectorSearch: {
index: 'faq_vector_index',
path: 'embedding',
queryVector: queryVector,
numCandidates: 50,
limit: topK
}
},
{
$project: {
question: 1,
answer: 1,
score: { $meta: 'vectorSearchScore' }
}
}
]).toArray();
return results;
}Full Chat Handler:
javascript
async function handleFAQChat(userId, userMessage) {
// 1. Find relevant FAQs
const relevantFAQs = await findRelevantFAQs(userMessage, 3);
// 2. Build context
let faqContext = '';
if (relevantFAQs.length > 0) {
faqContext = 'RELEVANT FAQ:\n';
for (const faq of relevantFAQs) {
faqContext += `Q: ${faq.question}\nA: ${faq.answer}\n\n`;
}
}
// 3. Generate response
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: `Kamu adalah CS AI untuk [BRAND].
${faqContext}
Jawab pertanyaan customer berdasarkan FAQ di atas.
Jika tidak relevan, bilang akan cek ke tim.
Bahasa Indonesia casual, ramah.`
},
{ role: 'user', content: userMessage }
]
});
// 4. Track FAQ usage
if (relevantFAQs.length > 0 && relevantFAQs[0].score > 0.8) {
await db.faqs.updateOne(
{ id: relevantFAQs[0].id },
{ $inc: { 'metadata.viewCount': 1 } }
);
}
return response.choices[0].message.content;
}Admin Panel untuk Update FAQ
javascript
// API untuk admin manage FAQ
const express = require('express');
const router = express.Router();
// List all FAQs
router.get('/faqs', async (req, res) => {
const faqs = await db.faqs.find({}).toArray();
res.json(faqs);
});
// Add new FAQ
router.post('/faqs', async (req, res) => {
const { question, variations, answer, keywords, categoryId } = req.body;
const faq = {
id: generateId(),
categoryId,
question,
variations: variations || [],
answer,
keywords: keywords || [],
metadata: {
lastUpdated: new Date(),
viewCount: 0,
helpfulCount: 0,
notHelpfulCount: 0
},
active: true
};
await db.faqs.insertOne(faq);
// Regenerate embedding
await generateEmbeddingForFAQ(faq.id);
res.json({ success: true, faq });
});
// Update FAQ
router.put('/faqs/:id', async (req, res) => {
const { id } = req.params;
const updates = req.body;
updates.metadata = updates.metadata || {};
updates.metadata.lastUpdated = new Date();
await db.faqs.updateOne({ id }, { $set: updates });
// Regenerate embedding if content changed
if (updates.question || updates.variations || updates.keywords) {
await generateEmbeddingForFAQ(id);
}
res.json({ success: true });
});
// Delete FAQ
router.delete('/faqs/:id', async (req, res) => {
const { id } = req.params;
await db.faqs.deleteOne({ id });
res.json({ success: true });
});
// Analytics
router.get('/faqs/analytics', async (req, res) => {
const stats = await db.faqs.aggregate([
{
$group: {
_id: '$categoryId',
totalViews: { $sum: '$metadata.viewCount' },
totalHelpful: { $sum: '$metadata.helpfulCount' },
count: { $sum: 1 }
}
}
]).toArray();
res.json(stats);
});Auto-Learn dari Conversations
javascript
// Track unanswered questions
async function trackUnansweredQuestion(userMessage, aiResponse) {
// Check if AI couldn't answer
const unsurePatterns = [
'saya cek dulu',
'akan konfirmasi',
'tidak ada informasi',
'hubungi admin'
];
const isUnanswered = unsurePatterns.some(p =>
aiResponse.toLowerCase().includes(p)
);
if (isUnanswered) {
await db.unansweredQuestions.insertOne({
question: userMessage,
aiResponse,
timestamp: new Date(),
resolved: false
});
}
}
// Admin review unanswered questions
router.get('/faqs/unanswered', async (req, res) => {
const questions = await db.unansweredQuestions
.find({ resolved: false })
.sort({ timestamp: -1 })
.limit(50)
.toArray();
res.json(questions);
});
// Convert unanswered to FAQ
router.post('/faqs/from-unanswered', async (req, res) => {
const { unansweredId, answer, keywords, categoryId } = req.body;
const unanswered = await db.unansweredQuestions.findOne({
_id: unansweredId
});
// Create new FAQ
const faq = {
id: generateId(),
categoryId,
question: unanswered.question,
variations: [],
answer,
keywords,
active: true
};
await db.faqs.insertOne(faq);
await generateEmbeddingForFAQ(faq.id);
// Mark as resolved
await db.unansweredQuestions.updateOne(
{ _id: unansweredId },
{ $set: { resolved: true, resolvedAs: faq.id } }
);
res.json({ success: true, faq });
});Best Practices
DO ✅
- Update FAQ regularly
- Track unanswered questions
- Use semantic search
- Monitor FAQ performance
- Allow admin easy updates
- Version control answersDON'T ❌
- Set and forget
- Ignore unanswered patterns
- Keyword-only matching
- No analytics
- Hard-coded FAQs
- No review processFAQ
Seberapa sering update FAQ?
Weekly review untuk unanswered questions. Monthly untuk full audit.
Perlu berapa FAQ untuk mulai?
Minimum 20-30 FAQ untuk coverage dasar. Tambah seiring waktu.
Kesimpulan
Dynamic FAQ = Always relevant!
| Static FAQ | Dynamic AI FAQ |
|---|---|
| Exact match | Semantic match |
| Manual update | Auto-learn |
| Limited | Comprehensive |