Ollama รัน LLM ในเครื่อง

🦙

Ollama — รัน LLM บนเครื่องตัวเอง
แล้วทำแชตบอทต่อกับ Nuxt

รันโมเดลอย่าง Llama · Qwen · Gemma · DeepSeek บนเครื่องของเราเอง ไม่ต้องพึ่งคลาวด์ ไม่มีค่า token และข้อมูลไม่ออกจากเครื่อง

🎯 เป้าหมาย: จบบทนี้จะติดตั้ง Ollama ใช้งานได้ · ต่อ API เข้ากับหน้าเว็บที่เขียนเองแบบพิมพ์ทีละคำ · ใส่บุคลิกให้บอท · และทำ RAG ให้ตอบจากเอกสารของหน่วยงานเอง
📖 Ollama คืออะไร และเหมาะกับงานแบบไหน

Ollama คือตัวรัน LLM บนเครื่องตัวเอง · พอติดตั้งแล้วมันจะเปิด REST API ให้อัตโนมัติที่ localhost:11434 — นี่คือหัวใจ เพราะแปลว่าแอปอะไรก็ยิงเข้ามาได้

 Ollama (ในเครื่อง)API บนคลาวด์ (Claude / GPT)
ค่าใช้จ่ายฟรี ไม่จำกัดจ่ายตาม token
ข้อมูลออกนอกเครื่องไม่ออกออก
คุณภาพคำตอบสู้ไม่ได้ดีกว่าชัดเจน
ต้องมีบัตรเครดิตไม่ต้องต้อง
Deploy ขึ้นเว็บยาก ต้องมีเครื่องที่แรม/GPU พอง่าย

👉 เหมาะกับ: งานที่ต้องการความเป็นส่วนตัวของข้อมูล · ทดลองฟรีไม่จำกัด · และใช้สอนนักเรียนให้เห็นว่า LLM ทำงานยังไงโดยไม่ต้องมีบัตรเครดิตหรือ API key

🎯 ภารกิจ 6 ขั้นตอน
1️⃣ ติดตั้งบน macOS
ติดตั้งและเช็กว่ารันอยู่
brew install ollama
brew services start ollama      # ให้รันเป็น background service

# หรือโหลด .app จาก ollama.com แล้วเปิด (จะมีไอคอนบน menu bar)

# เช็กว่ารันอยู่จริง
curl http://localhost:11434
# ควรตอบว่า  Ollama is running

💡 ถ้าใช้ brew services Ollama จะรันเป็นบริการเบื้องหลัง เปิดเครื่องมาก็พร้อมใช้ ไม่ต้องเปิดแอปเอง

2️⃣ เลือกและโหลดโมเดล

กฎง่าย ๆ เรื่องแรม

โมเดลขนาด Q4 กินแรมประมาณครึ่งหนึ่งของจำนวนพารามิเตอร์เป็น GB และต้องเผื่อแรมให้ระบบกับ context อีก

ขนาดโมเดลกินแรมประมาณเครื่องที่เหมาะ
8B~5 GBแรม 16 GB สบาย ๆ
9–14B~6–9 GBแรม 16 GB ยังไหว
27–30B~17–19 GBแรม 32 GB ขึ้นไป
⚠️ อย่าเชื่อชื่อรุ่นจากบทความเก่า รวมทั้งหน้านี้: เลขเวอร์ชันของโมเดลเปลี่ยนเร็วมาก · ก่อน pull ทุกครั้ง ให้เปิด ollama.com/library เช็ก tag จริงก่อนเสมอ · ตระกูลที่คนใช้กันเยอะตอนนี้คือ Qwen (ภาษาไทยดีที่สุดในกลุ่ม open weight) · Gemma · และ gpt-oss ของ OpenAI
โหลดโมเดลและลองคุย
ollama pull qwen3:8b          # เปลี่ยน tag ตามที่เช็กมาจาก ollama.com/library
ollama pull bge-m3            # โมเดล embedding ไว้ทำ RAG (รองรับไทย)

ollama list                   # ดูโมเดลที่มีในเครื่อง
ollama run qwen3:8b           # ลองคุยใน terminal ก่อน

