3369 คำ
17 นาที
เมื่อทำ Local LLM ไว้เข้าห้องสอบ Open Book เองแบบงงๆ
สารบัญ
0. ขออธิบายก่อน — Local LLM + RAG คืออะไร? 1. ทำไมไม่ใช้ ChatGPT/Claude API ไปเลย? 2. ลองเครื่องเก่าก่อน — HP Victus 15 + RTX 2050 4GB ปัญหา 1 — VRAM 4 GB ไม่พอ ปัญหา 2 — ลองโมเดลเล็กลง = แม่นยำพัง ปัญหา 3 — Token Loop วน ปัญหา 4 — ภาษาจีนหลุดเข้ามา 555+ ปัญหา 5 — แต่งกฎหมายปลอม (อันตรายมาก) ปัญหา 6 — Tokenizer ของไทยพัง ปัญหา 7 — Thinking mode ช้าจน timeout 3. ซื้อเครื่องใหม่ — HP Victus 16-r1265TX 4. Phase 0-1 — Infrastructure + Stack Setup Partition + BitLocker (ป้องกันโน้ตบุ๊คหายแล้วโดนแฮกข้อมูล) Folder structure Environment variables (User scope, ไม่ต้องใช้ admin) Stack ที่เลือกใช้ (ขออธิบายแต่ละตัว) ทุกตัวประกอบกันยังไง 5. Phase 1.5 — Math Test Gate: ใครจะเป็น Primary Model 6. Phase 2-4 — สร้าง RAG Pipeline Knowledge Base — เนื้อหาที่ต้องอ่าน Question Bank — Sample ที่หามาทดสอบ Chunking — หั่นเอกสารใหญ่เป็นชิ้นเล็ก Prompt template ที่ใช้จริง 7. Phase 5 — Baseline Benchmark = 74% 8. Phase 6 — Tuning Hell (สนุกที่สุดในโปรเจกต์) ทางที่ลอง 1: Reranker — เพิ่มเชฟตัวที่สอง ทางที่ลอง 2: Hybrid RRF — ตัวเปลี่ยนเกม ทางที่ลอง 3: top_k decision Stack หลัง Phase 6 9. Phase 7 — Full Benchmark พันกว่าข้อ Pre-flight check รันยาว 4 ชั่วโมง ตอนเฝ้า benchmark — เห็นอะไรแปลกๆ ผลรวม 3 ชั่วโมง 56 นาที 10. Parse Failure Retry — ฟื้น 15 ข้อที่ตอบไม่จบ Stage 1: Tier 1 + 2 retry (เพิ่ม token budget) Stage 2: Direct prompt (ไอเดียที่ลอง) Final Score 11. ask.py — Interactive Multi-KB 12. Cleanup — เคลียร์ 15.4 GB 13. Project Structure ทั้งหมด (เผื่อใครจะทำตาม) 14. บทเรียนสำคัญที่ได้ 15. สรุปสุดท้าย — สิ่งที่ได้จาก 3-4 วันนี้ บันทึกตอนปิด

เคยมั้ยครับ อาจารย์บอกว่าเอาคอมเข้าห้องสอบได้ แต่ห้ามต่อเน็ต…

“เอา Open Book มาได้ จะเอาคอมโน้ตบุ๊คเข้าห้องสอบมาช่วยก็ได้ แต่ห้ามต่อ internet”

ฟังแล้วก็คิดเล่นๆ มี 2 ทางที่ทำได้

ทาง 1: เอาชีทโน้ต ปริ้นมา ไฮไลท์เป็นสีๆ เปิดอ่านในห้องสอบเหมือนคนอื่น

ทาง 2: ในเมื่อมันให้เอาคอมเข้าได้… อ้าว งั้นทำ AI ติดเครื่องไปเองเลยสิ 555+

เลือกทาง 2 ครับ แล้วก็ดำดิ่งเข้าไปเต็มๆ 3-4 วัน หมดเป็นวันๆ เลย (ทำตั้งแต่เช้ายันดึก ไม่ได้ทำอะไรอย่างอื่นเลย)

โพสต์นี้คือบันทึกของตัวเอง เผื่อกลับมาอ่านวันหลังจะนึกออกว่าทำอะไรไปบ้าง ตั้งแต่เครื่องเก่าที่ไปไม่รอด ผ่านโมเดลที่หลอนเขียนภาษาจีนปะปนเข้ามา จนได้ระบบที่ตอบคำถามถูก 68.3% บน Question Sample พันกว่าข้อที่หามาทดสอบ

แต่ก่อนจะเล่าเรื่องเดินทาง ขอใช้เวลา 5 นาทีอธิบายก่อนว่า “AI พกพา” ที่ผมพูดถึงเนี่ย จริงๆ มันคืออะไร ทำงานยังไง เพราะถ้าไม่เข้าใจหลักการ ตามเรื่องเล่ายาก

0. ขออธิบายก่อน — Local LLM + RAG คืออะไร?#

ถ้าคุณเคยใช้ ChatGPT นั่นคือ LLM (Large Language Model) ตัวนึง วิ่งอยู่บน server ของ OpenAI ที่อเมริกา เราพิมพ์คำถามไป มันส่งกลับมาคำตอบ

Local LLM = LLM ตัวเล็กๆ ที่วิ่งอยู่ในเครื่องเราเอง ไม่ต้องต่อเน็ตเลย

ฟังดูดีใช่มั้ยครับ แต่มีปัญหาใหญ่ 2 ข้อ:

  1. โมเดลที่รันในเครื่องคนทั่วไปได้ มันตัวเล็กกว่า ChatGPT มาก (4-9 พันล้าน parameters เทียบกับ GPT-4 ที่ ~1.8 ล้านล้าน)
  2. Local LLM ไม่รู้เรื่องเฉพาะทาง ถามเรื่องกฎหมาย/การเงินไทยปี 2025? โอกาสมั่วสูงมาก

ตรงนี้แหละที่ RAG (Retrieval-Augmented Generation) เข้ามาช่วย

ลองนึกภาพ แทนที่จะให้ AI ตอบจากความจำตัวเองล้วนๆ (ซึ่งอาจมั่ว) เราเปลี่ยนวิธีเป็น:

flowchart LR
    A["💬 คุณถาม:<br/>กฎหมาย X คืออะไร?"] --> B["🔍 ระบบไปค้น<br/>เอกสารของเราก่อน"]
    B --> C["📄 เจอเอกสาร<br/>10 ชิ้นที่เกี่ยวข้อง"]
    C --> D["🤖 ส่งให้ AI พร้อมคำถาม:<br/>'อ่านนี่ก่อน แล้วตอบ'"]
    D --> E["✅ AI ตอบ<br/>อ้างอิงจากเอกสาร"]

    style A fill:#4f46e5,color:#fff,stroke:#4338ca,stroke-width:2px
    style B fill:#0891b2,color:#fff,stroke:#0e7490,stroke-width:2px
    style C fill:#06b6d4,color:#fff,stroke:#0891b2,stroke-width:2px
    style D fill:#7c3aed,color:#fff,stroke:#6d28d9,stroke-width:2px
    style E fill:#16a34a,color:#fff,stroke:#15803d,stroke-width:3px

= RAG = AI ที่ก่อนตอบคำถาม วิ่งไปเปิดคู่มือก่อน

