🔒

หน้านี้ต้องใส่รหัสผ่าน

คอร์ส n8n · แชทบอทตอบคำถามองค์กร

← กลับหน้าแรก

คอร์ส n8n · แชทบอทตอบคำถามองค์กร

🧠

สร้าง AI Chatbot ตอบคำถามองค์กรด้วย n8n

n8nRAGOpenAI ภาษาไทย12 บท~3 ชั่วโมง

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

คอร์สนี้แก้ปัญหานั้นตรง ๆ ระบบที่เราจะสร้างให้ ฝ่ายบุคคลหรือธุรการอัปโหลดไฟล์เองได้ ทั้งตาราง Excel และเอกสารทั่วไปอย่าง PDF, Word, ข้อความธรรมดา, Markdown และ HTML บอทอัปเดตความรู้ทันทีโดยไม่ต้องแตะ workflow เลยแม้แต่โหนดเดียว

ส่วนการค้นหา เราไม่ได้ใช้โค้ดสำเร็จรูปจากอินเทอร์เน็ต เพราะโค้ดพวกนั้น ใช้กับภาษาไทยไม่ได้เลย — บทที่ 7 จะพิสูจน์ให้ดูว่าทำไม แล้วเขียนตัวที่ใช้ได้จริงขึ้นมาแทน

🎁 สิ่งที่คุณจะได้
🧰 สิ่งที่ต้องเตรียม
🕘 ตารางเวลาแนะนำสำหรับผู้สอน
ช่วงเนื้อหาเวลา
1บทที่ 1–3 ติดตั้ง เปิดระบบ เห็นภาพรวม30 นาที
2บทที่ 4–6 นำเข้าไฟล์ 6 สกุล ตัดท่อนเอกสาร45 นาที
3บทที่ 7 อัลกอริทึมภาษาไทย (บทหลักของคอร์ส)40 นาที
4บทที่ 8–9 ต่อ OpenAI แบบ RAG และคำนวณต้นทุน40 นาที
5บทที่ 10–12 ตกแต่งหน้าแชท แก้ปัญหา ต่อยอด25 นาที
🖼️ ระบบที่จะได้ตอนจบคอร์ส
หน้าจอ n8n แสดง workflow สองเส้น เส้นบนเป็นการนำเข้าความรู้จากไฟล์หลายสกุล เส้นล่างเป็นการตอบคำถามผ่าน AI
ฝั่งหลังบ้าน — workflow สองเส้นที่ไม่ต่อกัน เส้นบนรับไฟล์เข้าฐานความรู้ เส้นล่างรับคำถามจากผู้ใช้ (บทที่ 2)
หน้าต่างแชทธีมสีส้ม ผู้ใช้ถามว่าเปิดกี่โมง บอทตอบว่าเปิดวันจันทร์ถึงศุกร์ 08.00 ถึง 17.00 น.
ฝั่งผู้ใช้ — หน้าแชทที่ตกแต่งเป็นสีขององค์กร ถาม “เปิดกี่โมง” แล้วได้คำตอบจากไฟล์ Excel ที่อัปโหลดไว้ (บทที่ 10)
💡 สังเกตคำถามในภาพ: ผู้ใช้พิมพ์ว่า “เปิดกี่โมง” แต่ในไฟล์ Excel คำถามเขียนไว้ว่า “บริษัทเปิดทำการกี่โมง” — คนละข้อความกัน แต่บอทหาเจอ เพราะคอลัมน์ keywords และอัลกอริทึมค้นหาที่ออกแบบมาเพื่อภาษาไทย ซึ่งเป็นหัวใจของบทที่ 7
📦 ไฟล์ประกอบคอร์ส

ดาวน์โหลดเก็บไว้ก่อนเริ่มบทที่ 1 ได้เลย — ไฟล์ความรู้ทั้ง 6 สกุลเป็นข้อมูลสมมติของ “บริษัท ตัวอย่าง จำกัด” ไม่ใช่ข้อมูลจริงขององค์กรไหน จึงเอาไปทดลองอัปโหลดได้อย่างสบายใจ

⭐ ไฟล์หลัก — Workflow ที่ import เข้า n8n ได้ทันที

📚 ไฟล์ความรู้ตัวอย่าง 6 สกุล — เอาไว้ทดลองอัปโหลด

🎨 ธีมหน้าแชท — วางในช่อง Custom CSS ของโหนด Chatbot (บทที่ 10)

⚠️ ก่อน import ไฟล์ workflow: ในไฟล์มีค่า webhookId ติดมาด้วย 3 จุด ถ้าเผลอ import ไฟล์เดียวกันซ้ำสองรอบในเครื่องเดียว จะเจอ error Conflicting Chat Path แล้ว Activate ไม่ได้ วิธีแก้อยู่ใน บทที่ 11 · ส่วนผู้เรียนแต่ละคนที่ติดตั้ง n8n บนเครื่องตัวเอง ต่างคนต่างใช้ได้ตามปกติ ไม่ชนกัน

💡 ไฟล์ .docx .pdf .xlsx เป็นไฟล์ไบนารี กดดาวน์โหลดแล้วเปิดด้วยโปรแกรมของตัวเองได้เลย ส่วนไฟล์ข้อความกด ดูเนื้อหา อ่านบนเว็บได้ก่อนโหลด

📚 บทเรียนทั้ง 12 บท

เรียนไปแล้ว 0 จาก 12 บท

บทที่ 1 — ติดตั้งและเปิด n8n

ติดตั้ง

Terminal
# วิธีที่ 1: npm
npm install -g n8n

# วิธีที่ 2: Docker (แนะนำสำหรับใช้งานจริง)
docker run -d --name n8n -p 5678:5678 \
  -e NODE_FUNCTION_ALLOW_BUILTIN=zlib \
  -v ~/.n8n:/home/node/.n8n \
  docker.n8n.io/n8nio/n8n

ถ้าใช้ Docker อย่าลืม -v ~/.n8n:/home/node/.n8n ไม่งั้นลบคอนเทนเนอร์เมื่อไร workflow หายหมด

เปิดสิทธิ์ให้โค้ดอ่านไฟล์ Word

n8n ไม่มีตัวอ่านไฟล์ .docx ในตัว คอร์สนี้จึงเขียนโค้ดแกะไฟล์เอง (จะอธิบายในบทที่ 5) ซึ่งต้องใช้โมดูล zlib ของ Node.js ที่ n8n ปิดไว้เป็นค่าเริ่มต้นด้วยเหตุผลด้านความปลอดภัย ต้องเปิดตอนสั่งรัน

macOS / Linux

Terminal
NODE_FUNCTION_ALLOW_BUILTIN=zlib n8n start

Windows (PowerShell)

PowerShell
$env:NODE_FUNCTION_ALLOW_BUILTIN="zlib"
n8n start

Windows (Command Prompt)

