ต้นแบบ RAG ส่วนใหญ่มักจะมีกลไกภายในที่ดูคล้ายกัน เริ่มจากการนำ PDF ใส่เข้าไปใน pipeline แบ่งข้อความเป็น chunk ขนาด 512 โทเคนอย่างเป็นระเบียบ แล้วโยนพวกมันลงใน vector database จากนั้นก็ถือว่างานเสร็จสิ้น สำหรับการสาธิตแบบคร่าวๆ (hallway demo) วิธีนี้อาจดูน่าประทับใจ แต่เมื่อนำไปใช้งานจริงในระบบ Production มันจะพังทลายลง
การแบ่ง chunk แบบคงที่ไม่ได้สนใจว่ามันจะตัดตรงไหน มันอาจจะตัดแบ่งสัญญาทางกฎหมายตรงกลางข้อกำหนดการชดใช้ค่าเสียหาย (indemnity clause) พอดี หรืออาจจะยัด API endpoint ที่ไม่เกี่ยวข้องกันห้าตัวเข้าไปใน context window เดียวกัน จนทำให้โมเดลจมกองข้อมูลขยะ (noise) นอกจากนี้ มันยังบังคับให้คุณต้องดึงเศษเสี้ยวข้อมูลออกมามากกว่าที่จำเป็น ซึ่งเป็นการเพิ่มความหน่วง (latency) และสิ้นเปลืองโทเคน ผลลัพธ์ที่ได้คือคำตอบที่ไม่สมบูรณ์, อาการหลอน (hallucinations) และผู้ใช้ที่หงุดหงิด
เราได้รื้อระบบการดึงข้อมูล (retrieval layer) ของเราลงมาจนถึงฐานรากแล้วสร้างมันขึ้นมาใหม่ ผลลัพธ์ที่ได้คือระบบที่ทำค่า recall ได้ถึง 95 เปอร์เซ็นต์ ในขณะที่ลดความหน่วงลงได้ถึง 40 เปอร์เซ็นต์ และนี่คือวิธีที่เราทำอย่างละเอียด
ทำไมการแบ่ง Chunk แบบคงที่ถึงไปไม่รอดใน Production
ค่าเริ่มต้นที่ 512 โทเคนไม่ใช่ทางเลือกในการออกแบบ แต่มันเป็นผลพลอยได้จากขนาด context window ของโมเดล embedding ยุคแรกๆ และค่าเริ่มต้นของไลบรารีต่างๆ มันทำได้ง่ายในการนำไปใช้ แต่การฝากความหวังไว้กับมันนั้นเป็นเรื่องที่อันตรายอย่างยิ่ง
เอกสารไม่ได้มีรูปแบบที่สม่ำเสมอ ข้อกำหนดทางกฎหมายหนึ่งข้ออาจมีความยาวถึงเจ็ดร้อยโทเคนโดยไม่มีจุดตัดที่ชัดเจน หากคุณตัดที่ห้าร้อยสิบสองโทเคน คุณจะสร้างเศษเสี้ยวข้อมูลที่ขาดออกจากกัน (orphaned fragments) ขึ้นมาสองชิ้น เมื่อทนายความหรือเจ้าหน้าที่ตรวจสอบการปฏิบัติตามกฎระเบียบถามเกี่ยวกับขีดจำกัดความรับผิดชอบ (liability caps) ระบบกลับคืนค่าพันธะผูกพันมาให้เพียงครึ่งเดียว โมเดลภาษาจะสร้างข้อมูลที่ขาดหายไปขึ้นมาเอง (hallucinate) หรือที่แย่กว่านั้นคือปฏิเสธว่าขีดจำกัดนั้นไม่มีอยู่จริง
เอกสาร API ก็ประสบปัญหาในทางตรงกันข้าม การแบ่ง chunk ขนาดห้าร้อยโทเคนอาจกลืนกินโมดูลทั้งโมดูลเข้าไป ทั้ง authentication headers, error codes, rate limits และ webhook schemas เมื่อนักพัฒนาถามถึงวิธีจัดการกับ AUTH_4027 ตัวดึงข้อมูล (retriever) กลับนำเสนอชุดฟังก์ชันที่ไม่เกี่ยวข้องกันมาให้ โมเดลจึงไม่มีทางเลือกอื่นนอกจากต้องนำข้อมูลเหล่านั้นมาเฉลี่ยรวมกันจนกลายเป็นข้อมูลที่ไร้ความหมาย
การแบ่ง chunk ที่ไม่ดีนอกจากจะทำให้ข้อมูลผิดพลาดแล้ว ยังเพิ่มความหน่วงอีกด้วย เศษเสี้ยวข้อมูลที่อ่อนแอหมายความว่าคุณต้องใช้ค่า top-k ที่ใหญ่ขึ้นเพื่อให้ครอบคลุมหัวข้อหนึ่งๆ การมี chunk มากขึ้นหมายถึง prompt ที่ยาวขึ้น และ prompt ที่ยาวขึ้นหมายถึงการสร้างคำตอบที่ช้าลงและค่าใช้จ่ายที่สูงขึ้น ประสบการณ์ของผู้ใช้จึงค่อยๆ พังทลายลงทีละน้อย
ปรับขนาด Chunk ให้เหมาะสมกับเอกสาร
เราเลิกนับโทเคนและเริ่มหันมาอ่านเนื้อหาแทน กลยุทธ์การแบ่ง chunk ที่ถูกต้องขึ้นอยู่กับโครงสร้างของแหล่งข้อมูลต้นฉบับ
เอกสารทางกฎหมาย (Legal documents) จำเป็นต้องใช้การแบ่งแบบ recursive character chunking ที่มีขอบเขตแบบ clause-aware ตัวแบ่ง (splitter) จะต้องเคารพลำดับชั้นของข้อมูล โดยมองหาหัวข้อส่วน (section headers) ก่อน ตามด้วยย่อหน้าที่เป็นตัวเลข และจุดตัดประโยคตามธรรมชาติ มันจะไม่ตัดแบ่งข้อกำหนดรอง (sub-clause) หรือแยกวลีที่เป็นพันธะผูกพันออกจากกัน เมื่อคุณดึงข้อความเกี่ยวกับการชดใช้ค่าเสียหาย คุณจะได้ทั้งข้อกำหนดฉบับเต็ม, ขีดจำกัดความรับผิดชอบ และข้อยกเว้นทั้งหมด
เอกสาร API (API documentation) ต้องการการแบ่งแบบ structure-aware เราจะทำการ parse ตามการนิยามฟังก์ชัน (function definition) แทนที่จะใช้โควตาโทเคน แต่ละ chunk จะประกอบด้วย function signature ที่สมบูรณ์, คำอธิบายพารามิเตอร์ และหมายเหตุการจัดการข้อผิดพลาดที่อยู่ติดกัน หากนักพัฒนาค้นหาวิธีการ (method) เฉพาะเจาะจง พวกเขาจะได้รับสัญญา (contract) ทั้งหมด ไม่ใช่แค่เศษเสี้ยวที่ถูกตัดแบ่งอย่างสุ่มๆ
ตั๋วสนับสนุนลูกค้า (Support tickets) มักจะมีข้อมูลขยะและไม่เป็นเส้นตรง เธรดหนึ่งอาจเริ่มด้วยการรายงานบั๊ก แนะนำวิธีแก้ไขชั่วคราว และจบด้วยบันทึกการส่งต่อเรื่องภายใน การแบ่งแบบ semantic chunking จะตรวจจับการเปลี่ยนหัวข้อโดยการวัดความคล้ายคลึงของ embedding ระหว่างประโยค เราจะอนุญาตให้มีการตัดแบ่งเฉพาะที่ขอบเขตของเนื้อหาตามธรรมชาติเท่านั้น เพื่อให้การสนทนาเรื่องความล้มเหลวในการเข้าสู่ระบบแยกออกจากเรื่องการติดตามรอบการเรียกเก็บเงิน
วิกิ (Wikis) เป็นส่วนที่ยากที่สุด เพราะมีเนื้อหาที่แผ่ขยาย มีการเชื่อมโยงกัน และจัดระเบียบอย่างหลวมๆ เราใช้การแบ่งแบบ agentic chunking โดยใช้ LLM ขนาดเล็กอ่านหน้าเว็บและตัดสินใจจุดตัดตามความสอดคล้องของเนื้อหา (thematic coherence) แม้ว่ามันจะมีต้นทุนสูงกว่าเล็กน้อยในช่วงการนำข้อมูลเข้า (ingestion time) แต่ chunk ที่ได้จะมีความสมบูรณ์ในตัวเองและพร้อมสำหรับการดึงข้อมูล หน้าเว็บเกี่ยวกับแนวทางปฏิบัติที่ดีที่สุดในการติดตั้ง (deployment best practices) จะถูกแบ่งออกเป็นหน่วยที่สมเหตุสมผล เช่น การตรวจสอบก่อนเริ่มงาน (pre-flight checks), ขั้นตอนการย้อนกลับ (rollback procedures) และการตั้งค่าการตรวจสอบ (monitoring setup) แทนที่จะเป็นบล็อกข้อความที่ถูกตัดแบ่งตามอำเภอใจ
การค้นหาแบบไฮบริด (Hybrid Retrieval): การผสาน Keyword และ Vector เข้าด้วยกัน
การค้นหาแบบ Dense vector search เข้าใจความหมาย แต่มันแย่มากในการค้นหาข้อความที่ตรงตัวเป๊ะๆ หากผู้ใช้ค้นหารหัสข้อผิดพลาดที่เฉพาะเจาะจงอย่าง AUTH_4027 หรือชื่อลูกค้าอย่าง "Stark Industries" ตัว vector embeddings อาจพลาดเป้าหมายได้ เพราะมันถูกปรับแต่งมาเพื่อหาความใกล้เคียงเชิงแนวคิด (conceptual proximity) ไม่ใช่ความแม่นยำในระดับตัวอักษร
ในทางกลับกัน การค้นหาด้วย keyword บริสุทธิ์ผ่าน BM25 ก็มีข้อบกพร่องในทิศทางตรงกันข้าม มันจะหา AUTH_4027 ได้อย่างสมบูรณ์แบบ แต่จะพลาดจุดเชื่อมโยงเชิงแนวคิดระหว่าง "authorization failure" และ "login denied"
We run both in parallel. BM25 and vector search operate independently over the same corpus. Their result lists are merged using Reciprocal Rank Fusion, which reorders candidates by balancing their positional ranks. You do not need calibrated weights. You simply get the precision of exact match and the intuition of semantic search in a single ranked list.
Then we add a cross-encoder reranker. This is a separate model that scores each passage against the original query, producing a relevance signal far finer than either retriever alone. It adds about 50 milliseconds of latency. It increases recall by 15 percent. If you care about answer quality, that trade is non-negotiable.
Query Expansion: Fix the Search Before It Starts
Bad queries are the dirty secret of every retrieval system. Users do not write like your embedding space. They type "it broke." They paste truncated stack traces. They use internal jargon your index has never seen.
We transform the query before it ever touches the index. First, we expand a single query into three to five diverse search terms. If the original is "payment failed," we also search for "transaction error," "billing declined," and "charge unsuccessful
