teach-me-ai / Lv1
⟵ กลับหน้า Roadmap
LEVEL 1 · PROMPT ENGINEERING

เขียน prompt ให้ได้ผลลัพธ์ที่ใช้งานได้จริง

⏱️ ~30 นาที 🎯 สั่ง LLM ให้ตอบตรง แม่น และอยู่ในรูปแบบที่โค้ดคุณเอาไปใช้ต่อได้

ใน Lv0 เราเข้าใจแล้วว่า LLM ทำงานยังไง บทนี้คือทักษะที่ทำเงินจริง: prompt engineering (วิศวกรรมคำสั่ง) — ศิลปะของการ "สั่งงาน" ให้ชัด จนโมเดลตอบสิ่งที่เราต้องการ ไม่ใช่สิ่งที่มันเดาว่าเราน่าจะต้องการ

คิดง่ายๆ ว่า prompt ก็เหมือน spec ให้ junior dev ที่เก่งมากแต่เพิ่งเข้างานวันแรก — เขาไม่รู้ context (บริบท) ของทีมเรา ไม่รู้ convention (ธรรมเนียมการเขียนโค้ด) ของโปรเจกต์ ถ้าเราสั่งกำกวม ก็ได้งานกำกวมกลับมา ถ้าเราสั่งชัด พร้อมตัวอย่าง เขาก็ทำได้ดีเกินคาด

กายวิภาคของ prompt ที่ดี

prompt ที่ได้ผลไม่ใช่แค่ "พิมพ์คำถาม" แต่ประกอบด้วย 4 ส่วนที่ทำงานร่วมกัน ยิ่งครบ ผลลัพธ์ยิ่งนิ่งและตรง:

โครงสร้าง prompt ที่ดี 1 บทบาท + context (บริบท) "คุณคือ senior reviewer ทีม NestJS..." 2 งานที่ต้องทำ (task) "รีวิวโค้ดนี้ หาบั๊กและปัญหา security" 3 รูปแบบ output (format) "ตอบเป็น JSON: severity, line, fix" 4 ข้อจำกัด (constraints) "ไม่เกิน 5 ข้อ, ห้ามเดาถ้าไม่แน่ใจ"
4 ส่วนของ prompt ที่ดี — ยิ่งใส่ครบ ผลลัพธ์ยิ่งตรงและตรวจได้
ส่วนตอบคำถามว่าตัวอย่างสำหรับงาน dev
บทบาท + contextใครกำลังพูด / รู้อะไรอยู่"คุณคือ reviewer สาย TypeScript ที่เข้มเรื่อง type safety"
งานที่ต้องทำให้ทำอะไร"เขียน unit test ครอบ edge case ของฟังก์ชันนี้"
รูปแบบ outputตอบมาหน้าตายังไง"ตอบเป็นโค้ด Jest เท่านั้น ไม่ต้องอธิบาย"
ข้อจำกัดอะไรทำได้/ทำไม่ได้"ใช้ได้เฉพาะ lib ที่อยู่ใน package.json ที่ให้มา"
💡

ไม่จำเป็นต้องมีครบ 4 ส่วนทุกครั้ง แต่ เมื่อไหร่ผลลัพธ์เพี้ยน ให้ไล่เช็กว่าขาดส่วนไหน — ส่วนใหญ่ที่พังเพราะขาด "รูปแบบ output" กับ "ข้อจำกัด"

System message vs User message

เวลาเรียก LLM ผ่าน API เราไม่ได้ส่งข้อความก้อนเดียว แต่แยกเป็น "บทบาท" ของข้อความ สองตัวที่ใช้บ่อยสุดคือ system message (ข้อความระบบ) กับ user message (ข้อความผู้ใช้)