เหมือนพนักงานใหม่เก่งๆ ที่ไม่รู้เรื่องบริษัท ให้ดู policy ก่อน เขาก็ตอบลูกค้าได้ดี แต่ถ้าไม่ดู เดาเอาเอง = พังแน่นอน

หัวใจของ RAG มี 3 ชิ้นใหญ่ๆ:

ชิ้นหน้าที่เหมือน…
LLMตอบคำถามนักศึกษาเก่งๆ
Embedding modelแปลงข้อความเป็นตัวเลข เพื่อหา “ความหมายคล้ายกัน”บรรณารักษ์ที่จัดหนังสือตามความหมาย
Vector databaseเก็บตัวเลขพวกนั้น แล้วค้นหาได้เร็วห้องสมุดที่จัดตามความหมาย

OK พอเห็นภาพแล้วใช่มั้ยครับ ทีนี้กลับมาที่เรื่องเล่ากัน

1. ทำไมไม่ใช้ ChatGPT/Claude API ไปเลย?#

คำถามแรกที่ทุกคนถาม

คำตอบ:

  1. กฎสอบ ห้ามต่อ internet เลย ใช้ cloud ไม่ได้
  2. อนาคต เมื่อตั้ง local stack ขึ้นมาแล้ว มันคือ personal knowledge base ของเราเอง เอาไปทำอย่างอื่นต่อได้อีกเป็นพันอย่าง
  3. ความเข้าใจ รัน local เอง = เข้าใจ stack ทั้งระบบลึกกว่าใช้ cloud หลายเท่า เพราะเวลามันพัง เราเห็นทุกชั้นว่าพังตรงไหน

ส่วนเรื่อง “งั้นใช้ Question Sample ไหนทดสอบดี?”

เลือก ชุด Question Sample ที่หาดาวน์โหลดได้สาธารณะ พันกว่าข้อ มาเป็น architecture validator แปลว่าใช้พิสูจน์ก่อนว่า “pipeline ที่สร้างขึ้นมา ทำงานได้จริงมั้ย” ก่อนจะเอาเทคนิคเดียวกันไปใช้กับเนื้อหาจริงทีหลัง

Architecture validator = ชุดทดสอบที่ใช้ “ตรวจสอบว่าวิธีที่เราเลือก ทำงานได้จริงตามทฤษฎี” ไม่ใช่เพื่อสอบจริงๆ แต่เพื่อพิสูจน์ “ทาง” ก่อน

เพราะถ้าไปทดสอบกับเนื้อหาจริงที่จะใช้เลย ตอน fail จะแยกไม่ออกว่า pipeline พัง หรือว่าเนื้อหาที่ใส่เข้าไปยังไม่พอ แยก architecture risk กับ content risk ก่อน = debug ง่ายกว่ามาก

2. ลองเครื่องเก่าก่อน — HP Victus 15 + RTX 2050 4GB#

เริ่มทดลองบนเครื่องเดิมที่มีอยู่:

HP Victus 15-fb2105AX (2024)
CPU: AMD Ryzen 5 8645HS (6 cores / 12 threads)
RAM: 16 GB DDR5-5600
GPU: NVIDIA RTX 2050 Laptop 4GB VRAM
Memory bandwidth: ~112 GB/s (แคบมาก)

ขอแปะเครื่องหมายดอกจัน VRAM = RAM ของการ์ดจอ คือพื้นที่ที่เก็บโมเดล AI ตอนรัน ส่วน Memory bandwidth = ความเร็วท่อระหว่าง VRAM กับ chip ของ GPU ยิ่งกว้างยิ่งคำนวณเร็ว

4GB VRAM = พอเก็บโมเดลขนาดเล็ก แต่ก็พอแค่นั้น

เจอปัญหารัวๆ ภายในวันแรก เล่าให้ฟัง 7 ข้อหลัก:

ปัญหา 1 — VRAM 4 GB ไม่พอ#

โมเดลที่อยากใช้ Qwen 3.5 9B ขนาดไฟล์ 6.6 GB → VRAM 4GB ใส่ไม่ได้ ต้อง offload ไป RAM (เอาโมเดลครึ่งนึงไปไว้ใน RAM ของเครื่อง แล้ว swap ไปมา)

ผลคือ ความเร็วร่วงจาก 25 token/วินาที เหลือ 2.4 token/วินาที (ช้าลง 10 เท่า)

Token = หน่วยที่ AI อ่าน/เขียน ภาษาอังกฤษประมาณ 0.75 คำต่อ token ภาษาไทยกินเยอะกว่า ~1-2 token/คำ

ปัญหา 2 — ลองโมเดลเล็กลง = แม่นยำพัง#

ลองสลับมา Qwen 3.5 4B (ขนาด 2.7 GB ใส่ VRAM ได้สบาย เร็วด้วย) แต่…

  • โจทย์ Present Value (มูลค่าปัจจุบันของเงินในอนาคต — ถ้าจะได้เงิน 100,000 บาทในอีก 5 ปี ที่ดอกเบี้ย 3% ตอนนี้เงินก้อนนั้นมีมูลค่ากี่บาท?) → AI ตอบ 86,378.16 เฉลย 86,383.76 — เพี้ยน 5.60 บาท → ดูน้อยใช่มั้ย? แต่โจทย์ multiple choice ที่ตัวเลือกใกล้ๆ กัน 5 บาทก็พอผิดข้อได้

  • โจทย์ NPV (Net Present Value — ลงทุนตอนต้น แล้วทยอยได้ cash flow คืนเป็นปีๆ สรุปคุ้มลงทุนมั้ย?) → AI คำนวณ cash flow รวมได้ 97,633 → สรุป “ไม่คุ้ม ไม่ลงทุน” → เฉลยจริงๆ ต้องคำนวณได้ 147,633 → สรุป “คุ้ม ลงทุน” → = ตอบกลับด้านกันเลย ถ้าเชื่อ AI ตามนี้ = พลาดดีลทำกำไร 50,000 บาท 😱

  • โจทย์ WACC (Weighted Average Cost of Capital — ต้นทุนเฉลี่ยของเงินทุนบริษัท คิดเป็น % ใช้ตัดสินใจว่าโครงการคุ้มลงทุนมั้ย) → AI ตอบ 19.20% เฉลย 9.12%คลาดเคลื่อน 10 เท่า จุดทศนิยมหายไปดื้อๆ → ระดับนี้ตัดสินใจกลับด้านได้เลย เห็น 19% ใครจะกล้าลงทุน เห็น 9% ใครจะไม่ลงทุน

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

ปัญหา 3 — Token Loop วน#

อยู่ๆ AI ก็พิมพ์วนซ้ำๆ:

"ทังทังเป็นผู้ขายทังทังเป็นผู้ซื้อทังทังเป็นผู้ขายทังทัง..."

วน 100+ รอบจน timeout

นี่คืออาการ token degeneration โมเดลเข้า loop ของ probability ที่หา exit ไม่เจอ ลอง parameter แก้ทุกค่า (repeat penalty, repeat_last_n) ก็ fix ไม่ได้

ปัญหา 4 — ภาษาจีนหลุดเข้ามา 555+#

อันนี้ขำมาก ถามคำถามกฎหมายไทย AI ตอบมาว่า:

"การกระทำดังกล่าวเป็นการ 误导 ผู้ลงทุน..."

