Apiratchai.netlify.app ไม่มีคนอ่านหรอก แต่ผมเอาไว้เทส feature แปลก ๆ

ถ้าใครเคยอ่านโพสต์เก่าอย่าง เมื่อแม่ขอให้ทำเว็บขายประกัน หรือ ผมฝึกงาน Cyber Security น่าจะพอเห็นทิศทางของบล็อกนี้แล้วว่า มันไม่ใช่แค่เว็บบล็อกสำหรับเขียนบันทึกธรรมดา แต่เป็น Playground ส่วนตัว ที่เอาไว้ลองฟีเจอร์แปลกๆ ลอง optimization และลองของใหม่ๆ ก่อนจะเอาไปปรับใช้จริง

ในโพสต์นี้เลยอยากมาสรุปกระชับๆ เข้าใจง่าย ว่าช่วงที่ผ่านมา domain apiratchai.netlify.app มีการ rework ปรับแต่งโครงสร้าง และอัปเดตฟีเจอร์อะไรไปแล้วบ้างครับ


1. แก้เว็บหนัก หน่วง & RAG Optimization

จากเดิมที่เว็บเคยมีปัญหาเรื่องความหนักหน่วง ทั้งอาการ Decoded Image Pixel Buffer บวมบน Safari/iOS

(website มันหนักขนาดที่ browser แบบ Safari เลือกที่จะ reset ตัวเองเลยอะ ทำให้ web พังฝั่ง client)

รวมถึงปัญหา RAG แบบเดิมที่เคย dump ไฟล์ knowledge_base.yaml ทั้งดุ้น (279KB, ~4,331 บรรทัด, 3,000–5,000 tokens) ใส่เข้าไปในทุก Prompt ส่งผลให้โมเดลประหยัดอย่าง DeepSeek ติด daily limit ทันทีตั้งแต่ Request แรก (wtf โคตรไม่ optimize)

สิ่งที่เรา rework และแก้ไขไป:

  • จำกัดขนาดรูปภาพและสร้าง Image Pipeline: Enforce ขนาดรูปภาพและทำ optimization แปลงเป็น WebP / AVIF พร้อมปรับขนาด max-width ช่วยลดภาระการโหลดหน่วยความจำฝั่ง Client ลงมหาศาล

  • Lightweight Keyword Retrieval (TF-IDF / N-gram): รื้อระบบ RAG จากเดิมที่ล้มเลิกการทำ vector db (หา API ฟรีที่ยอมให้เรา embed vector กับข้อความหลายพันคำหลาย ๆ ครั้งต่อวันไม่ได้ เลยเลือกส่งไฟล์ yaml ใหญ่แทน) เลยเปลี่ยนมาใช้ KeywordRetriever แบบที่ไม่เสียค่า API ซักบาท เป็นวิธีการแบ่ง chunks จาก knowledge_base.yaml ล่วงหน้าตอนที่ build แล้วใช้ TF-IDF ร่วมกับ Character Trigram (สำหรับภาษาไทย) และ Word Tokens (สำหรับภาษาอังกฤษ) สกัดเฉพาะ Top-3 Chunks ที่ตรงกับคำถามที่สุด ทำให้การส่ง request เพื่อถาม AI มันจะเสียแค่ ~300–500 tokens เท่านั้น!

  • Response Time & Token Efficiency:

    • จากเดิม dump knowledge_base.yaml ทั้งไฟล์ ~3,000 - 5,000 Tokens/req ยิงแป๊บเดียวโควต้าปลิว
    • ลดเหลือเพียง ~500 - 800 Tokens/req ช่วยให้ DeepSeek และโมเดลอื่นๆ ใช้งานต่อวันได้หลายสิบถึงหลายร้อย requests
    • ค้นหาใน memory เร็วมากประมาณ < 5ms ด้วย Javascript math เพียว ๆ ไม่ต้องพึ่งพา External API หรือ Vector DB ให้เสี่ยงล่ม

    kku intelsphere

    นักศึกษา มข มี AI เกือบ 70 โมเดลให้ใช้ฟรี reset ทุกวัน แต่ว่าปริมาณมันไม่พอสำหรับงานนี้ เพราะว่าได้แค่ตัวละ 100K token/day ประมาณนั้น

ต้องยอมรับว่าการ handle chunk matching เองมันโง่กว่าการโยนให้ frontier model อ่านทั้งก้อนอยู่แล้ว เพราะหาก match ไม่โดน = เนื้อหาไม่สอดคล้องทันทีเลย ในอนาคตอาจจะมี pipeline การ match ที่ดีกว่านี้โดยไม่เปลืองมาก แต่ขอคิดก่อน ผมเป็นวิศวกรปลอม

