ให้ AI คืน JSON: ผ่าน parser แล้วก็ยังผิด schema ได้

|ผู้เขียน: กองบรรณาธิการ QUASA|3 นาทีในการอ่าน
ให้ AI คืน JSON: ผ่าน parser แล้วก็ยังผิด schema ได้

หากต้องการให้ AI คืน JSON ที่แอปพลิเคชันอ่านตาม schema ได้สม่ำเสมอ ให้ส่ง schema ผ่านความสามารถ structured output ของ API ที่รองรับ และตรวจผลลัพธ์ในแอปพลิเคชันก่อนใช้งานจริง การเขียนใน prompt ว่าให้ตอบเป็น JSON ช่วยบอกความต้องการ แต่ไม่ได้บังคับให้คีย์ครบ ชนิดข้อมูลตรง หรือค่าที่สร้างขึ้นสอดคล้องกับข้อมูลต้นทาง

ทางรับข้อมูลควรแยกการตรวจเป็นสามชั้น ได้แก่ JSON parse, schema validation และ business-rule validation การผ่าน parser หมายถึงข้อความมีไวยากรณ์ JSON ที่อ่านได้เท่านั้น ส่วนการผ่าน schema หมายถึงข้อมูลมีรูปแบบตามสัญญาที่กำหนด ทั้งสองอย่างยังไม่บอกว่ารหัสคำสั่งซื้อเป็นของลูกค้าคนนั้น หรือจำนวนสินค้าตรงกับคำขอจริง

คำสั่งใน prompt, JSON mode และ structured output ต่างกันตรงไหน

สมมติว่าระบบรับข้อความลูกค้า ‘ขอยกเลิกคำสั่งซื้อ ORD-17 จำนวน 2 ชิ้น’ แล้วต้องดึง order_id, quantity และ intent หากสั่งเพียง ‘ตอบเป็น JSON’ โมเดลอาจคืน {"order_id":"ORD-17","quantity":"2","intent":"cancel"} ข้อความนี้ผ่าน parser แต่ quantity เป็นสตริง ไม่ใช่จำนวนเต็มตามที่แอปต้องการ อีกคำตอบที่อ่านได้คือ {"order_id":"ORD-17","quantity":2} ซึ่งไม่มี intent เลย ตัวอย่างทั้งคู่เป็นผลลัพธ์สมมติ ไม่ใช่ผลการทดสอบบริการใด

คู่มือ Structured Outputs ของ OpenAI แยก JSON mode ซึ่งรับประกัน JSON ที่ถูกไวยากรณ์ออกจาก Structured Outputs ซึ่งทำให้คำตอบที่สมบูรณ์ยึดตาม schema ที่รองรับ คู่มือเดียวกันระบุข้อยกเว้นสำคัญ: การปฏิเสธคำขออาจไม่อยู่ในรูป schema และคำตอบอาจไม่สมบูรณ์เมื่อการสร้างข้อความถูกตัด JSON mode จึงเหมาะกับงานที่ต้องการข้อความ JSON แต่ยังยอมให้แอปตรวจและจัดการความคลาดเคลื่อนของโครงสร้างเอง ส่วนงานที่โค้ดต้องอ่านฟิลด์แน่นอนควรใช้การจำกัดผลลัพธ์ด้วย schema เมื่อ API และโมเดลที่เลือกใช้รองรับ

schema ไม่ได้แทน prompt ทั้งหมด ในตัวอย่างนี้ prompt ยังต้องบอกให้ดึงรหัสจากข้อความลูกค้า ให้จัด intent เป็น cancel เมื่อมีคำขอยกเลิก และห้ามเดาข้อมูลที่ข้อความไม่ได้ให้ ส่วน schema กำหนดว่าคำตอบต้องมีฟิลด์อะไรและแต่ละฟิลด์รับค่าแบบใด การแยกหน้าที่เช่นนี้ช่วยให้แก้คำสั่งสกัดข้อมูลได้โดยไม่เผลอเปลี่ยนสัญญาข้อมูลที่แอปใช้รับผลลัพธ์

ออกแบบ schema จากคำขอเดียวให้แอปอ่านได้