🇹🇭 bge-m3 เป็นโมเดล embedding สำหรับทำ RAG โดยเฉพาะ ไม่ใช่โมเดลแชท — และรองรับภาษาไทยดีกว่า nomic-embed-text

3️⃣ รู้จัก API ก่อนเขียนโค้ด

Ollama เปิด API ให้ 2 แบบ ใช้ได้ทั้งคู่ เลือกตามสะดวก

แบบเส้นทางเหมาะเมื่อ
Ollama native/api/chatเขียนใหม่ตั้งแต่ต้น คุมรายละเอียดได้เต็มที่
OpenAI-compatible/v1/chat/completionsมีโค้ดเดิมที่ใช้กับ OpenAI อยู่แล้ว — เปลี่ยนแค่ baseURL
แบบ Ollama native — ตอบกลับเป็น NDJSON เวลา stream
curl http://localhost:11434/api/chat -d '{
  "model": "qwen3:8b",
  "messages": [
    {"role":"system","content":"คุณเป็นผู้ช่วยตอบภาษาไทย ตอบสั้น กระชับ"},
    {"role":"user","content":"สวัสดี"}
  ],
  "stream": true,
  "options": { "temperature": 0.7, "num_ctx": 8192 }
}' 
แบบ OpenAI-compatible — ใช้ openai SDK เดิมได้เลย
import OpenAI from 'openai'

const client = new OpenAI({
  baseURL: 'http://localhost:11434/v1',
  apiKey: 'ollama'          // ใส่อะไรก็ได้ Ollama ไม่ตรวจ
})

🔑 จุดขายของแบบนี้: เขียนโค้ดครั้งเดียว พอจะขึ้น production ค่อยสลับ baseURL ไปที่ผู้ให้บริการจริงโดยไม่ต้องรื้อโค้ด

🧠 ข้อควรรู้เรื่อง reasoning model: โมเดลรุ่นใหม่หลายตัวจะพ่นส่วน “กำลังคิด” ออกมาก่อนคำตอบจริง · ต้องดูว่ามันแยก field มาให้หรือปนอยู่ในข้อความ — ถ้าปนต้องตัดทิ้งก่อนแสดงผล ไม่งั้นผู้ใช้จะเห็นกระบวนการคิดทั้งหมด
4️⃣ ต่อกับ Nuxt
🚧 หลักการสำคัญ: อย่าให้เบราว์เซอร์ยิงตรงไปที่พอร์ต 11434 เพราะจะเจอปัญหา CORS และเวลา deploy จริงจะเปลี่ยนยาก · ให้ทำ server route ของ Nuxt เป็นตัวกลางแทน
server/api/chat.post.ts — ตัวกลางฝั่งเซิร์ฟเวอร์
// server/api/chat.post.ts
export default defineEventHandler(async (event) => {
  const { messages } = await readBody(event)

  const res = await fetch('http://localhost:11434/api/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      model: 'qwen3:8b',
      messages: [
        { role: 'system', content: 'คุณคือผู้ช่วยของ...' },
        ...messages
      ],
      stream: true
    })
  })

  setHeader(event, 'Content-Type', 'text/plain; charset=utf-8')
  setHeader(event, 'X-Accel-Buffering', 'no')   // กัน proxy อมข้อมูลไว้

  return new ReadableStream({
    async start(controller) {
      const reader = res.body.getReader()
      const decoder = new TextDecoder()
      let buffer = ''

      while (true) {
        const { done, value } = await reader.read()
        if (done) break
        buffer += decoder.decode(value, { stream: true })

        const lines = buffer.split('\n')
        buffer = lines.pop()          // บรรทัดสุดท้ายอาจยังไม่ครบ เก็บไว้รอบหน้า

        for (const line of lines) {
          if (!line.trim()) continue
          const json = JSON.parse(line)
          if (json.message?.content) {
            controller.enqueue(new TextEncoder().encode(json.message.content))
          }
        }
      }
      controller.close()
    }
  })
})
ฝั่งหน้าเว็บ — รับ stream มาแสดงทีละคำ
<script setup>
const messages = ref([])
const input = ref('')