เรื่องการทำ Tokenize หลังบ้าน ในโค้ด retriever.ts จริงๆ แล้วมันไม่ได้สุ่มทำ แต่ AI มันออกแบบให้ผสมผสาน 2 แบบคู่กันเลย คือ:

  • Word tokens เอาไว้จับพวก Keyword ภาษาอังกฤษหรือคำเดี่ยวๆ แบบตรงเป๊ะ

  • Character trigrams (n=3) เป็น sliding window แบ่งทีละ 3 อักษร

    อันนี้ทีเด็ด เอาไว้แก้ปัญหากับ ภาษาไทย (ที่พี่ไทยเล่นเขียนติดกันเป็นพืดไม่มีช่องว่างเคาะเว้นวรรค)

    แถมยังช่วยเก็บพวกคำพิมพ์ผิดในทั้งสองภาษาได้ เช่น สมมติมือลั่นพิมพ์ผิด docker เป็น "dockr" ตัว Trigram ก็ยังอุตส่าห์สกัดได้ "doc", "ock", "ckr" ซึ่งก็ยังไป Match เจอ Chunk ที่มีคำว่า "docker" (เพราะมี "doc" + "ock" เหมือนกัน) โคตรโกง

จากนั้นก็นับความถี่คำและจับมาคิดคะแนนแบบ TF-IDF (Term Frequency-Inverse Document Frequency) โดยให้แต่ละ chunk ว่าถ้าคำใน query ไปโผล่ใน chunk ไหนเยอะ แต่ไม่ใช่คำขยะที่โผล่ทั่วๆ ไปในทุก chunk (เช่นคำว่า “การ”, “ของ”) chunk นั้นจะได้คะแนนสูง คือมันเป็นแค่

TF-IDF=TF×log(NDF)\text{TF-IDF} = \text{TF} \times \log\left(\frac{N}{\text{DF}}\right)

  • TF = ความถี่ของคำใน chunk
  • DF = จำนวน chunk ทั้งหมด
  • N = จำนวน chunk ที่มีคำนี้

AI บอกว่าแม่งเป็น math ม.ปลาย (เหรอวะ ดูถูกสติปัญญากันมากเลยนี่)

สรุปคือ AI เขียน ~200 บรรทัดให้ ใน src/lib/retriever.ts ส่วนเราทำแค่ import มาใช้ใน rag.ts แล้วก็บอกมันว่าให้ RAG จาก “top-3 chunks พอ”

ผลลัพธ์: context จาก ~279KB (5000+ tokens) เหลือ ~1KB (300 tokens) ทำให้ DeepSeek API ของ KKU ที่เคยชน daily limit ตั้งแต่ req แรก ตอนนี้ใช้ได้เป็นสิบ ๆ requests โดยไม่ต้องเสียค่า embedding API แม้แต่บาทเดียว

(เอ้อ การ embed นอกจากเปลือง embedding api ไฟล์ vector มันก็โคตรจะใหญ่เลยด้วย ขนาดประมาณ 11MB นู่นเลยนะ)

แต่มันก็มี downside ชัด ๆ — ถ้า query ใช้คำที่ไม่ตรงกับใน KB (synonym, paraphrase) = chunks ที่ match ได้อาจไม่ใช่ chunks ที่ดีที่สุด เพราะมันไม่เข้าใจ semantics เหมือน embedding-based search มันแค่นับคำที่ตรงกัน

ไว้คิด pipeline ที่ดีกว่านี้ได้เมื่อไหร่จะมาอัพเดตอีกที

Update (29 Jul 2026): Revert กลับ YAML

ลองรันจริงไปแป๊บเดียวก็รู้เลยว่ามันไม่เวิร์ค

trigram มันดักแค่ตัวอักษร ไม่ได้ดักความหมาย — "docker" กับ "ด็อกเกอร์" คือเรื่องเดียวกัน แต่ keyword มันไม่รู้ เพราะไทยเราไม่มีช่องว่างระหว่างคำ การใช้แค่ sliding window 3 ตัวอักษรมัน brute-force เกินไป

เลยเป็นแบบนี้: ถามเรื่อง container → chunk ที่ match คือย่อหน้าที่มีคำว่า “contain” หรือ “tain” → เนื้อหาคนละเรื่อง → AI งง

แย่กว่าตอน dump YAML ทั้งก้อนอีก

เพราะอย่างน้อยวิธีเดิม context มันถูกต้องทุกตัวอักษร AI มันอ่านเอง ตีความเอง เราแค่เสีย token

ก็ revert กลับ commit ก่อนหน้า