system messageuser message
ทำหน้าที่ตั้งบุคลิก, กฎ, รูปแบบถาวรคำขอ/ข้อมูลเฉพาะครั้งนั้น
เปลี่ยนบ่อยไหมคงที่ตลอด sessionเปลี่ยนทุกคำขอ
เทียบกับโค้ดเหมือน config / middlewareเหมือน request body
ตัวอย่าง"คุณคือผู้ช่วยรีวิวโค้ด ตอบเป็นไทยเสมอ""รีวิวไฟล์ user.service.ts นี้ให้หน่อย"

หลักคิดง่ายๆ สำหรับ dev: อะไรที่ จริงทุกครั้ง เอาไว้ใน system (เช่น "ตอบเป็นไทย", "output ต้องเป็น JSON เสมอ") — อะไรที่ เปลี่ยนไปตามงาน เอาไว้ใน user (เช่น โค้ดที่จะรีวิว, ข้อมูลที่จะสรุป)

ถ้าคุณสร้าง endpoint ใน NestJS ที่เรียก LLM: system message มักถูก hardcode ไว้ใน service ส่วน user message มาจาก request ของผู้ใช้ปลายทาง แยกกันชัดแบบนี้ช่วยให้กฎของระบบไม่ถูก user เขียนทับ

Few-shot — สอนด้วยตัวอย่าง

บางงานอธิบายด้วยคำพูดยาก แต่โชว์ตัวอย่างแป๊บเดียวก็เข้าใจ นี่คือหลักของ few-shot prompting (การป้อนตัวอย่างสอน) — ใส่คู่ตัวอย่าง input → output ให้ดูก่อน แล้วค่อยให้ของจริง

  • zero-shot: สั่งเปล่าๆ ไม่มีตัวอย่าง — เร็วดี แต่รูปแบบอาจไม่นิ่ง
  • few-shot: ใส่ตัวอย่าง 2–5 คู่ — โมเดลจับ pattern ได้ ตอบตรงรูปแบบขึ้นมาก

ตัวอย่าง: อยากให้ LLM แปลง log ดิบเป็น label สั้นๆ ให้ใส่ตัวอย่างสอนก่อน

# --- ตัวอย่างสอน (few-shot) ---
Log: "GET /users 200 12ms"       → Label: ok
Log: "POST /orders 500 err db"    → Label: error
Log: "GET /health 200 2ms"        → Label: ok

# --- ของจริงที่อยากให้ตอบ ---
Log: "PUT /cart 429 rate limit"   → Label: 

โมเดลจะเรียนจากตัวอย่างว่า "429" ควรเป็นอะไร แล้วตอบในรูปแบบเดียวกัน (label สั้นคำเดียว) โดยที่เราไม่ต้องอธิบายกฎยาวๆ เลย

Chain-of-thought — สั่งให้คิดทีละขั้น

กับงานที่ต้อง "คิด" หลายขั้น (คำนวณ, ตัดสินใจแบบมีเงื่อนไข, ไล่ตรรกะ) การให้โมเดล แสดงขั้นตอนการคิดก่อนสรุป มักได้คำตอบแม่นขึ้นชัดเจน เทคนิคนี้เรียก chain-of-thought (คิดเป็นลำดับขั้น)

วิธีกระตุ้นง่ายสุดคือเติมประโยคทำนองนี้เข้าไปใน prompt:

"คิดทีละขั้นตอนก่อน แล้วค่อยสรุปคำตอบสุดท้าย"

เหมาะกับตอนไหน:

  • ช่วย: วิเคราะห์ว่าโค้ดนี้มี race condition ไหม, คำนวณ time complexity, ตัดสินใจ architecture
  • ไม่ค่อยจำเป็น: งานสั้นตรงไปตรงมา เช่น แปลง case ตัวอักษร, จัด label คำเดียว (เปลือง token เปล่าๆ)
⚠️