误导 = “หลอกลวง” ในภาษาจีน โผล่มาในคำตอบไทยเฉยเลย!

อีกข้อมี "买卖双方" (จีน = “ผู้ซื้อผู้ขาย”) โผล่ 3 ครั้งในคำตอบเดียว

เพราะ Qwen เป็นโมเดลจาก Alibaba ที่ train ด้วยข้อมูลจีน-อังกฤษ-ไทย รวมกัน โมเดล 4B ตัวเล็ก ไม่แข็งพอที่จะ constrain ตัวเองให้อยู่ภาษาเดียว

ปัญหา 5 — แต่งกฎหมายปลอม (อันตรายมาก)#

Qwen 4B กล้าตอบหน้าตาเฉย:

“ตามมาตรา 113 และมาตรา 114 ผู้กระทำผิดมีโทษปรับ 500,000 บาท จำคุก 2 ปี…”

ดูเหมือนจริงนะครับ แต่ไม่มีอยู่ในกฎหมายจริง มาตราที่อ้างไม่มี โทษที่อ้างไม่มี ทุกอย่างแต่งหมด

นี่คืออาการ AI Hallucination มันแต่งเรื่องอย่างมั่นใจ ใครเชื่อตามไปสอบ หรือไปคุยลูกค้า = ตายทันที

ปัญหา 6 — Tokenizer ของไทยพัง#

ดูคำตอบที่ออกมา เห็นอะไรแปลกๆ มั้ย?

"ท่ี" ที่ถูกควรเป็น "ที่"
"ค่านนั้" ที่ถูกควรเป็น "ค่านี้"
"ว่อ" ที่ถูกควรเป็น "ว่า"
"ดวย" ที่ถูกควรเป็น "ด้วย"
"เพิ่ย" ที่ถูกควรเป็น "เพิ่ม"

นี่ไม่ใช่พิมพ์ผิด นี่คือ fundamental limitation ของ tokenizer

Tokenizer = เครื่องที่หั่นข้อความเป็นชิ้นๆ ที่ AI อ่านได้ ภาษาอังกฤษหั่นง่าย (เห็น space ก็หั่น) แต่ภาษาไทยไม่มี space ระหว่างคำ + วรรณยุกต์/สระอยู่บน-ล่าง-ซ้าย-ขวา ทำให้ tokenizer ที่ออกแบบมาสำหรับภาษาอังกฤษ มองตัวอักษรไทยพังเป็นชิ้นๆ ผิดที่ผิดทาง

แก้ที่ prompt level ไม่ได้ มันคือปัญหาที่ระดับ training ของโมเดล

ปัญหา 7 — Thinking mode ช้าจน timeout#

โมเดลรุ่นใหม่ๆ มี “thinking mode” คือก่อนตอบ AI จะเขียน chain-of-thought ออกมาก่อน (คล้ายเรา “คิดดังๆ”) ทำให้คำตอบดีขึ้น แต่ใช้ token เยอะกว่าปกติ 3-5 เท่า

Qwen 9B + thinking + เครื่องช้า = 2.4 token/sec × thinking ยาว → timeout ที่ 9.67 นาที (HTTP limit อยู่ที่ 580 วินาที)


หลังหลายรอบของการ tune Modelfile (config โมเดล) ก็ยอมรับชะตา เครื่องนี้ไปไม่รอด ต้องเปลี่ยน

3. ซื้อเครื่องใหม่ — HP Victus 16-r1265TX#

เปรียบเทียบ 7 รุ่นในงบ 22-55k:

รุ่นราคาทำไมไม่เลือก
Victus 15 RTX 5060 GDDR7฿37,990RAM 24 GB ไม่พอสำหรับ MoE model
Lenovo LOQ RTX 5050฿35,990RAM 16 GB, GPU 65W ต่ำ
Acer Nitro V16 RTX 4060 85W฿35,990GPU TGP ต่ำ, RAM 16 GB
Victus 16 RTX 4070฿53,390+15,400 ไม่คุ้ม (LLM เร็วเท่ากัน)
Victus 16-r1265TX฿37,990🏆 ชนะ
HP Victus 16-r1265TX
CPU: Intel Core i5-14450HX (10c/16t, 55W, 14th gen)
RAM: 32 GB DDR5-5600 dual channel ← unlock MoE model ได้
GPU: NVIDIA RTX 4060 Laptop 8GB (120W TGP)
Memory bandwidth: ~272 GB/s (2.4x ของเครื่องเก่า)
SSD: 512 GB NVMe Gen4
จอ: 16" FHD 165Hz 100% sRGB

เหตุผลหลัก:

  1. RTX 4060 120W TGP — สูงสุดในคลาส (TGP = Total Graphics Power, ยิ่งสูงยิ่งแรง)
  2. RAM 32 GB pre-installed (ประหยัดอัพเอง ~4,500 บาท)
  3. CPU 14th gen ใหม่สุดในคลาสนี้
  4. ราคาดีสุดในกลุ่ม RTX 4060 family

Memory bandwidth 272 GB/s = ท่อกว้างกว่าเครื่องเก่า 2.4 เท่า อันนี้สำคัญมาก เพราะ LLM token generation speed = ขึ้นกับ memory bandwidth เป็นหลัก ไม่ใช่ CPU clock speed

4. Phase 0-1 — Infrastructure + Stack Setup#

Partition + BitLocker (ป้องกันโน้ตบุ๊คหายแล้วโดนแฮกข้อมูล)#

EFI System 260 MB
Windows (C:) 237.95 GB ← BitLocker เข้ารหัส
Drive D 237.95 GB ← BitLocker เข้ารหัส (LLM อยู่ที่นี่)
Recovery 782 MB
Hibernation ON (type=full, 13.6 GB)

BitLocker = ระบบเข้ารหัสฮาร์ดดิสก์ของ Windows — ใครเอาไดรฟ์ไปต่อเครื่องอื่นจะอ่านไม่ออกเลยถ้าไม่มี recovery key

Folder structure#

D:\LLM\
├── ollama\ ← Ollama models
├── cache\ ← HF, pip, uv caches
├── rag-benchmark\ ← Python project
└── backup\

Environment variables (User scope, ไม่ต้องใช้ admin)#

Terminal window
OLLAMA_MODELS = D:\LLM\ollama\models
HF_HOME = D:\LLM\cache\hf
PIP_CACHE_DIR = D:\LLM\cache\pip
UV_CACHE_DIR = D:\LLM\cache\uv

ทำให้โมเดลทั้งหมด + cache อยู่บน D: drive ไม่ปนกับ C: ที่บีบบ่อย

Stack ที่เลือกใช้ (ขออธิบายแต่ละตัว)#

1. Ollama — LLM inference server

ลองนึกภาพ โมเดล AI เป็นไฟล์ดิบขนาดหลาย GB ที่อยู่บน disk Ollama = โปรแกรมที่โหลดไฟล์เข้า VRAM แล้วเปิด HTTP API ให้เราเรียกใช้ผ่าน localhost:11434 เหมือนเรามี ChatGPT ของตัวเองวิ่งในเครื่อง แค่ต้องเรียก endpoint เอง

ที่เลือกเพราะ:

  • รันบน Windows ได้แบบ native ไม่ต้องผ่าน Docker หรือ WSL (Linux subsystem)
  • Auto manage VRAM โหลด/ปลดโมเดลให้เอง
  • Command line ง่าย: ollama run gemma4:e4b จบเลย

