Structured Output คืออะไร? คู่มือการออกแบบและใช้งาน LLM ให้ตอบกลับแบบ Type-safe

บทนำ
Structured Output คือกลไกที่ช่วยให้ LLM (Large Language Model) สร้างคำตอบตามรูปแบบที่กำหนดไว้ล่วงหน้า เช่น JSON Schema ต่างจากการตอบกลับด้วยภาษาธรรมชาติแบบอิสระ การบังคับรูปแบบของเอาต์พุตและประเภทของข้อมูล (Data Type) จะช่วยให้สามารถส่งต่อข้อมูลไปยังระบบถัดไปได้อย่างปลอดภัย บทความนี้จัดทำขึ้นสำหรับนักพัฒนาและสถาปนิกที่สร้าง AI Agent หรือระบบอัตโนมัติ (Automation Pipeline) โดยจะอธิบายอย่างเป็นระบบตั้งแต่เหตุผลที่ต้องใช้ Structured Output, การออกแบบ JSON Schema, วิธีการบังคับ LLM, แนวทางการหลีกเลี่ยงข้อผิดพลาดที่พบบ่อย ไปจนถึงรูปแบบการประยุกต์ใช้งานจริง
บทสรุป: การส่งผลลัพธ์จาก LLM เข้าสู่ระบบงานโดยตรง อาจทำให้เกิดความล้มเหลวในการแยกวิเคราะห์ (Parse) หรือเกิดข้อผิดพลาดได้จากความไม่คงที่ของรูปแบบข้อมูล การส่งออกข้อมูลแบบมีโครงสร้าง (Structured Output) จึงเป็นวิธีการลดความเสี่ยงนี้ตั้งแต่ขั้นตอนการออกแบบ เราจะมาสรุปความจำเป็นของเรื่องนี้ผ่านปัญหาของข้อความแบบอิสระ ความสัมพันธ์กับอาการประสาทหลอน (Hallucination) และความสำคัญในการทำงานร่วมกันของ Agent
ปัญหาของ Free-form Text ในการเชื่อมต่อระบบ
เมื่อเราสั่งให้ LLM "ช่วยดึงชื่อลูกค้าและวันที่ที่ต้องการจากคำถามนี้" บางครั้งมันจะตอบกลับมาว่า "ชื่อลูกค้า: ยามาดะ ทาโร่" แต่บางครั้งก็ตอบว่า "ชื่อของคุณลูกค้าคือคุณยามาดะ ทาโร่ครับ" สำหรับมนุษย์แล้วทั้งสองประโยคมีความหมายเหมือนกัน แต่สำหรับโปรแกรมที่ต้องนำข้อมูลไปประมวลผลต่อ ทั้งสองรูปแบบถือเป็นคนละเรื่องกัน
เมื่อเราพยายามจัดการกับข้อความรูปแบบอิสระ (Free-form text) ในระบบ เรามักจะพบกับปัญหาดังต่อไปนี้:
- ถ้อยคำหรือลำดับของผลลัพธ์เปลี่ยนไปทุกครั้ง ทำให้การใช้ Regular Expression หรือการตัดแบ่งสตริง (String splitting) ไม่เสถียร
- มีคำเกริ่นนำที่ไม่จำเป็นปะปนมาด้วย (เช่น "รับทราบครับ", "นี่คือผลลัพธ์ครับ")
- รูปแบบการเขียนตัวเลขหรือวันที่ไม่คงที่ ทำให้การแปลงชนิดข้อมูล (Type conversion) ล้มเหลว
กระบวนการเหล่านี้อาจทำงานได้ในตอนแรก แต่เมื่อรูปแบบข้อมูลขาเข้าเพิ่มมากขึ้น การจัดการกับข้อยกเว้น (Exception handling) จะบวมขึ้นเรื่อยๆ จนในที่สุดก็ไม่สามารถดูแลรักษาได้ การกำหนด "รูปแบบ" ของผลลัพธ์ให้ตายตัวล่วงหน้า นอกเหนือไปจาก "ความหมาย" ของผลลัพธ์ จึงเป็นเงื่อนไขสำคัญในการเชื่อมต่อระบบให้มีความเสถียร
ความสัมพันธ์ระหว่าง Hallucination กับ Type Error
โดยทั่วไปแล้ว ฮัลซิเนชัน (Hallucination) สามารถแบ่งออกได้เป็น 2 ประเภทหลัก ได้แก่ "ฮัลซิเนชันด้านเนื้อหา (การตอบกลับที่ไม่ตรงกับความจริง)" และ "ฮัลซิเนชันด้านรูปแบบ (การตอบกลับด้วยโครงสร้างที่ไม่คาดคิด)" ซึ่งการส่งออกข้อมูลแบบมีโครงสร้าง (Structured Output) จะช่วยยับยั้งประเภทหลังได้โดยตรง
การบังคับใช้ประเภทข้อมูล (Type) จะช่วยป้องกันความผิดพลาดด้านรูปแบบ เช่น การเพิ่มคีย์ที่ไม่มีอยู่จริงขึ้นมาเอง หรือการใส่ข้อความลงในช่องที่ควรจะเป็นตัวเลข อย่างไรก็ตาม แม้ข้อมูลจะเป็นไปตามสกีมา (Schema) แต่ก็ยังมีความเป็นไปได้ที่เนื้อหาภายในค่าเหล่านั้นจะไม่ตรงกับความจริง โปรดเข้าใจว่า "ประเภทข้อมูลที่ถูกต้อง" กับ "เนื้อหาที่ถูกต้อง" เป็นคนละเรื่องกัน
อย่างไรก็ตาม การจำกัดค่าที่สามารถเลือกได้ด้วย Enum หรือการระบุรายการที่จำเป็น (Required fields) จะช่วยลดช่องว่างที่ LLM จะกุเรื่องหรือสร้างค่าที่อยู่นอกเหนือตัวเลือกขึ้นมา การออกแบบสกีมาจึงเปรียบเสมือนการทำหน้าที่เป็น "การ์ดเรล" (Guardrail) ที่ช่วยสร้างเสถียรภาพด้านรูปแบบและยับยั้งฮัลซิเนชันด้านเนื้อหาได้ในระดับหนึ่ง
ความสำคัญใน AI Agent และ Multi-agent System
สำหรับการถามตอบแบบครั้งเดียวจบ หากผลลัพธ์มีความคลาดเคลื่อนบ้าง มนุษย์ก็ยังสามารถอ่านและทำความเข้าใจได้ แต่ในโครงสร้างที่ AI Agent ต้องเรียกใช้เครื่องมือหลายอย่างต่อเนื่องกัน ผลลัพธ์ของขั้นตอนหนึ่งจะกลายเป็นอินพุตของขั้นตอนถัดไป หากรูปแบบข้อมูลผิดพลาดเพียงจุดเดียว จะทำให้กระบวนการทั้งหมดหยุดชะงักลงทันที
ในช่วงแรก เรามักจะคิดว่า "ถ้าปรับแต่ง Prompt ให้ดีก็น่าจะทำให้ได้ผลลัพธ์เป็น JSON" แต่ในความเป็นจริง ยิ่งจำนวนขั้นตอนมากขึ้นเท่าใด โอกาสที่รูปแบบข้อมูลจะผิดพลาดในจุดใดจุดหนึ่งก็จะยิ่งสะสมมากขึ้นเท่านั้น การส่งผ่านข้อมูลระหว่าง Agent จึงควรใช้การกำหนดประเภทข้อมูล (Type) ให้เข้มงวดและปฏิบัติเสมือนเป็นสัญญา (Interface) เพื่อความเสถียรที่มากกว่า
โดยเฉพาะในระบบ Multi-agent การกำหนดความรับผิดชอบของแต่ละ Agent ด้วย Schema จะช่วยให้ขอบเขตการทำงานชัดเจนขึ้น และทำให้การดีบั๊กหรือการทำ Replay ทำได้ง่ายขึ้น หากมองว่า Structured Output คือภาษากลางที่ใช้เชื่อมต่อระหว่าง Agent จะช่วยให้การออกแบบระบบมีความชัดเจนและเป็นระเบียบมากขึ้น
ข้อกำหนดเบื้องต้นก่อนเริ่มใช้งาน Structured Output
บทสรุป: ความสำเร็จของ Structured Output ขึ้นอยู่กับว่า "LLM ที่ใช้รองรับหรือไม่" และ "สามารถออกแบบ Schema ที่เหมาะสมได้หรือไม่" การตรวจสอบข้อกำหนดเบื้องต้นก่อนเริ่มลงมือทำจะช่วยป้องกันการต้องกลับมาแก้ไขงานใหม่ โดยมี 3 ประเด็นสำคัญที่ต้องทำความเข้าใจ ได้แก่ ผู้ให้บริการและโหมด API ที่รองรับ, พื้นฐานของ JSON Schema และการเลือกใช้ให้เหมาะสมกับการทำ Tuning
การตรวจสอบ LLM Provider และ API Mode ที่รองรับ
วิธีการบรรลุผลลัพธ์แบบมีโครงสร้าง (Structured Output) จะแตกต่างกันไปตาม LLM หรือโหมด API ที่ใช้ ก่อนการนำไปใช้งานจริง โปรดตรวจสอบว่าโมเดลที่คุณวางแผนจะใช้รองรับวิธีการใดบ้าง
วิธีการหลักๆ มีดังนี้:
- โหมดที่ส่ง Schema เข้าไปโดยตรงเพื่อรับประกันประเภทข้อมูล (Strict Schema Compliance)
- วิธีการรับข้อมูลที่มีโครงสร้างผ่านอาร์กิวเมนต์ของคำจำกัดความฟังก์ชัน (Function Calling / Tool Use)
- โหมด JSON แบบหลวมๆ ที่ระบุเพียงแค่ว่า "ให้ตอบกลับเป็น JSON"
หากโมเดลรองรับ Strict Schema Compliance การใช้วิธีนี้จะมีความเสถียรที่สุด หากไม่รองรับ ให้เลือกใช้ Function Calling ตามลำดับ และหากวิธีดังกล่าวยังทำได้ยาก ให้ใช้วิธีการเขียนคำสั่งใน Prompt ควบคู่ไปกับการทำ Validation เพื่อเสริมความถูกต้อง ทั้งนี้ สถานะการรองรับอาจเปลี่ยนแปลงไปตามโมเดลและเวอร์ชัน จึงขอแนะนำให้ตรวจสอบเอกสารอย่างเป็นทางการฉบับล่าสุดอยู่เสมอ
พื้นฐานและทักษะการออกแบบ JSON Schema
คุณภาพของ Structured Output ขึ้นอยู่กับคุณภาพของ JSON Schema ที่คุณส่งให้เป็นสำคัญ การออกแบบจะราบรื่นขึ้นหากคุณสามารถจัดการองค์ประกอบพื้นฐานต่อไปนี้ได้:
type(ประเภทของค่า เช่น string / number / boolean / object / array)properties(คำจำกัดความของแต่ละฟิลด์ใน object)required(การระบุฟิลด์ที่จำเป็น)enum(การจำกัดค่าที่เป็นไปได้)description(การอธิบายความหมายของแต่ละฟิลด์ด้วยภาษาธรรมชาติ)
โดยเฉพาะ description มักถูกมองข้าม แต่ LLM จะใช้คำอธิบายนี้เป็นเบาะแสในการตีความความหมายของฟิลด์ การเขียนว่า "วันที่จอง ในรูปแบบ YYYY-MM-DD" จะช่วยให้ได้ผลลัพธ์ที่ตรงตามความต้องการมากกว่าการเขียนเพียงแค่ว่า "วันที่"
โปรดเข้าใจว่า Schema ไม่ใช่สิ่งที่เขียนครั้งเดียวแล้วจบ แต่เป็นสิ่งที่ต้องปรับจูนโดยการเพิ่มหรือผ่อนปรนข้อจำกัดในขณะที่ดูผลลัพธ์จริงประกอบไปด้วย
การเลือกใช้ Fine-tuning และ Prompt Engineering
คำถามที่พบบ่อยคือ "จำเป็นต้องทำ Fine-tuning เพื่อให้ได้ Structured Output หรือไม่" ซึ่งในหลายกรณี การใช้ Schema Enforcement และ Prompt Engineering ก็เพียงพอแล้ว ดังนั้นการเริ่มต้นโดยไม่ใช้การเรียนรู้เพิ่มเติม (Additional Training) จึงเป็นวิธีที่สมเหตุสมผลที่สุด
เกณฑ์ในการตัดสินใจมีดังนี้:
- หากเป็นการสกัดข้อมูลหรือการจำแนกประเภททั่วไป มักจะสามารถจัดการได้ด้วย Schema และ Prompt
- หากเกี่ยวข้องกับศัพท์เฉพาะทางในธุรกิจ หรือการตัดสินใจที่ซับซ้อนซึ่งไม่สามารถอธิบายได้ครบถ้วนผ่าน Prompt การทำ Fine-tuning จึงจะเป็นตัวเลือกที่ควรพิจารณา
แม้ว่า Fine-tuning จะมีช่องทางในการเพิ่มความแม่นยำ แต่ก็มีต้นทุนในการดำเนินงานทั้งด้านการเตรียมข้อมูลและการเรียนรู้ใหม่ (Retraining) การตรวจสอบให้แน่ใจก่อนว่า Prompt Engineering ไม่สามารถบรรลุความแม่นยำตามเป้าหมายได้ จะช่วยให้ประเมินความคุ้มค่าของการลงทุน (ROI) ได้ง่ายขึ้น เราขอแนะนำให้เริ่มจากวิธีที่เบากว่าก่อน และค่อยขยับไปใช้วิธีที่ซับซ้อนขึ้นเมื่อพบข้อจำกัดของวิธีเดิมแล้วเท่านั้น
วิธีการออกแบบ JSON Schema
บทสรุป: สกีมาที่ดีสามารถสร้างได้ด้วย 3 ขั้นตอน คือ "คัดเลือกรายการที่จำเป็นจาก Use Case, ระบุประเภทและข้อจำกัดให้ชัดเจน และรักษาโครงสร้างให้แบนราบที่สุดเท่าที่จะทำได้" สกีมาที่ซับซ้อนเกินไปจะทำให้ LLM ปฏิบัติตามได้ยากและส่งผลเสียมากกว่าผลดี ต่อไปเราจะมาดูขั้นตอนการออกแบบอย่างละเอียดกัน
Step 1: ระบุฟิลด์ที่จำเป็นจาก Use Case
การออกแบบ Schema ไม่ควรเริ่มจากการเขียน JSON ทันที แต่ให้ใช้วิธีคิดย้อนกลับจาก "หลังจากได้รับผลลัพธ์นี้แล้ว ระบบจะทำอะไรต่อ" หากกระบวนการถัดไปไม่ได้ใช้ข้อมูลส่วนใด ก็ไม่จำเป็นต้องให้แสดงผลออกมาตั้งแต่แรก
ตัวอย่างเช่น ในการจัดหมวดหมู่คำถาม หากกระบวนการถัดไปต้องทำ "การส่งต่อให้ทีมที่รับผิดชอบ" และ "การตั้งค่าลำดับความสำคัญ" สิ่งที่จำเป็นก็มีเพียง 2 รายการคือ หมวดหมู่ (Category) และระดับความเร่งด่วน (Urgency) เท่านั้น หากโลภมากไปใส่ "บทสรุป" หรือ "คะแนนความรู้สึก" (Sentiment score) เพิ่มเข้ามา จะทำให้ผลลัพธ์หนักขึ้นและส่งผลให้ความแม่นยำลดลง
เคล็ดลับในการคัดกรองคือ ให้ตรวจสอบแต่ละ Field ว่าสามารถอธิบายสั้นๆ ได้หรือไม่ว่า "ใครเป็นคนใช้ และใช้เมื่อไหร่" หากอธิบายไม่ได้ รายการนั้นมักจะไม่จำเป็นหรือควรไปดึงข้อมูลในจังหวะเวลาอื่น การจำกัดให้เหลือเพียงรายการที่จำเป็นและเพียงพอคือทางลัดสู่การได้ผลลัพธ์ที่มีโครงสร้างที่เสถียรครับ
Step 2: กำหนด Type, Required Fields และ Enum
เมื่อกำหนดรายการที่จำเป็นได้แล้ว ให้กำหนดประเภท (Type) และข้อจำกัด (Constraint) ให้กับแต่ละรายการ หากส่วนนี้ไม่ชัดเจน ผลลัพธ์ที่ได้จะมีความคลาดเคลื่อนแม้ว่าจะคัดกรองรายการมาดีแล้วก็ตาม
มี 3 ประเด็นสำคัญดังนี้:
- ระบุประเภทให้ชัดเจน: หากเป็น "ตัวเลข" ให้ระบุไปถึงว่าเป็นจำนวนเต็มหรือทศนิยม และมีช่วงของค่าเท่าใด
- แยกรายการที่จำเป็นและรายการทางเลือก: รายการที่กระบวนการถัดไปต้องใช้แน่นอนให้ใส่ไว้ใน
requiredเพื่อป้องกันข้อมูลสูญหาย - กำหนดค่าที่เป็นไปได้ด้วย
enum: สำหรับรายการที่มีตัวเลือกตายตัว เช่น หมวดหมู่หรือสถานะ ให้ระบุเป็นรายการแจงนับ
โดยเฉพาะ enum จะให้ผลลัพธ์ที่มีประสิทธิภาพสูง หากจำกัดค่าไว้เพียง 3 ค่าคือ "ด่วน" (Emergency), "ปกติ" (Normal) และ "ต่ำ" (Low) จะช่วยป้องกันไม่ให้ LLM สร้างคำนิยามกึ่งกลางขึ้นมาเอง เช่น "ด่วนปานกลาง" การแยกแยะว่าส่วนใดควรเป็นช่องกรอกอิสระและส่วนใดควรจำกัดด้วยตัวเลือก จะส่งผลโดยตรงต่อความเสถียรของกระบวนการถัดไป
Step 3: ลดการ Nest และ Recursion เพื่อเพิ่มประสิทธิภาพ Token
ยิ่งสกีมา (Schema) มีการซ้อนทับ (Nest) ลึกเท่าใด LLM ก็จะยิ่งทำตามได้ยากขึ้นและสิ้นเปลืองโทเค็นมากขึ้นเท่านั้น ขอแนะนำให้คลี่โครงสร้างที่ซ้อนทับเกิน 3 ชั้นหรือโครงสร้างแบบเรียกซ้ำ (Recursive) ให้แบนราบที่สุดเท่าที่จะเป็นไปได้
ในตอนแรกเราอาจอยากคัดลอกลำดับชั้นตามความเป็นจริง แต่ในทางปฏิบัติแล้ว การใช้ชุดของอาร์เรย์แบบแบน (Flat array) ร่วมกับคีย์ (Key) มักจะช่วยให้ผลลัพธ์มีเสถียรภาพมากกว่า ตัวอย่างเช่น แทนที่จะซ้อนทับ "แผนก-ทีม-สมาชิก" ให้ลึก การใส่ชื่อแผนกและชื่อทีมลงในอาร์เรย์ของสมาชิกจะช่วยให้ LLM จัดการได้ง่ายกว่า
ในแง่ของประสิทธิภาพของโทเค็น ยิ่งการซ้อนทับตื้นเท่าใด ปริมาณการเขียนสกีมาก็จะยิ่งลดลงและไม่กินพื้นที่บริบท (Context) มากเกินไป ควรตั้งคำถามว่าโครงสร้างที่ซับซ้อนนั้นจำเป็นจริงๆ หรือไม่ และการตัดสินใจที่จะไม่ให้ LLM ทำในส่วนที่สามารถนำไปประกอบใหม่ในขั้นตอนการประมวลผลภายหลังได้นั้นถือเป็นวิธีที่มีประสิทธิภาพ
วิธีบังคับให้ LLM แสดงผลแบบ Structured Output
บทสรุป: เพื่อให้ได้ผลลัพธ์ที่มีโครงสร้าง (Structured Output) อย่างแน่นอน การใช้กลยุทธ์ป้องกันหลายชั้น (Defense in Depth) โดยการจำกัดรูปแบบผลลัพธ์ด้วยโหมดอย่าง Function Calling, การระบุ Schema และตัวอย่างใน Prompt รวมถึงการตรวจสอบความถูกต้อง (Validation) และการลองใหม่ (Retry) หลังจากได้รับข้อมูล เป็นวิธีที่มีประสิทธิภาพ การใช้ทั้ง 3 วิธีร่วมกันแทนที่จะเลือกใช้อย่างใดอย่างหนึ่ง คือกุญแจสำคัญสู่การใช้งานที่เสถียรครับ
Step 4: จำกัดผลลัพธ์ด้วย Function Calling / Tool Use Mode
วิธีที่แน่นอนที่สุดคือการใช้โหมด Function Calling (Tool Use) ของ API เพื่อรับข้อมูลที่มีโครงสร้างเป็นอาร์กิวเมนต์ของฟังก์ชัน ในโหมดนี้ ฝั่ง API จะคอยควบคุมให้เอาต์พุตของ LLM เป็นไปตามสคีมาที่กำหนดไว้
ขั้นตอนการใช้งานนั้นเรียบง่าย:
- กำหนดโครงสร้างข้อมูลที่ต้องการรับเป็นสคีมาของอาร์กิวเมนต์ฟังก์ชัน
- ให้ LLM ตอบกลับในรูปแบบของการ "เรียกใช้" ฟังก์ชันนั้น
- รับอาร์กิวเมนต์ที่ส่งกลับมาในรูปแบบข้อมูลที่มีโครงสร้าง
เมื่อเทียบกับการสั่งใน Prompt ว่า "ให้ตอบกลับเป็น JSON" วิธีนี้จะช่วยลดปัญหาการมีข้อความเกริ่นนำแทรกเข้ามา และลดโอกาสที่รูปแบบข้อมูลจะผิดพลาดได้ หากมีโหมดเฉพาะที่รับประกันการปฏิบัติตามสคีมาให้เลือกใช้ก่อน และถ้าไม่มี ให้ใช้ Function Calling เป็นตัวเลือกแรก จะช่วยลดภาระการจัดการข้อผิดพลาดในขั้นตอนถัดไปได้อย่างมาก
Step 5: ระบุ Schema และตัวอย่างใน System Prompt
นอกจากจะกำหนดด้วยโหมดแล้ว การระบุเจตนาของสกีมา (Schema) และตัวอย่างที่ชัดเจนลงใน System Prompt จะช่วยให้คุณภาพของผลลัพธ์มีความเสถียรยิ่งขึ้น เนื่องจากโมเดลไม่ได้ต้องการเพียงแค่ "รูปแบบ" เท่านั้น แต่ยังต้องการข้อมูลประกอบการตัดสินใจว่า "ควรใส่อะไรลงไป" ด้วย
วิธีที่มีประสิทธิภาพคือการแสดงตัวอย่างคู่ของ Input และ Output ที่คาดหวังไว้ 1-2 ตัวอย่าง (Few-shot) การแสดงตัวอย่างผลลัพธ์จริงเพียง 1 ตัวอย่าง จะช่วยให้โมเดลเข้าใจระดับความละเอียดและโทนของฟิลด์ได้ดีกว่าการอธิบายด้วยข้อความว่า "ให้เขียนอย่างสุภาพ"
อย่างไรก็ตาม หากใส่ตัวอย่างมากเกินไปจะไปเบียดบังบริบท (Context) และอาจทำให้ผลลัพธ์ถูกชี้นำโดยตัวอย่างมากจนเกินไป การจำกัดจำนวนตัวอย่างไว้เพียงกรณีทั่วไป 1 กรณี และกรณีขอบเขตที่มักเกิดข้อผิดพลาดอีก 1 กรณี ถือเป็นโครงสร้างที่สร้างสมดุลระหว่างต้นทุนและประสิทธิภาพได้ดีที่สุด ทั้งนี้ ไม่จำเป็นต้องใช้ตัวอย่างชุดเดิมตลอดไป แต่ควรปรับเปลี่ยนตัวอย่างโดยดูจากผลลัพธ์ที่ผิดพลาด เพื่อยกระดับความแม่นยำให้สูงขึ้น
Step 6: ตรวจสอบความถูกต้อง (Validation) และทำ Retry Loop
แม้จะมีการกำหนดโหมดและ Prompt เพื่อควบคุมแล้ว แต่ก็ไม่มีการรับประกันว่าผลลัพธ์จะตรงกับ Schema อย่างสมบูรณ์ ดังนั้น คุณต้องตรวจสอบข้อมูลที่ได้รับด้วย Schema เสมอ และเตรียมกลไกเพื่อลองใหม่ (Retry) หากข้อมูลไม่ถูกต้อง
ลูปพื้นฐานจะเป็นดังนี้:
- ตรวจสอบความถูกต้อง (Validate) ของผลลัพธ์ด้วย Schema
- หากล้มเหลว ให้ขอให้สร้างผลลัพธ์ใหม่อีกครั้งโดยแนบรายละเอียดข้อผิดพลาดไปด้วย
- หากไม่สำเร็จภายในจำนวนครั้งที่กำหนด ให้เปลี่ยนไปใช้กระบวนการสำรอง (Fallback)
เมื่อลองใหม่ การส่งข้อมูลว่า "ฟิลด์ใดที่ไม่ถูกต้องและเพราะเหตุใด" ไปใน Prompt จะช่วยให้การแก้ไขในครั้งถัดไปทำได้ง่ายขึ้น เพื่อหลีกเลี่ยงการเกิดลูปไม่รู้จบ โปรดกำหนดจำนวนครั้งสูงสุดในการลองใหม่และตั้งค่า Timeout ไว้เสมอ การนำการตรวจสอบและการลองใหม่ไปใช้เป็นหน้าที่ของฝั่งแอปพลิเคชันถือเป็นปราการด่านสุดท้ายที่จะช่วยให้การส่งออกข้อมูลแบบมีโครงสร้าง (Structured Output) มีความเสถียรในสภาพแวดล้อมการใช้งานจริง (Production)
รูปแบบความผิดพลาดที่พบบ่อยและวิธีหลีกเลี่ยง
บทสรุป: ความล้มเหลวของ Structured Output ส่วนใหญ่มักเกิดจาก 3 ปัจจัยหลัก ได้แก่ "Schema ซับซ้อนเกินไป", "การจัดการ Output ไม่รัดกุมจนเกิดช่องโหว่ด้านความปลอดภัย" และ "บริบทไม่เพียงพอจนทำให้ Schema ถูกตัดทอน" ซึ่งทั้งหมดนี้สามารถป้องกันได้ตั้งแต่ขั้นตอนการออกแบบ ต่อไปเราจะมาดูสัญญาณเตือนและวิธีหลีกเลี่ยงของแต่ละปัญหาครับ
กรณี Schema ซับซ้อนเกินไปจน LLM ทำตามไม่ได้
เมื่อการทำ Structured Output ไม่เป็นไปตามที่คาดหวัง สาเหตุส่วนใหญ่มักไม่ได้อยู่ที่ตัวโมเดล แต่อยู่ที่ตัว Schema เอง ความซับซ้อน เช่น การมีฟิลด์มากเกินไป การซ้อนทับกันหลายชั้น (Nested) หรือข้อจำกัดที่ขัดแย้งกันเอง ล้วนทำให้โมเดลทำตามได้ยาก
สัญญาณที่พบบ่อยมีดังนี้:
- ข้อมูลในฟิลด์บังคับบางส่วนหายไป
- ค่าในลำดับชั้นที่ลึกเกินไปเกิดความผิดพลาด
- ผลลัพธ์ถูกตัดจบกลางคัน
วิธีแก้ไขคือการ "แบ่งส่วน" และ "ทำให้เรียบง่าย" โดยการปรับเปลี่ยนให้ผลลัพธ์ไม่ออกมาทั้งหมดในคราวเดียว แต่ใช้วิธีทยอยดึงข้อมูลทีละส่วน หรือปรับ Schema ให้แบนราบและลดจำนวนรายการลง แม้ว่าในช่วงแรกเรามักจะอยากเขียน Schema ในรูปแบบที่สมบูรณ์แบบที่สุด แต่ในความเป็นจริง การควบคุมความซับซ้อนให้อยู่ในระดับที่ LLM สามารถทำตามได้อย่างเสถียร จะส่งผลให้ได้ทั้งความแม่นยำและการบำรุงรักษาที่ดีกว่าในระยะยาว โปรดจำไว้ว่า Schema ไม่ได้เป็นเพียงเรื่องของ "ความถูกต้อง" เท่านั้น แต่ยังรวมถึงการออกแบบให้ "ทำตามได้ง่าย" อีกด้วย
ความเสี่ยง Injection จากการจัดการผลลัพธ์ที่ไม่ถูกต้อง
สิ่งที่มักถูกมองข้ามในการทำ Structured Output คือความเสี่ยงจากการเชื่อใจข้อมูลที่ได้รับมาโดยตรง แม้ว่าเอาต์พุตของ LLM จะอยู่ในรูปแบบ JSON ที่ถูกต้อง แต่ก็ไม่ได้หมายความว่าเนื้อหาข้างในจะปลอดภัยเสมอไป
สิ่งที่อันตรายคือการส่งค่าเอาต์พุตไปประมวลผลต่อโดยไม่มีการตรวจสอบ ตัวอย่างเช่น หากนำสตริงที่อยู่ในเอาต์พุตไปฝังลงใน SQL, เชลล์คอมมานด์ หรือ HTML โดยตรง อาจกลายเป็นช่องทางสำหรับการทำ Injection ได้ ซึ่งสิ่งนี้เรียกว่า "Improper Output Handling" และเป็นที่ทราบกันดีว่าเป็นความเสี่ยงหลักของแอปพลิเคชัน LLM
วิธีรับมือคือการปฏิบัติต่อเอาต์พุตด้วยทัศนคติเดียวกับการจัดการอินพุตจากภายนอก:
- ตรวจสอบประเภทข้อมูล (Type) และช่วงของค่า (Range) ด้วย Schema
- ต้องทำการ Escape หรือใช้ Parameterized เสมอเมื่อส่งข้อมูลไปยังฐานข้อมูลหรือคอมมานด์
- ปฏิเสธค่าที่ไม่คาดคิดด้วยระบบ Fallback
"การมีโครงสร้าง (Structured) ไม่ได้เท่ากับความปลอดภัย" การตรวจสอบรูปแบบ (Format) และการทำให้เนื้อหาปลอดภัย (Sanitization) เป็นมาตรการที่แยกจากกัน ดังนั้นควรดำเนินการทั้งสองอย่างควบคู่กันไป
ปัญหา Schema ถูกตัดทอนเนื่องจาก Context Window ไม่เพียงพอ
หากสกีมา (Schema) หรือข้อมูลนำเข้ามีขนาดใหญ่เกินไป อาจทำให้ถึงขีดจำกัดของ Context Window และส่งผลให้เอาต์พุตถูกตัดจบกลางคันได้ อาการที่พบบ่อยคือส่วนท้ายของ JSON ที่ยาวเกินไปขาดหายไป ทำให้การพาร์ส (Parse) ข้อมูลล้มเหลว
ในตอนแรกอาจดูเหมือนเป็น "ความผิดปกติของโมเดล" แต่ในความเป็นจริง บ่อยครั้งสาเหตุมาจากจำนวนโทเค็นไม่เพียงพอ โปรดตรวจสอบประเด็นต่อไปนี้:
- คำนิยามของสกีมาหรือตัวอย่าง Few-shot ยาวเกินไปหรือไม่
- ได้ส่งข้อความนำเข้าทั้งหมดไปโดยไม่มีการคัดกรองหรือไม่ (สามารถจำกัดเฉพาะส่วนที่จำเป็นได้หรือไม่)
- จำนวนโทเค็นสูงสุดของเอาต์พุตเพียงพอสำหรับความยาวของ JSON ที่คาดการณ์ไว้หรือไม่
แนวทางแก้ไขที่มีประสิทธิภาพ ได้แก่ การทำให้สกีมาเรียบง่ายขึ้น การสรุปหรือแบ่งส่วนข้อมูลนำเข้า และการลดจำนวนรายการในเอาต์พุต การแบ่งข้อมูลออกเป็นส่วนๆ ที่เหมาะสมแล้วเรียกใช้งานหลายครั้ง จะให้ผลลัพธ์ที่เสถียรกว่าและคาดการณ์ต้นทุนได้ง่ายกว่าการประมวลผลข้อมูลจำนวนมากในคราวเดียว
รูปแบบการออกแบบขั้นสูงที่ประยุกต์ใช้ Structured Output
บทสรุป: Structured Output ไม่ได้จำกัดอยู่เพียงแค่การดึงข้อมูลแบบเดี่ยวเท่านั้น แต่ยังเป็นรากฐานของการออกแบบขั้นสูง เช่น การจัดรูปแบบคำตอบของ RAG และการประสานงานระหว่าง Multi-agent แบบ Type-safe การกำหนดรูปแบบ Output ด้วย Type คือสิ่งที่ช่วยสนับสนุนความน่าเชื่อถือของ Pipeline ที่มีความซับซ้อน ต่อไปนี้คือตัวอย่างการประยุกต์ใช้งานที่สำคัญ 2 รูปแบบ
การใช้ Structured Response ใน RAG Pipeline
ใน RAG (Retrieval-Augmented Generation) นั้น LLM จะสร้างคำตอบโดยอ้างอิงจากเอกสารที่ค้นหามาได้ หากเราเปลี่ยนคำตอบนี้จากข้อความรูปแบบอิสระ (free-form text) ให้เป็นเอาต์พุตที่มีโครงสร้าง (structured output) จะช่วยให้การนำไปแสดงผลหรือตรวจสอบในขั้นตอนถัดไปทำได้ง่ายขึ้นมาก
ตัวอย่างเช่น นอกเหนือจากเนื้อหาคำตอบแล้ว เราสามารถออกแบบให้ส่งคืนข้อมูลในรูปแบบที่มีโครงสร้างดังนี้:
- ID ของเอกสารที่ใช้อ้างอิงเป็นที่มาของคำตอบ
- ระดับความมั่นใจของคำตอบ หรือแฟล็กที่ระบุว่าข้อมูลไม่เพียงพอ
การจัดเก็บข้อมูลอ้างอิงให้มีโครงสร้างจะช่วยให้ควบคุมการทำงานได้ง่ายขึ้น เช่น การแสดงแหล่งที่มาบน UI หรือการส่งคำตอบที่มีความมั่นใจต่ำไปให้คนตรวจสอบ
คำตอบที่เป็นข้อความอิสระเพียงอย่างเดียวมักจะทำให้ "แหล่งที่มา" ไม่ชัดเจน แต่การทำให้เป็นโครงสร้างจะช่วยให้สามารถเชื่อมโยงคำตอบกับแหล่งที่มาได้อย่างเป็นระบบ การปรับปรุงคุณภาพของ RAG สามารถเริ่มต้นได้ง่ายขึ้นจากการจัดรูปแบบเอาต์พุตให้เป็นระเบียบครับ
การส่งข้อความแบบ Type-safe ใน Multi-agent System
ในระบบที่เอเจนต์หลายตัวทำงานร่วมกัน หากรูปแบบของข้อความที่สื่อสารระหว่างเอเจนต์เกิดความผิดพลาด จะส่งผลให้ระบบโดยรวมขาดเสถียรภาพ การใช้ Structured Output เป็น "สัญญาตกลงระหว่างเอเจนต์" (Agent-to-Agent Contract) จะช่วยให้การส่งผ่านข้อมูลมีความปลอดภัยเชิงประเภท (Type-safe)
หัวใจสำคัญของการออกแบบมีดังนี้:
- กำหนด Schema สำหรับอินพุตและเอาต์พุตของเอเจนต์แต่ละตัวไว้ล่วงหน้า
- ตรวจสอบข้อความที่ได้รับด้วย Schema ก่อนดำเนินการทุกครั้ง
- หากพบการละเมิด Schema ให้ตรวจพบในทันทีเพื่อดำเนินการสร้างใหม่หรือจัดการข้อผิดพลาด
การทำเช่นนี้จะช่วยรับประกันได้โดยอัตโนมัติว่า เอาต์พุตของเอเจนต์หนึ่งตัวจะเป็นไปตามข้อกำหนดอินพุตของเอเจนต์ถัดไป นอกจากนี้ยังช่วยให้ระบุจุดที่เกิดปัญหาได้ง่าย และแม้จะมีการเปลี่ยนตัวเอเจนต์แต่ละตัว แต่ตราบใดที่ยังรักษาข้อตกลงไว้ได้ ระบบโดยรวมก็จะยังคงทำงานต่อไปได้
กุญแจสำคัญในการควบคุมความซับซ้อนของระบบหลายเอเจนต์ (Multi-agent) คือการไม่ปล่อยให้มีการสื่อสารอย่างอิสระ แต่ต้องควบคุมด้วยประเภทข้อมูล (Type) ซึ่ง Structured Output คือเทคโนโลยีพื้นฐานที่สนับสนุนระเบียบวินัยดังกล่าว
ผู้เขียน・ผู้ตรวจสอบ
Yusuke Ishihara
เริ่มเขียนโปรแกรมตั้งแต่อายุ 13 ปี ด้วย MSX หลังจบการศึกษาจากมหาวิทยาลัย Musashi ได้ทำงานพัฒนาระบบขนาดใหญ่ รวมถึงระบบหลักของสายการบิน และโครงสร้าง Windows Server Hosting/VPS แห่งแรกของญี่ปุ่น ร่วมก่อตั้ง Site Engine Inc. ในปี 2008 ก่อตั้ง Unimon Inc. ในปี 2010 และ Enison Inc. ในปี 2025 นำทีมพัฒนาระบบธุรกิจ การประมวลผลภาษาธรรมชาติ และแพลตฟอร์ม ปัจจุบันมุ่งเน้นการพัฒนาผลิตภัณฑ์และการส่งเสริม AI/DX โดยใช้ generative AI และ Large Language Models (LLM)


