ENDEAVOR RAG ทำงานอย่างไร — เจาะลึกสถาปัตยกรรม

บทความ เริ่มต้นใช้งาน บอกวิธีติดตั้งและใช้งาน บทความนี้เจาะลึกว่าข้างในทำงานอย่างไร — RAG ทั่วไปมักพังเวลาถามภาษาไทยแบบเป็นธรรมชาติ (คำถามสั้น มีคำเชื่อม ตัดคำยาก) ENDEAVOR RAG แก้ปัญหานี้ด้วยการไม่ค้นแค่ครั้งเดียวด้วย query เดียว แต่แตกคำถามเป็นหลายเวอร์ชันแล้วรวมผลลัพธ์
คำถาม
│
▼ ขั้น 1: แตกเป็น Q1/Q2/Q3 (บริบท / แปลภาษา / keyword)
│
▼ ขั้น 2: แต่ละ query ค้นด้วย dense + BM25 → รวมด้วย RRF
│
▼ ขั้น 3: LLM rerank เลือก chunk ที่ตรงคำถามที่สุด
│
▼ Quality Gate: ประเมิน High/Medium/Low ก่อนสรุปคำตอบ
│
▼
คำตอบ (+ prefix [low_quality] ถ้าคะแนนต่ำ)
ขั้นที่ 1: Query Expansion — แตกคำถามเป็น 3 แบบ
expander.py สร้าง query variant ได้สูงสุด 3 ตัว จากคำถามเดียว:
- Q1 — history-contextualized original: ใช้คำถามเดิม แต่เติมบริบทจากประวัติสนทนา เฉพาะกรณีที่คำถามสั้นหรือขึ้นต้นด้วยคำเชื่อม (เช่น “แล้ว…”, “ส่วน…”) ซึ่งบ่งบอกว่าคำถามนี้พึ่งพาบริบทก่อนหน้า ถ้าคำถามสมบูรณ์ในตัวเองอยู่แล้วจะไม่สร้าง Q1 ซ้ำ
- Q2 — cross-language translation: ให้ LLM แปลคำถามข้ามภาษา (ไทย↔อังกฤษ) เพราะเอกสารในคลังอาจเก็บเป็นคนละภาษากับที่ผู้ใช้ถาม
- Q3 — keyword extraction: ใช้
pythainlp.pos_tagตัดคำและติด part-of-speech แล้วเก็บเฉพาะคำที่เป็นเนื้อหาจริง (คำนาม คำกริยา ฯลฯ) ตัดคำเชื่อม/คำฟุ่มเฟือยทิ้ง เหลือแต่ keyword ที่มีน้ำหนักความหมาย
ทั้ง 3 query variant นี้ไม่ได้เลือกใช้แค่ตัวเดียว — ทุกตัวถูกนำไปค้นจริงแล้วรวมผล ในขั้นถัดไป
ขั้นที่ 2: Hybrid Retrieval — dense + BM25 ต่อทุก query variant
retriever.py เอา query แต่ละตัว (Q1, Q2, Q3) ไปค้นผ่านสองวิธีคู่ขนาน:
- Dense search — ใช้ embedding model
paraphrase-multilingual-MiniLM-L12-v2เทียบความใกล้เชิงความหมาย - BM25 (lexical) — ค้นแบบตรงคำ หลังตัดคำไทยด้วย
pythainlp
ผลลัพธ์จากทุกคู่ (query variant × วิธีค้น) ถูกรวมด้วย Reciprocal Rank Fusion (RRF):
score += 1 / (RRF_K + rank + 1) โดย RRF_K = 60
ค่าคงที่สำคัญ: TOP_K = 10 (ดึงมาต่อรายการค้นหนึ่งครั้ง), TOP_FUSED = 5 (จำนวนผลลัพธ์สุดท้ายหลังรวมคะแนน) เอกสารที่ถูกทั้ง dense และ BM25 เจอพร้อมกัน (คนละ query variant ก็นับ) จะถูกติด tag retriever: "hybrid" เพื่อบอกว่ามีหลักฐานยืนยันจากสองมุมมองที่ต่างกัน — โดยทั่วไปมีความน่าเชื่อถือสูงกว่าเจอจากวิธีเดียว
หลังรวมคะแนนแล้ว fetch_parents() ดึง context ระดับ “parent” (ข้อความก่อน/หลัง chunk ที่แมตช์) และdedupe ด้วยคู่ (source, parent_text) กันไม่ให้ context เดียวกันถูกนับซ้ำเพราะมาจากหลาย query variant
Thai-aware chunking: ไม่ตัดประโยคกลางคำ
chunker.py แบ่งเอกสารตามลำดับความสำคัญ ย่อหน้า → บรรทัด → คำ ไม่ใช่ตัดตามจำนวนตัวอักษรตรงๆ แบบ chunker ทั่วไป โดยใช้ pythainlp.word_tokenize(engine="newmm") ตัดคำไทยก่อนนับความยาว chunk — ป้องกันปัญหาคลาสสิกของ RAG ภาษาไทยที่ chunk มักตัดกลางคำเพราะนับความยาวแบบอังกฤษ (เว้นวรรคระหว่างคำ) ซึ่งภาษาไทยไม่มี มีoverlap ระหว่าง chunk ที่ติดกันเพื่อไม่ให้บริบทขาดตรงรอยต่อ และมีขั้นตอนล้าง timestamp ที่หลงเหลือจากการแปลงไฟล์บางประเภทออกก่อนเก็บ
ขั้นที่ 3: LLM Rerank — ให้โมเดลเลือกจาก candidate ที่ RRF จัดอันดับมาแล้ว
rag_search.py (tool หลักที่ agent เรียกใช้จริง) หลังได้ chunk ที่ผ่าน RRF fusion มาแล้ว ไม่ส่งตรงให้ LLM สรุปคำตอบทันที แต่ให้ LLM คัดอีกชั้นก่อน (_rerank):
- แสดง top-5 chunk (ตัดพรีวิวสั้นๆ พร้อมเลขกำกับ) ให้ LLM ดู
- ถาม LLM ตรงๆ ว่า “chunk ไหน 2-3 อันตอบคำถามนี้ได้ตรงที่สุด ตอบเป็นเลขอย่างเดียว”
- ถ้า parse คำตอบไม่ได้ (LLM ตอบไม่ตรงฟอร์แมต) — fallback เป็น top-3 ตามลำดับ RRF เดิม ไม่ค้าง ไม่ error
ขั้นนี้ช่วยกรอง chunk ที่ผ่าน retrieval มาได้เพราะ “คล้ายกันเชิง embedding” แต่จริงๆ ไม่ตรงคำถามออกไป
Quality Gate: ระบบรู้ตัวเองว่า “ไม่มั่นใจ”
_compute_quality ประเมินคุณภาพผลการค้นเป็น 3 ระดับ ก่อนส่งให้โมเดลสรุปคำตอบ:
| ระดับ | เงื่อนไข (ตัวอย่าง) |
|---|---|
| High | dense score สูงสุด > 0.78 และ จำนวนผลลัพธ์ ≥ 3 และ มี lexical hit ยืนยัน |
| Medium | เข้าเงื่อนไขบางส่วน |
| Low | คะแนนต่ำ หรือผลลัพธ์น้อยเกินไป |
ถ้าคุณภาพออกมา low ระบบจะแปะ prefix [low_quality] ไว้ที่ผลลัพธ์ — เป็นสัญญาณให้ agent ที่เรียกเครื่องมือนี้รู้ว่าควรลองถามใหม่ด้วยคำที่ต่างออกไป แทนที่จะสรุปคำตอบจาก context ที่ไม่น่าเชื่อถือราวกับมั่นใจเต็มร้อย — เป็นหลักการเดียวกับ AGENT LITE ที่ไม่ให้โมเดลตัดสินใจว่าข้อมูลน่าเชื่อถือแค่ไหนด้วยตัวเอง แต่คำนวณเป็นเกณฑ์ตายตัวจากตัวเลขจริง
เครื่องมืออื่นในชุดเดียวกัน
นอกจาก rag_search ยังมี:
list_knowledge()— สุ่มตัวอย่างไฟล์ในคลัง แล้วให้ LLM สรุปเป็นชุด (batch) ปรับจำนวนรอบสรุปตามจำนวนไฟล์ทั้งหมด ใช้ตอนผู้ใช้ถามกว้างๆ ว่า “ในคลังมีอะไรบ้าง”search_files()/read_file()— ค้นและอ่านไฟล์ตรงๆ โดยไม่ผ่าน retrieval pipeline เต็มรูปแบบ ใช้เมื่อรู้ชื่อไฟล์อยู่แล้วsave_memory()— บันทึกข้อมูลสั้นๆ ข้าม session
ทำไมเรื่องนี้ถึงสำคัญ
RAG ส่วนใหญ่ที่พังกับภาษาไทยพังเพราะ 3 จุด: ตัดคำผิด (chunk กลางคำ), ค้นแบบเดียว (query เดียว วิธีเดียว), และไม่รู้ตัวเองว่าตอบมั่วหรือตอบจริง ENDEAVOR RAG แก้ทั้ง 3 จุดด้วยกลไก deterministic ที่ตรวจสอบได้ — Thai-aware chunker แก้จุดแรก, hybrid multi-query + RRF แก้จุดสอง, quality gate ที่คำนวณจากตัวเลขจริงแก้จุดสาม ไม่มีจุดไหนฝากไว้กับ “หวังว่าโมเดลจะฉลาดพอ”
ความคิดเห็น
กำลังโหลดความคิดเห็น...