ตัวอย่างใช้งานจริงของโซลูชัน RAG แบบปรับแต่งเฉพาะ

ตัวอย่างจากการใช้งานจริงแสดงให้เห็นว่าระบบ Retrieval-Augmented Generation ที่ปรับให้เหมาะกับงานช่วยแก้ปัญหาความรู้ที่ซับซ้อนในองค์กรได้อย่างไร

ทุกวันนี้ RAG มักถูกมองในแง่ลบ บ้างก็คิดว่ามันง่ายนิดเดียวไปแล้ว (เริ่มต้นนั้นง่าย แต่ขยายระบบไม่ง่ายนัก) หรือคิดว่าถูก “ระบบแบบเอเจนต์” แซงหน้าไปแล้ว (ทั้งที่หลายกรณี หากพิจารณาให้ลึกลงไปก็จะพบว่าคล้าย RAG อย่างรวดเร็ว...)

บล็อกนี้จะยกตัวอย่างที่ลงมือทำจริงสักหนึ่งถึงสองกรณี เพื่อแสดงวิธีรับมือกับความท้าทายที่พบบ่อย ได้แก่

  • การจัดการข้อมูลที่มีทั้งข้อความและตัวเลข และเหตุผลที่ทำให้ RAG แบบพื้นฐานใช้ไม่ได้ผล: คีย์เวิร์ดซ้ำกัน ขณะที่ตัวเลขไม่มีความหมายเชิงความหมายศาสตร์

  • เหตุใดการออกแบบ Embedding โดยเน้นบทสรุปก่อนจึงช่วยได้: สร้างบทสรุปสั้นๆ ที่อธิบายแต่ละส่วนย่อย แล้วจึงทำ Embedding และค้นจากบทสรุปนั้น

  • วิธีสร้างบทสรุปที่มีบริบท: เพิ่มบริบทจากเอกสารหลัก เพื่อแยกแยะสถิติที่มีรูปแบบคล้ายกัน

  • เมื่อใดควรใช้โค้ดและโมเดล Pydantic: เมื่อต้องคงเนื้อหาไว้ทุกคำ ให้ผสานโค้ดที่เขียนขึ้นเองและ/หรือโมเดล Pydantic เข้ากับการเรียกใช้ LLM เพื่อให้ได้ผลลัพธ์ที่เชื่อถือได้

การสร้างโซลูชัน RAG แบบเฉพาะทาง

พื้นฐาน

ระบบ RAG ขับเคลื่อนสารพัดโซลูชัน ตั้งแต่บอตฝ่ายสนับสนุนไปจนถึงผู้ช่วยค้นหาความรู้ภายในองค์กร

โดยทั่วไป เบื้องหลังระบบจะมีขั้นตอนดังนี้

  1. แบ่งเอกสารต้นฉบับเป็นส่วนย่อย

  2. ทำ Embedding แต่ละส่วนย่อยลงในปริภูมิเวกเตอร์

  3. ดึงข้อมูลส่วนย่อย K อันดับแรกเมื่อมีคำค้น

  4. สร้างคำตอบโดยอิงส่วนย่อยเหล่านั้น

ชุดเครื่องมือยอดนิยมอย่าง LangChain, LlamaIndex และ Filestore ของ OpenAI ทำให้ขั้นตอนเหล่านี้แทบจะง่ายนิดเดียว แต่ในกระบวนการทำงานจริง คุณจะพบข้อมูลที่ไม่ได้มีเพียงข้อความหนาแน่น และ RAG พื้นฐานอาจรับมือได้ไม่ดีนัก ส่วนต่อไปนี้จะแสดงตัวอย่างความท้าทายด้านข้อมูลที่เป็นรูปธรรม และค่อยๆ พัฒนาโซลูชันเมื่อมีความซับซ้อนเพิ่มขึ้น

เมื่อทุกอย่างเริ่มซับซ้อนขึ้น

  1. เมื่อข้อมูลไม่ได้มีแค่ข้อความ (ซึ่งจริงๆ แล้วพบได้บ่อย)

ลองพิจารณาส่วนย่อยของข้อมูลต่อไปนี้ในบริบทของเกม

JSON

{ "Attack": { "Range": { "default": 5, "with_Draconic_Ascension": 5.5, }, "Speed": { "default": "2 seconds", "with_Draconic_Ascension": "2.2 seconds", }, "Damage": { "default": 10, "with_Draconic_Ascension": 12, } }} 

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

เรื่องนี้จะไม่เป็นปัญหาหากเรามีข้อมูลประเภทนี้เพียงอย่างเดียว เพราะยังดึงข้อมูลจาก Embedding ของคำบรรยายเพียงไม่กี่คำ ที่มีอยู่ได้ (หรือใช้ text-to-sql ไปเลย) แต่จะเกิดอะไรขึ้นหากส่วนย่อยนี้ปะปนอยู่ท่ามกลางส่วนย่อยที่มีข้อความหนาแน่นจำนวนมาก ซึ่งมีคำเหล่านี้อยู่ด้วย ตัวอย่างเช่น