2. Python 3.14 + uv package manager

uv = package manager ของ Python สมัยใหม่ (เขียนด้วย Rust) เร็วกว่า pip 10-100 เท่า ลง dependency ที่ pip ใช้ 30 วินาที uv จบใน 2 วินาที

ใช้ Python เพราะ ecosystem ML/AI สมบูรณ์สุด

3. ChromaDB — Vector database

อันนี้สำคัญ ขอเล่ายาวหน่อย

ลองนึกภาพ เรามีเอกสาร 1,000 หน้า เวลาผู้ใช้ถาม “DNS คืออะไร” เราต้องไปค้นหน้าที่เกี่ยวข้องในเอกสาร 1,000 หน้านั้น ส่งให้ AI อ่านก่อนตอบ

แบบ Ctrl+F ทั่วไป (keyword match) มันโง่ ถ้าเอกสารเขียนว่า “Domain Name System” ผู้ใช้ถาม “DNS” จะหาไม่เจอ

Vector database ใช้วิธี แปลงทั้งคำถามและทั้งเอกสารเป็น embedding = ตัวเลข 1,024 มิติ ที่บรรยาย “ความหมาย” ของข้อความ

"DNS คืออะไร" → [0.21, -0.34, 0.88, ...] (1,024 ตัวเลข)
"Domain Name System" → [0.20, -0.35, 0.87, ...] (ใกล้เคียงกัน!)
"สูตรไก่ทอด" → [-0.91, 0.44, 0.12, ...] (ห่างไกล)

แล้วหาเอกสารที่ embedding “ใกล้กัน” ที่สุด = เอกสารที่ความหมายตรงสุด ไม่ใช่ตัวอักษรตรงสุด

ChromaDB = Vector DB ที่รันแบบ embedded แปลว่าไม่ต้องตั้ง server แยก แค่ install Python library แล้วใช้เลย เหมาะสำหรับงาน local

4. bge-m3 — Embedding model

โมเดลที่ทำหน้าที่แปลงข้อความเป็นตัวเลข 1,024 มิติด้านบน ที่เลือกตัวนี้เพราะ รองรับภาษาไทยดีที่สุดใน open-source ปัจจุบัน

ทุกตัวประกอบกันยังไง#

ภาพรวมทั้ง stack เวลาพิมพ์คำถาม 1 ข้อ มันเดินทางผ่านอะไรบ้างกว่าจะได้คำตอบกลับ:

flowchart TB
    Q["💬 คำถาม"] --> ASK["ask.py<br/>(orchestrator)"]
    ASK --> BGE["bge-m3<br/>Embedding"]
    ASK --> BM["BM25<br/>Keyword tokenize"]

    BGE --> CHROMA[("ChromaDB<br/>745 chunks")]
    BM --> BMIDX[("BM25 index<br/>in-memory")]

    CHROMA --> SEM["Top 30<br/>semantic"]
    BMIDX --> KW["Top 30<br/>keyword"]

    SEM --> RRF["⚡ RRF Fusion<br/>vote คะแนน"]
    KW --> RRF

    RRF --> TOP["Top 10 chunks"]
    TOP --> OLLAMA["Ollama server<br/>localhost:11434"]
    OLLAMA --> GEMMA["🧠 Gemma 4 E4B"]
    GEMMA --> ANS["✅ คำตอบ + sources"]

    style Q fill:#4f46e5,color:#fff,stroke:#4338ca,stroke-width:2px
    style ASK fill:#7c3aed,color:#fff,stroke:#6d28d9,stroke-width:2px
    style BGE fill:#0891b2,color:#fff,stroke:#0e7490,stroke-width:2px
    style BM fill:#0891b2,color:#fff,stroke:#0e7490,stroke-width:2px
    style CHROMA fill:#0e7490,color:#fff,stroke:#155e75,stroke-width:2px
    style BMIDX fill:#0e7490,color:#fff,stroke:#155e75,stroke-width:2px
    style SEM fill:#06b6d4,color:#fff,stroke:#0891b2,stroke-width:2px
    style KW fill:#06b6d4,color:#fff,stroke:#0891b2,stroke-width:2px
    style RRF fill:#db2777,color:#fff,stroke:#be185d,stroke-width:2px
    style TOP fill:#ec4899,color:#fff,stroke:#db2777,stroke-width:2px
    style OLLAMA fill:#059669,color:#fff,stroke:#047857,stroke-width:2px
    style GEMMA fill:#10b981,color:#fff,stroke:#059669,stroke-width:2px
    style ANS fill:#16a34a,color:#fff,stroke:#15803d,stroke-width:3px

ทุกชิ้นรันบนเครื่องเรา ไม่มีอะไรออก internet เลย

5. Phase 1.5 — Math Test Gate: ใครจะเป็น Primary Model#

นี่คือ gate ที่สำคัญที่สุดทั้งโปรเจกต์

เพราะการสอบที่จะใช้ = ต้องคำนวณตัวเลข ถ้าโมเดลคำนวณผิด ไม่ว่า RAG จะดีแค่ไหน ก็พังหมด แม่นยำของการตอบ ไม่มีทางเกินแม่นยำของการคำนวณพื้นฐาน

เตรียม 55 โจทย์คำนวณการเงินแบบ multi-step (PV, NPV, WACC, ratio analysis แบบที่ต้องคำนวณหลายขั้นแล้วตัดสินใจจากผลลัพธ์) แล้วเทียบ 3 โมเดล:

ModelVRAMSpeedAccuracy
Qwen 3.5 9B (no-think)6.5 GB25 tok/s91.4%
Qwen 3.5 35B MoE A3B24 GB (offload RAM)12 tok/s91.8%
Gemma 4 E4B Q4_K_M3.4 GB45 tok/s98.2% 🏆

Q4_K_M = ระดับ “quantization” ของโมเดล อธิบายง่ายๆ คือ MP3 vs WAV โมเดลต้นฉบับ 16-bit (FP16) ไฟล์ใหญ่ใช้ RAM เยอะ “Q4” = ลดความละเอียดเหลือ 4-bit ไฟล์เล็กลง 4 เท่า แต่คุณภาพหายไปแค่ 2-5% เป็น sweet spot ที่ใช้กันส่วนมาก

MoE A3B = Mixture of Experts (Active 3B) โมเดลใหญ่ 35B parameters แต่ตอนใช้แค่ 3B (เลือกเฉพาะ “expert” ที่เกี่ยวข้องกับคำถาม) เหมือนทีมหมอ 35 คน แต่ตอบคำถามเดียวเรียกแค่ 3 คนพอ

ผลที่ไม่คาดคิดเลย Gemma 4 E4B (โมเดลเล็กที่สุด) ชนะทุกด้าน:

  • VRAM น้อยที่สุด (3.4 GB เหลือ 4.5 GB ให้ RAG context)
  • เร็วที่สุด (45 tok/s)
  • แม่นที่สุด (54/55 = 98.2%)

ลบ Qwen 35B ทิ้ง (ช้าเกิน) แล้วต่อมาก็ลบ Qwen 9B ทิ้งด้วย

Stack lock: Gemma 4 E4B + bge-m3 + ChromaDB

6. Phase 2-4 — สร้าง RAG Pipeline#

Knowledge Base — เนื้อหาที่ต้องอ่าน#