token กลับไป ~3000-5000 ต่อ req เท่าเดิม แต่อย่างน้อย DeepSeek ก็ไม่ใช่ default แล้ว Gemini Flash Lite รับไปเกือบหมด

ไว้หา embedding API ฟรีได้ค่อยว่ากันใหม่

Update (31 Jul 2026): กลับมาใช้ Vector Search — แต่คราวนี้ Local ทั้งหมด

จากที่เมื่อวานบอกว่าจะรอหา embedding ฟรีก่อน เอาจริงวันนี้ก็ได้คำตอบแล้ว ว่าจริง ๆ ไม่ต้องพึ่งบริการ API ภายนอกก็ได้นี่หว่า เพราะแค่การ embed text → vector มันทำในเครื่องกาก ๆ ได้ แค่ laptop ก็ทำได้แล้ว ไม่ช้าเท่าไรด้วย

ด้วยโมเดล paraphrase-multilingual-MiniLM-L12-v2 ซึ่งก็รองรับทั้งภาษาไทย-อังกฤษ และที่เก็บเวกเตอร์ก็ใช้ฟีเจอร์ pgvector ในฐานข้อมูลที่มีอยู่แล้ว ไม่ต้องสร้างอะไรใหม่

โดยเราจะ embed เนื้อหาโพสต์เยอะ ๆ จากฝั่ง local เท่านั้น (build knowledge ใช้ hardware laptop) ซึ่งถ้าหากลืมกด = เราอัปขึ้น remote และ deploy ไปแบบที่ข้อมูลหน้าเว็ปอัปเดตแต่ AI ไม่รับรู้เหมือนเดิม << กรณีนี้มันคงจะ match เจอใกล้เคียงสุด แล้ว hallucinate หรือแค่ตอบว่าไม่เจอ

ส่วนเวลามีคนถาม ระบบจะแปลงคำถามเป็นเวกเตอร์ (เร็วมาก ผ่าน serverside backend เราเอง) แล้วค้นหาข้อความที่ใกล้เคียงที่สุดจากฐานข้อมูล ถ้าฐานข้อมูลมีปัญหาก็สลับไปใช้ไฟล์เวกเตอร์สำรองซึ่งเป็น ไฟล์ json (ขนาด 2.7MB, 223 chunks, 384 มิติ ต่อ chunk) แล้วเทียบ cosine similarity เอา และถ้ายังไม่มีข้อความไหนตรงจริง ๆ ถึงจะยอมส่งไฟล์ความรู้เดิมทั้งไฟล์เป็นทางสุดท้าย เป็นไฟล์ yaml ทั้งก้อน ไม่ตัดไม่หั่น มีครบทุกอย่าง (เปลือง token มาก)

สรุปคือ วิธีแรก (หั่น n-gram) รู้แค่ว่าตัวอักษรเหมือนกันแต่ไม่รู้ความหมาย ส่วนวิธีนี้เข้าใจความหมาย และไม่เสียเงินเพิ่มสักบาท


Why not just use Google’s free embedding API?

มีคนถามมาว่า “Google ก็มี embedding API ฟรีนี่ จะไปโหลดโมเดลครึ่งกิ๊กใน serverless function ทำไม”

เราเคยลองแล้ว ตั้งแต่วันแรกที่ทำ vector RAG ใช้ text-embedding-004 ของ Google สร้าง embeddings สำหรับ 223 chunks ใช้งานได้ 6 วัน แล้วก็ชน quota limit

ปัญหาไม่ได้อยู่ที่ query (ถามทีละคำถาม ไม่มีทางเกิน quota) แต่อยู่ที่ build step: ต้อง embed 223 chunks รวดเดียว ถ้าไม่ delay ระหว่าง call ก็โดน rate limit ถ้า delay 3 วิต่อ chunk ก็ใช้เวลา 11 นาที แถม network flakiness ก็ทำให้ build พังกลางคันได้

KKU ก็ไม่มี embedding endpoint ให้ใช้

ทางเดียวที่เหลือคือ local model แต่พอ blog เป็นภาษาไทย ก็ต้องใช้ multilingual model ซึ่งหนักกว่า English-only หลายเท่า

ทำไม multilingual ถึงหนักกว่า English-only 5 เท่า

Embedding model ใช้พื้นที่ส่วนใหญ่ไปกับ Token Embedding Matrix:

Size=V×dmodel×4 bytes (float32)\text{Size} = V \times d_{model} \times 4 \text{ bytes (float32)}

English-onlyMultilingual (TH+EN)
Vocab size30,522250,037
Token Matrix (float32)46.9 MB384.1 MB
ONNX quantized (int8)22 MB113 MB