Command Prompt
set NODE_FUNCTION_ALLOW_BUILTIN=zlib
n8n start
จุดที่พลาดกันบ่อย — ตัวแปร environment อ่านตอนโปรเซสเริ่มทำงานเท่านั้น ถ้า n8n เปิดค้างอยู่ก่อนแล้วต้องปิดก่อน (Control+C หรือ pkill -f n8n) แล้วสั่งรันใหม่ ไม่งั้นตั้งไปก็ไม่มีผล

ตรวจว่าเปิดสำเร็จ

สร้าง Code node ใหม่ วางโค้ดนี้แล้วกด Test step

JavaScript
return [{ json: { ok: typeof require('zlib').inflateRawSync } }];

ได้ "function" แปลว่าพร้อม ถ้าขึ้น error ว่า require ไม่ได้ แสดงว่าตัวแปรยังไม่ติด

ถ้าไม่อยากตั้งค่านี้ ระบบยังทำงานได้ครบทุกอย่าง เพียงแต่ต้องแปลงไฟล์ Word เป็น PDF ก่อนอัปโหลด

บทที่ 2 — Import workflow และดูภาพรวม

Import ไฟล์ n8n_Thai_QA_Chatbot_MultiFormat.json ผ่านเมนู Import from File

Workflow แบ่งเป็นสองเส้นที่ไม่ต่อกันเลย แต่คุยกันผ่านฐานความรู้ที่ใช้ร่วมกัน

เส้นที่ 1 — นำเข้าความรู้ (ผู้ดูแลใช้)

ผังการทำงาน
Upload_Knowledge → Route_File_Type ─┬─ .xlsx .xls     → Extract_Excel  → Rows_To_QA ─────┐
                                    ├─ .pdf           → Extract_PDF    → Text_To_Chunks ─┤
                                    ├─ .docx          → Docx_To_Text   → Text_To_Chunks ─┼→ Store_QA → Import_Result
                                    ├─ .txt .md .html → Extract_Text   → Text_To_Chunks ─┘
                                    └─ สกุลอื่น        → Unsupported_File ────────────────┘

เส้นที่ 2 — ตอบคำถาม (ผู้ใช้ทั่วไปใช้)

ผังการทำงาน
Chatbot → Find_Answer → IF_Use_AI ─┬─ true  → AI_Answer (+ OpenAI_Model + Chat_Memory)
                                   └─ false → Direct_Reply
หน้าจอ n8n แสดง workflow ทั้งสองเส้นหลัง import เสร็จ
หน้าตาบนจอจริงหลัง import เสร็จ — เทียบกับผังด้านบนได้เลยว่าโหนดไหนคืออันไหน

การแยกสองเส้นแบบนี้คือหัวใจของงาน automation ที่ดี คนเพิ่มข้อมูลกับคนใช้ข้อมูลไม่ต้องรู้จักกัน ฝ่ายบุคคลเห็นแค่ฟอร์มอัปโหลด ผู้ใช้เห็นแค่ช่องแชท ไม่มีใครต้องเปิดหน้าจอ n8n เลย

ฐานความรู้ตรงกลางเก็บใน workflow static data ซึ่งเป็นพื้นที่เล็ก ๆ ที่ n8n ผูกไว้กับ workflow แต่ละตัว ข้อดีคือไม่ต้องต่อฐานข้อมูลอะไรเลย เหมาะกับการอบรมและงานขนาดเล็กถึงกลาง ข้อจำกัดจะอธิบายในบทที่ 12

ใส่ OpenAI API key

เปิดโหนด OpenAI_Model → Create new credential → วาง API key → เลือกโมเดล gpt-4o-mini

เปิดใช้งาน

กด Save แล้วสลับสวิตช์ Active มุมขวาบนให้เป็นสีเขียว ข้อนี้ห้ามข้าม เหตุผลอยู่ในบทที่ 6

บทที่ 3 — ไฟล์ความรู้ 2 ประเภท

ระบบนี้รับความรู้เข้ามาสองแบบ ซึ่งเหมาะกับงานคนละอย่าง

ตาราง Q&A (.xlsx)เอกสาร (.pdf .docx .txt .md .html)
โครงสร้าง1 แถว = 1 คำถาม-คำตอบตัดเป็นท่อนอัตโนมัติ
ความแม่นยำสูงมาก ควบคุมคำตอบได้เป๊ะปานกลาง ขึ้นกับการเขียนเอกสาร
แรงที่ต้องลงต้องนั่งเขียนคำถาม-คำตอบเองโยนเอกสารที่มีอยู่แล้วเข้าไปได้เลย
เหมาะกับคำถามยอดฮิตที่ต้องตอบให้ถูกเป๊ะระเบียบ คู่มือ ประกาศ ที่มีอยู่แล้ว

ในทางปฏิบัติควรใช้ทั้งสองอย่างผสมกัน เอาเอกสารที่มีอยู่โยนเข้าไปก่อนให้ครอบคลุมกว้าง ๆ แล้วค่อยเขียนตาราง Q&A เสริมเฉพาะคำถามที่ต้องตอบให้ถูกเป๊ะ

ไฟล์ตัวอย่างที่ใช้ในคอร์ส

ไฟล์เนื้อหาทดสอบเส้นทาง
QA_Chatbot_Knowledge_TH.xlsxQ&A 45 รายการ 7 หมวดExcel
ระเบียบการลาพนักงาน.docxลาพักร้อน ลาป่วย ลาคลอด ลาทหาร ขาดงาน + ตารางDOCX
คู่มือสวัสดิการ.pdfประกันกลุ่ม กองทุนสำรองฯ เบิกค่าเดินทาง เงินช่วยเหลือPDF
นโยบายความปลอดภัยไอที.txtรหัสผ่าน 2FA ฟิชชิง สำรองข้อมูลText
คู่มือใช้งานระบบภายใน.mdระบบ HR, Helpdesk, จองห้อง, จัดซื้อMarkdown
ประกาศบริษัท.htmlนโยบายไฮบริด WFH ค่าอินเทอร์เน็ต ยืมครุภัณฑ์HTML

เนื้อหาทั้ง 6 ไฟล์ไม่ซ้ำกัน จึงใช้ตรวจได้ว่าบอทดึงคำตอบมาจากไฟล์ไหน ข้อมูลทั้งหมดเป็นเรื่องสมมติ ตัวเลขแต่งขึ้นเพื่อการอบรม ห้ามนำไปอ้างอิงจริง

รูปแบบไฟล์ Excel

questionanswercategorykeywords
บริษัทเปิดทำการกี่โมงเปิดวันจันทร์–ศุกร์ เวลา 08.00–17.00 น.ทั่วไปเวลาทำการ,เปิดกี่โมง
ลาพักร้อนได้กี่วันเป็นไปตามอายุงานและระเบียบบริษัทHRพักร้อน,annual leave