สำหรับตัวอย่างสมมติ อาจกำหนด schema เป็น {"type":"object","properties":{"order_id":{"type":"string"},"quantity":{"type":"integer"},"intent":{"type":"string","enum":["cancel","other"]}},"required":["order_id","quantity","intent"],"additionalProperties":false} โครงสร้างนี้กำหนดให้ออบเจ็กต์มีคีย์ทั้งสาม ระบุว่า quantity ต้องเป็นจำนวนเต็ม จำกัด intent ไว้ในชุดค่าที่ตกลงกัน และไม่รับคีย์อื่น ผลลัพธ์ {"order_id":"ORD-17","quantity":2,"intent":"cancel"} จึงตรงรูปแบบที่กำหนดสำหรับข้อความสมมติข้างต้น

การเลือกชนิดข้อมูลควรตามการใช้งานจริง ไม่ใช่ตามหน้าตาของตัวอย่าง รหัสคำสั่งซื้อเป็นสตริงแม้บางระบบใช้รหัสที่มีแต่ตัวเลข เพราะรหัสมีไว้ระบุรายการ ไม่ได้มีไว้คำนวณ quantity เป็นจำนวนเต็มเพราะใช้แทนจำนวนชิ้น และ intent ใช้ enum เพื่อไม่ให้แอปต้องเดาว่าคำอย่าง ‘ยกเลิก’, ‘cancel_order’ หรือ ‘stop’ มีความหมายเดียวกันหรือไม่ หากระบบต้องแยกหลายเจตนา ควรเพิ่มค่าที่รองรับในสัญญาข้อมูลอย่างตั้งใจ พร้อมกำหนดพฤติกรรมของแอปสำหรับแต่ละค่า

ต้องออกแบบกรณีที่ข้อความต้นทางไม่ให้ข้อมูลครบด้วย ถ้าลูกค้าพิมพ์เพียง ‘ขอยกเลิก ORD-17’ การบังคับ quantity ให้เป็น integer ทุกครั้งอาจผลักให้โมเดลสร้างตัวเลขขึ้นมา หากค่าดังกล่าวอาจไม่มีจริง ให้ใช้รูปแบบรับ null ตามข้อกำหนดของ API และกำหนดให้แอปขอข้อมูลเพิ่มหรือส่งงานให้เจ้าหน้าที่เมื่อพบ null การใช้สตริงว่างหรือเลขศูนย์แทน ‘ไม่ทราบ’ โดยไม่ได้ตกลงความหมายไว้จะทำให้ข้อมูลที่ขาดดูเหมือนข้อมูลที่ยืนยันแล้ว

ใน Structured Outputs ของ OpenAI ฟิลด์ทั้งหมดต้องอยู่ใน required และการทำฟิลด์ที่อาจไม่มีทำได้ด้วยชนิดที่รับ null; ออบเจ็กต์ต้องกำหนด additionalProperties เป็น false ด้วย เงื่อนไขเหล่านี้ทำให้ schema ตัวอย่างเหมาะกับกรณีที่ข้อความระบุ quantity ชัดเจน แต่ถ้าต้องรองรับข้อความที่ไม่ระบุจำนวน ต้องปรับชนิดของ quantity และกฎรับข้อมูลไปพร้อมกัน การเพิ่ม null ลงใน schema เพียงอย่างเดียวไม่ตอบว่าแอปจะทำอะไรเมื่อค่าหายไป

ตรวจรับสามชั้นก่อนบันทึกหรือเรียกเครื่องมือ

