AI Engineer › Prompt และ structured output
ดึงและตรวจ JSON จากผลลัพธ์ของโมเดล
ประมาณ 35 นาที
เป้าหมายของบท
- parse JSON จากผลลัพธ์ของโมเดลแบบ strict และดึงจากข้อความที่ปนกันด้วย regex
- ตรวจ schema (ฟิลด์ที่ต้องมี enum ชนิด ช่วงค่า) และ coerce เฉพาะกรณีที่ปลอดภัย
- มองผลลัพธ์ของโมเดลเป็นข้อมูลที่ต้องตรวจก่อนใช้
โปรแกรมต้องการข้อมูลเชิงโครงสร้าง แต่โมเดลตอบเป็นข้อความ
เมื่อต้องส่งผลของ LLM ไปเก็บลงฐานข้อมูลหรือให้โค้ดอื่นใช้ เราขอให้ตอบเป็น JSON — และต้องเตรียมรับความจริงว่า บางครั้งมันไม่ใช่ JSON ที่ถูกต้อง เช่น มีคำเกริ่นนำ ห่อด้วย ```json, ใช้เครื่องหมาย ’ แทน “, ขาดฟิลด์ หรือค่าผิดชนิด
ตัวอย่างในบทนี้ใช้ผลลัพธ์ที่ จำลอง ไว้ 8 แบบ (list
RAW) เพื่อให้ทดสอบซ้ำได้ ไม่ได้เรียกโมเดลจริง — ของจริงลดปัญหาได้ด้วยการใช้ความสามารถ structured output / tool use ที่ให้ระบุ JSON schema (ตามเอกสารของผู้ให้บริการ) แต่ก็ยัง ต้องตรวจฝั่งเราเสมอ
ขั้น 1: parse แบบเข้มงวดก่อน
import json
try:
obj = json.loads(text)
except json.JSONDecodeError:
obj = None
จาก 8 ตัวอย่าง parse ตรงๆ ได้ 5 ตัว (ตัวที่พังคือ: มีข้อความนำหน้า, ใช้ single quote, ไม่มี JSON เลย)
ขั้น 2: ดึง JSON ออกจากข้อความที่ปนกัน
import re
match = re.search(r"\{.*\}", text, re.S) # re.S ให้ . ข้ามบรรทัดใหม่ได้
obj = json.loads(match.group(0)) if match else None
วิธีแบบ lenient (ลอง strict ก่อน ไม่ได้ค่อยดึงด้วย regex) ทำให้ได้ 6 จาก 8 — ตัวที่ใช้ single quote ยังพัง (เพราะไม่ใช่ JSON) และตัวที่ตอบเป็นประโยคไม่มี JSON ให้ดึง
regex แบบ {.*} เป็นแบบง่าย: ใช้ได้เมื่อมีวัตถุ JSON ก้อนเดียว ถ้ามีหลายก้อนต้องเขียนให้รัดกุมกว่านี้
ขั้น 3: ตรวจ schema (parse ได้ ≠ ใช้ได้)
ตรวจทุกครั้งว่า: มีฟิลด์ที่ต้องมีครบ, ค่าอยู่ในชุดที่อนุญาต (enum), ชนิดถูก, ช่วงค่าถูก
| ผลลัพธ์ | ปัญหา |
|---|---|
"confidence": "0.7" |
ชนิดผิด (string) — แก้ด้วยการ coerce เป็น float ได้ |
"category": "billing" |
ไม่อยู่ในป้ายกำกับที่อนุญาต |
ไม่มี confidence |
ขาดฟิลด์ |
"confidence": 1.4 |
นอกช่วง 0–1 |
การ coerce (แปลงชนิดให้) ควรทำเฉพาะกรณีที่ชัดเจนและปลอดภัย (สตริงตัวเลข → float) ส่วนค่าที่ผิดความหมาย (หมวดไม่มีจริง, ค่านอกช่วง) ต้องปฏิเสธหรือขอให้โมเดลแก้ (บทถัดไป) ผลของ 8 ตัวอย่าง: ผ่านตรวจแบบไม่ coerce 2 ตัว, แบบ coerce 3 ตัว
หลักการ: ถือว่าผลลัพธ์ของโมเดลเป็นข้อมูลที่ไม่น่าไว้ใจ เหมือนอินพุตจากผู้ใช้ — ตรวจก่อนใช้เสมอ (ไลบรารีอย่าง pydantic / jsonschema ทำหน้าที่นี้ในงานจริง; ที่นี่เขียนเองเพื่อเข้าใจหลักการ)
ตัวอย่างแก้โจทย์
กด "ขั้นต่อไป" ทีละขั้น ลองรันและแก้โค้ดในแต่ละขั้นได้
parse แบบ strict vs lenient
ทดลองกับผลลัพธ์จำลอง 8 แบบ
parse ตรงๆ ด้วย json.loads — ได้กี่ตัว (ที่เหลือ JSONDecodeError): 5
ตรวจ schema
parse ได้แล้วยังต้องตรวจความถูกต้อง
ผลตรวจแบบไม่ coerce ของแต่ละตัวอย่าง (None = ผ่าน)
เติมช่องว่าง
แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ
parse สตริง JSON แล้วอ่านค่า category เก็บไว้ในตัวแปร category เติม ___ ให้ครบ
แบบฝึกท้ายบท
บทนี้จะนับว่าเสร็จเมื่อผ่านข้อพื้นฐานและประยุกต์ครบ ข้อท้าทายได้ดาวเพิ่ม
🟢 พื้นฐาน
จาก RAW: parse ด้วย json.loads ตรงๆ สำเร็จกี่ตัว (n_strict) และล้มเหลวกี่ตัว (n_fail)
ตัวอย่างแรกของ RAW (index 0) parse แล้วได้ dict — เก็บ category ไว้ใน first_category และ confidence ไว้ใน first_conf
ดึงข้อความ JSON ออกจาก RAW[1] (ที่มีคำเกริ่นนำและ ```json) ด้วย re.search(r"{.*}", text, re.S) เก็บข้อความที่จับได้ไว้ในตัวแปร extracted
แปลง dict {"category": "shipping", "confidence": 0.92} กลับเป็นสตริง JSON ด้วย json.dumps (ค่าเริ่มต้น) เก็บไว้ในตัวแปร dumped
🟡 ประยุกต์
ใช้ parse_lenient (ให้ไว้แล้ว) กับทุกตัวใน RAW — มีกี่ตัวที่ได้ผล (ไม่ใช่ None) เก็บไว้ในตัวแปร n_lenient และ index (นับจาก 0) ของตัวที่ล้มเหลวทุกตัวเก็บเป็น list ไว้ใน failed_idx
ใช้ first_error (ให้ไว้แล้ว ไม่ coerce) กับผลของ parse_lenient — กี่ตัวที่ผ่าน (n_valid_strict) เมื่อ error เป็น None ถือว่าผ่าน
เหมือนข้อ e6 แต่เปิด coerce=True — ผ่านกี่ตัว (n_valid_coerced) และ index ของตัวที่ผ่านเก็บเป็น list ไว้ใน valid_idx
🔴 ท้าทาย
นับชนิดของ error ของทั้ง 8 ตัว (first_error แบบไม่ coerce) — เก็บเฉพาะที่ไม่ใช่ None เป็น dict ชนิด → จำนวน (Counter แปลงเป็น dict) ไว้ในตัวแปร error_counts
เก็บเฉพาะผลที่ผ่านการตรวจ (coerce=True) เข้า DataFrame ที่มีคอลัมน์ category และ confidence (แปลง confidence เป็น float) — เก็บจำนวนแถวไว้ใน n_rows และค่าเฉลี่ย confidence (ปัด 4 ตำแหน่ง) ไว้ใน mean_conf
Quiz
สรุป
- ผลลัพธ์ของโมเดลอาจไม่ใช่ JSON ที่ถูกต้อง: strict parse ได้ 5 จาก 8 ตัวอย่างจำลอง, lenient (ดึงด้วย regex) ได้ 6
- parse ได้ ≠ ใช้ได้: ตรวจฟิลด์ครบ enum ชนิด และช่วงค่า (ผ่าน 2 ตัวแบบไม่ coerce, 3 ตัวแบบ coerce)
- coerce เฉพาะกรณีชัดเจน (สตริงตัวเลข) ส่วนค่าผิดความหมายให้ปฏิเสธหรือขอให้โมเดลแก้; ตัวอย่างในบทนี้เป็นผลลัพธ์จำลอง ไม่ได้เรียกโมเดลจริง
คำถามสัมภาษณ์
คุณจะทำให้ระบบที่ใช้ผลลัพธ์ JSON จาก LLM ทนต่อข้อมูลผิดรูปแบบได้อย่างไร
ใช้ structured output/tool schema ของผู้ให้บริการเพื่อลดปัญหา แต่ตรวจฝั่งเราเสมอ: parse แบบ strict ก่อน ถ้าไม่ได้ลองดึง JSON จากข้อความ ตรวจฟิลด์/ชนิด/enum/ช่วงค่าด้วย schema validator (เช่น pydantic, jsonschema) coerce เฉพาะกรณีปลอดภัย และถ้าไม่ผ่านให้ retry พร้อมส่ง error กลับให้โมเดลแก้ หรือ fallback/ส่งให้คนตรวจ พร้อมบันทึก log อัตราความล้มเหลว
ทำไมผลลัพธ์ของ LLM ควรถูกมองเป็นข้อมูลที่ไม่น่าไว้ใจ
เพราะโมเดลสร้างข้อความตามความน่าจะเป็น ไม่ได้รับประกันรูปแบบหรือความถูกต้อง อาจมีฟิลด์ขาด ชนิดผิด ค่านอกช่วง หรือถูกชักนำด้วย prompt injection การใช้โดยไม่ตรวจอาจทำให้ข้อมูลเสียหรือเกิดช่องโหว่ จึงควรปฏิบัติเหมือนอินพุตจากภายนอกและตรวจก่อนใช้