คอลัมน์ keywords ไม่บังคับ แต่ช่วยได้มาก ใส่คำที่ผู้ใช้น่าจะพิมพ์แต่ไม่มีในคำถามหลัก คั่นด้วยจุลภาค ตัวอย่างที่เห็นผลชัดคือคำถาม "ขอ WFH ได้ไหม" จับคู่กับ "ทำงานที่บ้านได้หรือไม่" ได้ ทั้งที่ไม่มีตัวอักษรตรงกันสักตัว เพราะคำว่า WFH อยู่ใน keywords

โค้ดรองรับหัวคอลัมน์ภาษาไทยด้วย: คำถาม, คำตอบ, หมวดหมู่, คำค้น

บทที่ 4 — แยกเส้นทางตามสกุลไฟล์

โหนด Route_File_Type เป็น Switch node ที่อ่านนามสกุลไฟล์จาก binary แล้วส่งไปตามสาขา

ผังการทำงาน
={{ $binary.QA_File.fileExtension.toLowerCase() }}

เทียบด้วย regex ทีละกฎ ^xlsx?$^pdf$^docx$^(txt|md|markdown|html?|htm)$ และถ้าไม่เข้ากฎไหนเลยจะตกลง fallback output ไปยังโหนดที่ตอบกลับผู้ใช้ว่าไม่รองรับสกุลนี้

จุดที่พลาดกันบ่อย — ทำไม label ของฟิลด์ไฟล์ในฟอร์มต้องเป็น QA_File ที่เป็นอังกฤษล้วน? เพราะ n8n เอา label ของฟิลด์ ไปตั้งเป็นชื่อ binary property แล้วทุกโหนดถัดไปต้องอ้างชื่อนี้ให้ตรงเป๊ะ ถ้าตั้ง label เป็น "ไฟล์ Q&A (Excel)" ชื่อ binary จะกลายเป็นอะไรที่มีวงเล็บและช่องว่างซึ่งเดายาก การใช้ ASCII ล้วนทำให้ชีวิตง่ายขึ้นมาก

บทที่ 5 — อ่านเนื้อหาจากแต่ละสกุลไฟล์

Excel และ PDF: ใช้โหนดสำเร็จรูป

n8n มีโหนด Extract from File ที่ทำให้อยู่แล้ว ตั้ง Operation เป็น Extract From XLSX หรือ Extract From PDF และตั้ง Input Binary Field เป็น QA_File

ข้อจำกัดของ PDF ที่ต้องบอกผู้เรียนไว้ก่อน: PDF ที่เป็นภาพสแกนจะอ่านไม่ได้ เพราะข้างในไม่มีข้อความ มีแต่รูป ต้องผ่าน OCR มาก่อน ระบบจะแจ้งเตือนให้เองว่าอ่านไฟล์ได้แต่ไม่พบข้อความ

DOCX: ต้องเขียนเอง

n8n ไม่มีตัวอ่าน .docx แต่เรื่องนี้แก้ได้ไม่ยากถ้ารู้ความลับข้อหนึ่ง — ไฟล์ .docx คือไฟล์ ZIP ที่ข้างในมี XML หลายไฟล์ ตัวเนื้อหาอยู่ที่ word/document.xml

โค้ดในโหนด Docx_To_Text จึงทำสามขั้น

JavaScript
// 1. ดึงไฟล์ binary ออกมาเป็น Buffer
const buf = await this.helpers.getBinaryDataBuffer(0, 'QA_File');

// 2. หา End of Central Directory ของ ZIP แล้วไล่หาไฟล์ที่ต้องการ
//    เจอแล้วคลายบีบอัดด้วย zlib.inflateRawSync
const xml = unzipEntry(buf, 'word/document.xml').toString('utf8');

// 3. แปลง XML เป็นข้อความ โดยรักษาโครงสร้างย่อหน้าและตารางไว้
const text = xml
  .replace(/<w:tab[^>]*\/>/g, '\t')
  .replace(/<w:br[^>]*\/>/g, '\n')
  .replace(/<\/w:p>/g, '\n')      // จบย่อหน้า = ขึ้นบรรทัดใหม่
  .replace(/<\/w:tr>/g, '\n')     // จบแถวตาราง
  .replace(/<\/w:tc>/g, ' | ')    // จบช่องตาราง
  .replace(/<[^>]+>/g, '')        // ตัดแท็กที่เหลือทิ้ง
  .replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&')
  .trim();

ลำดับการ replace สำคัญมาก ต้องแปลงแท็กที่มีความหมายทางโครงสร้าง (</w:p> </w:tr> </w:tc>) ให้เป็นตัวคั่นก่อน แล้วค่อยตัดแท็กที่เหลือ ถ้าตัดแท็กทั้งหมดทีเดียว ย่อหน้าและตารางจะกลายเป็นข้อความก้อนเดียวติดกันหมด

TXT, Markdown, HTML: อ่านเป็นข้อความแล้วทำความสะอาด

ทั้งสามสกุลอ่านด้วย Operation Extract From Text File เหมือนกัน แล้วไปทำความสะอาดต่อในโหนดถัดไป

HTML ต้องถอดแท็กออก โดยแปลงแท็กปิดของบล็อกเป็นการขึ้นบรรทัดใหม่ก่อน ไม่งั้นหัวข้อกับย่อหน้าจะติดกันเป็นพืด ส่วน Markdown ต้องตัด #, **, backtick และบล็อกโค้ดทิ้ง เพราะสัญลักษณ์พวกนี้ไม่ใช่เนื้อหา แต่จะไปรบกวนการจับคู่ตัวอักษรในบทที่ 7

บทที่ 6 — ตัดเอกสารเป็นท่อน และบันทึกลงฐานความรู้

ทำไมต้องตัดท่อน

ถ้าเก็บเอกสารทั้งไฟล์เป็นรายการเดียว เวลาค้นเจอเราจะต้องส่งเอกสารทั้งฉบับให้ AI ซึ่งเปลืองโทเคนมหาศาลและ AI จะหาคำตอบในกองข้อความไม่เจอ การตัดเป็นท่อนย่อยทำให้ส่งเฉพาะส่วนที่เกี่ยวข้องได้

โหนด Text_To_Chunks ตัดที่ประมาณ 700 ตัวอักษรต่อท่อน ซ้อนทับกัน 120 ตัวอักษร โดยพยายามตัดตามขอบเขตย่อหน้าก่อน ถ้าย่อหน้าไหนยาวเกินก็ค่อยตัดตรงกลาง

ส่วนที่ซ้อนทับกันสำคัญมาก ถ้าตัดแบบไม่ซ้อน แล้วประโยคสำคัญดันอยู่คร่อมรอยตัดพอดี จะไม่มีท่อนไหนมีข้อมูลครบเลย

ผลกับไฟล์ตัวอย่าง: docx ได้ 4 ท่อน, pdf ได้ 4 ท่อน, txt ได้ 4 ท่อน, md ได้ 3 ท่อน, html ได้ 2 ท่อน รวม 17 ท่อน บวก Q&A 45 รายการ เป็น 62 รายการในฐานความรู้