แม้ใช้ structured output ก็ควรวางจุดตรวจในแอปพลิเคชัน เพราะข้อมูลอาจผ่านการแปลง การส่งต่อ หรือการเปลี่ยน API ก่อนถึงโค้ดที่บันทึกฐานข้อมูล แต่ละชั้นควรให้เหตุผลข้อผิดพลาดต่างกัน เพื่อไม่ให้ระบบนำคำตอบที่ใช้ไม่ได้ไปทำงานต่อโดยอาศัยเพียงความสำเร็จของขั้นก่อนหน้า

  1. JSON parse: ตรวจว่ามีเนื้อหาผลลัพธ์ครบและแปลงเป็นค่า JSON ได้จริง ถ้าข้อความถูกตัดกลางทาง มีอักขระเกิน หรือไม่มีคำตอบให้แปลง ให้หยุดที่ชั้นนี้ อย่าดึงบางฟิลด์จากข้อความที่ไม่ครบแล้วถือว่าเป็นระเบียนที่รับได้
  2. Schema validation: นำค่าที่ parse แล้วไปเทียบกับ schema ที่แอปตกลงใช้ ตรวจคีย์ที่จำเป็น ชนิดข้อมูล ค่า enum และคีย์ส่วนเกินโดยไม่แปลงชนิดเงียบ ๆ เช่น อย่าเปลี่ยน "2" เป็น 2 ก่อนรายงานว่าผลลัพธ์เดิมผิดชนิด การตรวจเช่นนี้ทำให้เห็นปัญหาที่ JSON parser ไม่มีหน้าที่ตรวจ
  3. Business-rule validation: ตรวจความสัมพันธ์ระหว่างค่ากับข้อความต้นทางและข้อมูลที่ระบบเชื่อถือได้ สำหรับคำขอยกเลิก ต้องดูว่ารหัสที่ดึงมาตรงกับข้อความจริง ลูกค้ามีสิทธิ์จัดการรายการนั้น จำนวนชิ้นสอดคล้องกับรายการสินค้า และสถานะคำสั่งซื้ออนุญาตให้ยกเลิกหรือไม่

คำอธิบาย JSON Schema ระบุว่า schema เป็นภาษาสำหรับกำหนดโครงสร้างและข้อจำกัด ส่วนการตัดสินว่าข้อมูลหนึ่งชุดตรง schema หรือไม่ต้องใช้ validator การส่ง schema ไปให้โมเดลจึงเป็นกลไกควบคุมการสร้างคำตอบ ขณะที่การตรวจค่าซึ่งแอปกำลังจะยอมรับเป็นหน้าที่อีกจุดหนึ่งในระบบ ควรให้ทั้งสองจุดอ้างอิงสัญญาข้อมูลฉบับเดียวกัน เพื่อไม่ให้ฝั่งสร้างและฝั่งรับคาดหวังคีย์คนละชุด

เมื่อคีย์และชนิดถูก แต่ค่าพาไปผิดรายการ

ผลลัพธ์สมมติ {"order_id":"ORD-18","quantity":2,"intent":"cancel"} ผ่าน parser และผ่าน schema ตัวอย่างทุกข้อ แต่ข้อความลูกค้าระบุ ORD-17 ความผิดพลาดนี้แก้ด้วยการเพิ่ม required หรือ enum ไม่ได้ เพราะ ORD-18 ยังเป็นสตริงที่ถูกชนิด การตรวจเชิงความหมายต้องเทียบรหัสที่โมเดลส่งกับข้อความต้นทางก่อนนำไปค้นรายการ และต้องตรวจสิทธิ์กับสถานะจากระบบคำสั่งซื้อก่อนอนุมัติการกระทำ

แม้รหัสจะตรงกับข้อความ ก็ยังมีช่องว่างระหว่าง ‘ดึงข้อมูลถูก’ กับ ‘ทำรายการได้’ ลูกค้าอาจระบุจำนวน 2 ชิ้น แต่คำสั่งซื้อมีสินค้าหลายชนิดและข้อความไม่ได้บอกว่าจะยกเลิกรายการใด หรือคำสั่งซื้อนั้นอาจเปลี่ยนสถานะหลังจากรับข้อความมาแล้ว schema บอกได้ว่า quantity เป็น integer แต่ไม่รู้จำนวนที่มีอยู่ สินค้าที่เกี่ยวข้อง หรือกฎยกเลิกล่าสุด ข้อมูลเหล่านี้ต้องมาจากฐานข้อมูลและกฎธุรกิจ ณ เวลาที่จะลงมือทำ

สำหรับงานที่ผลลัพธ์จะกลายเป็นอาร์กิวเมนต์ของเครื่องมือ ให้แยกขั้น ‘โมเดลเสนอค่า’ ออกจากขั้น ‘แอปอนุญาตให้เรียก’ อย่างชัดเจน ตัวอย่างเช่น เครื่องมือยกเลิกคำสั่งซื้ออาจรับ order_id ที่ผ่าน schema แล้ว แต่แอปยังต้องผูกคำขอนั้นกับตัวตนลูกค้าที่เข้าสู่ระบบ และตรวจว่ารายการอยู่ในขอบเขตสิทธิ์ของเขา การให้โมเดลส่งฟิลด์ว่า authorised เป็น true ไม่ใช่หลักฐานสิทธิ์ เพราะเป็นอีกค่าหนึ่งที่โมเดลสร้างขึ้นเอง

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