chain-of-thought ทำให้ output ยาวขึ้น = เปลือง token และช้าลง ถ้าคุณต้องการแค่คำตอบสุดท้ายไปใช้ในโค้ด ให้สั่งแยกส่วนคิดออกจากคำตอบ เช่น "คิดในใจก่อน แล้วตอบเฉพาะผลลัพธ์สุดท้ายเป็น JSON" หรือใช้ field แยกใน output

คุม output format ให้โค้ดกินต่อได้

นี่คือหัวใจของการเอา LLM มาต่อกับ backend: ถ้าคุณจะ JSON.parse() ผลลัพธ์ คุณต้องมั่นใจว่ามันส่ง JSON กลับมาจริงๆ ไม่ใช่ JSON ห่อด้วยคำอธิบายภาษาคน อย่าหวังว่าโมเดลจะเดารูปแบบถูกเอง — ระบุให้ชัด

เทคนิคที่ได้ผลจริง:

  • บอก schema ให้ชัด: ระบุ field ทุกตัวและชนิดของมัน
  • สั่งตรงๆ ว่า "ตอบเป็น JSON ที่ valid เท่านั้น ห้ามมีข้อความอื่นนำหน้าหรือต่อท้าย"
  • ให้ตัวอย่าง output 1 อัน (few-shot ช่วยเรื่อง format ได้ดีมาก)
  • ในโค้ดจริง ให้ ตรวจ (validate) เสมอ ด้วย schema เช่น Zod ก่อนใช้ค่า

ตัวอย่าง prompt คุม format สำหรับสกัดข้อมูลจาก error message:

สกัดข้อมูลจาก error log ด้านล่าง ตอบเป็น JSON ที่ valid เท่านั้น
ตามรูปแบบนี้เป๊ะ ห้ามมีข้อความอื่นนอก JSON:

{
  "service": "string",       // ชื่อ service
  "severity": "low|medium|high",
  "summary": "string"        // สรุปสั้น ไม่เกิน 1 บรรทัด
}

ถ้าข้อมูลไม่พอให้ระบุ field ไหน ใส่ null — ห้ามเดา

Log:
"[order-svc] FATAL: mongo connection timeout after 30s"

ฝั่งโค้ด NestJS/TypeScript ก็ยังต้อง validate อีกชั้น อย่าเชื่อ output ตรงๆ

import { z } from "zod";

// สร้าง schema ให้ตรงกับที่สั่งไปใน prompt
const ErrorInfo = z.object({
  service: z.string(),
  severity: z.enum(["low", "medium", "high"]),
  summary: z.string().max(120),
});

function parseLlmOutput(raw: string) {
  const json = JSON.parse(raw);       // อาจ throw ถ้าไม่ใช่ JSON
  return ErrorInfo.parse(json);      // throw ถ้า field ไม่ตรง schema
}