ท่าไม้ตายเล็ก ๆ สำหรับ PDF ภาษาไทย

PDF ที่ export จาก Word หรือ LibreOffice มักแทรกช่องว่างกลางคำไทย เช่น ฝ่ าย ซึ่งทำให้ข้อความที่ส่งให้ AI อ่านแปลก โค้ดจึงมีบรรทัดนี้

JavaScript
raw = raw.replace(/[ \t]+([\u0E30-\u0E3A\u0E47-\u0E4E])/g, '$1');

ตรรกะคือ ภาษาไทยไม่มีกรณีที่เว้นวรรคหน้าสระหรือวรรณยุกต์ได้ ช่องว่างตรงนั้นจึงผิดเสมอ ลบทิ้งได้อย่างปลอดภัย

กับดักใหญ่ที่สุดของคอร์สนี้

อ่านหัวข้อนี้ให้ดี เพราะเป็นจุดที่คนติดกันมากที่สุด

$getWorkflowStaticData() จะถูก เขียนลงฐานข้อมูลจริงเฉพาะตอนที่ workflow ทำงานในโหมด production เท่านั้น คือ workflow ต้องถูก Activate และถูกเรียกผ่าน Production URL

ถ้ากดปุ่ม Test workflow แล้วอัปโหลดผ่าน URL ที่มีคำว่า /form-test/ ข้อมูลจะอยู่แค่ใน memory ของ execution นั้น พอจบก็หายเกลี้ยง อาการที่เจอคือ อัปโหลดสำเร็จ เห็นข้อความว่านำเข้า 17 ท่อน แต่พอไปถามในแชทกลับบอกว่ายังไม่มีข้อมูลในฐานความรู้

ขั้นตอนที่ถูกต้อง

  1. Save แล้วสลับ Active ให้เป็นสีเขียว
  2. เปิดโหนด Upload_Knowledge เลือกแท็บ Production URL (ต้องเป็น /form/upload-knowledge ไม่ใช่ /form-test/)
  3. อัปโหลดไฟล์ผ่าน URL นั้น ครั้งแรกเลือกโหมด "ล้างของเดิมแล้วนำเข้าใหม่" ครั้งต่อไปเลือก "เพิ่มต่อจากของเดิม"
  4. ตรวจด้วยคำสั่ง /stats และ /sources ในแชท

ฝั่งแชทเป็นการอ่านอย่างเดียว จึงทำงานได้ทั้งโหมด test และ production ทดสอบผ่านหน้าต่าง Chat ในเอดิเตอร์ได้เลย

อีกเรื่องคือ static data ผูกกับ workflow ต่อหนึ่ง instance ถ้าทดสอบบน n8n Cloud แล้วมา import ลงเครื่องตัวเอง ข้อมูลไม่ตามมาด้วย ต้องอัปโหลดใหม่

การจัดการข้อมูลซ้ำ

🔎 ยืนยันกับเอกสาร n8n แล้ว: พฤติกรรมของ $getWorkflowStaticData() ตรงตามที่บทนี้เตือนไว้จริง — เอกสารทางการระบุว่า static data ไม่ถูกบันทึกตอนกดทดสอบ workflow workflow ต้องถูก Activate และถูกเรียกผ่าน trigger หรือ webhook จริงเท่านั้น นี่คือสาเหตุอันดับหนึ่งที่ผู้เรียนอัปโหลดไฟล์แล้วบอทบอกว่าไม่มีข้อมูล

บทที่ 7 — หัวใจของคอร์ส: ทำไมโค้ดค้นหาแบบฝรั่งใช้กับภาษาไทยไม่ได้

ปัญหา

ถ้าไปหา tutorial การทำ Q&A matching ในเน็ต เกือบทั้งหมดจะเขียนประมาณนี้

JavaScript
function tokens(s) {
  return s.toLowerCase().split(/\s+/).filter(Boolean);
}
function score(a, b) {
  const A = new Set(tokens(a));
  const B = new Set(tokens(b));
  let hit = 0;
  for (const x of A) if (B.has(x)) hit++;
  return hit / Math.max(A.size, B.size);
}

ตรรกะคือ "ตัดประโยคเป็นคำด้วยช่องว่าง แล้วนับว่าคำซ้ำกันกี่คำ" ทำงานได้ดีมากกับภาษาอังกฤษ

แต่กับภาษาไทย มันพังทั้งระบบ เพราะภาษาไทยไม่เว้นวรรคระหว่างคำ

JavaScript
tokens("บริษัทเปิดทำการกี่โมง")   // → ["บริษัทเปิดทำการกี่โมง"]  ได้ token เดียว
tokens("เปิดกี่โมง")              // → ["เปิดกี่โมง"]
// เทียบกัน: ไม่มี token ไหนตรงกันเลย → score = 0

ผู้ใช้พิมพ์ "เปิดกี่โมง" ซึ่งเป็นส่วนหนึ่งของคำถามในฐานข้อมูลตรง ๆ แต่ระบบให้คะแนน 0 แล้วตอบว่าไม่พบคำตอบ นี่คือสาเหตุที่แชทบอทภาษาไทยหลายตัวโง่แบบอธิบายไม่ถูก

ทางแก้ที่ 1: character n-gram

แทนที่จะตัดเป็น "คำ" เราตัดเป็น ชุดตัวอักษรที่ต่อเนื่องกัน ยาว n ตัว วิธีนี้ไม่ต้องรู้จักขอบเขตของคำเลย จึงใช้ได้กับทุกภาษา

JavaScript
grams("เปิดกี่โมง", 2)
// → {"เป", "ปิ", "ิด", "ดก", "กี", "ี่", "่โ", "โม", "มง"}

แล้ววัดความคล้ายด้วย Dice coefficient

JavaScript
function dice(A, B) {
  if (!A.size || !B.size) return 0;
  let hit = 0;
  for (const g of A) if (B.has(g)) hit++;
  return (2 * hit) / (A.size + B.size);
}

function simText(a, b) {
  // ผสม bi-gram กับ tri-gram คนละครึ่ง
  let s = 0.5 * dice(grams(a, 2), grams(b, 2)) + 0.5 * dice(grams(a, 3), grams(b, 3));

  // โบนัส: ถ้าฝั่งหนึ่งเป็นส่วนหนึ่งของอีกฝั่งเป๊ะ ๆ
  const ca = compact(a), cb = compact(b);
  if (ca.length >= 2 && cb.length >= 2 && (cb.includes(ca) || ca.includes(cb))) {
    s = Math.max(s, 0.72 + 0.28 * (Math.min(ca.length, cb.length) / Math.max(ca.length, cb.length)));
  }
  return Math.min(1, s);
}

