AI Engineer › Prompt และ structured output
Guardrails: retry พร้อม feedback, PII, injection
ประมาณ 35 นาที
เป้าหมายของบท
- เขียน retry ที่ส่งเหตุผลที่ถูกปฏิเสธกลับให้โมเดลแก้ พร้อมจำกัดรอบและ fallback
- ปกปิด PII ด้วย regex และรู้ข้อจำกัดของมัน
- ประเมินตัวตรวจ prompt injection แบบ heuristic ด้วย precision/recall และเข้าใจว่าเป็นเพียงชั้นแรก
Guardrail = ชั้นตรวจก่อนและหลังโมเดล
โมเดลอยู่ตรงกลางระบบ ส่วนที่ทำให้ใช้งานจริงได้อย่างปลอดภัยคือชั้นควบคุม ก่อนส่งเข้า (อินพุต) และ หลังโมเดลตอบ (เอาต์พุต) เขียนด้วยโค้ดปกติที่ทดสอบได้ ไม่ใช่หวังพึ่ง prompt อย่างเดียว
บทนี้ใช้
MockLLMที่ ตอบตามสคริปต์ (ไม่ได้เรียก API จริง) เพื่อให้ทดสอบ logic ของ retry ได้ซ้ำทุกครั้ง โครงเดียวกันนี้ใช้กับ SDK จริงโดยเปลี่ยนแค่ฟังก์ชันที่เรียกโมเดล
1) retry พร้อมส่ง error กลับให้โมเดลแก้
เมื่อเอาต์พุตไม่ผ่านการตรวจ อย่าแค่เรียกซ้ำเฉยๆ — ต่อประวัติด้วยคำตอบที่ถูกปฏิเสธ + เหตุผลที่แน่ชัด แล้วให้โมเดลลองใหม่ (จำกัดจำนวนรอบ)
def ask_with_retry(llm, prompt, max_retries=3):
messages = [{"role": "user", "content": prompt}]
for attempt in range(1, max_retries + 2):
reply = llm(messages)
error = validate(reply)
if error is None:
return json.loads(reply), attempt
messages.append({"role": "assistant", "content": reply})
messages.append({"role": "user", "content": "Your reply was rejected: " + error})
return None, max_retries + 1 # หมดรอบ → ให้ผู้เรียกใช้ fallback
ถ้ารอบสุดท้ายยังไม่ผ่าน ต้องมี fallback ที่ปลอดภัย (ค่าเริ่มต้น / ส่งต่อให้คน) ไม่ใช่ปล่อยให้ระบบพังหรือใช้ค่าที่ผิด
2) ปกปิดข้อมูลส่วนบุคคล (PII) ก่อนส่งออกหรือบันทึก
EMAIL = r"[\w.+-]+@[\w-]+\.[\w.]+"
PHONE = r"\b0\d{1,2}-?\d{3}-?\d{4}\b"
text = re.sub(EMAIL, "[EMAIL]", text)
text = re.sub(PHONE, "[PHONE]", text)
regex จับรูปแบบที่คาดเดาได้เท่านั้น (ชื่อ ที่อยู่ เลขบัตร ต้องใช้เครื่องมือ/กฎเพิ่ม) และควรปกปิดทั้งก่อนส่งให้ผู้ให้บริการภายนอกและก่อนเก็บ log
3) ตรวจ prompt injection แบบ heuristic — และข้อจำกัด
INJECTION = [r"ignore (all )?(previous|prior) instructions", r"system prompt", r"you are now"]
flagged = any(re.search(p, text, re.I) for p in INJECTION)
ทดสอบกับตัวอย่างที่มีเฉลย 6 ข้อความ (จริง 4 ข้อ ไม่จริง 2 ข้อ) ตัวตรวจแบบคำสำคัญจับได้ 3 ใน 4 (พลาดประโยคที่เรียบเรียงใหม่ “disregard everything you were told earlier”) และมี false positive 1 ข้อ (คำถามเรื่องงานถักที่บังเอิญมีวลี “ignore previous instructions”) → precision 0.75, recall 0.75 บทเรียน: heuristic เป็นตาข่ายชั้นแรกที่ถูกและเร็ว ไม่ใช่เกราะป้องกันหลัก — ชั้นที่แน่นอนคือ การจำกัดสิทธิ์และตรวจผลฝั่งโค้ด
4) อื่นๆ ที่ควรมี
- จำกัดความยาวเอาต์พุต (
max_tokensและตัดข้อความก่อนแสดง) - log ทุกครั้งที่ guardrail ทำงาน (ถูกปฏิเสธ/retry/fallback กี่ครั้ง) เพื่อรู้ว่าระบบพลาดตรงไหนบ่อย
- ทดสอบ guardrail ด้วยชุดตัวอย่างที่มีเฉลย เหมือนทดสอบโมเดล
ตัวอย่างแก้โจทย์
กด "ขั้นต่อไป" ทีละขั้น ลองรันและแก้โค้ดในแต่ละขั้นได้
retry พร้อม feedback ด้วย MockLLM
โมเดลจำลองตอบผิด 2 ครั้งแล้วค่อยตอบถูก
ask_with_retry ได้คำตอบสำเร็จที่ครั้งที่ 3 (ตัวแรกไม่ใช่ JSON ตัวที่สองเป็นหมวดที่ไม่มีจริง)
ปกปิด PII
ใช้ regex แทนอีเมลและเบอร์โทรด้วยป้ายกำกับ
ค้นหาอีเมลและเบอร์โทรในข้อความ
เติมช่องว่าง
แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ
แทนที่อีเมลในข้อความด้วย [EMAIL] เก็บไว้ในตัวแปร redacted เติม ___ ให้ครบ
แบบฝึกท้ายบท
บทนี้จะนับว่าเสร็จเมื่อผ่านข้อพื้นฐานและประยุกต์ครบ ข้อท้าทายได้ดาวเพิ่ม
🟢 พื้นฐาน
มีอีเมลกี่อันใน TEXT (re.findall ด้วย EMAIL) เก็บไว้ในตัวแปร n_emails
แทนที่อีเมลใน TEXT ด้วย "[EMAIL]" เก็บผลไว้ในตัวแปร redacted_email
จากข้อความ 4 ข้อความใน samples มีกี่ข้อความที่ตรงกับรูปแบบใดรูปแบบหนึ่งใน INJECTION (ไม่สนตัวพิมพ์ใหญ่เล็ก re.I) เก็บไว้ในตัวแปร n_flagged
ตัดข้อความ reply ให้ยาวไม่เกิน 20 ตัวอักษร ถ้าเกินให้ตัดเหลือ 20 ตัวแล้วต่อท้ายด้วย "…" เก็บผลไว้ในตัวแปร short
🟡 ประยุกต์
ปกปิดทั้งอีเมลและเบอร์โทรใน TEXT (แทนด้วย [EMAIL] และ [PHONE]) เก็บไว้ในตัวแปร redacted และจำนวนตัวอักษรที่ลดลงไว้ใน chars_removed (len เดิม − len ใหม่)
ใช้ ask_with_retry (ให้ไว้) กับ llm = MockLLM(replies) และ prompt "Classify: My card was declined" — เก็บ category ที่ได้ไว้ใน final_category และจำนวนรอบไว้ใน attempts
ในการเรียกโมเดลครั้งสุดท้าย (llm.calls[-1]) มีข้อความในประวัติกี่ข้อความ (n_messages_last) และมีข้อความ role assistant กี่ข้อความ (n_assistant)
🔴 ท้าทาย
โมเดลตอบผิดตลอด (replies = ["oops"]) และ max_retries = 2: ask_with_retry จะให้ result เป็น None — เขียนโค้ดที่ใช้ fallback {"category": "support", "needs_human": True} เมื่อ result เป็น None เก็บผลลัพธ์สุดท้ายไว้ใน final และจำนวนครั้งที่เรียกโมเดลไว้ใน calls_made
วัดตัวตรวจ injection (INJECTION, re.I) กับ labelled ที่ให้ไว้ (ข้อความ, เป็น injection จริงหรือไม่): เก็บ precision = TP ÷ (TP + FP) และ recall = TP ÷ (TP + FN) ปัด 4 ตำแหน่งไว้ใน precision, recall
Quiz
สรุป
- Guardrail คือโค้ดตรวจก่อน/หลังโมเดล: retry พร้อมส่ง error ให้โมเดลแก้ (จำกัดรอบ) แล้วมี fallback ที่ปลอดภัย
- ปกปิด PII (อีเมล เบอร์โทร) ก่อนส่งออกหรือ log แต่ regex ครอบคลุมเฉพาะรูปแบบที่คาดเดาได้
- ตัวตรวจ injection แบบคำสำคัญได้ precision/recall 0.75/0.75 ในตัวอย่าง จึงเป็นเพียงชั้นแรก — ชั้นหลักคือจำกัดสิทธิ์และตรวจผลฝั่งโค้ด (MockLLM ในบทนี้ตอบตามสคริปต์ ไม่ใช่โมเดลจริง)
คำถามสัมภาษณ์
ออกแบบ guardrail สำหรับแชตบอตลูกค้าอย่างไร
ก่อนส่งเข้า: จำกัดความยาว ปกปิด PII ตรวจ injection เบื้องต้น และแยกข้อมูลออกจากคำสั่ง หลังโมเดลตอบ: ตรวจ schema/ค่าที่อนุญาต ตรวจเนื้อหาที่ห้าม จำกัดความยาว และถ้าไม่ผ่านให้ retry พร้อม feedback จำกัดรอบ แล้ว fallback (ส่งต่อคน) พร้อม log และวัดอัตราการทำงานของแต่ละ guardrail
ทำไมเราจึงไม่ควรพึ่ง regex อย่างเดียวในการป้องกัน prompt injection
ผู้โจมตีเรียบเรียงประโยคได้หลากหลายจนกฎคำสำคัญตามไม่ทัน (recall ต่ำ) และกฎอาจแจ้งเตือนผิดกับข้อความปกติ (precision ต่ำ) จึงควรใช้เป็นตาข่ายชั้นแรก และป้องกันจริงด้วยการจำกัดสิทธิ์ของระบบ ตรวจผลลัพธ์ และให้มนุษย์อนุมัติการกระทำที่มีผลข้างเคียง