benchmarks/sample/data/knowledge/
├── Module-A.md (~95K tokens)
├── Module-B.md (~130K tokens)
└── Module-C.md (~140K tokens)
Total = ~365K tokens

Question Bank — Sample ที่หามาทดสอบ#

QuestionsSample.xlsx → questions.jsonl
- ~1,080 ข้อ (drop 2 ที่มีรูป → ใช้จริง 1,078)
- 3 หัวข้อ × 3 ระดับความยาก (ง่าย / ปานกลาง / ยาก)
- Format: { id, topic, difficulty, question, choices: {A,B,C,D}, correct }

Chunking — หั่นเอกสารใหญ่เป็นชิ้นเล็ก#

Chunking = หั่นเอกสารยาวๆ เป็นชิ้น ขนาดที่ AI กินได้ทีเดียว ทำไมต้องหั่น? เพราะ LLM มี context window จำกัด (Gemma 4 E4B = 8,192 tokens) ถ้าเอกสารยาวกว่านั้น = ส่งทั้งก้อนเข้าไม่ได้

วิธีที่ใช้: Hybrid Semantic Chunking หั่นตาม heading H2/H3 ก่อน (แบ่งตามหัวข้อ) ถ้าชิ้นไหนยังใหญ่เกิน 1,024 tokens ค่อยแตกเพิ่ม

ทำไมหั่นตาม heading? เพราะคนเขียนเอกสารแบ่งหัวข้อตาม “เรื่องเดียวกัน” อยู่แล้ว หั่นตามนี้ = ชิ้นที่ออกมาเป็น “หน่วยความหมายเดียว” ไม่ตัดกลางประโยค

