เขียน prompt ให้ได้ผลลัพธ์ที่ใช้งานได้จริง
ใน Lv0 เราเข้าใจแล้วว่า LLM ทำงานยังไง บทนี้คือทักษะที่ทำเงินจริง: prompt engineering (วิศวกรรมคำสั่ง) — ศิลปะของการ "สั่งงาน" ให้ชัด จนโมเดลตอบสิ่งที่เราต้องการ ไม่ใช่สิ่งที่มันเดาว่าเราน่าจะต้องการ
คิดง่ายๆ ว่า prompt ก็เหมือน spec ให้ junior dev ที่เก่งมากแต่เพิ่งเข้างานวันแรก — เขาไม่รู้ context (บริบท) ของทีมเรา ไม่รู้ convention (ธรรมเนียมการเขียนโค้ด) ของโปรเจกต์ ถ้าเราสั่งกำกวม ก็ได้งานกำกวมกลับมา ถ้าเราสั่งชัด พร้อมตัวอย่าง เขาก็ทำได้ดีเกินคาด
กายวิภาคของ prompt ที่ดี
prompt ที่ได้ผลไม่ใช่แค่ "พิมพ์คำถาม" แต่ประกอบด้วย 4 ส่วนที่ทำงานร่วมกัน ยิ่งครบ ผลลัพธ์ยิ่งนิ่งและตรง:
| ส่วน | ตอบคำถามว่า | ตัวอย่างสำหรับงาน 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 message | user 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 ก็ได้):
- เตรียมชุด input ตัวอย่างที่รู้คำตอบที่ถูก (เหมือน test case)
- รัน prompt แล้วเทียบผลกับที่คาดหวัง
- เจอจุดพลาด → แก้ prompt ทีละจุด
- รันซ้ำกับชุดเดิม เช็กว่าไม่ทำให้ 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 นี้ให้หน่อยว่าโอเคไหม"
- เขียน prompt ใหม่ที่มีครบ: บทบาท+context, task ที่เจาะจง, รูปแบบ output, และข้อจำกัดกันมั่ว
- บอกว่าส่วนไหนควรอยู่ใน system message ส่วนไหนอยู่ใน user message เพราะอะไร
- งานนี้ควรตั้ง temperature ประมาณเท่าไร เพราะอะไร?
- (โบนัส) เพิ่มตัวอย่าง few-shot 1 คู่ เพื่อล็อกรูปแบบ output ให้นิ่งขึ้น
ลองตอบในแชท แล้วพิมพ์ /quiz Lv1 เพื่อทดสอบความเข้าใจ