สร้าง AI Chatbot ตอบคำถามองค์กรด้วย n8n
แชทบอทองค์กรส่วนใหญ่ตายเพราะเรื่องเดียว คือ ไม่มีใครอัปเดตความรู้ให้มัน ถ้าการเพิ่มคำถามต้องรอโปรแกรมเมอร์แก้โค้ด สุดท้ายบอทก็จะตอบข้อมูลเก่าไปเรื่อย ๆ จนไม่มีใครใช้
คอร์สนี้แก้ปัญหานั้นตรง ๆ ระบบที่เราจะสร้างให้ ฝ่ายบุคคลหรือธุรการอัปโหลดไฟล์เองได้ ทั้งตาราง Excel และเอกสารทั่วไปอย่าง PDF, Word, ข้อความธรรมดา, Markdown และ HTML บอทอัปเดตความรู้ทันทีโดยไม่ต้องแตะ workflow เลยแม้แต่โหนดเดียว
ส่วนการค้นหา เราไม่ได้ใช้โค้ดสำเร็จรูปจากอินเทอร์เน็ต เพราะโค้ดพวกนั้น ใช้กับภาษาไทยไม่ได้เลย — บทที่ 7 จะพิสูจน์ให้ดูว่าทำไม แล้วเขียนตัวที่ใช้ได้จริงขึ้นมาแทน
- Workflow n8n ที่ import ไปใช้ได้ทันที รองรับไฟล์ความรู้ 6 สกุล
- เข้าใจว่าทำไมอัลกอริทึมค้นหาข้อความมาตรฐานพังกับภาษาไทย และแก้อย่างไร
- เข้าใจรูปแบบ RAG ว่าทำไมต้องค้นก่อนแล้วค่อยให้ AI เรียบเรียง แทนที่จะถาม AI ตรง ๆ
- ตัวเลขต้นทุนจริงต่อคำถาม ที่วัดจากระบบที่รันจริง ไม่ใช่ตัวเลขในโบรชัวร์
- หน้าแชทที่ตกแต่งด้วยสีและโลโก้ขององค์กรตัวเอง
- n8n เวอร์ชัน 1.x หรือใหม่กว่า (self-host ด้วย npm หรือ Docker)
- OpenAI API key พร้อมเครดิตประมาณ 5 ดอลลาร์ (พอสำหรับผู้เรียนทั้งห้องทดลองทั้งวัน)
- ไฟล์ตัวอย่าง 6 ไฟล์ที่แจกในคอร์ส
- พื้นฐาน JavaScript ระดับอ่านออก ไม่ต้องเขียนเองก็เรียนได้
| ช่วง | เนื้อหา | เวลา |
|---|---|---|
| 1 | บทที่ 1–3 ติดตั้ง เปิดระบบ เห็นภาพรวม | 30 นาที |
| 2 | บทที่ 4–6 นำเข้าไฟล์ 6 สกุล ตัดท่อนเอกสาร | 45 นาที |
| 3 | บทที่ 7 อัลกอริทึมภาษาไทย (บทหลักของคอร์ส) | 40 นาที |
| 4 | บทที่ 8–9 ต่อ OpenAI แบบ RAG และคำนวณต้นทุน | 40 นาที |
| 5 | บทที่ 10–12 ตกแต่งหน้าแชท แก้ปัญหา ต่อยอด | 25 นาที |
ดาวน์โหลดเก็บไว้ก่อนเริ่มบทที่ 1 ได้เลย — ไฟล์ความรู้ทั้ง 6 สกุลเป็นข้อมูลสมมติของ “บริษัท ตัวอย่าง จำกัด” ไม่ใช่ข้อมูลจริงขององค์กรไหน จึงเอาไปทดลองอัปโหลดได้อย่างสบายใจ
⭐ ไฟล์หลัก — Workflow ที่ import เข้า n8n ได้ทันที
- 🔧n8n_Thai_QA_Chatbot_MultiFormat.json
Workflow ครบทั้ง 20 โหนด ทั้งเส้นนำเข้าความรู้และเส้นตอบคำถาม — ใช้ในบทที่ 2
📚 ไฟล์ความรู้ตัวอย่าง 6 สกุล — เอาไว้ทดลองอัปโหลด
- 📊QA_Chatbot_Knowledge_TH.xlsx
ตาราง Q&A 45 รายการ 7 หมวด (ทั่วไป · HR · IT · สวัสดิการ · เงินเดือน · จัดซื้อ · สถานที่) พร้อมชีตวิธีใช้
- 📝ระเบียบการลาพนักงาน.docx
ลาพักร้อน ลาป่วย ลาคลอด ลากิจ — มีตารางในเอกสาร ใช้ทดสอบโค้ดแกะ .docx ในบทที่ 5
- 📕คู่มือสวัสดิการ.pdf
ประกันสุขภาพกลุ่ม กองทุนสำรองเลี้ยงชีพ เบิกค่าเดินทาง เงินช่วยเหลือ — PDF 2 หน้าที่มีตัวข้อความจริง ไม่ใช่ภาพสแกน
- 📄นโยบายความปลอดภัยไอที.txt
รหัสผ่าน 2FA อีเมลฟิชชิง สำรองข้อมูล — ไฟล์ข้อความธรรมดา
- 📘คู่มือใช้งานระบบภายใน.md
ระบบ HR ออนไลน์ Helpdesk จองห้องประชุม จัดซื้อ — ไฟล์ Markdown
- 🌐ประกาศบริษัท.html
ประกาศเรื่องแนวปฏิบัติการทำงานแบบไฮบริด — ไฟล์ HTML ใช้ทดสอบการถอดแท็กออก
🎨 ธีมหน้าแชท — วางในช่อง Custom CSS ของโหนด Chatbot (บทที่ 10)
- 🎨n8n-chat-theme-orange.css
ธีมสีส้ม #FFA503 หัวแชทพื้นน้ำตาลเข้ม ตัวอักษรบนพื้นส้มเป็นสีเข้มตามเกณฑ์คอนทราสต์
💡 ไฟล์ .docx .pdf .xlsx เป็นไฟล์ไบนารี กดดาวน์โหลดแล้วเปิดด้วยโปรแกรมของตัวเองได้เลย ส่วนไฟล์ข้อความกด ดูเนื้อหา อ่านบนเว็บได้ก่อนโหลด
เรียนไปแล้ว 0 จาก 12 บท
บทที่ 1 — ติดตั้งและเปิด n8n
ติดตั้ง
# วิธีที่ 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
NODE_FUNCTION_ALLOW_BUILTIN=zlib n8n start
Windows (PowerShell)
$env:NODE_FUNCTION_ALLOW_BUILTIN="zlib" n8n start
Windows (Command Prompt)
set NODE_FUNCTION_ALLOW_BUILTIN=zlib n8n start
ตรวจว่าเปิดสำเร็จ
สร้าง Code node ใหม่ วางโค้ดนี้แล้วกด Test step
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
การแยกสองเส้นแบบนี้คือหัวใจของงาน 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.xlsx | Q&A 45 รายการ 7 หมวด | Excel |
| ระเบียบการลาพนักงาน.docx | ลาพักร้อน ลาป่วย ลาคลอด ลาทหาร ขาดงาน + ตาราง | DOCX |
| คู่มือสวัสดิการ.pdf | ประกันกลุ่ม กองทุนสำรองฯ เบิกค่าเดินทาง เงินช่วยเหลือ | |
| นโยบายความปลอดภัยไอที.txt | รหัสผ่าน 2FA ฟิชชิง สำรองข้อมูล | Text |
| คู่มือใช้งานระบบภายใน.md | ระบบ HR, Helpdesk, จองห้อง, จัดซื้อ | Markdown |
| ประกาศบริษัท.html | นโยบายไฮบริด WFH ค่าอินเทอร์เน็ต ยืมครุภัณฑ์ | HTML |
เนื้อหาทั้ง 6 ไฟล์ไม่ซ้ำกัน จึงใช้ตรวจได้ว่าบอทดึงคำตอบมาจากไฟล์ไหน ข้อมูลทั้งหมดเป็นเรื่องสมมติ ตัวเลขแต่งขึ้นเพื่อการอบรม ห้ามนำไปอ้างอิงจริง
รูปแบบไฟล์ Excel
| question | answer | category | keywords |
|---|---|---|---|
| บริษัทเปิดทำการกี่โมง | เปิดวันจันทร์–ศุกร์ เวลา 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 ไปยังโหนดที่ตอบกลับผู้ใช้ว่าไม่รองรับสกุลนี้
บทที่ 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 จึงทำสามขั้น
// 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(/</g, '<').replace(/>/g, '>').replace(/&/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 อ่านแปลก โค้ดจึงมีบรรทัดนี้
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 ท่อน แต่พอไปถามในแชทกลับบอกว่ายังไม่มีข้อมูลในฐานความรู้
ขั้นตอนที่ถูกต้อง
- Save แล้วสลับ Active ให้เป็นสีเขียว
- เปิดโหนด Upload_Knowledge เลือกแท็บ Production URL (ต้องเป็น /form/upload-knowledge ไม่ใช่ /form-test/)
- อัปโหลดไฟล์ผ่าน URL นั้น ครั้งแรกเลือกโหมด "ล้างของเดิมแล้วนำเข้าใหม่" ครั้งต่อไปเลือก "เพิ่มต่อจากของเดิม"
- ตรวจด้วยคำสั่ง /stats และ /sources ในแชท
ฝั่งแชทเป็นการอ่านอย่างเดียว จึงทำงานได้ทั้งโหมด test และ production ทดสอบผ่านหน้าต่าง Chat ในเอดิเตอร์ได้เลย
อีกเรื่องคือ static data ผูกกับ workflow ต่อหนึ่ง instance ถ้าทดสอบบน n8n Cloud แล้วมา import ลงเครื่องตัวเอง ข้อมูลไม่ตามมาด้วย ต้องอัปโหลดใหม่
การจัดการข้อมูลซ้ำ
- Q&A จาก Excel ตัดซ้ำด้วยคำถามที่ normalize แล้ว แถวใหม่ทับแถวเก่า จึงแก้คำตอบได้โดยอัปโหลดไฟล์ที่มีคำถามเดิม
- เอกสาร ตัดซ้ำด้วยชื่อไฟล์ อัปโหลดเอกสารชื่อเดิมซ้ำจะแทนที่ท่อนเก่าของไฟล์นั้นทั้งหมด ใช้อัปเดตเอกสารที่แก้ไขแล้วได้เลยโดยไม่ต้องล้างทั้งฐาน
บทที่ 7 — หัวใจของคอร์ส: ทำไมโค้ดค้นหาแบบฝรั่งใช้กับภาษาไทยไม่ได้
ปัญหา
ถ้าไปหา tutorial การทำ Q&A matching ในเน็ต เกือบทั้งหมดจะเขียนประมาณนี้
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);
}ตรรกะคือ "ตัดประโยคเป็นคำด้วยช่องว่าง แล้วนับว่าคำซ้ำกันกี่คำ" ทำงานได้ดีมากกับภาษาอังกฤษ
แต่กับภาษาไทย มันพังทั้งระบบ เพราะภาษาไทยไม่เว้นวรรคระหว่างคำ
tokens("บริษัทเปิดทำการกี่โมง") // → ["บริษัทเปิดทำการกี่โมง"] ได้ token เดียว
tokens("เปิดกี่โมง") // → ["เปิดกี่โมง"]
// เทียบกัน: ไม่มี token ไหนตรงกันเลย → score = 0ผู้ใช้พิมพ์ "เปิดกี่โมง" ซึ่งเป็นส่วนหนึ่งของคำถามในฐานข้อมูลตรง ๆ แต่ระบบให้คะแนน 0 แล้วตอบว่าไม่พบคำตอบ นี่คือสาเหตุที่แชทบอทภาษาไทยหลายตัวโง่แบบอธิบายไม่ถูก
ทางแก้ที่ 1: character n-gram
แทนที่จะตัดเป็น "คำ" เราตัดเป็น ชุดตัวอักษรที่ต่อเนื่องกัน ยาว n ตัว วิธีนี้ไม่ต้องรู้จักขอบเขตของคำเลย จึงใช้ได้กับทุกภาษา
grams("เปิดกี่โมง", 2)
// → {"เป", "ปิ", "ิด", "ดก", "กี", "ี่", "่โ", "โม", "มง"}แล้ววัดความคล้ายด้วย Dice coefficient
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 ของคำถามไปปรากฏในเนื้อหากี่เปอร์เซ็นต์
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 ไปเลย
แก้ด้วยการ เลื่อนหน้าต่างไปตามเนื้อหา แล้วเอาเฉพาะช่วงที่ตรงที่สุด
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 หรือไม่ กรณีที่ ไม่เรียก คือ
- คำสั่งพิเศษ /stats /list /sources /misses /help
- ค้นแล้วไม่เจออะไรถึงเกณฑ์เลย
- ยังไม่มีข้อมูลในฐานความรู้
กรณีเหล่านี้ตอบจากโค้ดโดยตรง ไม่เสียเงินสักบาท
System prompt ที่คุมไม่ให้มั่ว
หัวใจอยู่ที่ 5 ข้อนี้
- ตอบโดยอ้างอิงจากข้อมูลอ้างอิงเท่านั้น ห้ามเดา ห้ามเติมจากความรู้ทั่วไป แม้จะมั่นใจว่าถูกก็ตาม
- ถ้าข้อมูลไม่พอ ให้บอกตรง ๆ ว่าไม่มีข้อมูล และแนะนำให้ติดต่อผู้ดูแล
- ห้ามพูดถึงคำว่า "ข้อมูลอ้างอิง" หรือ "เอกสาร" ให้ตอบเหมือนเจ้าหน้าที่ที่รู้เรื่องนี้อยู่แล้ว
- ถามต่อเนื่องได้ ใช้บริบทการสนทนาประกอบ แต่เนื้อหาต้องมาจากข้อมูลอ้างอิงเท่านั้น
- ห้ามเปลี่ยนตัวเลข วันเวลา หรือชื่อหน่วยงาน ให้คงไว้ตามเดิมเป๊ะ ๆ
ข้อ 5 สำคัญกว่าที่คิด โมเดลชอบ "ปรับ" รูปแบบเวลา 08.00–17.00 น. หรือปัดตัวเลขให้สวยงามโดยไม่ได้ตั้งใจ ซึ่งในเอกสารระเบียบคือความผิดพลาดร้ายแรง
ตั้ง temperature ไว้ที่ 0.2 ด้วย เพราะงานนี้ต้องการความสม่ำเสมอ ไม่ใช่ความสร้างสรรค์
ความจำบทสนทนา
Chat_Memory ตั้งไว้ 10 ข้อความ = จำย้อนหลัง 5 คู่ถาม-ตอบ ทำให้ถามต่อเนื่องได้
ผู้ใช้: ลาพักร้อนได้กี่วัน บอท: ปีละ 6 วันทำการ เพิ่มขึ้นตามอายุงาน สูงสุด 12 วัน ผู้ใช้: แล้วต้องยื่นล่วงหน้ากี่วัน ← บอทเข้าใจว่ายังพูดเรื่องลาพักร้อนอยู่
บทที่ 9 — คำนวณต้นทุนจริง
หัวข้อนี้มักเป็นคำถามแรกของผู้บริหาร และเป็นหัวข้อที่ tutorial ส่วนใหญ่ข้าม
ขั้นที่ 1: ประมาณจากขนาด prompt
วัด prompt จริงที่ workflow ส่งออกไป คำถาม "ลาคลอดได้กี่วัน" ประกอบด้วย
- system prompt 695 ตัวอักษร
- context 3 รายการ 1,109 ตัวอักษร
- ตัวคำถามอีกเล็กน้อย
- รวม 1,819 ตัวอักษร
ภาษาไทยกินโทเคนมากกว่าอังกฤษ ผมประมาณไว้ที่ 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 |
|---|---|
| 1 | 467 |
| 2–5 | ไต่ขึ้นทีละ ~55 โทเคน |
| 6 เป็นต้นไป | ~750 (คงที่ เพราะข้อความเก่าสุดถูกดันออก) |
งาน FAQ องค์กรส่วนใหญ่คนถาม 2-3 คำถามแล้วจบ ตัวเลขที่ผมแนะนำให้ใช้วางแผนคือ
| ปริมาณ | ค่าใช้จ่าย |
|---|---|
| 1,000 คำถาม | ~5 บาท |
| 10,000 คำถาม | ~50 บาท |
| 100,000 คำถาม | ~500 บาท |
เอาเข้าจริง องค์กรที่มีพนักงาน 200 คน ถามเฉลี่ยคนละ 3 คำถามต่อเดือน = 600 คำถามต่อเดือน คิดเป็น ค่าใช้จ่ายเดือนละประมาณ 3 บาท
แนะนำให้ตั้ง usage limit ในบัญชี OpenAI ไว้ที่ 5 ดอลลาร์ต่อเดือนก่อน จะได้ไม่ต้องกังวลตอนเปิดให้คนอื่นทดลองใช้ และถ้าเติมเครดิตล่วงหน้าควรรู้ว่าเครดิตของ OpenAI มีอายุ 1 ปี สำหรับสเกลนี้เติมทีละน้อยคุ้มกว่าเติมทีเดียวเยอะ ๆ
⚠️ ตัวเลขบาทในตารางคิดที่ 33.3 บาทต่อดอลลาร์ อัตราแลกเปลี่ยนเปลี่ยนตลอด ให้ดูที่ตัวเลขดอลลาร์เป็นหลัก
บทที่ 10 — ตกแต่งหน้าแชทให้เป็นแบรนด์ตัวเอง
โหนด Chat Trigger มีช่อง Custom CSS ที่รับ CSS variable ของหน้าแชทได้ทั้งหมด
: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;
}
สองเรื่องที่ต้องรู้เมื่อทำธีมภาษาไทย
ข้อแรก ถ้าสีแบรนด์เป็นโทนสว่างอย่างส้ม #FFA503 ห้ามใช้ตัวอักษรสีขาวทับ คอนทราสต์จะได้แค่ประมาณ 1.9:1 ต่ำกว่าเกณฑ์ WCAG ที่ต้องการ 4.5:1 อ่านไม่ออกบนมือถือกลางแดด เปลี่ยนเป็นตัวอักษรสีเข้มจะได้ประมาณ 11:1 และยังดูเป็นแบรนด์เหมือนเดิม
ข้อสอง ตั้ง line-height ที่ 1.7–1.8 ไม่ใช่ 1.5 ตามค่าเริ่มต้น เพราะภาษาไทยมีสระบนและสระล่างที่จะชนกันเมื่อบรรทัดแคบ จุดนี้ต่างชัดเจนเวลาบอทตอบยาว ๆ
ใส่โลโก้และลายน้ำ ทำผ่าน pseudo element
.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 เพื่อให้ผู้ดูแลรู้ทันทีว่ามีคำถามที่ตอบไม่ได้
- ง่าย — เพิ่ม Q&A ใหม่ 5 ข้อในไฟล์ Excel เกี่ยวกับหน่วยงานของตัวเอง แล้วอัปโหลดโหมด "เพิ่มต่อจากของเดิม" ตรวจผลด้วย /stats
- ง่าย — เขียนเอกสาร .txt เกี่ยวกับระเบียบของหน่วยงานตัวเอง 1 หน้า อัปโหลดแล้วถามคำถามที่ตอบได้จากเอกสารนั้น
- กลาง — เขียนคำสั่ง /category HR ที่แสดงเฉพาะคำถามในหมวด HR
- กลาง — ทดลองปรับ MIN_DOC เป็น 0.25 และ 0.45 แล้วบันทึกว่าคำถามชุดเดิม 10 ข้อได้ผลต่างกันอย่างไร สรุปว่าค่าไหนเหมาะกับข้อมูลของคุณ
- กลาง — ปรับ contextWindowLength จาก 10 เป็น 4 แล้ววัด token ที่ใช้จริงเปรียบเทียบกัน
- ยาก — เพิ่มฟีเจอร์ export ฐานความรู้ปัจจุบันออกมาเป็นไฟล์ Excel ผ่านฟอร์มอีกตัว (ใช้โหนด Convert to File)
- ยาก — เปลี่ยนที่เก็บจาก static data เป็น Google Sheets โดยไม่แก้ตรรกะการค้นหา
สิ่งที่ทำให้ระบบนี้ใช้งานได้จริงไม่ใช่เทคโนโลยีที่ล้ำ แต่เป็นการตัดสินใจออกแบบสี่อย่าง
หนึ่ง แยกคนเพิ่มข้อมูลออกจากคนใช้ข้อมูล ระบบจึงอัปเดตตัวเองได้โดยไม่ต้องพึ่งโปรแกรมเมอร์
สอง เลือกอัลกอริทึมให้เหมาะกับภาษาที่ใช้จริง character n-gram ไม่ใช่ของใหม่หรือของฉลาด แต่มันตรงกับธรรมชาติของภาษาไทยที่ไม่เว้นวรรค ในขณะที่วิธีมาตรฐานที่ทุกคนคัดลอกกันมาใช้ไม่ได้เลย
สาม ให้ AI ทำเฉพาะสิ่งที่มันเก่ง คือเรียบเรียงภาษา ไม่ใช่จำข้อมูล การค้นหาปล่อยให้โค้ดทำ เพราะโค้ดไม่แต่งเรื่องและไม่คิดเงิน
สี่ เก็บคำถามที่ตอบไม่ได้ไว้เสมอ ระบบที่รู้ว่าตัวเองไม่รู้อะไร คือระบบที่พัฒนาตัวเองได้
ต้นทุนรวมอยู่ที่หลักสตางค์ต่อคำถาม และทุกคำตอบอ้างอิงจากเอกสารขององค์กรเท่านั้น
*ไฟล์ workflow และไฟล์ตัวอย่างทั้ง 6 ไฟล์ ดาวน์โหลดได้จากลิงก์ท้ายบทความ — import แล้วอย่าลืม Activate ก่อนทดสอบ*
ตรวจกับเอกสารทางการเมื่อ กันยายน 2569 — ราคา ชื่อเมนู และเงื่อนไขบริการภายนอก เปลี่ยนแปลงได้ตลอด ก่อนนำไปสอนควรเปิดเช็กซ้ำจากลิงก์ด้านล่าง
- เอกสารทางการ n8n — โหนด Extract from File, Code node และตัวแปร environment
- n8n Docs · getWorkflowStaticData — ยืนยันว่า static data ไม่ถูกบันทึกตอนกดทดสอบ
- OpenAI · หน้าราคาทางการ — ยืนยันราคา gpt-4o-mini $0.15 / $0.60 ต่อล้านโทเค็น
- LINE · ประกาศปิดบริการ LINE Notify — สิ้นสุดบริการ 31 มีนาคม 2568
- LINE Messaging API — ตัวที่ LINE แนะนำให้ใช้แทน LINE Notify
- WCAG · เกณฑ์คอนทราสต์ 4.5:1 — ที่มาของข้อแนะนำเรื่องสีตัวอักษรในบทที่ 10
ยังไม่เคยใช้ n8n มาก่อน เริ่มที่บทเรียนนี้ก่อนแล้วค่อยกลับมา
อยากให้บอทอยู่บน LINE แทนหน้าเว็บ ดูข้อเสนอโครงการฉบับเต็ม
ไม่อยากส่งข้อมูลองค์กรออกนอกบ้าน ใช้โมเดลในเครื่องแทน OpenAI
ก่อนอัปโหลดเอกสารองค์กรเข้าระบบ ควรรู้ว่าข้อมูลไปไหนบ้าง