JSON

{"Draconic_Ascension": { "description": "Transform into dragon form. Attack range and speed are increased by 10%, damage is boosted by 20%.", "details": { "activation_conditions": "Can only be activated when HP is below 50%or Fury meter is full.", "visual_effects": "Wings unfurl, scales shimmer with embers, voice lines change to echoing growls.", "lore": "An ancient bloodline awakens. The bearer of the mark channels the soul of the last Flamewing Wyrm, becoming a living storm of fire and fury." } }}

ทีนี้ลองนึกว่าเราต้องการดึงคำตอบของ “What is the attack range with Draconic Ascension?” เป็นไปได้สูงว่าเราจะดึงส่วนย่อยที่เกี่ยวข้องไม่ได้ เพราะมันจมหายอยู่ท่ามกลางส่วนย่อยอื่นๆ ที่มีคีย์เวิร์ดเดียวกัน

ต้นตอของปัญหาคือเราแยกแยะส่วนย่อยเหล่านี้ได้ไม่ดี แม้แต่ละส่วนจะมีข้อมูลคนละประเภทเกี่ยวกับหัวข้อเดียวกันก็ตาม เราจะเพิ่ม ข้อมูลหรือปรับปรุงให้ดีขึ้นได้ไหม ได้แน่นอน:smile:

  1. เพิ่มคุณค่าให้ข้อมูลด้วยการสรุป ใช่แล้ว คุณอ่านไม่ผิด

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

ดังนั้น สำหรับตัวอย่างส่วนย่อยสองรายการข้างต้น เราจะสร้างบทสรุปประมาณนี้

  1. สถิติระยะ ความเร็ว และความเสียหายของการโจมตี (ค่าปกติและเมื่อใช้ Draconic Ascension)

  2. คำอธิบายและรายละเอียดของ Draconic Ascension รวมถึงเงื่อนไขการเปิดใช้ เอฟเฟกต์ภาพ และเนื้อเรื่องเบื้องหลัง

จากนั้นเรายังปรับคำค้นให้ “สอดคล้อง” กับบทสรุปด้วย ตัวอย่างเช่น เราจะเปลี่ยน “What is the attack range with Draconic Ascension?” เป็น “What is the statistics of attack range with Draconic Ascension?” เรื่องนี้สำคัญอย่างยิ่งเมื่อคำค้นมาจากผู้ใช้นอกสายเทคนิคซึ่งถามด้วยภาษา ~~“ตามใจฉัน”~~ ที่คนทั่วไปใช้ เพราะท้ายที่สุดแล้ว พวกเขาไม่จำเป็นต้องรู้หรือใส่ใจว่า RAG ทำงานอย่างไรเพื่อเพิ่ม Precision/Recall ให้สูงสุด

แผนภาพอธิบายสถานการณ์เมื่อสิ่งต่างๆ ซับซ้อนขึ้น

  1. อย่าตัดสิ่งใดออกจากบริบท (หลักนี้ใช้ได้กับชีวิตโดยทั่วไป)

สถานการณ์ถัดมาคือการรับมือกับส่วนย่อยของข้อมูลจำนวนมากที่ดูเหมือนกัน ดังตัวอย่างต่อไปนี้

Plain Text

# Chunk one{ "Attack": { "Range": { "default": 9, }, ... }}# Chunk two{ "Attack": { "Range": { "default": 6, }, ... }}# Chunk three{ "Attack": { "Range": { "default": 7, }, ... }}

หากยังใช้แนวทางเดิม ลองนึกภาพว่าถามว่า “what is character X’s attack range?” การเลือกจากบทสรุปที่เพิ่งสร้างจะไม่ต่างจากเกมเดาสุ่ม เพราะบทสรุปเหล่านั้นก็ดูคล้ายกันมาก แล้วเราจะแยกความแตกต่างได้อย่างไร

คำตอบง่ายๆ คือเพิ่มบริบท เราอาจเพิ่มข้อมูลอ้างอิงถึงเอกสารหลักของส่วนย่อยไว้ในส่วนย่อยนั้น เช่น {”character”: “X”} ในกรณีนี้ เท่านี้เราก็ดึงข้อมูลที่ถูกต้องของคาแรคเตอร์ X ได้อย่างแม่นยำ แม้จะมีข้อมูลแบบเดียวกันของคาแรคเตอร์ Y และ Z อยู่ด้วย

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

  1. ส่วนย่อยนี้ให้สถิติโดยละเอียดเกี่ยวกับ... ของคาแรคเตอร์ X ส่วนย่อยนี้เชื่อมโยงกับเอกสารฉบับเต็มโดยแสดงจุดแข็งด้านความเร็วโจมตีของ X...

  2. ส่วนย่อยนี้ให้สถิติโดยละเอียดเกี่ยวกับ... ของคาแรคเตอร์ Y ส่วนย่อยนี้เชื่อมโยงกับเอกสารฉบับเต็มโดยแสดงค่าสถานะที่เพิ่มขึ้นของ Y เมื่อใช้ความสามารถพิเศษ...

  3. ส่วนย่อยนี้ให้สถิติโดยละเอียดเกี่ยวกับ... ของคาแรคเตอร์ Z ส่วนย่อยนี้เชื่อมโยงกับเอกสารฉบับเต็มโดยแสดงค่าสถานะของ Z ซึ่งเหมาะอย่างยิ่งกับบทบาทแทงก์ในการแข่งขัน แบบทีม...