แยก refusal และคำตอบที่ถูกตัดออกจากข้อมูล JSON

ก่อน parse ให้ตรวจสถานะคำตอบตามรูปแบบของ API ที่ใช้อยู่ หากมีสัญญาณ refusal ให้จัดการเป็นคำขอที่ถูกปฏิเสธ ไม่ควรพยายามแปลงข้อความปฏิเสธเป็นออบเจ็กต์คำสั่งซื้อ หรือเติมคีย์ที่ขาดให้ผ่าน schema กรณีนี้ต่างจากคำตอบที่ตั้งใจคืนข้อมูลแต่พิมพ์ JSON ผิด และควรมีเส้นทางจัดการแยกกันในโค้ด

คำตอบที่สร้างไม่จบก็ต้องหยุดก่อนนำไปใช้ แม้ส่วนที่ได้รับจะบังเอิญปิดวงเล็บครบและ parser อ่านได้ สถานะที่บอกว่าถึงขีดจำกัดความยาวหรือการสร้างถูกขัดจังหวะมีความหมายต่างจากคำตอบที่จบสมบูรณ์ หลังยืนยันสถานะและไม่มี refusal แล้วจึงค่อย parse ตรวจ schema และตรวจค่าจริงตามลำดับ การเรียงเช่นนี้ป้องกันไม่ให้แอปตีความข้อความจากเส้นทางข้อผิดพลาดเป็นข้อมูลธุรกิจ

ใช้ schema ข้าม API ต้องตรวจข้อกำหนดของแต่ละบริการ

เอกสาร structured outputs ของ Gemini API ระบุการใช้ JSON Schema กับงานสกัดข้อมูลและจัดหมวดหมู่ พร้อมเตือนว่ารองรับข้อกำหนดของ JSON Schema เพียงบางส่วน และให้ตรวจค่าที่ตรงรูปแบบแต่ผิดความหมายในแอปพลิเคชัน หลักการแยกโครงสร้างออกจากความถูกต้องของข้อมูลจึงใช้กับตัวอย่างคำสั่งซื้อเดียวกันได้ แต่พารามิเตอร์ที่ส่ง schema และคีย์เวิร์ดที่รองรับต้องดูตาม API และรุ่นโมเดลที่เลือก

ก่อนย้าย schema ให้เทียบข้อกำหนดที่ตัวอย่างนี้พึ่งพาทีละข้อ: object, properties, required, integer, enum, additionalProperties และการรับ null หากมีข้อมูลที่อาจขาด อย่าสมมติว่าบริการที่รับคำว่า JSON Schema จะรองรับคีย์เวิร์ดทุกตัวหรือจัดการคีย์ที่ไม่รู้จักเหมือนกัน หาก API ไม่รองรับข้อจำกัดที่แอปต้องใช้ ให้คงข้อจำกัดนั้นไว้ใน validator ฝั่งแอป และอย่าแปลง schema ให้หลวมลงโดยไม่ปรับกฎรับข้อมูล

การเลือกระหว่าง structured response กับ function calling ขึ้นอยู่กับบทบาทของผลลัพธ์ หากต้องการข้อมูลเพื่อแสดงหรือบันทึก แอปรับออบเจ็กต์จากคำตอบแล้วตรวจตามสัญญาข้อมูลได้ หากต้องการให้โมเดลเสนอการเรียกเครื่องมือ อาร์กิวเมนต์ของการเรียกก็ต้องผ่านการตรวจชนิด ความหมาย สิทธิ์ และสถานะล่าสุดก่อนเครื่องมือทำงานจริง เส้นแบ่งสำคัญอยู่ตรงจุดที่ข้อมูลจากโมเดลจะเปลี่ยนเป็นการกระทำของระบบ

แชร์:

สมัครรับจดหมายข่าวของเรา

รับข่าวสารล่าสุดเกี่ยวกับ Web3, AI และคริปโตส่งตรงถึงกล่องจดหมายของคุณ

0