ผู้เขียน: nutttaro

  • สร้าง Local LLM ใช้เองตั้งแต่ศูนย์

    สร้าง Local LLM ใช้เองตั้งแต่ศูนย์

    บทเรียนจากการลอง 7 โมเดลบน RTX 5060 Ti 16GB

    เปิด OpenWebUI ให้ทีมใช้ผ่าน internet, ต่อ Claude Code เข้า local model, แล้วเสิร์ฟ API แบบ OpenAI ให้ WordPress — ทั้งหมดบนการ์ดใบเดียว พร้อมหลุมพรางที่เจอจริงระหว่างทาง

    ทำไมต้อง Local LLM

    ผมอยากได้ระบบ AI ส่วนตัวที่: คุยได้ผ่านเว็บแบบ ChatGPT, ค้นเว็บและตอบจากเอกสารของเราเองได้, ต่อกับ Claude Code ไว้ช่วยเขียนโค้ด, และเปิด API ให้แอปอื่น (เช่น WordPress) เรียกไปใช้ generate ข้อความ — โดยข้อมูลไม่ต้องออกไปไหน ไม่มีค่า subscription รายเดือน

    ฮาร์ดแวร์ที่มี: Ubuntu, การ์ดจอ RTX 5060 Ti VRAM 16GB ฟังดูเหมือนพอ แต่ตัวเลข 16GB นี่แหละที่กลายเป็นตัวกำหนดทุกการตัดสินใจตลอดทาง

    สิ่งที่ผมเรียนรู้คือ การ setup ส่วน “ให้มันทำงาน” ใช้เวลาไม่นาน แต่การ “ทำให้มันทำงานได้ดีจริง” ต้องผ่านการลองผิดลองถูกเยอะมาก โดยเฉพาะเรื่องเลือกโมเดลให้เหมาะกับ VRAM ที่มี บทความนี้เลยเล่าทั้งวิธีทำและหลุมที่ตกลงไป เผื่อใครจะทำตามจะได้ข้ามหลุมเหล่านี้

    ส่วนที่ 1: วาง stack หลัก — Ollama + OpenWebUI

    Local LLM and Ollama

    ตัวเลือก framework ที่ลงตัวที่สุดสำหรับผู้เริ่มต้นคือ Ollama — ติดตั้งง่าย จัดการ model ง่าย มี REST API ในตัว และเข้ากับ OpenWebUI ได้ตรงๆ

    curl -fsSL https://ollama.com/install.sh | sh
    ollama pull qwen3:14b

    จากนั้นรัน OpenWebUI ผ่าน Docker เป็นหน้าเว็บสำหรับใช้งาน และต่อ Cloudflare Tunnel เพื่อเปิดให้เข้าถึงจาก internet ได้โดยไม่ต้อง forward port (tunnel เป็น outbound connection จึงทำงานได้แม้อยู่หลัง NAT หรือ IP สาธารณะเปลี่ยน)

    หลุมที่ 1: container เชื่อม Ollama ไม่ได้

    อาการแรกที่เจอคือ OpenWebUI ขึ้น “Ollama: Network Problem” ทั้งที่ Ollama รันอยู่ ไล่เช็คจนเจอว่า OpenWebUI ใน container ยิงไปที่ host ผ่าน host.docker.internal แต่ packet ถูก UFW (firewall) ดรอป เพราะมีแต่ rule allow port 22 (อาจเป็นเพราะ IPv6 ?)

    วิธีแก้คืออนุญาต traffic จาก docker subnet:

    sudo ufw allow from 172.16.0.0/12 to any port 11434
    sudo ufw reload

    บทเรียน: ถ้า container เชื่อม service บน host ไม่ได้ และ curl จากใน container ขึ้น Connection timed out (ไม่ใช่ refused) — มักเป็น firewall ดรอป ไม่ใช่ service ไม่ทำงาน

    หลุมที่ 2: ดึง Docker image ไม่ได้

    ตอนจะติดตั้ง SearXNG (search engine สำหรับ web search) เจอ TLS handshake timeout เวลา pull จาก Docker Hub ทั้งที่ image จาก ghcr.io ดึงได้ปกติ ต้นเหตุคือ DNS ของ ISP resolve CDN ของ docker.io ได้ไม่ดี แก้โดยตั้ง DNS ของ Docker daemon ตรงๆ:

    echo '{"dns": ["1.1.1.1", "8.8.8.8"]}' | sudo tee /etc/docker/daemon.json
    sudo systemctl restart docker

    แค่นี้ก็ดึง image ได้ บทเรียน: ปัญหา network ของ Docker หลายอย่างแก้ที่ DNS ได้โดยไม่ต้องเปลี่ยนเน็ต

    ส่วนที่ 2: ทำให้ฉลาดขึ้น — Web Search + RAG

    LocalLLM with OpenWebUI

    OpenWebUI มี web search และ RAG (ตอบจากเอกสาร) ในตัว แค่เปิดใช้และตั้งค่า SearXNG เป็น search engine แต่พอลองค้นจริง เจออาการประหลาด: ค้นเจอ แต่สรุปมั่ว บางทีก็ขึ้น “No search result” แล้วตอบจากความรู้เก่า

    ไล่ดู log ของ Ollama เจอบรรทัดชี้เป็น: task.n_tokens = 23 — แปลว่าผลค้นหาไม่ได้ถูกส่งเข้า model เลย model เห็นแค่คำถามเปล่าๆ

    ต้นเหตุจริงคือ embedding model พัง (error 500) ทำให้ผลค้นหา embed ไม่ได้ → vector DB ว่าง → retrieve ได้ 0 ท่อน วิธีแก้คือเปลี่ยน embedding model เป็น bge-m3 ซึ่งรองรับภาษาไทยได้ดีด้วย:

    ollama pull bge-m3

    ตั้งใน Admin Settings → Documents → Embedding Model เป็น bge-m3 แล้วลองใหม่ คราวนี้ log ขึ้น n_tokens = 16324, truncated = 0 — ผลค้นหาเข้า model ครบ สรุปได้ถูกต้อง

    อีกตัวที่สำคัญคือ num_ctx (context window) — Ollama ตั้ง default ไว้แค่ 4096 ซึ่งเล็กเกินไปสำหรับ web search ที่ต้องยัดผลหลายหน้าเข้า context ต้องขยายเป็น 16384–32768 ผ่าน Advanced Params ของ model

    ส่วนที่ 3: บทเรียนใหญ่ — เลือกโมเดลให้เหมาะกับ VRAM

    นี่คือส่วนที่ใช้เวลามากที่สุด และได้บทเรียนเยอะที่สุด ผมลองโมเดลไป 7 ตัวกว่าจะเจอตัวที่ใช่ ขอสรุปเป็นตารางก่อน:

    Modelขนาดลง GPU เต็ม?eval rateผลสรุป
    qwen3.5:9b6.6GB✅ 100%~70 tok/sเร็วสุด
    qwen3:14b9.3GB✅ 100%~37 tok/sสมดุล ฉลาดดี
    gemma4:e4b9.6GB✅ 100%~50 tok/sมั่ว, output overflow
    glm-4.7-flash19GB❌ offload 26%~50 tok/sMoE เร็วแม้ offload แต่ prompt eval ผันผวน
    batiai/qwen3.6-27b:q313–19GB⚠️ ขึ้นกับ context~26 tok/soffload ถ้า context ใหญ่
    qwen3.6:27b17GB❌ offload หนัก~14 tok/sช้าเกินไป

    บทเรียน A: dense model ที่ใหญ่เกิน VRAM = ช้าทันที

    qwen3.6:27b (17GB) ใหญ่กว่า VRAM 16GB ทำให้ Ollama ต้อง offload บางส่วนไป CPU (ollama ps ขึ้น 12%/88% CPU/GPU) ผลคือ eval rate ดิ่งจาก ~70 เหลือ 14 tok/s ตอบคำถามที่ต้องเขียนยาวๆ ใช้เวลาเป็นนาที

    บทเรียน B: context window กิน VRAM มหาศาล

    เคสที่ทำให้ผมเข้าใจเรื่องนี้ชัดสุดคือ batiai/qwen3.6-27b:q3 ตัว model เองแค่ 13GB (ลง GPU ได้) แต่พอ default context เป็น 128K ตัว KV cache บวมจน SIZE รวมเป็น 19GB → offload → ช้า พอลด context เหลือ 16K ตัวเดียวกันกลับมาเป็น 100% GPU และเร็วขึ้นเกือบ 3 เท่า

    บทเรียน: บนการ์ด VRAM จำกัด คุณต้องเลือกระหว่าง “params เยอะ” กับ “context ใหญ่” — เลือกได้อย่างเดียว

    บทเรียน C: thinking mode ทำให้ “รู้สึก” ช้า

    ผมเคยสงสัยว่าทำไม Ollama บน Ubuntu รู้สึกช้ากว่าบน Windows ทั้งที่ใช้ CUDA เหมือนกัน (เช็คจาก log ว่าใช้ CUDA จริง ไม่ใช่ Vulkan) คำตอบคือ thinking mode — model ตระกูล qwen3 จะ “คิด” ก่อนตอบ ถาม “Hello” คำเดียวมันคิด 259 tokens ก่อนตอบ 10 tokens

    ทดสอบเทียบ: ถาม “Hello” บน qwen3.5:9b เปิด thinking ใช้ 4.2 วินาที, ปิด thinking (/set nothink ใน CLI) เหลือ 0.87 วินาที — eval rate เท่าเดิม (~70 tok/s) แต่ total duration ต่างกัน 5 เท่า เพราะจำนวน token ที่ต้อง generate ต่างกัน

    บทเรียน: GPU ความเร็วเท่าเดิม แต่ thinking ทำให้ต้องผลิต token เยอะกว่ามากต่อคำตอบ สำหรับงาน chat ทั่วไปปิดได้เลย คุ้มกว่า

    ส่วนที่ 4: ต่อ Claude Code เข้า local model

    Ollama v0.14+ รองรับ Anthropic Messages API แล้ว Claude Code จึงต่อกับ local model ได้โดยตรงไม่ต้องใช้ proxy แค่ตั้ง environment variables:

    export ANTHROPIC_BASE_URL=http://localhost:11434
    export ANTHROPIC_AUTH_TOKEN=ollama
    export ANTHROPIC_API_KEY=""
    export ANTHROPIC_DEFAULT_SONNET_MODEL=qwen3.5:9b
    # ตั้ง HAIKU และ OPUS เป็น model เดียวกันด้วย เพราะ Claude Code route ไปหลาย tier

    หลุมที่ 3: tool calling format ต้องถูก

    ปัญหาที่ปวดหัวสุดคือ บาง model สั่งให้ “สร้างไฟล์ index.html” แล้วมันพ่นโค้ดออกมาเป็นข้อความแทนที่จะสร้างไฟล์จริง

    วิธี debug ที่ได้ผลคือ ยิง API ใส่ tool definition ตรงๆ แล้วดูว่า response มี field tool_calls แยกออกมา (ถูก) หรือ JSON ปนอยู่ใน content (ผิด):

    curl http://localhost:11434/api/chat -d '{
      "model": "MODEL_NAME",
      "messages": [{"role":"user","content":"create index.html"}],
      "tools": [{"type":"function","function":{"name":"write_file","parameters":{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}}}}}],
      "stream": false
    }'

    ผลที่เจอน่าสนใจมาก: qwen2.5-coder (coding model แท้ๆ) กลับพ่น tool call เป็นข้อความใน content — Claude Code parse ไม่ได้ ส่วน qwen3.5:9b และ qwen3:14b ให้ tool_calls ถูกต้อง

    บทเรียน: สำหรับ Claude Code ปัจจัยสำคัญไม่ใช่ว่าเป็น “coding model” หรือไม่ แต่คือ tool-call format ถูกต้องไหม — ทดสอบด้วย curl ก่อนเสมอ

    ผู้ชนะ: qwen3.5:9b

    หลังลองมาทั้งหมด qwen3.5:9b กลายเป็นตัวที่ลงตัวสุดสำหรับ Claude Code บนการ์ดนี้ เพราะ:

    • ขนาดแค่ 6.6GB → เหลือ VRAM ~8GB เอาไปทำ context ได้ถึง 64K (ที่ Claude Code แนะนำ) โดยยังลง GPU เต็ม
    • tool-call format ถูกต้อง
    • เร็ว ไม่ offload → ไม่ timeout
    • ที่เซอร์ไพรส์สุด: ทดสอบสั่ง “Create HTML Hello World” โดยไม่บอกให้ save และไม่มี CLAUDE.md ช่วย — มันเข้าใจ intent เอง สร้างไฟล์ hello.html ให้ พร้อมตั้งชื่อไฟล์เอง

    บทเรียน: VRAM ที่ “เหลือเยอะ” ไม่ได้เสียเปล่า — มันเอาไปทำ context window ใหญ่ได้ ซึ่ง model ใหญ่ที่กิน VRAM หมดทำไม่ได้

    หลุมที่ 4: Cloudflare Tunnel มี timeout 100 วินาที

    ตอนเปิด Claude Code ให้เรียก API ผ่าน tunnel จากเครื่องนอก เจอ Error 524 — Cloudflare มี hard limit ที่ ~100 วินาทีต่อ request ถ้า model ตอบไม่ทันจะถูกตัด โมเดลที่ offload (ตอบช้า) จะชน limit นี้บ่อย แต่ qwen3.5:9b ที่เร็วและไม่ offload ผ่านได้สบาย

    สำหรับการเปิด API ออก internet ผมใช้ nginx เป็น reverse proxy หน้า Ollama แล้วบังคับ Bearer token auth (เพราะ Ollama API ไม่มี authentication ในตัว — ห้ามเปิด port 11434 ออก internet ตรงๆ เด็ดขาด):

    server {
        listen 8443;
        location / {
            if ($http_authorization != "Bearer YOUR_SECRET_TOKEN") {
                return 401 '{"error":"unauthorized"}';
            }
            proxy_pass http://127.0.0.1:11434;
            proxy_buffering off;
            proxy_read_timeout 600s;
        }
    }

    ส่วนที่ 5: เสิร์ฟ API แบบ OpenAI ให้ WordPress

    ข้อดีของ Ollama คือ expose endpoint หลายแบบพร้อมกันบน port เดียว — /v1/messages (Anthropic format สำหรับ Claude Code) และ /v1/chat/completions (OpenAI format) ดังนั้น nginx + Bearer auth ตัวเดียวรองรับได้ทั้ง Claude Code, WordPress, และแอปอื่นๆ

    หลุมที่ 5: thinking ของ qwen3.5 ปิดยากผ่าน API

    WordPress plugin ขอ generate ข้อความ แต่ qwen3.5 ดัน thinking ทำให้ response มี field reasoning ยาวเป็นพัน token ทั้งที่คำตอบจริงแค่ 3 ประโยค ปัญหาคือ:

    • SYSTEM /no_think ใน Modelfile ไม่ทำงานเมื่อ client ส่ง system prompt เอง
    • PARAMETER think false ใน Modelfile ยังไม่รองรับ (error)
    • วิธีเดียวที่ได้ผลคือส่ง think: false ใน API request — แต่ WordPress plugin หลายตัวใส่ custom param ไม่ได้

    ทางออกที่ practical สำหรับงาน generate text คือใช้ model ที่ ไม่มี thinking ตั้งแต่แรก เช่น qwen2.5 ทั่วไป (generation เก่ากว่า ตอบตรงๆ ไม่มี reasoning กวน) — แยกหน้าที่ชัดเจน: qwen3.5:9b สำหรับ Claude Code, qwen2.5 สำหรับงานเขียนผ่าน WordPress

    สรุป

    หลังผ่านหลุมทั้งหมด ระบบที่ได้คือ:

    • OpenWebUI + web search + RAG สำหรับ chat ทั่วไป เปิดให้ทีมใช้ผ่าน Cloudflare Tunnel
    • Claude Code ต่อ qwen3.5:9b (context 64K) สำหรับช่วยเขียนโค้ด
    • OpenAI-compatible API ให้ WordPress และแอปอื่นเรียก generate ข้อความ
    • ทั้งหมดผ่าน nginx + Bearer auth, ทุก service auto-start หลัง reboot

    บทเรียนที่สำคัญที่สุดสำหรับใครที่มีการ์ด 16GB:

    1. เลือก model ที่ลง GPU เต็ม สำคัญกว่าเลือก model ที่ params เยอะ — model เล็กที่เร็วและไม่ offload ชนะ model ใหญ่ที่ offload เสมอ
    2. VRAM ที่เหลือไม่เสียเปล่า — เอาไปทำ context window ใหญ่ได้
    3. ทดสอบ tool-call format ด้วย curl ก่อนสรุปว่า model ใช้กับ Claude Code ได้
    4. ปิด thinking สำหรับงานที่ไม่ต้องการเหตุผลหลายขั้น — ช่วยเรื่องความเร็วและ token usage มาก
    5. อย่าเปิด Ollama API ออก internet ตรงๆ — ครอบด้วย auth เสมอ

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

  • เจอปัญหาเข้า code.visualstudio.com ไม่ได้ สุดท้ายต้นเหตุคือ IPv6

    เจอปัญหาเข้า code.visualstudio.com ไม่ได้ สุดท้ายต้นเหตุคือ IPv6

    ช่วงนี้เจอปัญหาแปลก ๆ กับอินเทอร์เน็ตของค่ายหนึ่ง คือไม่สามารถเข้า code.visualstudio.com ผ่าน Chrome และ Firefox ได้ แต่กลับเข้าได้ปกติผ่าน Safari

    ตอนแรกคิดว่าเป็นปัญหาที่ Browser แต่พอลองไปเรื่อย ๆ ก็พบว่าไม่ใช่

    ลองติดต่อ Call Center ก็ทำได้ค่อนข้างยาก และคำแนะนำที่ได้รับก็มีเพียงให้รีเซ็ต Router ซึ่งไม่ได้ช่วยแก้ปัญหาอะไร

    เริ่มไล่หาสาเหตุ

    ลองตรวจสอบเส้นทางด้วย traceroute

    IPv4

    traceroute code.visualstudio.com
    

    IPv6

    traceroute6 code.visualstudio.com
    

    ผลที่ได้ดูเหมือนจะปกติทั้งคู่

    จากนั้นลองทดสอบด้วย curl

    curl -4 -I https://code.visualstudio.com
    

    IPv4 ตอบกลับปกติ

    HTTP/2 200
    

    แต่เมื่อบังคับให้ใช้ IPv6

    curl -6 -I https://code.visualstudio.com
    

    กลับได้ผลลัพธ์

    curl: (35) Recv failure: Connection reset by peer
    

    ตรงนี้ทำให้เริ่มสงสัยว่าปัญหาน่าจะเกี่ยวกับ IPv6 มากกว่า Browser

    ผลกระทบที่เจอ

    ที่บ้านผมมี Raspberry Pi ทำหน้าที่เป็น Server และบางครั้งก็ใช้ VS Code Remote SSH เข้าไปทำงาน

    ปัญหาคือ VS Code ฝั่ง Client จะพยายามดาวน์โหลด VS Code Server ลงบนเครื่องปลายทาง แต่เมื่อเชื่อมต่อผ่าน IPv6 ก็โหลดไม่สำเร็จ ทำให้ใช้งาน Remote SSH ไม่ได้

    ทางแก้ชั่วคราวคือต้องปิด IPv6 บน Server หรือบังคับให้ใช้งานผ่าน IPv4

    อีกเรื่องที่นึกขึ้นได้คือ เมื่อปีที่แล้วผมเคยเจอปัญหาคล้ายกันกับเกม HELLDIVERS 2 ซึ่งสุดท้ายก็ต้องปิด IPv6 ถึงจะเล่นได้ตามปกติ

    ลองวิเคราะห์เพิ่มเติม

    ผมลองถาม Claude พร้อมแนบผลการทดสอบไป ได้คำอธิบายประมาณนี้

    สาเหตุที่เป็นไปได้

    1. Azure Front Door ไม่รับ IPv6 TLS Handshake จาก IP ของคุณ
    2. ISP อาจมีปัญหาเรื่อง IPv6 Routing ไปยัง Azure
    3. Azure บาง PoP อาจมีปัญหากับ IPv6 ชั่วคราว

    จาก Log ที่เห็น

    • TCP เชื่อมต่อไปยัง code.visualstudio.com ผ่าน IPv6 ได้
    • ส่ง TLS Client Hello ได้
    • Server ตอบกลับด้วย Server Hello
    • หลังจากนั้นการเชื่อมต่อถูก Reset ระหว่าง TLS Handshake

    สรุปคือ TCP เชื่อมต่อได้ แต่การเชื่อมต่อถูกตัดระหว่าง TLS Handshake ซึ่งไม่น่าจะเป็นปัญหาของ Browser หรือเครื่องผู้ใช้

    หากการวิเคราะห์นี้ถูกต้อง ก็มีความเป็นไปได้ว่าปัญหาอยู่ระหว่าง IPv6 Routing ของ ISP กับ Azure Front Doorมากกว่าจะเป็นปัญหาที่เครื่องฝั่งผู้ใช้

    สรุป

    จากที่ทดสอบมา ผมพบว่า

    • Safari ยังเข้า code.visualstudio.com ได้
    • Chrome และ Firefox เข้าไม่ได้
    • IPv4 ใช้งานได้ปกติ
    • IPv6 จะถูกตัดการเชื่อมต่อระหว่าง TLS Handshake (Connection reset by peer)
    • VS Code Remote SSH ได้รับผลกระทบ เพราะดาวน์โหลด VS Code Server ไม่สำเร็จผ่าน IPv6
    • ก่อนหน้านี้ก็เคยเจอปัญหาลักษณะคล้ายกันกับ HELLDIVERS 2 ซึ่งแก้ได้ด้วยการปิด IPv6

    เลยอยากสอบถามว่ามีใครใช้อินเทอร์เน็ตค่ายเดียวกัน หรือเคยเจออาการแบบนี้บ้างไหมครับ?

    ถ้าใครเคยเจอ รบกวนแชร์ด้วยว่าเป็น ISP ไหน และมีวิธีแก้ที่ดีกว่าการปิด IPv6 หรือเปล่า จะได้ลองทดสอบเพิ่มเติมครับ

  • เอ้า…เปิด

    เอ้า…เปิด

    เอ้า…เปิดบล็อกของ NUTTTARO พื้นที่สำหรับแบ่งปันความรู้ ประสบการณ์ และไอเดียเกี่ยวกับการพัฒนาเว็บไซต์, SEO, AI, เครื่องมือสำหรับนักพัฒนา และเทคโนโลยีที่ช่วยให้การทำงานมีประสิทธิภาพมากขึ้น


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


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


    ขอบคุณที่แวะมาเยี่ยมชม แล้วพบกันในบทความถัดไป!