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

ภาพประกอบ: 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):

  1. แสดง top-5 chunk (ตัดพรีวิวสั้นๆ พร้อมเลขกำกับ) ให้ LLM ดู
  2. ถาม LLM ตรงๆ ว่า “chunk ไหน 2-3 อันตอบคำถามนี้ได้ตรงที่สุด ตอบเป็นเลขอย่างเดียว”
  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 ที่คำนวณจากตัวเลขจริงแก้จุดสาม ไม่มีจุดไหนฝากไว้กับ “หวังว่าโมเดลจะฉลาดพอ”

อ่านเพิ่มเติม

ความคิดเห็น

กำลังโหลดความคิดเห็น...