benchmarks/sample/data/knowledge/*.md
↓ (chunk_data.py)
chunks.json ← 745 ชิ้น
↓ (ingest.py)
chroma_db/sample/ ← ChromaDB เก็บ vector + เนื้อหา

Prompt template ที่ใช้จริง#

You are an expert answering a multiple-choice question.
Use the context below to inform your answer.
- Plain text only (no LaTeX, no markdown)
- Read the question carefully; watch for MOST, BEST, FIRST, LEAST, NOT
- Eliminate clearly wrong choices before picking the best one
End with exactly two lines:
REASONING: <one or two sentences explaining why>
ANSWER: <single letter: A, B, C, or D>
=== CONTEXT ===
{context_block}
=== QUESTION ===
{question_text}
{choices_block}

ที่บอก “End with exactly two lines” เพื่อให้ parser ของเราดึงคำตอบออกมาง่าย (regex หา ANSWER: X ตรงท้าย)

Regex (Regular Expression) = สูตรหา pattern ของข้อความ เช่น “หาตัวอักษร A-D ที่อยู่หลังคำว่า ANSWER:” เขียนสั้นๆ เป็นสูตรเดียว ใช้กันแพร่หลายในการ parse text

7. Phase 5 — Baseline Benchmark = 74%#

รัน 320 ข้อ (stratified sample แบบสุ่มกระจาย):

Stratified sample = สุ่มแบบกระจายตามชั้น แทนที่จะสุ่ม 320 ข้อ มั่วๆ จากกองรวม เราแบ่งเป็นกลุ่ม (ง่าย/ปานกลาง/ยาก × 3 หัวข้อ = 9 กลุ่ม) แล้วสุ่มจากแต่ละกลุ่มสัดส่วนเท่ากัน ทำให้ผลที่ได้ representative ของทั้งกอง พันกว่าข้อ ไม่ใช่แค่กลุ่มที่ง่ายๆ

Config: Gemma 4 E4B + ChromaDB semantic + top_k=5
Result: 74% accuracy
Time: 12.2 วินาที/ข้อ

top_k=5 = retrieve 5 chunks ที่คล้ายกับคำถามที่สุด ส่งให้ LLM อ่านก่อนตอบ

OK เกินคาดมาก แต่ยังไม่ถึง target 85% เริ่ม tuning

8. Phase 6 — Tuning Hell (สนุกที่สุดในโปรเจกต์)#

ทางที่ลอง 1: Reranker — เพิ่มเชฟตัวที่สอง#

ทฤษฎีคือ แทนที่จะดึง top 5 ส่งให้ LLM เลย ให้ดึง top 30 มาก่อน แล้วเอาตัว Reranker มาคัดให้เหลือ 10 ที่ “ตรงโจทย์ที่สุด”

Reranker = โมเดลตัวที่สอง ออกแบบมาเพื่อ “อ่านคู่” ระหว่างคำถามกับ chunk เปรียบเทียบความตรงโจทย์ (คะแนน 0-1) เป็น cross-encoder คำตอบแม่นกว่า embedding ธรรมดา (bi-encoder)

เหมือนเชฟตัวแรกตักของจาก buffet มา 30 อย่าง เชฟตัวที่ 2 มาคัดให้เหลือ 10 อย่างที่เข้ากับคำถามจริงๆ

ผลที่ได้:

  • accuracy ดีขึ้น +1-2%
  • speed ลดลง 15 เท่า (จาก 0.6 วินาที/ข้อ เป็น 44 วินาที/ข้อ)

เริ่ม panic โทษว่า VRAM contention (4GB Gemma + 2GB reranker = ใกล้ 8GB cap)

ทดสอบแยกๆ:

Reranker เปล่าๆ → 0.6 วินาที ✅
Ollama loaded + Reranker → 44 วินาที ❌

ก็เลยท้าทายตัวเอง “VRAM ตัวเลขนิ่ง แต่ทำไมช้า แน่ใจว่า CUDA ที่โหลดไม่ได้กวนกัน?”

หา root cause จริงๆ ก็เจอ:

CUDA context switching ระหว่าง Ollama process กับ Python torch process ทุกครั้งที่ swap = flush cache, reload kernels VRAM ดูนิ่งเพราะมัน lazy-allocate แต่ context switch overhead = killer

CUDA = แพลตฟอร์มของ NVIDIA ที่ใช้ GPU มาคำนวณเรื่องที่ไม่ใช่กราฟิก (AI, simulation, etc.) Context switching = การสลับงานระหว่าง 2 process ที่ใช้ resource เดียวกัน เหมือนเชฟ 2 คนใช้เตาเดียวกัน ต้องแย่งกันใช้ ผลัดกันรอ

Decision: ตัด reranker ทิ้งครับ +1-2% accuracy ไม่คุ้มกับ 15x slowdown

แทนที่ด้วย…

ทางที่ลอง 2: Hybrid RRF — ตัวเปลี่ยนเกม#

อันนี้อยากเล่ายาวหน่อยเพราะมันคือ hero ของโปรเจกต์

ปัญหาที่เจอ บางคำถามที่ retrieval miss คือคำถามที่ มีคำเฉพาะทางเป็น keyword เช่น “DNS”, “API”, “JSON” embedding model มันจับ “ความหมาย” ได้ดี แต่จับ keyword ตรงเป๊ะ ไม่เก่ง

ฝั่งกลับกัน search แบบ keyword (BM25) ก็แย่เรื่อง paraphrase

BM25 = สูตรหาคำสำคัญในเอกสารแบบ “TF-IDF เวอร์ชันใหม่กว่า” เป็นตัวที่ Google ก่อนยุค semantic search ใช้ ฉลาดกว่า Ctrl+F ที่ rank ด้วยความถี่ + ความหายาก

Reciprocal Rank Fusion (RRF) — Cormack et al. 2009 — รวมผลจาก 2 retriever ที่ใช้คนละวิธี

# Retrieve top-30 จาก semantic (bge-m3)
sem_results = chromadb.query(query_embedding, k=30)
# Retrieve top-30 จาก BM25 (rank_bm25 library, in-memory)
bm25_results = bm25.get_top_n(query_tokens, k=30)
# RRF fusion
def rrf_score(rank, k=60):
return 1.0 / (k + rank)
combined = {}
for rank, doc in enumerate(sem_results):
combined[doc.id] = combined.get(doc.id, 0) + rrf_score(rank)
for rank, doc in enumerate(bm25_results):
combined[doc.id] = combined.get(doc.id, 0) + rrf_score(rank)
# Sort และเอา top-k
top_k = sorted(combined.items(), key=lambda x: -x[1])[:10]

อธิบายแบบไม่ใช่โค้ด:

flowchart TB
    Q["💬 คำถามเข้ามา"] --> S["🧭 Semantic search<br/>bge-m3 → top 30"]
    Q --> B["🔑 BM25 keyword search<br/>top 30"]
    S --> F["⚡ RRF Fusion<br/>vote คะแนน"]
    B --> F
    F --> T["📤 Top 10 chunks<br/>ส่งให้ LLM"]

    style Q fill:#4f46e5,color:#fff,stroke:#4338ca,stroke-width:2px
    style S fill:#0891b2,color:#fff,stroke:#0e7490,stroke-width:2px
    style B fill:#0891b2,color:#fff,stroke:#0e7490,stroke-width:2px
    style F fill:#db2777,color:#fff,stroke:#be185d,stroke-width:2px
    style T fill:#16a34a,color:#fff,stroke:#15803d,stroke-width:3px

= เอาผลของ 2 หมอมา vote กัน — chunk ที่ทั้ง 2 หมอเลือก = น่าจะเข้าจุดสุด

ทำไมมัน work:

  • Semantic catches paraphrase (“Transport Layer Security” = “TLS”)
  • BM25 catches keyword exact match (เจอตัว “TLS” ในเอกสารตรงๆ)
  • คำถามมีทั้ง 2 type → รวมกันชนะแยก

Speed? BM25 เป็น in-memory algorithm ไม่ต้องใช้ GPU เลย ~50ms/query ไม่มี CUDA contention

ทางที่ลอง 3: top_k decision#

ทดสอบ k=5, 10, 15 บน 9 ข้อที่ retrieval miss:

k=5 → ฟื้น 3/9
k=10 → ฟื้น 4/9 (Pareto better — fix ทุก k=5 fix + เพิ่ม Q0009)
k=15 → ฟื้น 1/9 (เริ่ม noise)

ตอนแรกเขียนสรุปไปว่า “k=10 ดีกว่า +1 = marginal, prefer simpler k=5”

แล้วตัวเองมาเถียงตัวเอง “อ้าว k=10 มัน strict superset ของ k=5 fixes + เร็วกว่าด้วย (13.3s vs 15.5s)”

จริงด้วย Occam’s Razor บอกว่า “เลือกอันง่ายเมื่อสมมุติฐานเสมอกัน” ตรงนี้ k=10 คือ Pareto-superior (ดีกว่าทุกด้าน) ไม่ใช่ marginal

Pareto-superior = ดีกว่าใน metric หนึ่งโดยไม่แย่ลงใน metric อื่น Occam’s Razor ใช้ตอนทุกอย่างเสมอกันเท่านั้น

Decision: top_k=10

Stack หลัง Phase 6#

config = {
"model": "gemma4:e4b",
"retrieval": "hybrid_rrf",
"top_k_final": 10,
"sem_candidates": 30, # bge-m3 top 30
"bm25_candidates": 30, # BM25 top 30
"temperature": 0.0,
"num_predict": 2500,
"num_ctx": 8192,
"think": False,
}

temperature=0.0 = บังคับให้ AI ตอบแบบ deterministic (ส่วนใหญ่) ค่าสูงๆ AI จะ “creative” มากขึ้น แต่ก็มั่วได้ การสอบ = ต้องการตอบเหมือนเดิมทุกครั้ง

9. Phase 7 — Full Benchmark พันกว่าข้อ#

Pre-flight check#

ก่อนรัน 4 ชั่วโมง:

Terminal window
# ปิด sleep ที่อาจ kill benchmark กลางทาง
powercfg /change standby-timeout-ac 0
# เช็คเสียบสายชาร์จ
Get-CimInstance Win32_Battery | Select-Object BatteryStatus
# 1 = Discharging (ยังไม่เสียบ!), 2 = AC (OK)

ตอนแรกอ่าน BatteryStatus=1 ผิดว่าโอเค จริงๆ คือ Discharging แล้ว ต้องเสียบสาย → 2 (AC) ทุกครั้ง

รันยาว 4 ชั่วโมง#

Terminal window
cd /d/LLM/rag-benchmark
uv run python scripts/run_full_1078_benchmark.py
Loaded 1078 questions from sample_full.jsonl
Configuration: gemma4:e4b + hybrid_rrf + top_k=10
Setting up retrievers...
[+] ChromaDB: 745 chunks
[+] BM25 index: 745 chunks
Checkpoint every 50 questions
Benchmarking: 1/1078 [00:24<7:13:12, 24.31s/it]
Benchmarking: 2/1078 [00:38<5:30:35, 18.57s/it]
...

ตอนเฝ้า benchmark — เห็นอะไรแปลกๆ#

ระหว่างที่รัน Task Manager โชว์ Intel UHD GPU 100% panic นิดหน่อย คิดว่ามันใช้ GPU ผิดตัวหรือเปล่า

หลังเช็ค Performance Counters:

PID 19524 claude engine=3d util=71.9% (Electron UI)
PID 4068 dwm engine=3d util= 8.6% (Windows desktop)

อ้าว นั่นคือ Claude Code UI render chart real-time ใช้ Intel iGPU ไม่ใช่ NVIDIA RTX 4060 (ที่ทำงาน benchmark) แยกกันคนละการ์ด หายห่วง

ผลรวม 3 ชั่วโมง 56 นาที#

Q1078/1078 [3:56:12]
Final accuracy: 67.62% (729/1078)
Parse failures: 15 (1.39%)
Latency: p50=8.5s, p95=19.3s, mean=10.5s
Throughput: 26.6 tokens/sec

Latency p50/p95 = เวลาที่ใช้ตอบคำถาม วัดที่ percentile p50=8.5s แปลว่า 50% ของคำถามตอบเสร็จภายใน 8.5 วินาที ส่วน p95=19.3s แปลว่า 95% ตอบเสร็จภายใน 19.3 วินาที (= ส่วนหางที่ช้า)

แยกตามความยาก:

ง่าย 87.0% (167/192) ✅
ปานกลาง 71.3% (495/694) ✅
ยาก 34.9% ( 67/192) ⚠️ ← ตัวพ่อ ขนาดผมยังตอบไม่ถูกหลายข้อ

แยกตามหัวข้อ:

Module-A 69.6% (249/358) ← strongest
Module-B 67.4% (292/433)
Module-C 65.5% (188/287)

vs Phase 5 baseline 74%: ต่ำกว่า 6 points

แต่… Phase 5 เทสบน 320 ข้อที่กลุ่ม “ยาก” น้อย ตัดกลุ่ม “ยาก” ออกจาก 1,078 = 74.7% (ดีกว่า Phase 5 ด้วยซ้ำ)

= sample bias, ไม่ใช่ regression ระบบไม่ได้แย่ลง แค่เจอข้อยากมากขึ้นในรอบนี้

10. Parse Failure Retry — ฟื้น 15 ข้อที่ตอบไม่จบ#

15 ข้อที่ parse fail = LLM เขียน reasoning ยาวเกิน → output ถูกตัด ก่อนถึง “ANSWER: X”

ขอให้ดู roadmap ของการฟื้นก่อน แต่ละขั้นช่วยแค่ไหน:

flowchart TB
    A["📋 1,078 ข้อ benchmark"] --> R0["Raw result<br/>67.6% (729 ถูก)<br/>+ parse fail 15 ข้อ"]
    R0 --> T1["Tier 1 retry<br/>num_predict 2500 → 4000"]
    T1 --> R1["ฟื้น 7/15 → +2 ถูก<br/>67.8% (731 ถูก)<br/>+ parse fail 8 ข้อ"]
    R1 --> T2["Tier 2 retry<br/>num_predict 4000 → 6000"]
    T2 --> R2["ฟื้น 0/8 → +0 ถูก<br/>67.8% (731 ถูก)<br/>+ parse fail 8 ข้อ"]
    R2 --> T3["💡 Direct prompt<br/>+ think:False"]
    T3 --> FINAL["🏆 68.3% (736/1078)<br/>0 parse failures"]

    style A fill:#4f46e5,color:#fff,stroke:#4338ca,stroke-width:2px
    style R0 fill:#d97706,color:#fff,stroke:#b45309,stroke-width:2px
    style T1 fill:#dc2626,color:#fff,stroke:#b91c1c,stroke-width:2px
    style R1 fill:#f59e0b,color:#fff,stroke:#d97706,stroke-width:2px
    style T2 fill:#dc2626,color:#fff,stroke:#b91c1c,stroke-width:2px
    style R2 fill:#f59e0b,color:#fff,stroke:#d97706,stroke-width:2px
    style T3 fill:#7c3aed,color:#fff,stroke:#6d28d9,stroke-width:2px
    style FINAL fill:#facc15,color:#000,stroke:#eab308,stroke-width:3px

= Tier 1 ช่วยฟื้นได้ 7/15 → Tier 2 เพิ่ม token budget เปล่าๆ ฟื้น 0 → Direct prompt + think:False = silver bullet ฟื้นทุกข้อที่เหลือ

Stage 1: Tier 1 + 2 retry (เพิ่ม token budget)#

# Tier 1: num_predict 2500 → 4000
# Tier 2: num_predict 4000 → 6000 (สำหรับที่ Tier 1 ยัง fail)

ผลลัพธ์:

  • Tier 1: ฟื้น 7/15
  • Tier 2: ฟื้น 0/8 (เพิ่ม token ก็ไม่ช่วย ปัญหาคนละชั้น)

Stage 2: Direct prompt (ไอเดียที่ลอง)#

“งั้นส่ง prompt ตรงๆ ไม่ผ่าน RAG ลองดู ถ้า model จำได้เอง ไม่ต้อง context”

prompt = f"""Question: {q['question']}
A: {opts['A']}
B: {opts['B']}
C: {opts['C']}
D: {opts['D']}
Answer with ONLY the letter (A, B, C, or D):"""

รันครั้งแรก LLM ตอบว่างเปล่า!?

เจอ bug 1: Field ชื่อ choices ไม่ใช่ options

opts = q.get('options', {}) # ← None ตลอด, prompt ส่ง A: B: C: D: ว่าง
opts = q.get('choices', {}) # ← OK

เจอ bug 2: Gemma 4 E4B think: true เป็น default ตั้ง num_predict=50 (ตอบสั้นๆ พอ) แต่ model ใช้ 50 tokens ไป คิดใน <think> block หมด ไม่เหลือ token ตอบจริง

# เพิ่ม think: False เข้าไปใน body
body = json.dumps({
"model": "gemma4:e4b",
"prompt": prompt,
"stream": False,
"think": False, # ← ต้องใส่
...
})

รันใหม่ทั้ง 8 ข้อที่เหลือ:

[1/8] Q0110 | ปานกลาง | correct=D | pred=A | WRONG
[2/8] Q0114 | ยาก | correct=B | pred=B | CORRECT
[3/8] Q0161 | ปานกลาง | correct=A | pred=A | CORRECT
[4/8] Q0176 | ปานกลาง | correct=C | pred=C | CORRECT
[5/8] Q0197 | ปานกลาง | correct=B | pred=A | WRONG
[6/8] Q0051 | ยาก | correct=B | pred=B | CORRECT
[7/8] Q0192 | ง่าย | correct=B | pred=B | CORRECT
[8/8] Q0204 | ปานกลาง | correct=A | pred=B | WRONG
Parsed: 8/8 ← parse fail = 0 🎉
Correct: 5/8 ← ฟื้น 5 ข้อ

Final Score#

Stage Acc Correct Parse_fail
Raw benchmark 67.6% 729 15
+ Tier 1+2 retry 67.8% 731 8
+ Direct prompt 68.3% 736 0 ← 🏆

11. ask.py — Interactive Multi-KB#

วันสอบจริงต้องมี UI ที่ใช้ได้จริงๆ เลยเขียน scripts/ask.py:

Features:

  • Stack เดียวกับ benchmark 100% (Gemma 4 E4B + Hybrid RRF + top_k=10)
  • Multi-KB routing สั่ง /exam หรือ /another หรือ /direct
  • Multi-line input: /m (paste โจทย์ยาวๆ)
  • Source display: /sources (ดูว่าคำตอบอ้างจาก chunk ไหน)
  • Lazy KB loading (cached) โหลดทีเดียวต่อ session
  • Offline 100%

Architecture:

/direct ──→ Gemma 4 E4B (ไม่ผ่าน RAG)
/
ask.py ── router ── /exam ──→ Hybrid RRF (745 chunks) → Gemma
\
/another ─→ Hybrid RRF (อีก KB) → Gemma

ตัวอย่างการใช้:

[/exam] Question: What is the primary purpose of <topic>?
Retrieving from /exam... OK (10 chunks, 7.2s)
Generating answer... OK (24.5s)
REASONING: ...
ANSWER: ...
Sources (10 chunks):
[ 1] Module-B > 2.3 > 2.3.4
[ 2] Module-A > 1.1 > 1.1.2
...

12. Cleanup — เคลียร์ 15.4 GB#

หลัง pipeline เสถียร โยน 4 batch ให้ subagents ทำพร้อมกัน:

Batchงานผล
1ลบ Qwen 9B + drop torch/sentence-transformers + uv prune-15.4 GB
2Archive 8 diagnostic scripts → scripts/archive/clean code
3ลบ empty folders + duplicate JSON results-6 MB
4Rewrite main.py เป็น project entry menuusability
Before: 17 GB (Ollama) + 13 GB (uv cache) + 104 MB (venv)
After: 11 GB (Ollama) + 3.7 GB (uv cache) + 48 MB (venv)
Freed: ~15.4 GB

หลัง cleanup รัน regression test — ask.py ยังทำงานปกติ ✅

13. Project Structure ทั้งหมด (เผื่อใครจะทำตาม)#

D:\LLM\rag-benchmark\
├── main.py ← entry menu
├── pyproject.toml ← uv project config
├── README.md
├── benchmarks\sample\ ← Question Sample ที่ทดสอบ
│ ├── data\
│ │ ├── chunks.json ← 745 chunks
│ │ ├── knowledge\Module-*.md ← 3 KB files
│ │ ├── questions.jsonl ← 1,078 questions
│ │ └── samples\sample_full.jsonl
│ └── schemas\
├── chroma_db\sample\ ← vector DB
├── results\sample\
│ ├── full_1078_*_final.json ← 🏆 final 68.3%
│ └── analysis\
├── scripts\
│ ├── ask.py ← 🌟 interactive Q&A
│ ├── run_full_1078_benchmark.py
│ ├── retry_parse_failures.py
│ ├── analyze_benchmark.py
│ ├── prepare_data.py ← Excel → JSONL
│ ├── chunk_data.py ← MD → chunks
│ ├── ingest.py ← chunks → ChromaDB
│ └── archive\ ← deprecated diagnostics
└── src\rag_benchmark\
├── ingest.py ← MD chunking + embedding
├── retrieve.py ← ChromaDB queries
├── retrieve_hybrid.py ← 🌟 Hybrid RRF
├── generate.py ← prompt + Ollama call
├── evaluate.py ← parsing + scoring
└── pipeline.py ← orchestrator

14. บทเรียนสำคัญที่ได้#

  1. VRAM size > everything สำหรับเลือก laptop รัน LLM
  2. Memory bandwidth > CPU สำหรับ generation speed
  3. MoE A3B ต้อง VRAM ใหญ่จริง (24GB+) บน 8GB ไม่ได้ประโยชน์
  4. โมเดล 4B parameters = ไม่เหมาะกับงานภาษาไทยที่เดิมพันสูง (loops, hallucination, ภาษาจีนหลุด)
  5. Reranker บนเครื่อง CUDA contention = ฆ่า speed 15x ตัดทิ้งดีกว่า
  6. Hybrid RRF (semantic + BM25) > semantic-only เกือบทุกกรณี
  7. Pareto-superior ≠ marginal Occam’s Razor ใช้ตอน sample เสมอกันเท่านั้น
  8. CUDA non-determinism มีจริง same temp=0 ก็ให้ผลต่างได้บน GPU
  9. Pre-flight check ก่อนรัน job ยาวๆ ปิด sleep, เสียบสาย, เช็ค Battery status / GPU ที่ใช้จริงให้ครบ
  10. Direct prompt + think:False ฟื้น parse failures ที่ RAG prompt ทำไม่ได้

15. สรุปสุดท้าย — สิ่งที่ได้จาก 3-4 วันนี้#

Stack ที่ lock แล้ว: Gemma 4 E4B + Hybrid RRF + top_k=10 ✅ Question Sample พันกว่าข้อ: ตอบถูก 68.3% (736/1078) ✅ Parse failures = 0 หลัง retry เป็นขั้น ✅ Interactive ask.py พร้อมใช้งาน multi-KB ✅ Disk เคลียร์ไป 15.4 GB จาก uv cache + โมเดลเก่าที่ไม่ใช้

แล้วถ้าจะถามว่าตัวเลข 68.3% เนี่ยเอาไปทำอะไรได้?

ของจริงคือ สอบที่ผมต้องไป ไม่ใช่ multiple choice มันคือสอบเขียน

คำตอบที่ AI ให้มา ก็ต้องเอาไปเขียนเองอยู่ดี แต่คุณค่ามันอยู่ที่:

  1. ไม่ต้องเปิดหาเองว่าเรื่องนี้อยู่บทไหน RAG ดึง chunk ที่เกี่ยวข้องมาให้แล้ว ทั้งย่อหน้าก็มีให้อ่าน
  2. มีตัวช่วยคิดวิเคราะห์ AI สรุปประเด็นให้ก่อน เอาไปเรียบเรียงเป็นคำตอบของตัวเองได้
  3. ค้นเร็วกว่าไฮไลท์ในชีท ถามคำถามภาษาคน ได้ context ทันที ไม่ต้องเปิดสารบัญ

= AI ไม่ได้สอบแทน AI เป็นผู้ช่วยที่นั่งข้างๆ เปิดหนังสือให้ทันที + สรุปประเด็นให้คิด ส่วนการเขียนคำตอบ = ยังเป็นของผมเอง


บันทึกตอนปิด#

ทั้งหมดที่ลงทุนไป:

  • 💻 Laptop ใหม่ HP Victus 16: ฿37,990 (ไม่ถึง 40K ถูกกว่าที่ตอนแรกผมคิดเยอะ คิดว่าต้องไป 50-60K แน่ๆ)
  • เวลา: 3-4 วัน หมดเป็นวันๆ เลย
  • 💸 ค่า cloud / API: 0 บาท

ที่เรียนรู้สำคัญที่สุดคือ process มันสำคัญกว่า outcome

ทุกหลุมที่ลงไปงม (โมเดลเล็กๆ ที่หลอน, reranker ช้าจน 15x, top_k debate ที่เถียงกับตัวเอง, parse failure ที่ต้องแกะหา bug 2 ตัว) คือ knowledge ที่ติดตัวไป ราคาเป็น 0 ผมรู้สนามนี้ดีกว่าตอนเริ่มเป็น 100 เท่าแน่นอน

Stack อยู่นี่ Reference อยู่นี่ เผื่อตัวเอง 3 เดือนข้างหน้าจะมา refer ก็ตาม


ป.ล. สำหรับคนที่อยากลองทำตาม สเปคเครื่องที่แนะนำ minimum:

  • GPU: RTX 3060 / 4060 / 5060 8GB+ VRAM (12GB ดีกว่าถ้างบถึง)
  • RAM: 32GB DDR5 (16GB พอใช้ แต่ตึงสำหรับโมเดลใหญ่ๆ)
  • SSD: NVMe 512GB+ (โมเดลหนักหลาย GB ต้องมีที่)
  • CPU: ไม่สำคัญเท่า GPU/RAM i5/Ryzen 5 ปัจจุบันก็เพียงพอ

ใครเดินตามจะใช้เวลาน้อยกว่าเยอะแน่นอน เพราะหลุมหลายอันผมตกไปก่อนแล้ว ไม่ต้องไปงมซ้ำ

ส่วนผมตอนนี้ กลับไปอ่านชีทเตรียมสอบต่อครับ 555+


อ้อ เกือบลืม

ถ้าไม่มี Claude Code ช่วยทำกับไกด์ทางตลอดเส้นทางนี้ ผมไม่มีทางจบได้ใน 3-4 วันแน่ๆ ฮาาาาา 555+

จะให้ debug CUDA context switching เอง? คิดสูตร RRF เอง? นั่งเถียงเรื่อง Pareto-superior กับตัวเองรอบสอง? ไม่ไหวจริงๆ ครับ ตอนเจอ bug ของ choices vs options field ก็คือ Claude เป็นคนช่วยจับให้

ก็เลยกลายเป็นว่า AI ช่วยสร้าง AI กลับมาใช้ในห้องสอบเรื่อง AI อีกที 🤝

มันเริ่มจะวนซ้ำกันแล้วนะ 555+