async function send() {
  messages.value.push({ role: 'user', content: input.value })
  const reply = reactive({ role: 'assistant', content: '' })
  messages.value.push(reply)
  input.value = ''

  const res = await fetch('/api/chat', {
    method: 'POST',
    body: JSON.stringify({ messages: messages.value.slice(0, -1) })
  })

  const reader = res.body.getReader()
  const decoder = new TextDecoder()
  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    reply.content += decoder.decode(value, { stream: true })
  }
}
</script>
🇹🇭 จุดที่มักพลาดที่สุดสำหรับภาษาไทย: ต้องใส่ { stream: true } ใน decoder.decode() ทุกที่ · ไม่งั้นภาษาไทยจะเพี้ยนเป็นเครื่องหมายคำถามสี่เหลี่ยม เพราะตัวอักษรไทยกิน 3 ไบต์ แล้วอาจถูกตัดคาบระหว่าง chunk พอดี
5️⃣ ใส่บุคลิกถาวรด้วย Modelfile

แทนที่จะส่ง system prompt ทุกครั้ง สร้างโมเดลเวอร์ชันของตัวเองไว้เลย แล้วเรียกใช้ชื่อนั้นใน API

ไฟล์ชื่อ Modelfile
FROM qwen3:8b

PARAMETER temperature 0.6
PARAMETER num_ctx 8192

SYSTEM """
คุณคือ "น้องหม้อ" ผู้ช่วยของเว็บ soccersuck
ตอบเป็นภาษาไทยเสมอ น้ำเสียงเป็นกันเอง
ถ้าไม่รู้ให้บอกว่าไม่รู้ ห้ามเดา
"""
สร้างและเรียกใช้
ollama create nongmor -f Modelfile
ollama run nongmor

# จากนั้นเรียกใน API ได้เลย  "model": "nongmor"

✅ สังเกตบรรทัด “ถ้าไม่รู้ให้บอกว่าไม่รู้ ห้ามเดา” — เป็นด่านกันบอทมั่วที่ควรใส่ทุกครั้ง โดยเฉพาะบอทที่ให้คนนอกใช้

6️⃣ ทำ RAG ให้ตอบจากเอกสารของเราเอง

RAG คือการหาข้อมูลที่เกี่ยวข้องมาแปะใน prompt ก่อนส่งให้โมเดลตอบ แบ่งเป็น 2 ฝั่ง

ฝั่งเตรียมข้อมูล — ทำครั้งเดียว หรือตอนข้อมูลอัปเดต

  1. เอาเอกสารมาตัดเป็นชิ้นเล็ก (chunk) ประมาณ 300–800 ตัวอักษร ให้เหลื่อมกันนิดหน่อยกันเนื้อหาขาดตอน
  2. ส่งแต่ละ chunk เข้าโมเดล embedding แปลงเป็นเวกเตอร์ตัวเลข
  3. เก็บเวกเตอร์ + ข้อความต้นฉบับลง vector store

ฝั่งตอบคำถาม — ทุกครั้งที่ผู้ใช้ถาม

  1. แปลงคำถามผู้ใช้เป็นเวกเตอร์ด้วยโมเดล embedding ตัวเดียวกัน
  2. ค้นหา chunk ที่เวกเตอร์ใกล้เคียงที่สุด เอามาสัก 3–5 ชิ้น
  3. ยัดข้อความพวกนั้นลง prompt แล้วส่งให้โมเดลแชท
แทรกขั้นตอนค้นหาก่อนเรียก /api/chat
// 1. แปลงคำถามเป็นเวกเตอร์
const emb = await $fetch('http://localhost:11434/api/embed', {
  method: 'POST',
  body: { model: 'bge-m3', input: userQuestion }
})

// 2. หา chunk ที่ใกล้เคียงที่สุด (ถ้าข้อมูลน้อย คำนวณเองได้เลย)
const top = chunks
  .map(c => ({ ...c, score: cosine(c.vector, emb.embeddings[0]) }))
  .sort((a, b) => b.score - a.score)
  .slice(0, 4)

// 3. ยัดเข้า system prompt
const context = top.map(c => c.text).join('\n---\n')