ทำไมผสม bi-gram กับ tri-gram? bi-gram จับความคล้ายได้กว้างแต่มั่วง่าย เพราะตัวอักษรคู่ซ้ำกันบ่อยในภาษาไทย ส่วน tri-gram แม่นกว่าแต่เข้มเกินไปกับคำถามสั้น ผสมกันคนละครึ่งได้จุดสมดุลที่ใช้งานได้จริง

ทำไมต้องมีโบนัส substring? คำถาม "เปิดกี่โมง" (9 ตัวอักษร) เทียบกับ "บริษัทเปิดทำการกี่โมง" (21 ตัวอักษร) ต่อให้ตรงกันทุก gram ของฝั่งสั้น ค่า Dice ก็ยังต่ำเพราะตัวหารรวมฝั่งยาวไปด้วย ทั้งที่ในความหมายมันคือคำถามเดียวกัน โบนัสตัวนี้ดันคะแนนขึ้นไปที่ 0.95 ตามที่ควรจะเป็น

ผลทดสอบกับ Q&A จริง

ผู้ใช้พิมพ์คะแนนผลลัพธ์
เปิดกี่โมง0.950ตอบถูก
อยากลาพักร้อน กี่วัน0.705ตอบถูก
โอทีคิดยังไง0.950ตอบถูก (จับ "ค่าล่วงเวลาคิดอย่างไร")
เงินเดือนไม่เข้าทำไง0.508ตอบถูก
ขอสูตรทำต้มยำกุ้ง0.075ปฏิเสธถูกต้อง

ถ้าใช้โค้ดแบบ token เดิม สี่ข้อแรกจะได้ 0 ทั้งหมด

ทางแก้ที่ 2: coverage สำหรับเอกสารยาว

Dice ใช้ได้ดีกับการเทียบ "คำถามกับคำถาม" ที่ยาวพอ ๆ กัน แต่เทียบ "คำถามสั้นกับย่อหน้ายาว" ไม่ได้ เพราะตัวหารจะบวมจนคะแนนต่ำเสมอ

สำหรับเอกสารจึงใช้ coverage แทน คือดูว่า n-gram ของคำถามไปปรากฏในเนื้อหากี่เปอร์เซ็นต์

JavaScript
function coverage(A, B) {
  if (!A.size || !B.size) return 0;
  let hit = 0;
  for (const g of A) if (B.has(g)) hit++;
  return hit / A.size;   // หารด้วยขนาดของคำถามอย่างเดียว
}

ปัญหาที่เจอตอนทดสอบจริง และวิธีแก้

ตอนแรกใช้ coverage กับทั้งท่อน ผลคือ ท่อนที่ยาวกว่าชนะเสมอ เพราะยิ่งยาวยิ่งมีตัวอักษรให้บังเอิญตรงเยอะ

ถามว่า "ขาดงานกี่วันถึงโดนไล่ออก" ระบบกลับหยิบท่อนเรื่องลาพักร้อนที่ยาว 666 ตัวอักษรมาเป็นอันดับหนึ่ง ส่วนท่อนเรื่องการขาดงานจริง ๆ ที่ยาว 175 ตัวอักษรหลุดจาก Top 3 ไปเลย

แก้ด้วยการ เลื่อนหน้าต่างไปตามเนื้อหา แล้วเอาเฉพาะช่วงที่ตรงที่สุด

JavaScript
function windowCoverage(query, body) {
  const t = compact(body);
  const cq = compact(query);
  if (!t || !cq) return 0;

  const W = Math.max(120, cq.length * 6);   // หน้าต่างกว้าง 6 เท่าของคำถาม
  const step = Math.max(40, Math.floor(W / 2));
  const A2 = grams(query, 2), A3 = grams(query, 3);

  let best = 0;
  for (let i = 0; i < Math.max(1, t.length - W + step); i += step) {
    const w = t.slice(i, i + W);
    const s = 0.6 * coverage(A2, grams(w, 2)) + 0.4 * coverage(A3, grams(w, 3));
    if (s > best) best = s;
  }
  return best;
}

หลังแก้ คำถามเดิมหยิบท่อนเรื่องการขาดงานขึ้นมาเป็นอันดับ 1 ทันที

นี่คือบทเรียนที่อยากให้ผู้เรียนจำ: อัลกอริทึมที่ดูถูกต้องบนกระดาษ อาจมีอคติที่มองไม่เห็นจนกว่าจะทดสอบกับข้อมูลจริง ในกรณีนี้คืออคติเรื่องความยาว ซึ่งเป็นกับดักคลาสสิกของงาน information retrieval

ผลทดสอบข้ามไฟล์

ทดสอบ 8 คำถามที่คำตอบกระจายอยู่ใน 5 ไฟล์ หยิบไฟล์ถูกทั้ง 8

คำถามไฟล์ที่หยิบมา
เบิกค่าที่พักได้คืนละเท่าไรคู่มือสวัสดิการ.pdf
รหัสผ่านต้องยาวกี่ตัวนโยบายความปลอดภัยไอที.txt
ยืมจอคอมไปใช้ที่บ้านได้ไหมประกาศบริษัท.html
ลาคลอดได้กี่วันระเบียบการลาพนักงาน.docx
ใบขอซื้อเกินแสนต้องผ่านใครคู่มือใช้งานระบบภายใน.md

ข้อจำกัดที่ต้องพูดตรง ๆ

การจับคู่ระดับตัวอักษรแยกคำถามจริงกับคำถามนอกเรื่องได้ไม่ 100% จากการวัด คำถามที่เกี่ยวข้องจริงบางข้อได้ 0.40 ขณะที่คำถามนอกเรื่องอย่าง "ราคาทองวันนี้" ได้ 0.37 ช่วงคะแนนมันทับกัน

ตั้งเกณฑ์สูงไปคำถามจริงจะถูกปฏิเสธ ตั้งต่ำไปคำถามมั่วจะหลุดเข้ามา ค่าที่ใช้อยู่คือ MIN_QA = 0.22 และ MIN_DOC = 0.33 เป็นจุดที่สมดุลที่สุดจากการทดสอบ

ด่านที่สองที่รับไม้ต่อคือ system prompt ของ AI ที่สั่งให้บอกว่าไม่มีข้อมูลเมื่อ context ไม่เกี่ยวข้อง ซึ่งจะพูดถึงในบทถัดไป

บทที่ 8 — ต่อ OpenAI แบบ RAG

ทำไมไม่ถาม AI ตรง ๆ

ถ้าโยนคำถามให้ ChatGPT ตรง ๆ ว่า "ลาพักร้อนได้กี่วัน" มันจะตอบตามกฎหมายแรงงานทั่วไปหรือแต่งขึ้นมาเอง ซึ่งไม่ใช่ระเบียบของบริษัทคุณ และที่แย่กว่าคือมันจะตอบด้วยน้ำเสียงมั่นใจจนคนอ่านไม่เอะใจ

รูปแบบที่เราใช้เรียกว่า RAG (Retrieval-Augmented Generation) คือ ค้นก่อน แล้วให้ AI เรียบเรียงเฉพาะสิ่งที่ค้นเจอ

