วิเคราะห์: การดึงผลลัพธ์ที่เครื่องอ่านได้จากโมเดลภาษา และเหตุใดคำสั่งว่า 'ให้ตอบเป็น JSON' จึงไม่ใช่ข้อกำหนดที่เพียงพอ
ผลิตภัณฑ์ทุกตัวที่นำโมเดลภาษาไปฝังไว้ในไปป์ไลน์ ในที่สุดแล้วย่อมต้องการผลลัพธ์ที่มีโครงสร้างแน่นอนและเชื่อถือได้ รูปแบบความล้มเหลวที่พบบ่อยนั้นเป็นที่เข้าใจกันดีอยู่แล้ว ส่วนวิธีแก้ก็ไม่ใช่เรื่องหวือหวาแต่อย่างใด นี่คือคู่มือภาคสนามสำหรับเรื่องนี้
คำตอบโดยย่อ
จะทำอย่างไรให้โมเดลภาษาส่งคืนข้อมูลที่มีโครงสร้างได้อย่างน่าเชื่อถือ?
ควรใช้ระบบถอดรหัสแบบมีข้อจำกัด (constrained decoding) หรือโหมดบังคับสคีมาที่แพลตฟอร์มมีให้ใช้งาน เพราะวิธีนี้ทำให้ไวยากรณ์ที่ผิดรูปแบบเกิดขึ้นไม่ได้เลย แทนที่จะเป็นเพียงแค่ลดโอกาสเกิดขึ้น จากนั้นให้ออกแบบสคีมาโดยคำนึงถึงตัวโมเดลเป็นหลัก นั่นคือโครงสร้างที่แบนราบ มีฟิลด์บังคับให้น้อยที่สุด กำหนดค่าที่เป็นไปได้ (enum) อย่างชัดเจน มีวิธีระบุความไม่แน่นอนอย่างชัดแจ้ง และไม่มีฟิลด์ใดที่ต้องอาศัยการคำนวณทางคณิตศาสตร์ ต้องตรวจสอบทุกคำตอบเทียบกับสคีมาเสมอ และควรมองว่าความถูกต้องเชิงความหมาย นั่นคือค่าที่ถูกต้องจริง ไม่ใช่แค่รูปแบบที่ถูกต้อง เป็นปัญหาอีกเรื่องหนึ่งที่การตรวจสอบสคีมาเพียงอย่างเดียวไม่สามารถแก้ไขได้
ประเด็นสำคัญ
- การถอดรหัสแบบจำกัดด้วยสคีมาแก้ปัญหาเรื่องไวยากรณ์ ไม่ใช่ความหมาย คำตอบอาจเป็น JSON ที่ถูกต้องสมบูรณ์แบบ แต่ผิดโดยสิ้นเชิงในเนื้อหา
- การออกแบบสคีมาคือการออกแบบพรอมป์ ชื่อฟิลด์ ค่าของ enum และคำอธิบายทุกตัวล้วนเป็นคำสั่งที่โมเดลอ่านและตีความ
- ต้องเปิดทางให้โมเดลบอกได้ว่า 'ไม่มีข้อมูล' หรือ 'ไม่แน่ใจ' อย่างถูกต้องตามหลัก หากไม่มีทางออกนี้ ฟิลด์ที่บังคับกรอกจะกลายเป็นคำเชิญให้โมเดลกุข้อมูลขึ้นมาเอง
- อย่าให้โมเดลคำนวณผลรวม เปอร์เซ็นต์ หรือวันที่ที่สามารถคำนวณได้เอง ควรดึงเฉพาะข้อมูลตั้งต้นออกมา แล้วไปคำนวณด้วยโค้ด
- ตรวจสอบความถูกต้องก่อน แล้วค่อยขอให้โมเดลลองใหม่อีกครั้งพร้อมแนบข้อผิดพลาดจากการตรวจสอบ การซ่อมแซมแบบเจาะจงเพียงรอบเดียวจะแก้ไขความผิดพลาดที่หลงเหลืออยู่ส่วนใหญ่ได้ในต้นทุนที่ต่ำกว่าการวนลูปลองซ้ำ
ทันทีที่โมเดลภาษาเลิกเขียนให้มนุษย์อ่านและเริ่มเขียนให้โปรแกรมใช้ ข้อกำหนดอีกชุดหนึ่งก็เข้ามาแทนที่ ร้อยแก้วอาจถูกต้องโดยประมาณได้ แต่ฟิลด์ชื่อ total_amount จะเป็นเช่นนั้นไม่ได้
ทีมส่วนใหญ่มักค้นพบเรื่องนี้ตามลำดับเดียวกัน พวกเขาระบุในพรอมป์ให้ส่งออกเป็น JSON แล้วมันก็ใช้ได้ จึงนำขึ้นใช้งานจริง หนึ่งสัปดาห์ต่อมา พาร์เซอร์เกิดข้อผิดพลาดเพราะโมเดลครอบออบเจ็กต์ไว้ในกรอบโค้ด เติมประโยคเป็นมิตรก่อนหน้า หรือใส่จุลภาคส่วนเกินท้ายรายการ พวกเขาจึงเพิ่มฟังก์ชันทำความสะอาด แล้วมันก็ใช้ได้ หนึ่งเดือนต่อมา ฟิลด์ที่ควรเป็น null กลับมีค่าที่แต่งขึ้นอย่างแนบเนียน และระบบไม่ล่มเลย ซึ่งเลวร้ายกว่ามาก
ความล้มเหลวสองอย่างนี้แตกต่างกันโดยสิ้นเชิง และการเหมารวมว่าเป็นปัญหาเดียวกันคือเหตุผลที่ทำให้ใช้เวลานานกว่าจะแก้ได้
สองปัญหาภายใต้ชื่อเดียวกัน
ความล้มเหลวทางไวยากรณ์ คือผลลัพธ์ที่พาร์สไม่ได้หรือไม่ตรงตามสคีมา ปัญหานี้เห็นได้ชัด เกิดขึ้นทันที และแก้ไขได้อย่างสมบูรณ์
ความล้มเหลวทางความหมาย คือผลลัพธ์ที่พาร์สได้อย่างสมบูรณ์แต่ให้ข้อมูลผิด เช่น วันที่ที่กล่าวอย่างมั่นใจทั้งที่ไม่เคยปรากฏในเอกสาร หมวดหมู่ที่อยู่ใน enum แต่ไม่ตรงกับเนื้อหา หรือยอดรวมที่ไม่สอดคล้องกับรายการย่อย
การถอดรหัสแบบมีข้อจำกัด ซึ่งเป็นโหมดบังคับใช้สคีมาที่แพลตฟอร์มรายใหญ่ทุกแห่งมีให้ในปัจจุบัน กำจัดปัญหาประเภทแรกได้เกือบทั้งหมด วิธีนี้จำกัดโทเค็นที่อนุญาตในแต่ละขั้นตอนของการสร้าง เพื่อไม่ให้เกิดผลลัพธ์ที่อยู่นอกไวยากรณ์ นี่คือการเปลี่ยนแปลงครั้งสำคัญอย่างแท้จริงเมื่อเทียบกับการขอผ่านพรอมป์ ซึ่งเป็นเพียงคำแนะนำที่โมเดลมักปฏิบัติตาม และความแตกต่างจะปรากฏชัดในกรณีส่วนน้อยสุดขั้ว เพราะคำขอหนึ่งครั้งในพันครั้งที่เคยล้มเหลวนั่นเองคือครั้งที่ทำให้ต้องเรียกคนมารับมือ
วิธีนี้ไม่ช่วยแก้ปัญหาประเภทที่สองแต่อย่างใด และผลทางจิตวิทยาที่ตามมามีความเสี่ยงอยู่จริง ทีมที่นำการบังคับใช้สคีมามาใช้มักเลิกตรวจสอบความถูกต้อง เพราะแพลตฟอร์ม “รับประกัน” สคีมา แต่สิ่งที่รับประกันคือรูปแบบเท่านั้น คำถามที่เหลือทั้งหมด ไม่ว่าจะเป็นค่านี้ถูกต้องหรือไม่ ฟิลด์เหล่านี้สอดคล้องกันหรือไม่ หรือวันที่นี้อยู่ในช่วงที่สมเหตุสมผลหรือไม่ ยังคงเป็นความรับผิดชอบของคุณ
สคีมาเป็นส่วนหนึ่งของพรอมป์
นี่คือการปรับกรอบความคิดที่มีประโยชน์ที่สุด แต่กลับถูกมองข้ามอยู่เสมอ โมเดลอ่านสคีมา ชื่อฟิลด์ ค่า enum คำอธิบาย และลำดับ ล้วนเป็นคำสั่ง และทั้งหมดแข่งขันกับพรอมป์ระบบในการชี้นำโมเดล
นั่นหมายความว่าการตัดสินใจออกแบบสคีมาส่งผลต่อคุณภาพ
ตั้งชื่อฟิลด์แบบเดียวกับที่คุณจะอธิบายให้คนฟัง is_urgent ให้ผลลัพธ์ดีกว่า flag2 คำอธิบายในฟิลด์ เช่น “เป็น true เฉพาะเมื่อลูกค้าระบุกำหนดเวลาภายใน 48 ชั่วโมง” มักมีประสิทธิผลมากกว่าการใส่ประโยคเดียวกันไว้ในพรอมป์ระบบ เพราะอยู่ติดกับจุดที่ต้องตัดสินใจ
ใช้ enum แทนสตริงอิสระทุกครั้งที่ขอบเขตของค่ามีจำกัด ฟิลด์ category แบบข้อความอิสระจะสร้างคำพ้องหรือคำใกล้เคียงสารพัดแบบในกรณีส่วนน้อย ซึ่งโค้ดปลายทางของคุณต้องคอยปรับให้เป็นมาตรฐานไปตลอด การใช้ enum ทำให้การตัดสินใจเกิดขึ้นตั้งแต่ขั้นตอนการสร้างผลลัพธ์
รักษาโครงสร้างให้แบนราบ ออบเจ็กต์ที่ซ้อนกันหลายชั้นเพิ่มทั้งโอกาสเกิดข้อผิดพลาดทางโครงสร้างและขอบเขตความเสียหายเมื่อแขนงหนึ่งผิดพลาด หากโดเมนมีลำดับชั้นอย่างแท้จริง การเรียกสองครั้งมักเชื่อถือได้มากกว่าการตอบกลับครั้งเดียวที่ซ้อนกันหลายชั้น และยังประเมินได้ง่ายกว่า เพราะมองเห็นได้ว่าขั้นตอนไหนล้มเหลว
จัดลำดับฟิลด์ให้การให้เหตุผลมาก่อนข้อสรุป สคีมาที่วางฟิลด์ reasoning หรือ evidence_quote ไว้ก่อน classification ให้ผลการจำแนกที่ดีขึ้นอย่างวัดผลได้เมื่อเทียบกับลำดับตรงกันข้าม เพราะโมเดลสร้างข้อความจากซ้ายไปขวา และฟิลด์ก่อนหน้าจะกำหนดเงื่อนไขให้ฟิลด์ถัดไป นี่เป็นจุดเดียวที่ร้อยแก้วสั้น ๆ ซึ่งโมเดลสร้างขึ้นให้ความคุ้มค่าภายในคำตอบที่มีโครงสร้าง
เปิดทางให้โมเดลตอบว่าไม่มีข้อมูล
สาเหตุที่พบบ่อยที่สุดของค่าที่แต่งขึ้นคือสคีมาไม่มีทางเลือกที่ชอบธรรมอื่นนอกจากการแต่งค่า
หาก contract_end_date เป็นสตริงบังคับที่ห้ามเป็น null แต่สัญญาไม่ได้ระบุวันที่สิ้นสุด โมเดลก็จำเป็นต้องสร้างค่าใดค่าหนึ่งขึ้นมา และจะสร้างค่าที่ดูสมเหตุสมผล โดยไม่มีส่วนใดในไปป์ไลน์คัดค้าน
วิธีแก้คือทำให้สามารถแสดงการไม่มีข้อมูลได้ ไม่ว่าจะใช้ฟิลด์ที่เป็น null ได้ เพิ่มสมาชิก enum ชื่อ not_stated อย่างชัดเจน หรือใช้บูลีนแยกต่างหากเพื่อควบคุมว่าค่านั้นควรมีหรือไม่ เมื่อใช้ควบคู่กับคำสั่งที่ชัดเจนว่าการไม่มีข้อมูลเป็นคำตอบที่ยอมรับได้และคาดหมายไว้ วิธีนี้จะขจัดค่าฟิลด์ที่หลอนขึ้นมาได้เป็นสัดส่วนมาก โดยมีต้นทุนเพียงหนึ่งบรรทัดในสคีมา
ตรรกะเดียวกันนี้ใช้กับระดับความเชื่อมั่นได้ด้วย enum ชื่อ confidence ที่มีสามค่า ได้แก่ สูง ปานกลาง และต่ำ มีประโยชน์กว่าคะแนนตัวเลข เพราะโมเดลปรับเทียบการประเมินตนเองเชิงตัวเลขได้ไม่ดีนัก แต่ค่อนข้างสม่ำเสมอในการเลือกกลุ่มแบบหยาบ ส่งกรณีที่มีความเชื่อมั่นต่ำให้มนุษย์ตรวจสอบ เท่านี้คุณก็เปลี่ยนปัญหาความแม่นยำให้เป็นปัญหาปริมาณงาน ซึ่งเป็นปัญหาที่รับมือได้ดีกว่ามาก
อย่าให้โมเดลคำนวณเลข
สคีมาที่มี subtotal, tax และ total กำลังขอค่าสามค่า ทั้งที่ค่าหนึ่งคำนวณได้จากค่าอื่น โมเดลจะกรอกครบทั้งสามค่า และบางครั้งค่าเหล่านั้นจะไม่สอดคล้องกัน
ให้สกัดเฉพาะอินพุต แล้วคำนวณค่าที่หาได้จากค่าอื่นในโค้ด กฎเดียวกันนี้ใช้กับการคำนวณวันที่ เปอร์เซ็นต์ จำนวน และทุกอย่างที่ต้องแปลงหน่วย ประเด็นนี้ไม่ได้หมายความว่าโมเดลไม่มีความสามารถ แต่การคำนวณแบบกำหนดแน่นอนมีอัตราความผิดพลาดเป็นศูนย์ ขณะที่ค่าที่สร้างขึ้นไม่มี และไม่มีเหตุผลที่จะยอมรับวิธีหลังเมื่อวิธีแรกพร้อมใช้งาน
การตรวจสอบความถูกต้องและการซ่อมแซมเพียงรอบเดียว
ตรวจสอบทุกคำตอบ รวมถึงคำตอบจากเอนด์พอยต์ที่บังคับใช้สคีมา เมื่อการตรวจสอบล้มเหลว วิธีรับมือที่มีประสิทธิผลที่สุดคือ ลองซ่อมแซมหนึ่งครั้งโดยระบุข้อผิดพลาดอย่างเจาะจง ไม่ใช่ลองใหม่โดยไม่ให้ข้อมูล การส่งข้อความกลับไปว่า “ฟิลด์ priority ต้องเป็นค่าใดค่าหนึ่งใน low, medium, high แต่คุณส่งกลับมาเป็น 'urgent'” แก้ความล้มเหลวที่หลงเหลืออยู่ส่วนใหญ่ได้ภายในการโต้ตอบเพิ่มเพียงรอบเดียว
จำกัดไว้ที่หนึ่งครั้ง วงจรลองใหม่ไม่จำกัดเมื่อพบอินพุตที่เป็นปฏิปักษ์หรือกำกวมอย่างแท้จริง จะเปลี่ยนความล้มเหลวที่มองเห็นได้ให้เป็นต้นทุนที่มองไม่เห็น และกลบสัญญาณสำคัญที่สุดที่คุณต้องการ นั่นคืออินพุตนี้มีบางอย่างไม่เข้ากับสคีมาที่คุณออกแบบไว้
หลังจากลองใหม่แล้ว ต้องมีทางเลือกสำรองแบบกำหนดแน่นอน เช่น ระเบียนค่าเริ่มต้นที่ทำเครื่องหมายไว้ให้ตรวจสอบ เส้นทางที่อิงกฎ หรือคิวสำหรับมนุษย์ ไปป์ไลน์ที่มีวิธีรับมือความล้มเหลวเพียง “ลองอีกครั้ง” เท่ากับไม่มีวิธีรับมือความล้มเหลวเลย ซึ่งเป็นข้อบกพร่องด้านการออกแบบ ไม่ใช่คุณสมบัติ
การประเมินผลลัพธ์แบบมีโครงสร้าง
ผลลัพธ์แบบมีโครงสร้างประเมินได้ง่ายเป็นพิเศษ และทีมควรใช้ประโยชน์จากจุดนี้
เนื่องจากผลลัพธ์อยู่ในรูปแบบที่เครื่องอ่านได้ การตรวจสอบส่วนใหญ่จึงทำได้อย่างแม่นยำ ได้แก่ ความถูกต้องตามสคีมา การเป็นสมาชิก enum การมีฟิลด์บังคับครบถ้วน ความสอดคล้องข้ามฟิลด์ และความแม่นยำระดับฟิลด์เมื่อเทียบกับชุดข้อมูลที่ติดป้ายกำกับไว้ ความแม่นยำต่อฟิลด์คือเมตริกที่ควรติดตาม ไม่ใช่ความแม่นยำของทั้งระเบียน เพราะการให้คะแนนระเบียนเป็นหน่วยเดียวบดบังว่าฟิลด์ใดกำลังมีคุณภาพลดลง และโดยมากปัญหาเกิดกับฟิลด์เดียว ไม่ใช่การสกัดข้อมูลทั้งหมด
ติดตามอัตรา null ด้วย หากจำนวนค่า null ลดลงอย่างฉับพลันหลังเปลี่ยนพรอมป์หรือโมเดล มักหมายความว่าโมเดลเริ่มแต่งค่าในจุดที่ก่อนหน้านี้เคยงดตอบ และปัญหานี้จะไม่ปรากฏในการตรวจสอบใด ๆ ที่พิจารณาเฉพาะระเบียนที่ถูกต้อง
บทสรุปที่ไม่หวือหวา
เรื่องนี้ไม่มีเทคนิคอันชาญฉลาด เปิดใช้การถอดรหัสแบบมีข้อจำกัด ออกแบบสคีมาโดยถือว่าโมเดลจะอ่าน เพราะมันอ่านจริง เปิดโอกาสให้ตอบว่า “ไม่ทราบ” เก็บการคำนวณไว้ในโค้ด ตรวจสอบความถูกต้องอยู่ดี ซ่อมแซมหนึ่งครั้ง แล้วใช้ทางเลือกสำรอง
ทุกข้อมีต้นทุนต่ำ และเมื่อรวมกันแล้ว สิ่งเหล่านี้คือความแตกต่างระหว่างโมเดลที่เป็นส่วนประกอบหนึ่งของระบบ กับโมเดลที่คอยสร้างเรื่องเหนือความคาดหมายขึ้นมาในระบบเป็นครั้งคราว
คำถามที่พบบ่อย
- การถอดรหัสแบบจำกัด (constrained decoding) เหมือนกับการขอให้โมเดลตอบเป็น JSON ในพรอมป์หรือไม่
- ไม่เหมือนกัน คำขอในพรอมป์เป็นเพียงข้อเสนอแนะที่โมเดลมักจะทำตาม แต่การถอดรหัสแบบจำกัดจะจำกัดโทเคนที่สามารถสร้างขึ้นได้ในแต่ละขั้นตอน ทำให้ผลลัพธ์ที่อยู่นอกไวยากรณ์ที่กำหนดเป็นไปไม่ได้เลย ความแตกต่างนี้จะปรากฏชัดในกรณีขอบ (tail) ซึ่งเป็นจุดที่ความล้มเหลวในระบบจริงมักเกิดขึ้น
- เหตุใดโมเดลจึงกุข้อมูลขึ้นมาเองสำหรับฟิลด์ที่ฉันกำหนดให้บังคับกรอก
- เพราะฟิลด์ที่บังคับกรอกโดยไม่มีค่าว่างที่ได้รับอนุญาต ก็คือคำสั่งให้ผลิตข้อมูลบางอย่างขึ้นมาเสมอ ควรปรับให้ฟิลด์นั้นสามารถเป็นค่าว่างได้ หรือเพิ่มค่า enum ที่ระบุชัดเจนว่า 'ไม่พบข้อมูล' ซึ่งโดยทั่วไปจะช่วยให้การกุข้อมูลหยุดลง
- ควรใช้สคีมาที่ซ้อนกันหลายชั้นหรือไม่
- ควรเลือกโครงสร้างแบบแบนราบมากกว่า การซ้อนชั้นลึกจะเพิ่มทั้งโอกาสเกิดข้อผิดพลาดด้านโครงสร้างและต้นทุนเมื่อเกิดความล้มเหลวบางส่วน หากโดเมนข้อมูลมีลักษณะเป็นลำดับชั้นจริง ๆ ควรพิจารณาแยกเป็นสองการเรียกใช้งานแทนที่จะรวมไว้ในคำตอบเดียวที่ซ้อนกันลึก
- การลองใหม่ (retry) ควรทำงานอย่างไร
- ควรมีการซ่อมแซมเพียงหนึ่งครั้งซึ่งแนบข้อผิดพลาดจากการตรวจสอบที่เจาะจงไปด้วย จากนั้นจึงเปลี่ยนไปใช้เส้นทางที่กำหนดแน่นอนหรือส่งต่อให้มนุษย์ตัดสินใจ การลองใหม่แบบไม่จำกัดจำนวนครั้งจะเปลี่ยนคำตอบที่แย่ให้กลายเป็นค่าใช้จ่ายที่แย่ และยังปิดบังปัญหาที่แท้จริงไว้ด้วย
- หากเอพีไอรับประกันเรื่องสคีมาอยู่แล้ว ยังจำเป็นต้องตรวจสอบความถูกต้องอีกหรือไม่
- จำเป็น การรับประกันนั้นครอบคลุมเพียงรูปแบบโครงสร้าง ไม่ใช่คุณค่าของข้อมูล การตรวจสอบสมาชิกใน enum ความสอดคล้องกันระหว่างฟิลด์ ช่วงค่าที่ยอมรับได้ และการอ้างอิงข้อมูลข้ามกัน ล้วนยังเป็นความรับผิดชอบของคุณอยู่เช่นเดิม
แหล่งข้อมูล
- JSON Schema specification — JSON Schema
- Model Context Protocol — specification — Model Context Protocol
- OpenAPI Specification — OpenAPI Initiative