วิธีนี้ (ซึ่งได้แรงบันดาลใจบางส่วนจาก Anthropic) อาจดูเกินความจำเป็นสำหรับตัวอย่างข้างต้น แต่มีประสิทธิภาพมากสำหรับส่วนย่อยที่อาจถูกตีความผิดเมื่อ “ขาดบริบท” อีกทั้งยังเป็นแนวทางเดียวที่ใช้ได้กับทุกส่วนย่อย ทำให้กระบวนการทางวิศวกรรมเป็นระเบียบ

แผนภาพอธิบายสถานการณ์เมื่อสิ่งต่างๆ ซับซ้อนขึ้น

  1. เมื่อคุณต้อง ~~ควบคุมทุกอย่าง~~ ทำงานอย่างเคร่งครัด

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

แผนภาพอธิบายสถานการณ์เมื่อสิ่งต่างๆ ซับซ้อนขึ้น

ความพยายามครั้งแรกของเราคือส่งข้อมูลทั้งหมดให้ LLM ขอให้จัดกลุ่มตามที่เห็นว่าเหมาะสม แล้วส่งคืนเนื้อหาที่จัดกลุ่มแล้ว LLM น่าจะทำเรื่องนี้ได้ดีมาก ใช่ไหม ทั้งใช่และไม่ใช่

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

ส่วนที่ตอบว่า “ใช่” ก็คือ LLM เข้าใจความหมายและโครงสร้างของส่วนย่อยที่แตกออกจากกันได้ดีเยี่ยม ถ้ามันยอมส่งเนื้อหาที่ตรงตามต้นฉบับกลับมาน่ะนะ ให้ตายสิ:/

แล้วเราจะใช้ประโยชน์จากสิ่งที่ LLM ถนัด พร้อมหลีกเลี่ยงสิ่งที่มันทำได้ไม่น่าเชื่อถืออย่างไร เราจึงหันไปหาเพื่อนเก่าแสนดีอย่างโค้ด (หรือก็คือฟังก์ชัน Python ที่เขียนขึ้นเอง) พร้อมโมเดล Pydantic ที่ “เรียบง่ายกว่านี้ไม่ได้แล้ว” โซลูชันมีดังนี้

  • ไล่ตรวจแต่ละส่วน โดยเก็บส่วนย่อยเชิงตรรกะปัจจุบันไว้

  • ในทุกส่วน ให้ถาม LLM ว่า “ส่วนนี้เป็นส่วนหนึ่งของส่วนย่อยเชิงตรรกะปัจจุบันหรือไม่” แล้วให้ตอบว่าใช่หรือไม่ใช่ (ตามโมเดล Pydantic)

  • หากใช่ ให้นำส่วนนั้นไปต่อกับส่วนย่อย หากไม่ใช่ ให้ส่งออกส่วนย่อยเชิงตรรกะปัจจุบันซึ่งสมบูรณ์แล้ว จากนั้นเริ่มส่วนย่อยใหม่ด้วยส่วนนั้น

แผนภาพอธิบายสถานการณ์เมื่อสิ่งต่างๆ ซับซ้อนขึ้น

แน่นอนว่าวิธีนี้ใช้โทเค็นมากกว่าการประมวลผลเนื้อหาทั้งหมดในครั้งเดียวเล็กน้อย แต่สำหรับกรณีที่การรักษาเนื้อหาให้ตรงตามต้นฉบับสำคัญที่สุด ค่าใช้จ่ายเพิ่มเติม (เพียงเล็กน้อย) นี้ถือว่าคุ้มค่า

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

เราสามารถใช้โค้ดหรือฟังก์ชันที่เขียนขึ้นเองร่วมกับโมเดล Pydantic เพื่อให้ได้ผลลัพธ์ที่คาดการณ์ได้และเชื่อถือได้ พร้อมดึงศักยภาพของ LLM ออกมาอย่างเต็มที่

สรุปส่งท้าย

การสร้างโซลูชัน Generative AI เป็นทั้งความท้าทายด้านวิศวกรรมและด้าน AI ในระดับพอๆ กัน เราหวังว่าตัวอย่างเหล่านี้จะสร้างแรงบันดาลใจให้คุณรับมือกับความท้าทายเฉพาะของตนเอง หากต้องการอ่านเพิ่มเติมเกี่ยวกับโซลูชัน Generative AI ที่ให้ความสำคัญกับวิศวกรรม โปรดดูบล็อกของเราเรื่อง การออกแบบระบบแบบเอเจนต์ที่ใช้เราเตอร์

ผู้เขียน

Cynthia Yu