ผังการทำงาน
คำถาม → ค้นหาด้วยโค้ด (เร็ว ฟรี แม่นยำเรื่องที่มา) → ส่ง Top 3 ให้ AI → AI เรียบเรียงให้ลื่น

ได้ความลื่นของ AI โดยยังคุมความถูกต้องไว้ได้ และประหยัดกว่ามากเพราะส่งเฉพาะข้อมูลที่เกี่ยวข้อง ไม่ใช่ทั้งฐานความรู้

ประตูกันโทเคน

โหนด IF_Use_AI ตัดสินว่าจะเรียก AI หรือไม่ กรณีที่ ไม่เรียก คือ

กรณีเหล่านี้ตอบจากโค้ดโดยตรง ไม่เสียเงินสักบาท

System prompt ที่คุมไม่ให้มั่ว

หัวใจอยู่ที่ 5 ข้อนี้

  1. ตอบโดยอ้างอิงจากข้อมูลอ้างอิงเท่านั้น ห้ามเดา ห้ามเติมจากความรู้ทั่วไป แม้จะมั่นใจว่าถูกก็ตาม
  2. ถ้าข้อมูลไม่พอ ให้บอกตรง ๆ ว่าไม่มีข้อมูล และแนะนำให้ติดต่อผู้ดูแล
  3. ห้ามพูดถึงคำว่า "ข้อมูลอ้างอิง" หรือ "เอกสาร" ให้ตอบเหมือนเจ้าหน้าที่ที่รู้เรื่องนี้อยู่แล้ว
  4. ถามต่อเนื่องได้ ใช้บริบทการสนทนาประกอบ แต่เนื้อหาต้องมาจากข้อมูลอ้างอิงเท่านั้น
  5. ห้ามเปลี่ยนตัวเลข วันเวลา หรือชื่อหน่วยงาน ให้คงไว้ตามเดิมเป๊ะ ๆ

ข้อ 5 สำคัญกว่าที่คิด โมเดลชอบ "ปรับ" รูปแบบเวลา 08.00–17.00 น. หรือปัดตัวเลขให้สวยงามโดยไม่ได้ตั้งใจ ซึ่งในเอกสารระเบียบคือความผิดพลาดร้ายแรง

ตั้ง temperature ไว้ที่ 0.2 ด้วย เพราะงานนี้ต้องการความสม่ำเสมอ ไม่ใช่ความสร้างสรรค์

ความจำบทสนทนา

Chat_Memory ตั้งไว้ 10 ข้อความ = จำย้อนหลัง 5 คู่ถาม-ตอบ ทำให้ถามต่อเนื่องได้

ผังการทำงาน
ผู้ใช้: ลาพักร้อนได้กี่วัน
บอท:   ปีละ 6 วันทำการ เพิ่มขึ้นตามอายุงาน สูงสุด 12 วัน
ผู้ใช้: แล้วต้องยื่นล่วงหน้ากี่วัน     ← บอทเข้าใจว่ายังพูดเรื่องลาพักร้อนอยู่

บทที่ 9 — คำนวณต้นทุนจริง

หัวข้อนี้มักเป็นคำถามแรกของผู้บริหาร และเป็นหัวข้อที่ tutorial ส่วนใหญ่ข้าม

ขั้นที่ 1: ประมาณจากขนาด prompt

วัด prompt จริงที่ workflow ส่งออกไป คำถาม "ลาคลอดได้กี่วัน" ประกอบด้วย

ภาษาไทยกินโทเคนมากกว่าอังกฤษ ผมประมาณไว้ที่ 2 ตัวอักษรต่อโทเคน จึงได้ราว 900 โทเคน คิดเป็นเงินประมาณ 0.006 บาทต่อคำถาม

ขั้นที่ 2: วัดของจริง

พอรันจริงแล้วดูตัวเลขจากโหนด OpenAI ปรากฏว่าใช้ 467 input tokens ไม่ใช่ 900

คิดกลับเป็น 3.9 ตัวอักษรต่อโทเคน แปลว่าตัวแบ่งคำ o200k ของ GPT-4o จัดการภาษาไทยได้ดีกว่าที่ประมาณไว้เกือบเท่าตัว การประมาณของผมพลาดไป 100%

บทเรียนสำหรับผู้เรียน: ตัวเลขที่วัดได้ชนะตัวเลขที่ประมาณเสมอ และวิธีวัดคือดู tokenUsage ในโหนด OpenAI_Model หลังรัน หรือดูหน้า Usage ในบัญชี OpenAI

ขั้นที่ 3: ตัวเลขสำหรับวางแผน

เรต gpt-4o-mini อยู่ที่ $0.15 ต่อล้านโทเคนขาเข้า และ $0.60 ต่อล้านโทเคนขาออก (เรตแลกเปลี่ยน 33.3 บาทต่อดอลลาร์)

รายการโทเคนค่าใช้จ่าย
Input (คำถามแรกของเซสชัน)467$0.000070
Output (คำตอบ 2 ประโยค)~70$0.000042
รวมต่อคำถาม~540$0.00011 ≈ 0.0037 บาท

แต่ตัวเลข 467 เป็นคำถามแรกที่ยังไม่มีประวัติสนทนา พอคุยต่อเนื่อง Chat_Memory จะแนบข้อความเก่าไปด้วย

รอบที่Input tokens
1467
2–5ไต่ขึ้นทีละ ~55 โทเคน
6 เป็นต้นไป~750 (คงที่ เพราะข้อความเก่าสุดถูกดันออก)

งาน FAQ องค์กรส่วนใหญ่คนถาม 2-3 คำถามแล้วจบ ตัวเลขที่ผมแนะนำให้ใช้วางแผนคือ

ค่าเฉลี่ย 600 โทเคน = 0.0042 บาทต่อคำถาม กันงบไว้ที่ 5 บาทต่อ 1,000 คำถาม
ปริมาณค่าใช้จ่าย
1,000 คำถาม~5 บาท
10,000 คำถาม~50 บาท
100,000 คำถาม~500 บาท

เอาเข้าจริง องค์กรที่มีพนักงาน 200 คน ถามเฉลี่ยคนละ 3 คำถามต่อเดือน = 600 คำถามต่อเดือน คิดเป็น ค่าใช้จ่ายเดือนละประมาณ 3 บาท

แนะนำให้ตั้ง usage limit ในบัญชี OpenAI ไว้ที่ 5 ดอลลาร์ต่อเดือนก่อน จะได้ไม่ต้องกังวลตอนเปิดให้คนอื่นทดลองใช้ และถ้าเติมเครดิตล่วงหน้าควรรู้ว่าเครดิตของ OpenAI มีอายุ 1 ปี สำหรับสเกลนี้เติมทีละน้อยคุ้มกว่าเติมทีเดียวเยอะ ๆ

