การทำ RAG ในระดับ Production ให้รองรับสเกลใหญ่: บทเรียนจากการประมวลผลประกาศรับสมัครงานกว่า 10,000 รายการต่อวัน
ผมได้สร้าง RAG pipeline สำหรับเว็บไซต์ประกาศรับสมัครงาน มันทำงานได้ดีในขั้นตอน staging แต่กลับประสบปัญหาเมื่อต้องรับโหลดจริง การประมวลผลประกาศรับสมัครงานหลายพันรายการต่อวันนั้นต้องการอะไรที่มากกว่าแค่ vector store ที่ดี คุณจำเป็นต้องเข้าใจว่าระบบจะพังที่จุดไหน
และนี่คือบทเรียนของผมเกี่ยวกับเรื่อง chunking, embeddings, ต้นทุน และ observability
1. อย่าเดาสุ่มกลยุทธ์การทำ chunking
บทเรียนส่วนใหญ่มักมองว่า chunking เป็นเพียงการตั้งค่าธรรมดาๆ แต่ในระดับ production กลยุทธ์ของคุณจะเป็นตัวกำหนดทั้งความแม่นยำและต้นทุน
ผมได้ทดสอบ 3 วิธีสำหรับประกาศรับสมัครงาน:
- Fixed-size chunks: วิธีนี้ล้มเหลว เพราะมันตัดแบ่งส่วนต่างๆ เช่น "requirements" (คุณสมบัติ) และ "benefits" (สวัสดิการ) แบบสุ่ม ซึ่งทำให้การดึงข้อมูล (retrieval) เกิดสัญญาณรบกวน (noisy)
- Semantic chunking: วิธีนี้ดีขึ้นแต่ยังไม่สม่ำเสมอ บาง chunk ยาวเกินไป และบาง chunk ก็สั้นเกินไป
- Recursive character splitting with overlap: วิธีนี้ได้ผลดีที่สุด ผมใช้วิธีตัดแบ่งตามการขึ้นบรรทัดใหม่และประโยค โดยใช้ขนาด 400 tokens และมี overlap 50 tokens เพื่อให้แน่ใจว่าประโยคที่คาบเกี่ยวระหว่างสอง chunk จะยังคงเชื่อมต่อกันอยู่
Pro tip: ทำข้อมูลให้เป็นมาตรฐาน (Normalize) ก่อนทำ chunking แหล่งข้อมูลที่ต่างกันอย่าง Greenhouse หรือ Lever จะส่งข้อมูลกลับมาในรูปแบบที่ต่างกัน ควรทำความสะอาดข้อความก่อน เพื่อให้ตัว chunker เห็นโครงสร้างที่สม่ำเสมอ
2. Embeddings: ต้นทุน vs ความแม่นยำ
ผมได้ทดสอบ Llama 3.1 ผ่าน Ollama เปรียบเทียบกับ OpenAI text-embedding-3-small โมเดลแบบ local นั้นฟรี แต่มีปัญหาเมื่อเจอคำศัพท์เฉพาะทางอย่าง "equity compensation" ทำให้ผลลัพธ์ที่ได้มีความคลาดเคลื่อน (noisy) ส่วน OpenAI มีค่าใช้จ่ายสูงกว่าแต่ให้ผลลัพธ์ที่แม่นยำ ผมจึงเลือก OpenAI เพราะการดึงข้อมูลที่ผิดพลาดจะทำให้เสียค่าใช้จ่ายในการเรียกใช้ LLM มากขึ้นในภายหลัง
เพื่อประหยัดเวลา ผมใช้วิธีส่งคำขอแบบ batch โดยส่งสูงสุด 100 chunks ในการเรียกครั้งเดียว วิธีนี้ช่วยลด latency และทำให้ pipeline ทำงานได้รวดเร็ว
3. ข้อแลกเปลี่ยนในการเลือก Vector Store
ผมใช้ Pinecone ในช่วงทำ prototype เพราะตั้งค่าได้รวดเร็ว อย่างไรก็ตาม เมื่อขยายขนาด (scale) ต้นทุนก็สูงขึ้นมาก
ผมจึงเปลี่ยนมาใช้ pgvector ภายใน PostgreSQL แทน
- ต้องใช้ความพยายามในการตั้งค่ามากกว่า
- ช่วยประหยัดเงินได้มหาศาล
- ให้ความสม่ำเสมอของข้อมูลในระดับ transaction (transactional consistency)
เนื่องจาก embeddings อยู่ในฐานข้อมูลเดียวกับข้อมูลงาน คุณจึงมีแหล่งข้อมูลที่ถูกต้องเพียงแห่งเดียว (one source of truth) โดยไม่จำเป็นต้องซิงค์ข้อมูลระหว่างสองระบบที่ต่างกัน
4. การควบคุมต้นทุน LLM
การให้คะแนน (scoring) ทุกประกาศด้วย GPT-4o นั้นมีราคาแพง ผมจึงใช้ 3 กลยุทธ์เพื่อลดต้นทุน:
- OpenAI Batch API: ผมประมวลผลงานการให้คะแนนในช่วงข้ามคืน ซึ่งช่วยให้ได้รับส่วนลดจำนวนมาก
- Caching: ผมทำ cache ผลลัพธ์สำหรับโปรไฟล์ผู้สมัครที่ซ้ำกัน
- Model tiering: ผมใช้ GPT-4o-mini สำหรับตำแหน่งงานทั่วไปอย่าง "Sales Representative" และจะใช้ GPT-4o เฉพาะกับตำแหน่งงานเฉพาะทาง (niche roles) ที่ต้องการความแม่นยำสูงเท่านั้น
5. สร้าง Observability ก่อนเป็นอันดับแรก
ครั้งหนึ่ง pipeline ของผมเคยล้มเหลวโดยไม่มีการแจ้งเตือน (failed silently) ข้อมูลที่ผิดรูปแบบ (malformed data) ทำให้เกิด chunk ที่ว่างเปล่า ซึ่งระบบข้ามไปโดยไม่มีการแจ้งข้อผิดพลาดใดๆ
ผมแก้ไขปัญหานี้ด้วยการเพิ่ม structured logging พร้อมกับ correlation ID ซึ่งช่วยให้ผมสามารถติดตาม (trace) ประกาศงานหนึ่งรายการได้ตั้งแต่ขั้นตอนการนำเข้า (ingestion) ไปจนถึงการให้คะแนน (scoring) ในที่สุดผมก็สามารถระบุได้ว่าแหล่งข้อมูลใดที่เป็นต้นเหตุของความล้มเหลว
บทเรียนที่สำคัญที่สุดคือ: ปัญหาส่วนใหญ่มาจากข้อมูลที่ยุ่งเหยิง ไม่ใช่จาก AI จงจัดการกับระบบจัดการข้อมูล (data plumbing) ของคุณให้เรียบร้อยก่อน
Optional learning community: https://t.me/GyaanSetuAi