const system = `ตอบคำถามโดยอ้างอิงจากข้อมูลด้านล่างเท่านั้น
ถ้าข้อมูลไม่พอ ให้ตอบว่าไม่พบข้อมูล ห้ามแต่งเอง

<ข้อมูล>
${context}
</ข้อมูล>`
ฟังก์ชัน cosine เขียนเองสั้น ๆ ได้
function cosine(a, b) {
  let dot = 0, na = 0, nb = 0
  for (let i = 0; i < a.length; i++) {
    dot += a[i] * b[i]
    na  += a[i] * a[i]
    nb  += b[i] * b[i]
  }
  return dot / (Math.sqrt(na) * Math.sqrt(nb))
}

📐 คือ dot product หารด้วยผลคูณของ norm ทั้งสอง — ไม่ต้องลงไลบรารีเพิ่ม

ที่เก็บเวกเตอร์ เลือกตามขนาดงาน

ขนาดข้อมูลใช้อะไร
ไม่กี่ร้อยชิ้นเก็บเป็น JSON แล้วคำนวณ cosine เองใน JS ก็พอ ไม่ต้องลง DB
ระดับกลางSQLite + sqlite-vec หรือ Chroma
ขึ้น production บน CloudflareVectorize ต่อกับ Workers ได้เลย และมี Workers AI ทำ embedding ให้ในตัว
🚫 จุดที่คนพลาดบ่อยใน RAG:
⚡ ทางลัดถ้าไม่อยากเขียนเอง
เครื่องมือได้อะไรเหมาะกับ
Open WebUIรันด้วย Docker ตัวเดียว ได้หน้าแชทครบเครื่อง มีระบบผู้ใช้ อัปโหลดไฟล์ทำ RAG ให้ในตัวสาธิตให้นักเรียนดู หรือให้ทีมใช้ภายใน
n8nมี Ollama Chat Model node กับ Embeddings node ลากต่อกับ Vector Store node เป็นเส้นเดียวจบงานที่ต้องต่อกับ Google Workspace อยู่แล้ว เพราะดึงไฟล์จาก Drive เข้า pipeline ได้ตรง ๆ
🚀 เรื่อง deploy ที่ต้องตัดสินใจ
🔌 Ollama ผูกกับเครื่องที่มีแรม/GPU เสมอdeploy ขึ้น Cloudflare Workers ตรง ๆ ไม่ได้ ถ้าจะให้คนอื่นใช้ต้องเลือกทางใดทางหนึ่ง
แนวทางเหมาะกับข้อเสีย
Cloudflare Tunnel ชี้มาที่เครื่องเราเดโม · ใช้ในทีมเล็กปิดเครื่องแล้วดับ · รับคนพร้อมกันได้น้อย
เช่า VPS ที่มี GPUใช้งานจริงแต่ต้องการความเป็นส่วนตัวแพง เดือนละหลายพันขึ้นไป
ใช้ Workers AI + Vectorize แทนproduction บน Cloudflareไม่ใช่ local แล้ว
คำแนะนำจริง ๆ: ใช้ Ollama เป็นเครื่องมือพัฒนาและสอน — เขียนโค้ดฝั่งหน้าเว็บให้เป็น OpenAI-compatible interface ไว้ พอจะขึ้น production ค่อยสลับ baseURL ไปที่ Workers AI หรือ Claude API โดยไม่ต้องรื้อโค้ด · ส่วนตอนสอนนักเรียนก็ให้เขาเห็นว่าโมเดลทำงานยังไง โดยไม่ต้องมีบัตรเครดิตหรือ API key
🎛️ ปรับจูนตอนใช้จริง
ตัวแปรสภาพแวดล้อมที่ควรตั้ง
# กันโมเดลถูก unload ออกจากแรม (คำถามแรกหลังพักจะได้ไม่ช้า)
OLLAMA_KEEP_ALIVE=30m

# ให้เครื่องอื่นในวงแลนเรียกได้ — ระวัง ไม่มี auth ในตัว
OLLAMA_HOST=0.0.0.0
📌 ข้อมูลอาจเปลี่ยนแปลง: Ollama และรายชื่อโมเดลอัปเดตเร็วมาก · หน้านี้เน้นสอนหลักการและโครงโค้ดซึ่งใช้ได้ยาว ส่วนชื่อ tag โมเดลให้เช็กจาก ollama.com/library ทุกครั้ง · ตรวจ endpoint กับเอกสารทางการแล้วเมื่อกันยายน 2569
🔗 บทเรียนที่เกี่ยวข้อง
🔗 แหล่งอ้างอิง