🔎 ตรวจกับหน้าราคาทางการแล้ว (ก.ย. 2569): ราคา gpt-4o-mini ยังเป็น $0.15 ต่อล้านโทเค็นขาเข้า และ $0.60 ต่อล้านโทเค็นขาออก ตรงตามที่คำนวณไว้ในบทนี้ แต่ตอนนี้ มีรุ่นที่ถูกกว่าให้เลือกแล้ว เช่น gpt-5-nano ที่ $0.05 / $0.40 ถ้างานเป็นการเรียบเรียงคำตอบสั้น ๆ แบบในคอร์สนี้ ลองสลับโมเดลดูแล้ววัดคุณภาพคำตอบเทียบกัน อาจลดต้นทุนลงได้อีกราวครึ่งหนึ่ง
⚠️ ตัวเลขบาทในตารางคิดที่ 33.3 บาทต่อดอลลาร์ อัตราแลกเปลี่ยนเปลี่ยนตลอด ให้ดูที่ตัวเลขดอลลาร์เป็นหลัก

บทที่ 10 — ตกแต่งหน้าแชทให้เป็นแบรนด์ตัวเอง

โหนด Chat Trigger มีช่อง Custom CSS ที่รับ CSS variable ของหน้าแชทได้ทั้งหมด

CSS
:root {
  --brand: #FFA503;
  --ink:   #241A06;

  --chat--header--background: var(--brand);
  --chat--header--color: var(--ink);
  --chat--message--user--background: var(--brand);
  --chat--message--user--color: var(--ink);
  --chat--color-light: #FFFFFF;
  --chat--message-line-height: 1.75;
  --chat--font-family: 'Noto Sans Thai', -apple-system, sans-serif;
}
ผลลัพธ์หน้าแชทหลังใส่ธีมสีส้ม
ผลลัพธ์หลังวางไฟล์ธีมลงช่อง Custom CSS — หัวแชทพื้นเข้มตัดเส้นส้ม ฟองข้อความของผู้ใช้เป็นสีส้มพร้อมตัวอักษรสีเข้มตามที่อธิบายไว้ด้านล่าง

สองเรื่องที่ต้องรู้เมื่อทำธีมภาษาไทย

ข้อแรก ถ้าสีแบรนด์เป็นโทนสว่างอย่างส้ม #FFA503 ห้ามใช้ตัวอักษรสีขาวทับ คอนทราสต์จะได้แค่ประมาณ 1.9:1 ต่ำกว่าเกณฑ์ WCAG ที่ต้องการ 4.5:1 อ่านไม่ออกบนมือถือกลางแดด เปลี่ยนเป็นตัวอักษรสีเข้มจะได้ประมาณ 11:1 และยังดูเป็นแบรนด์เหมือนเดิม

ข้อสอง ตั้ง line-height ที่ 1.7–1.8 ไม่ใช่ 1.5 ตามค่าเริ่มต้น เพราะภาษาไทยมีสระบนและสระล่างที่จะชนกันเมื่อบรรทัดแคบ จุดนี้ต่างชัดเจนเวลาบอทตอบยาว ๆ

ใส่โลโก้และลายน้ำ ทำผ่าน pseudo element

CSS
.chat-header::before {
  content: '';
  display: block;
  width: 120px; height: 48px;
  background: url('โลโก้ของคุณ.webp') left center / contain no-repeat;
  margin-bottom: 0.55rem;
}

.chat-body { position: relative; isolation: isolate; }
.chat-body::before {
  content: '';
  position: absolute; inset: 0;
  background: url('พื้นหลังของคุณ.webp') center / 55% no-repeat;
  opacity: 0.06;          /* จางมาก ไม่กวนการอ่าน */
  pointer-events: none;
  z-index: -1;
}

เหตุที่ลายน้ำต้องใส่ผ่าน pseudo element แทนที่จะเป็น background-image ของ .chat-body ตรง ๆ เพราะ opacity จะไปกดตัวอักษรให้จางตามไปด้วยทั้งหมด

บทที่ 11 — ปัญหาที่เจอบ่อยและวิธีแก้

Custom data key can only contain letters, numbers, and underscores

ชื่อโหนด Chat Trigger มีอีโมจิหรืออักษรไทย n8n เอาชื่อโหนดไปประกอบเป็นคีย์ภายใน (input_<ชื่อโหนด>) ซึ่งรับเฉพาะ a-z, 0-9 และ underscore เปลี่ยนชื่อโหนดเป็น Chatbot แล้วบันทึกใหม่ แนะนำให้ตั้งชื่อโหนดทุกตัวเป็น ASCII เพราะต้องเอาไปอ้างในนิพจน์อย่าง $('ชื่อโหนด') อยู่แล้ว

Conflicting Chat Path — activate ไม่ได้

มี workflow อื่นที่ Active อยู่และใช้ webhook เดียวกัน เกิดจากการ import ไฟล์ JSON เดียวกันซ้ำ เพราะ webhookId ติดมากับไฟล์ ทางแก้คือปิด Active ของตัวเดิม หรือถ้าอยากให้ทั้งคู่ทำงานพร้อมกัน ต้องสุ่ม webhookId ใหม่และเปลี่ยน path ของฟอร์ม (การ Duplicate ผ่านเมนูของ n8n จะสุ่มให้อัตโนมัติ แต่การ import จากไฟล์ไม่สุ่มให้)

อัปโหลดสำเร็จแต่แชทบอกว่าไม่มีข้อมูล

ยังไม่ได้ Activate หรืออัปโหลดผ่าน Test URL — กลับไปอ่านกับดักท้ายบทที่ 6

อ่านไฟล์ DOCX ไม่ได้

ยังไม่ได้ตั้ง NODE_FUNCTION_ALLOW_BUILTIN=zlib หรือตั้งแล้วแต่ไม่ได้รีสตาร์ท n8n

PDF อ่านได้แต่ไม่มีข้อความ

เป็น PDF ที่สแกนมาเป็นภาพ ต้องผ่าน OCR ก่อน

บอทตอบผิดข้อบ่อย

ลด MIN_QA ถ้าปฏิเสธบ่อยเกิน หรือเพิ่มถ้าตอบมั่ว แต่ทางแก้ที่ดีกว่าคือเติมคอลัมน์ keywords ให้คำถามที่มีปัญหา

keywords ชนกันระหว่างสองแถว

เช่นคำว่า "รถบริษัท" อยู่ทั้งในแถว "รถรับส่งพนักงาน" และ "ขอใช้รถส่วนกลาง" ระบบอาจหยิบผิดแถว แต่เพราะเราส่ง Top 3 ให้ AI เสมอ AI จึงเห็นทั้งสองแถวและเลือกตอบข้อที่ตรงกว่าได้เอง — ข้อดีของการต่อ AI ที่ระบบค้นหาอย่างเดียวไม่มี

บทที่ 12 — ข้อจำกัดและทางต่อยอด

ข้อจำกัดที่ต้องรู้ก่อนใช้งานจริง

