🦙
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
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 ก่อน
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 ไม่ตรวจ
})
🧠 ข้อควรรู้เรื่อง 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 ฝั่ง
ฝั่งเตรียมข้อมูล — ทำครั้งเดียว หรือตอนข้อมูลอัปเดต
- เอาเอกสารมาตัดเป็นชิ้นเล็ก (chunk) ประมาณ 300–800 ตัวอักษร ให้เหลื่อมกันนิดหน่อยกันเนื้อหาขาดตอน
- ส่งแต่ละ chunk เข้าโมเดล embedding แปลงเป็นเวกเตอร์ตัวเลข
- เก็บเวกเตอร์ + ข้อความต้นฉบับลง vector store
ฝั่งตอบคำถาม — ทุกครั้งที่ผู้ใช้ถาม
- แปลงคำถามผู้ใช้เป็นเวกเตอร์ด้วยโมเดล embedding ตัวเดียวกัน
- ค้นหา chunk ที่เวกเตอร์ใกล้เคียงที่สุด เอามาสัก 3–5 ชิ้น
- ยัดข้อความพวกนั้นลง 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))
}
ที่เก็บเวกเตอร์ เลือกตามขนาดงาน
| ขนาดข้อมูล | ใช้อะไร |
|---|---|
| ไม่กี่ร้อยชิ้น | เก็บเป็น JSON แล้วคำนวณ cosine เองใน JS ก็พอ ไม่ต้องลง DB |
| ระดับกลาง | SQLite + sqlite-vec หรือ Chroma |
| ขึ้น production บน Cloudflare | Vectorize ต่อกับ Workers ได้เลย และมี Workers AI ทำ embedding ให้ในตัว |
🚫 จุดที่คนพลาดบ่อยใน RAG:
- ใช้โมเดล embedding คนละตัวระหว่างตอน index กับตอน query — ผลลัพธ์จะมั่วทันที
- chunk ใหญ่เกินไปทำให้ค้นแล้วได้ข้อมูลปนเปื้อน · chunk เล็กเกินไปทำให้บริบทขาด
- ภาษาไทยไม่มีเว้นวรรค การตัด chunk ตามคำจะยากกว่าอังกฤษ — ตัดตามย่อหน้าหรือหัวข้อจะปลอดภัยกว่า
⚡ ทางลัดถ้าไม่อยากเขียนเอง
| เครื่องมือ | ได้อะไร | เหมาะกับ |
|---|---|---|
| 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
- num_ctx ยิ่งมากยิ่งกินแรม — ตั้งเท่าที่จำเป็นจริง อย่าตั้ง 128K เพียงเพราะเห็นว่าโมเดลรองรับ
- ภาษาไทยกิน token มากกว่าอังกฤษราว 2–3 เท่า — คำนวณ context ให้เผื่อไว้
- OLLAMA_HOST=0.0.0.0 ไม่มี auth ในตัว — ถ้าเปิดให้เครื่องอื่นเรียกได้ ต้องระวังเรื่องความปลอดภัย
📌 ข้อมูลอาจเปลี่ยนแปลง: Ollama และรายชื่อโมเดลอัปเดตเร็วมาก · หน้านี้เน้นสอนหลักการและโครงโค้ดซึ่งใช้ได้ยาว ส่วนชื่อ tag โมเดลให้เช็กจาก ollama.com/library ทุกครั้ง · ตรวจ endpoint กับเอกสารทางการแล้วเมื่อกันยายน 2569
🔗 บทเรียนที่เกี่ยวข้อง
🎙️
AI พูดภาษาไทยฟรี รันในเครื่อง
อีกตัวที่รันในเครื่องเหมือนกัน ใช้คู่กันทำบอทที่พูดได้
เปิด →
🤖อีกตัวที่รันในเครื่องเหมือนกัน ใช้คู่กันทำบอทที่พูดได้
สอน AI Agent (n8n)
ทางลัดต่อ Ollama เข้ากับ Vector Store แบบลากวาง
เปิด →
💬ทางลัดต่อ Ollama เข้ากับ Vector Store แบบลากวาง
บอทตอบคำถาม LINE OA
ตัวอย่างโครงการจริงที่ใช้หลักการเดียวกัน
เปิด →
🔒ตัวอย่างโครงการจริงที่ใช้หลักการเดียวกัน
ความเป็นส่วนตัว AI
เทียบให้เห็นว่าทำไมการรันในเครื่องถึงต่างจากใช้คลาวด์
เปิด →
เทียบให้เห็นว่าทำไมการรันในเครื่องถึงต่างจากใช้คลาวด์
🔗 แหล่งอ้างอิง