กฎเหล็กของ dev: prompt คุม format + code validate ทำทั้งสองอย่างเสมอ อย่าพึ่ง prompt อย่างเดียว เพราะโมเดลอาจเผลอใส่ ```json ครอบ หรือเติมคำอธิบายในบางครั้ง

Debug และ iterate เมื่อผลไม่ดี

prompt แรกไม่เคยเพอร์เฟกต์ — การเขียน prompt คือการ debug แบบวนรอบ เหมือนแก้บั๊ก: เปลี่ยนทีละอย่าง แล้วดูว่าดีขึ้นไหม อย่าเปลี่ยนหลายอย่างพร้อมกันจนไม่รู้ว่าอะไรได้ผล

อาการสาเหตุที่พบบ่อยวิธีแก้
ตอบยาวเกิน / นอกเรื่องไม่มีข้อจำกัดเพิ่ม "ตอบไม่เกิน N ข้อ" / จำกัดขอบเขต
format ไม่นิ่งไม่ได้ระบุ schema ชัดให้ตัวอย่าง output + สั่ง "JSON เท่านั้น"
เข้าใจงานผิดtask กำกวมเขียน task ให้เจาะจง + ยกตัวอย่าง (few-shot)
มั่ว / แต่งข้อมูลไม่มีทางออกเมื่อไม่รู้สั่ง "ถ้าไม่แน่ใจให้ตอบ null / บอกว่าไม่รู้"
ผลไม่คงเส้นคงวาtemperature สูงไปลด temperature ลงใกล้ 0 สำหรับงานสกัดข้อมูล

ขั้นตอน iterate ที่แนะนำ (คิดแบบ TDD ก็ได้):

  1. เตรียมชุด input ตัวอย่างที่รู้คำตอบที่ถูก (เหมือน test case)
  2. รัน prompt แล้วเทียบผลกับที่คาดหวัง
  3. เจอจุดพลาด → แก้ prompt ทีละจุด
  4. รันซ้ำกับชุดเดิม เช็กว่าไม่ทำให้ case อื่นพัง (regression)

ประกอบร่าง: system prompt ผู้ช่วยรีวิวโค้ด

รวมทุกอย่างในบทนี้เข้าด้วยกัน — นี่คือ system prompt ที่ครบ 4 ส่วน คุม format และมีข้อจำกัดกันมั่ว

# บทบาท + context
คุณคือ senior code reviewer ที่เชี่ยวชาญ TypeScript, NestJS และ Go
คุณเข้มเรื่อง type safety, การจัดการ error และช่องโหว่ security

# งานที่ต้องทำ
รีวิวโค้ดที่ผู้ใช้ส่งมา แล้วชี้ปัญหาที่สำคัญที่สุด
โฟกัส: บั๊กที่ทำระบบพัง, ช่องโหว่ security, และ error handling ที่หายไป

# รูปแบบ output
ตอบเป็น JSON ที่ valid เท่านั้น ตามรูปแบบนี้:
{
  "issues": [
    {
      "severity": "critical|high|medium|low",
      "line": 0,
      "problem": "อธิบายปัญหาสั้นๆ เป็นภาษาไทย",
      "fix": "วิธีแก้ที่ทำได้จริง"
    }
  ]
}

# ข้อจำกัด
- รายงานไม่เกิน 5 ปัญหา เรียงจากรุนแรงสุดก่อน
- ถ้าไม่แน่ใจเรื่องบรรทัด ให้ใส่ line เป็น null — ห้ามเดาเลขบรรทัด
- ถ้าโค้ดไม่มีปัญหาสำคัญ ให้ตอบ {"issues": []}
- ตอบเป็นภาษาไทย ยกเว้นศัพท์เทคนิคและชื่อ API

สังเกตว่า prompt นี้เอาไป reuse ได้ทุกคำขอ (system) ส่วนโค้ดที่จะรีวิวจริงส่งมาเป็น user message — ฝั่ง NestJS ก็แค่ validate JSON ที่ได้กลับด้วย Zod ก่อนเอาไปแสดงหรือเก็บลง Postgres/Mongo

🎯 แบบฝึกหัด

ด้านล่างคือ prompt ห่วยๆ ที่ dev มือใหม่ชอบเขียน จงปรับให้ดีขึ้นโดยเติมให้ครบ 4 ส่วน + คุม format:

"ช่วยดู code นี้ให้หน่อยว่าโอเคไหม"
  1. เขียน prompt ใหม่ที่มีครบ: บทบาท+context, task ที่เจาะจง, รูปแบบ output, และข้อจำกัดกันมั่ว
  2. บอกว่าส่วนไหนควรอยู่ใน system message ส่วนไหนอยู่ใน user message เพราะอะไร
  3. งานนี้ควรตั้ง temperature ประมาณเท่าไร เพราะอะไร?
  4. (โบนัส) เพิ่มตัวอย่าง few-shot 1 คู่ เพื่อล็อกรูปแบบ output ให้นิ่งขึ้น

ลองตอบในแชท แล้วพิมพ์ /quiz Lv1 เพื่อทดสอบความเข้าใจ