Static data ไม่ได้ออกแบบมาเก็บข้อมูลเยอะ เอกสารสัก 50–100 หน้ายังไหว แต่ถ้ามากกว่านั้นควรย้ายที่เก็บ และถ้ามีคนแก้ข้อมูลหลายคนพร้อมกัน static data ก็ไม่เหมาะ

การจับคู่ระดับตัวอักษรไม่เข้าใจความหมาย ถามว่า "ขอวันหยุดชดเชย" กับเอกสารที่เขียนว่า "การชดเชยวันทำงานในวันหยุด" ระบบจะจับได้เพราะตัวอักษรซ้ำ แต่ถ้าถามว่า "อยากพักผ่อน" กับเอกสารที่เขียนว่า "การลาพักร้อน" ระบบจะจับไม่ได้เลย เพราะไม่มีตัวอักษรตรงกัน

ทางต่อยอด

เปลี่ยนที่เก็บเป็น Google Sheets เหมาะเมื่อต้องการให้หลายคนแก้ไขพร้อมกันและเห็นข้อมูลตรง ๆ ตรรกะการค้นหาใช้โค้ดเดิมได้ทั้งหมด เปลี่ยนแค่โหนดอ่าน-เขียน

ย้ายไป Cloudflare D1 หรือ KV เมื่อมีข้อมูลหลักพันรายการและต้องการความเร็ว เขียน Worker เล็ก ๆ ทำหน้าที่ค้นหา แล้วให้ n8n เรียกผ่าน HTTP Request node

เปลี่ยนไปใช้ embedding + vector store คือทางแก้ที่แท้จริงของข้อจำกัดเรื่องความหมาย ระบบจะเข้าใจว่า "อยากพักผ่อน" กับ "ลาพักร้อน" เกี่ยวกัน แลกกับค่า API ตอนสร้าง embedding และความซับซ้อนที่เพิ่มขึ้น n8n มีโหนด Vector Store ให้ใช้อยู่แล้ว

ฝังลงเว็บบริษัท Chat Trigger เปิดโหมด public ได้ n8n จะให้ URL หน้าแชทสำเร็จรูป หรือใช้แพ็กเกจ @n8n/chat ฝังเป็น widget มุมล่างขวา

แจ้งเตือนเมื่อบอทตอบไม่ได้ ต่อโหนด LINE Messaging API หรือ Slack หลังจากบันทึก miss เพื่อให้ผู้ดูแลรู้ทันทีว่ามีคำถามที่ตอบไม่ได้

⚠️ แก้จากต้นฉบับ: ต้นฉบับของคอร์สเขียนว่าให้ต่อโหนด LINE Notify เพื่อแจ้งเตือนผู้ดูแล แต่ LINE Notify ปิดบริการถาวรไปแล้วตั้งแต่ 31 มีนาคม 2568 (1 เม.ย. 2568 เป็นต้นมาใช้ไม่ได้ทุกฟีเจอร์) หน้านี้จึงเปลี่ยนเป็น LINE Messaging API ซึ่งเป็นตัวที่ LINE แนะนำให้ใช้แทน และมีโควตาข้อความฟรีต่อเดือนให้อยู่ — วิธีตั้งค่าดูได้ที่บทเรียน แจ้งเตือนผ่าน LINE
✏️ แบบฝึกหัด
  1. ง่าย — เพิ่ม Q&A ใหม่ 5 ข้อในไฟล์ Excel เกี่ยวกับหน่วยงานของตัวเอง แล้วอัปโหลดโหมด "เพิ่มต่อจากของเดิม" ตรวจผลด้วย /stats
  2. ง่าย — เขียนเอกสาร .txt เกี่ยวกับระเบียบของหน่วยงานตัวเอง 1 หน้า อัปโหลดแล้วถามคำถามที่ตอบได้จากเอกสารนั้น
  3. กลาง — เขียนคำสั่ง /category HR ที่แสดงเฉพาะคำถามในหมวด HR
  4. กลาง — ทดลองปรับ MIN_DOC เป็น 0.25 และ 0.45 แล้วบันทึกว่าคำถามชุดเดิม 10 ข้อได้ผลต่างกันอย่างไร สรุปว่าค่าไหนเหมาะกับข้อมูลของคุณ
  5. กลาง — ปรับ contextWindowLength จาก 10 เป็น 4 แล้ววัด token ที่ใช้จริงเปรียบเทียบกัน
  6. ยาก — เพิ่มฟีเจอร์ export ฐานความรู้ปัจจุบันออกมาเป็นไฟล์ Excel ผ่านฟอร์มอีกตัว (ใช้โหนด Convert to File)
  7. ยาก — เปลี่ยนที่เก็บจาก static data เป็น Google Sheets โดยไม่แก้ตรรกะการค้นหา
🎯 สรุป

สิ่งที่ทำให้ระบบนี้ใช้งานได้จริงไม่ใช่เทคโนโลยีที่ล้ำ แต่เป็นการตัดสินใจออกแบบสี่อย่าง

หนึ่ง แยกคนเพิ่มข้อมูลออกจากคนใช้ข้อมูล ระบบจึงอัปเดตตัวเองได้โดยไม่ต้องพึ่งโปรแกรมเมอร์

สอง เลือกอัลกอริทึมให้เหมาะกับภาษาที่ใช้จริง character n-gram ไม่ใช่ของใหม่หรือของฉลาด แต่มันตรงกับธรรมชาติของภาษาไทยที่ไม่เว้นวรรค ในขณะที่วิธีมาตรฐานที่ทุกคนคัดลอกกันมาใช้ไม่ได้เลย

สาม ให้ AI ทำเฉพาะสิ่งที่มันเก่ง คือเรียบเรียงภาษา ไม่ใช่จำข้อมูล การค้นหาปล่อยให้โค้ดทำ เพราะโค้ดไม่แต่งเรื่องและไม่คิดเงิน

สี่ เก็บคำถามที่ตอบไม่ได้ไว้เสมอ ระบบที่รู้ว่าตัวเองไม่รู้อะไร คือระบบที่พัฒนาตัวเองได้

ต้นทุนรวมอยู่ที่หลักสตางค์ต่อคำถาม และทุกคำตอบอ้างอิงจากเอกสารขององค์กรเท่านั้น

*ไฟล์ workflow และไฟล์ตัวอย่างทั้ง 6 ไฟล์ ดาวน์โหลดได้จากลิงก์ท้ายบทความ — import แล้วอย่าลืม Activate ก่อนทดสอบ*

🔗 แหล่งอ้างอิง

ตรวจกับเอกสารทางการเมื่อ กันยายน 2569 — ราคา ชื่อเมนู และเงื่อนไขบริการภายนอก เปลี่ยนแปลงได้ตลอด ก่อนนำไปสอนควรเปิดเช็กซ้ำจากลิงก์ด้านล่าง

🔗 บทเรียนที่ต่อยอดได้