แค่คำศัพท์ภาษาไทยที่เพิ่มเข้ามาทำให้โมเดลใหญ่ขึ้น 5 เท่า

Cold start = download 113MB ใหม่ทุกครั้งที่ function recycle

บน Netlify Functions (serverless) function จะถูก recycle ทุก 5-15 นาทีถ้าไม่มีคนใช้ และทุก cold start โมเดลไม่ได้ถูก bundle ไปกับ deploy เพราะ cache อยู่ใน node_modules (ซึ่งถูกสร้างใหม่ตอน build และเป็น read-only ตอนรัน)

ดาวน์โหลด 113MB บน timeout 10 วิ แทบไม่มีทางทัน

แต่โค้ดถูกออกแบบให้ gracefully degrade: ถ้า embedding fail ก็ fallback กลับไป dump YAML ทั้งไฟล์เข้าสู่ prompt แทน แปลว่าช่วงที่ function อุ่น vector search ทำงานปกติ แต่ช่วงโล่ง ๆ ฟีเจอร์ semantic search จะเงียบหาย

Bundle โมเดลเข้าไปตอน build

ปัญหาจริง ๆ ไม่ได้อยู่ที่ตัวโมเดล แต่อยู่ที่ library มันเก็บ cache ไว้ใน node_modules/@xenova/transformers/.cache/ ซึ่งหายทุกครั้งที่ deploy (เพราะ node_modules สร้างใหม่ตอน build) และถึงไฟล์จะอยู่ดี ๆ มันก็อ่านไม่ได้อยู่ดีเพราะ filesystem ของ Lambda เป็น read-only

วิธีแก้คือเปลี่ยนที่เก็บ cache ออกมานอก node_modules แล้วให้ Netlify include เข้าไปใน function bundle ตอน build เลย โมเดลก็จะกลายเป็นส่วนหนึ่งของ function เหมือนโค้ดอื่น ๆ ไม่ต้อง download ตอนรันอีก

แก้แค่ 6 ไฟล์ รวมประมาณ 30 บรรทัด

Timeline

ช่วงวิธีผลลัพธ์
n-gram TF-IDF (~3 วัน)keyword match เองพังเพราะไทยไม่มีช่องว่างระหว่างคำ
revert YAML (~2 วัน)dump ทั้งไฟล์เสีย token 5000/req
local vector unbundled (~3 วัน)MiniLM บน Netlifycold start timeout ตลอด
local vector bundledprecache ตอน buildใช้งานได้จริง

รวมประมาณ 1 สัปดาห์ของการลองผิดลองถูก กับ 20 นาทีในการแก้ bundle เท่านั้นเอง

สรุปผลหลัง deploy

ก่อนหลัง
Cold startdownload 113MB (timeout)init ONNX จาก bundle (3-5 วิ)
Token ต่อ req3000-5000 (YAML fallback)500-800 (vector RAG)
Vector search บน productionไม่เคยทำงานจริงทำงานได้

สิ่งที่แลก: function bundle หนักขึ้น 130MB (จาก ~50MB เป็น ~180MB) ยังอยู่ในลิมิต 250MB ของ Netlify


2. แก้ปัญหา Multimodal & OCR ด้วย Typhoon API

ย้อนกลับไปตอนทำ AI Chatbot ระบบเคยเจอปัญหาที่ว่า ไม่ใช่ว่าไอ้ Model ที่อ่านรูปรู้เรื่องเนี่ยมันจะหาง่าย ๆ นะเว้ย มันแพงนา พวก Gemini เนี่ย แถมการดูรูปโง่ ๆ ที่นึงโคตรที่จะเปลือง token เลยด้วย แต่จะให้ยกเลิกกการส่งรูปก็ไม่ได้ เพราะมันสะดวกจริงนั่นแหละ คราวนี้เราเลยลองแก้ปัญหาจาก root cause โดยการคิดแบบฆาตรกร (ว่าไปนั่น) สาเหตุอะไรที่คนจะแคปรูปมาถาม AI ก็ตกตะกอนได้ 2 อย่างคือ

  1. เขาอยากแคปเนื้อหาให้ AI อ่าน แต่เลือกที่จะ screenshot แทนการแคป text
  2. ส่วนที่อยากให้ AI ดูมันเป็นรูปจริง ๆ ไม่ใช่ text

สำหรับ 1. เราแก้ได้ด้วยการใช้ OCR model ซึ่งมันประหยัดกว่า image มาก ๆ มันทำหน้าที่แค่ดึง text ออกจากรูปมาทำเป็น markdown ส่วน 2. จะทำงานเมื่อ OCR model บอกว่าไม่เจอ text จริง ๆ นะโว้ย เท่านั้น

Solution ใหม่ด้วย Typhoon OCR Pipeline:

  • แยก Pipeline ชัดเจน: หากผู้ใช้อัปโหลดรูปภาพเข้ามาในแชท ระบบจะยิงรูปไปที่ Typhoon OCR API (โมเดล typhoon-ocr) ก่อนเป็นอันดับแรก
  • สกัดข้อความภาษาไทย-อังกฤษเป๊ะๆ: Typhoon OCR จะทำการดึงข้อความทั้งหมดในภาพออกมาในรูปแบบ Markdown แบบ verbatim (ก็คือเห็นอะไร เอาอันนั้นมาเลย ไม่แปล ไม่มโนเพิ่ม)
  • OCR-to-Text Fallback: ข้อความที่ได้จาก OCR จะถูกนำไปต่อประกอบเข้ากับ Prompt ของผู้ใช้ทันที ทำให้ LLM ทั่วไปรับบริบทได้แม่นยำ 100% โดยไม่ต้องพึ่งพา Multimodal หนักๆ ทุกครั้ง
  • ถ้า OCR พัง: ระบบถึงจะ Fallback สลับไปใช้โมเดล Multimodal เพื่อวิเคราะห์ visual โดยตรงต่อ (กรณีรูปมันไร้ text ก็ทำแบบนี้เหมือนกัน)

OCR Showcase using typhoon OCR and deepseek V4

ตัวอย่างการ OCR เมนูชายสี่หมี่เกี๋ยวด้วย Typhoon OCR และใช้ deepseek V4 flash ในการตอบ

โดย feature การอัพรูปได้กลับมาแล้ว และ limit แค่ 3 รูปเท่านั้น (ป้องกันการ DOS หรืออะไรก็ตาม) และมันจะแสดงปุ่มเล็ก ๆ ให้ user เห็นด้วยว่าไอ้รูปที่ส่งไป มัน OCR ได้อะไร เผื่อมันทำผิด user ก็แค่เถียงมัน


3. State ล่าสุดของ Feature ต่างๆ เทียบกับ Post เก่า

ลองมาดูตารางเปรียบเทียบระหว่างสิ่งที่เคยเล่าไว้ในโพสต์เก่า (why-web, cybersec-trainee) กับสถานะปัจจุบันกันดีกว่า:

ฟีเจอร์ / ระบบState ในโพสต์เก่า (why-web)State ล่าสุดในปัจจุบัน
RAG Retrievalส่งไฟล์ความรู้ทั้งไฟล์ หรือพึ่งพา Vector DB ภายนอกคำนวณเวกเตอร์ในเครื่อง + ค้นหาความหมายในฐานข้อมูลเดิม (Neon) มีไฟล์สำรองและ YAML เป็นทางสุดท้าย
Image & OCR Askingปิดฟังก์ชันรูปภาพไปชั่วคราวเพราะโดนบีบ Base64 payloadกลับมาใช้ได้เสถียร 100% ด้วย Typhoon OCR API Pipeline
Model Fallback Chainใช้ Google แล้วสลับไป KKU ด้วยมือตอนติดปัญหาสลับโมเดลอัตโนมัติ เริ่มจาก Gemini Flash Lite (KKU) จนถึง GPT-5 Mini พร้อมแจ้งเตือน
Ask AI by Selectionดึงเฉพาะคำสั้นๆ คลุมดำแล้วตอบดึง DOM Parent (<p>, <li>, <code>) หุ้มทั้งย่อหน้าส่งไปเป็นบริบท
Interaction & DatabaseNeon PostgreSQL (รองรับ Comment, Reaction)เพิ่ม logging สำหรับ debugging และ monitor การใช้งาน
WASM / 404 Playgroundวางแผนอยากลอง WASM บน BrowserRust Zombie Survival Game 100% Native WASM ประจำการที่หน้า 404 เรียบร้อย

สรุป

ตอนนี้ apiratchai.netlify.app กลายเป็นทั้งบล็อกและห้องทดลอง (Playground) ที่รวมเอาเทคโนโลยี (ที่ไร้สาระเอาแต่ใจทั้งหลาย) มาไว้ด้วยกัน ใครอยากลองฟีเจอร์ไหน ไม่ว่าจะเป็นการคลุมดำกด Ask AI, พิมพ์คุยกับ Saab AI, อัปโหลดรูปภาพผ่าน Typhoon OCR, ทดสอบรัน Local Qwen2.5 1.5B Playground บน WebGPU หรือแอบไปยิงซอมบี้ที่หน้า 404 Page Not Found ก็ลองเล่นกันได้เลย