Journal Building the Knowledge Index
เปลี่ยนกองเอกสารให้กลายเป็นคำตอบ
ระบบ AI ตอบจากเอกสารจริงได้ ต้องค้น (retrieve) ให้เจอก่อน — และจะค้นได้ต้องมี index ก่อน · บทนี้พาดูฝั่งเขียนของ RAG — เปลี่ยนเอกสารดิบจำนวนมากให้กลายเป็นคลังที่ค้นได้ ผ่าน extract → chunk → embed → store · ทำไมต้องตัด chunk · embed อะไรไม่ embed อะไร · recall แล้ว rerank ทำงานคู่กันยังไง · provenance ที่ทำให้คำตอบอ้างกลับได้ · และอะไรพังเมื่อเอกสารโตเป็นแสน บทที่ 6 ของ LLM systems series · คู่ฝั่งเขียนของบทที่ 5
ลองนึกถึงระบบที่พนักงานพิมพ์คำถามเป็นภาษาคน เช่น “เครื่องรุ่นนี้ตั้งแรงบิดเท่าไหร่” แล้ว AI ตอบให้ได้ โดยอ้างอิงจากคู่มือจริงที่หนาเป็นพันหน้า
ระบบแบบนี้ทำงานสองจังหวะเสมอ
จังหวะแรกคือ ค้น (retrieve) ระบบไปไล่หาในคู่มือ แล้วดึงเฉพาะท่อนที่เกี่ยวกับคำถามขึ้นมา จังหวะที่สองคือ เรียบเรียง ระบบส่งท่อนเหล่านั้นให้ LLM ช่วยเขียนเป็นคำตอบ สังเกตว่า LLM ตอบจากท่อนที่ค้นเจอ ไม่ใช่จากความจำของตัวเอง เทคนิคนี้เรียกรวม ๆ ว่า RAG
แต่ระบบจะค้นได้ ก็ต้องมีอะไรให้ค้นก่อน และกอง PDF ดิบ ๆ นั้นค้นตรง ๆ ไม่ได้ เราต้องแปลงมันให้เป็น index ที่ค้นได้ เสียก่อน
งานแปลงเอกสารให้เป็น index นี้ เราเรียกว่า ฝั่งเขียน ส่วนการถาม–ค้น–ตอบ เราเรียกว่า ฝั่งอ่าน บทที่ 5 เล่าฝั่งอ่านไปแล้ว บทนี้จะเล่าฝั่งเขียน ซึ่งเป็นงานเงียบ ๆ ที่เกิดขึ้นก่อน และทำเสร็จตั้งแต่ก่อนที่ผู้ใช้จะพิมพ์คำถามแรกด้วยซ้ำ
ขอบเขต — บทที่ 6 ของ LLM systems series · คู่ฝั่งเขียนของ บทที่ 5 · ต่อยอดจาก data layer ใน บทที่ 4 · ส่วนชั้นความทรงจำข้ามบทสนทนา แยกเล่าในบทถัดไป
เอกสารดิบยังไม่ใช่ข้อมูลที่ค้นได้
PDF หนึ่งไฟล์ดูเผิน ๆ เหมือนเป็นข้อมูลที่พร้อมใช้ เพราะคำตอบก็อยู่ในนั้นจริง ๆ แต่สำหรับเครื่องแล้ว มันคือกล่องทึบ ข้างในมีแต่เลย์เอาต์กับตัวอักษร ไม่มี key ให้ค้น ต่อให้ “หน้า 128 มี gear ratio” เป็นความจริง เครื่องก็เข้าไม่ถึงความจริงนั้น จนกว่าเราจะแปลงมันให้อยู่ในรูปที่ค้นได้
ระหว่าง “ไฟล์มีคำตอบ” กับ “ระบบหาคำตอบเจอ” จึงมีช่องว่างอยู่ และฝั่งเขียนคือส่วนที่ปิดช่องว่างนี้ งานหนักทั้งหมดอยู่ตอนต้นนี่เอง ถ้าทำดีตอนสร้าง index ฝั่งอ่านก็ทำงานง่าย แต่ถ้าทำพลาดตรงนี้ จะไปแก้ตอนค้นยังไงก็ไม่คืน
ฝั่งเขียนมีสี่ขั้น ทุกขั้นทำครั้งเดียวตอนเอกสารเข้า ไม่ใช่ตอนถาม
พูดอีกอย่างคือ เราจ่ายค่าของการ “เข้าใจเอกสาร” ไปล่วงหน้าทั้งหมดตรงนี้ เพื่อให้ตอนถามเหลืองานแค่เทียบเวกเตอร์ ทีนี้มาดูทีละขั้น
ขั้น 1 · แตกข้อความ
ขั้นแรกคือดึงข้อความออกจาก PDF แต่เป้าหมายไม่ใช่แค่ได้ข้อความ เราต้องได้ข้อความ พร้อมที่มา ด้วย คือรู้ว่าข้อความนี้มาจากเอกสารไหน หน้าไหน
ป้ายบอกที่มานี้มีศัพท์เรียกว่า provenance แปลตรงตัวว่า “ที่มา” คำนี้ยืมมาจากวงการศิลปะ ที่ใช้มันพิสูจน์ว่างานชิ้นหนึ่งมีต้นทางจากไหน เราจะติด provenance ไว้กับข้อความทุกชิ้น แล้วถือมันไปจนจบกระบวนการ ส่วนเหตุผลว่าทำไมต้องเก็บ เดี๋ยวจะเห็นชัดตอนท้ายบท
ก่อนจะดึงข้อความได้ ต้องรู้ก่อนว่า PDF ที่เจอเป็นชนิดไหน เพราะแต่ละชนิดดึงไม่เหมือนกัน
- text-based — ข้อความฝังอยู่ในไฟล์จริง ดึงออกมาตรง ๆ ได้เลย เร็วและแม่น PDF ส่วนใหญ่เป็นแบบนี้
- encrypted — ข้อความมีอยู่ แต่ถูกล็อกไว้ (หลายเล่มเข้ารหัสด้วยรหัสผ่านว่าง) พอปลดล็อกก็ดึงได้ตามปกติ แต่ระบบต้องรู้ว่าควรลองปลดล็อกก่อน
- image-only — เป็นสแกนหรือไดอะแกรม ทั้งหน้าเป็นภาพ ไม่มีตัวอักษรให้ดึง ต้องส่งเข้า OCR ก่อน ไม่งั้นก็ต้องยอมรับว่าหน้านี้ค้นด้วยข้อความไม่ได้
มีกับดักหนึ่งที่เจอบ่อย เวลาไฟล์ดึงข้อความไม่ออก มันมักกลายเป็นหน้าเปล่าแบบเงียบ ๆ คือเอกสารขึ้นสถานะว่า “เสร็จ” แต่พอค้นจริงกลับไม่เจออะไร เพราะข้างในว่างเปล่า ฝั่งเขียนจึงต้องทำให้ความล้มเหลวแบบนี้เห็นได้ชัด ด้วยการแยกสถานะ failed ออกจาก indexed ไม่เหมารวมทุกอย่างว่า “เสร็จ”
ขั้น 2 · ตัดเป็น chunk
พอได้ข้อความมาแล้ว คำถามถัดมาคือ ทำไมเราไม่แปลงทั้งหน้าให้เป็นเวกเตอร์ก้อนเดียวไปเลย
เหตุผลคือ เวกเตอร์หนึ่งตัวเก็บใจความได้แค่เรื่องเดียว ลองนึกว่าหน้านั้นพูดทั้งวิธีติดตั้ง วิธีดูแล และค่าความปลอดภัยปนกัน ถ้าเรายัดทั้งหมดลงเวกเตอร์ตัวเดียว มันจะกลายเป็นค่าเฉลี่ยที่ไม่ตรงกับคำถามไหนเลย
ทางแก้คือตัดข้อความเป็นท่อนเล็ก ๆ ให้แต่ละท่อนพูดเรื่องเดียว พอมีคนถามเรื่องความปลอดภัย ระบบก็ไปเจอท่อนที่พูดเรื่องนั้นพอดี
แต่ตัดเล็กเกินไปก็มีปัญหาอีกแบบ คือประโยคขยายความอาจหลุดออกจากสิ่งที่มันขยาย ทางสายกลางคือใช้ หน้าต่างซ้อนเหลื่อม เราตัดเป็นท่อนขนาดพอเหมาะ แล้วให้ปลายของแต่ละท่อนเหลื่อมเข้าไปในท่อนถัดไปนิดหน่อย วิธีนี้ทำให้ประโยคที่คร่อมรอยตัด ยังอยู่ครบในท่อนใดท่อนหนึ่งเสมอ
และเส้นด้าย provenance จากขั้นที่แล้วก็เดินต่อมาที่นี่ ทุก chunk จะติด (เอกสาร, หน้า, ลำดับ) ไว้ด้วย ถึงเราจะตัดข้อความออกมาเป็นท่อน แต่ที่มาของมันไม่หายไปไหน
ขั้น 3 · embed — และของที่ไม่ควร embed
ขั้นนี้เราแปลงแต่ละ chunk ให้เป็นเวกเตอร์ ด้วยสิ่งที่เรียกว่า embedding model
embedding model มีข้อผูกมัดอย่างหนึ่ง คือเลือกตัวไหนแล้วต้องใช้ตัวนั้นยาว ๆ เพราะตอนผู้ใช้ค้น คำถามก็ต้องถูกแปลงเป็นเวกเตอร์ด้วยโมเดลตัวเดียวกัน เวกเตอร์ถึงจะเทียบกันได้ ถ้าวันหนึ่งอยากเปลี่ยน embedding เราต้อง re-index คลังใหม่ทั้งหมด การเลือก embedding จึงเป็นการตัดสินใจที่เปลี่ยนทีหลังแพง
แต่มีคำถามที่สำคัญกว่า “จะใช้ embedding ตัวไหน” นั่นคือ “ข้อมูลอันไหนควร embed และอันไหนไม่ควร” หลักง่าย ๆ คือ ข้อมูลที่มี key ชัดเจนอยู่แล้ว ไม่ต้องเอาไป embed
| รูปข้อมูล | ปลายทาง | เพราะ |
|---|---|---|
| ร้อยแก้ว (คำอธิบาย ขั้นตอน นโยบาย) | chunk + embed → vector | คำตอบคือความหมาย |
| ตาราง (สเปก ราคา รหัสอะไหล่) | fact-store / SQL | คำตอบคือค่าเป๊ะ มี key |
| เมตาเอกสาร (มีกี่เล่ม กี่หน้า) | query ตารางเอกสาร | นับ/ลิสต์ ไม่ใช่ค้นเนื้อใน |
ยกตัวอย่างให้เห็นภาพ ถ้าเราเอา “รหัส 448” ไป vector search เราจะได้ของที่ใกล้เคียงแต่ผิด เพราะ vector ออกแบบมาจับความหมาย ไม่ได้จับค่าเป๊ะ ๆ ด้วยเหตุนี้ คลังความรู้ที่ดีจึงไม่ใช่ vector store ก้อนเดียว แต่เป็นการแยกข้อมูลไปตามรูปของมันตั้งแต่ตอนเขียน ข้อมูลแต่ละแบบจะได้ไปอยู่ในที่ที่ค้นมันได้ดีที่สุด เอกสารที่มีทั้งร้อยแก้วและตาราง ก็จะถูกแยกออกเป็นสองทางตั้งแต่ขั้นนำเข้า
ขั้น 4 · เก็บลง index
เมื่อได้เวกเตอร์แล้ว เราเก็บมันลง store พร้อมกับสร้าง HNSW index ทับไว้ HNSW คือโครงสร้างที่ช่วยให้ค้น “เพื่อนบ้านที่ใกล้ที่สุด” ได้เร็ว โดยไม่ต้องเอาคำถามไปเทียบกับเวกเตอร์ทุกตัวในคลัง
ตรงนี้มีสองอย่างที่ต้องทำให้ถูกตั้งแต่แรก
- re-index ซ้ำได้ — เวลานำเอกสารเดิมเข้ามาใหม่ ต้องลบ chunk เก่าของมันทิ้งก่อนเสมอ ไม่งั้นคลังจะมีของซ้ำที่ขัดกันเอง
- dedup ด้วย hash — ถ้าผู้ใช้อัปโหลดไฟล์เดิมซ้ำ เราจับได้ด้วยการเทียบ hash ของเนื้อไฟล์ แล้วข้ามการ embed ใหม่ ส่วนตัวไฟล์จริงเราเก็บไว้นอกฐานข้อมูล เพื่อให้รอดตอน redeploy และไม่ทำให้ DB บวม
พอถึงตรงนี้ index ก็พร้อมแล้ว ที่เหลือคือทำให้การดึงจากมันแม่นที่สุด
ดึงสองด่าน · recall แล้ว rerank
มาถึงตอนค้นจริง การดึงด่านเดียวมักไม่พอ และต้นเหตุอยู่ที่ตัว embedding เอง
embedding เป็นสิ่งที่เรียกว่า bi-encoder มันแปลงเวกเตอร์ของคำถามกับของ chunk แยกกันคนละที แล้วค่อยเอามาวัดระยะว่าใกล้กันแค่ไหน วิธีนี้เร็วมาก ค้นในคลังเป็นล้านชิ้นได้สบาย แต่ก็หยาบ เพราะมันไม่เคยเห็นคำถามกับ chunk พร้อมกันเลย ผลที่ตามมาคือ chunk ที่ใช่จริง ๆ มักไปโผล่อันดับ 12 แทนที่จะเป็นอันดับ 1
ทางแก้ที่หลายคนคิดถึงคือดึง top-k ให้เยอะขึ้น แต่วิธีนี้ไม่ช่วย เพราะถึงของถูกจะติดมาด้วย แต่ context ก็ท่วมจน LLM จมไปกับกองข้อมูล กุญแจที่แท้จริงคือ จัดอันดับใหม่ ไม่ใช่ ดึงมาให้เยอะ เราจึงแบ่งการดึงเป็นสองด่าน
- ด่าน 1 · recall เน้นเร็วแต่หยาบ — ให้ embedding ดึง candidate กว้าง ๆ มาสัก ~30 ชิ้น ขอแค่ “ของถูกติดมาอยู่ในนี้” ก็พอ ยังไม่ต้องเรียงให้ถูก
- ด่าน 2 · rerank เน้นช้าแต่แม่น — ใช้ cross-encoder อ่านคำถามกับ chunk เป็นคู่พร้อมกัน มันเลยตัดสินความเข้ากันได้แม่นกว่ามาก แต่เพราะมันแพง (ต้องรันทีละคู่) เราจึงให้มันทำงานแค่กับ ~30 ชิ้นที่ด่านแรกคัดมา ไม่ใช่ทั้งคลัง แล้วเรียงใหม่ให้เหลือ top ~6
จุดที่สำคัญต่อบทนี้คือ ลำดับ ของสองด่านนี้ พูดสั้น ๆ คือ embedding ทำหน้าที่ recall ส่วน reranker ทำหน้าที่ precision และ recall ต้องมาก่อน เพราะถ้าของที่ใช่ไม่ติดมาใน ~30 ชิ้นแรกตั้งแต่ด่าน recall ด่าน rerank ก็ไม่มีทางกู้คืนได้ ด้วยเหตุนี้ embedding ในขั้น 3 จึงเป็นตัวกำหนด เพดาน ของทั้งระบบ เราต้องเลือกให้ดีมาตั้งแต่ต้น ส่วน rerank นั้นแค่จัดอันดับ ไม่ได้แตะ index เราจึงสลับมันไปลองตัวอื่นได้ง่าย
พอข้อมูลโตเป็นแสน
เวลาพูดถึง “ข้อมูลปริมาณมาก” คำถามที่แท้จริงคือ อะไรจะพังก่อน เมื่อเอกสารโตจากหลักร้อยเป็นหลักหมื่นหลักแสน
เริ่มจากข่าวดีก่อน เวลาในการค้นแทบไม่โตตามจำนวนเอกสาร เพราะ HNSW หาเพื่อนบ้านได้ในเวลาราว ๆ log ของขนาดคลัง อีกอย่างคือ ในการตอบหนึ่งครั้ง เวลาส่วนใหญ่หมดไปกับ LLM ที่เรียบเรียงคำตอบ ซึ่งไม่ขึ้นกับขนาดคลังอยู่แล้ว สรุปคือคลังโตขึ้นสิบเท่า latency แทบไม่ขยับ
ของที่พังจริงอยู่ที่อื่น มีสี่จุด
- index ต้องอยู่ใน RAM — HNSW เร็วได้เพราะมันอยู่ในหน่วยความจำ พอคลังโต RAM ก็ต้องโตตาม นี่คือเพดานจริงข้อแรก ไม่ใช่เรื่องความเร็ว
- ความแม่นเจือจาง — ยิ่งเอกสารเยอะ ของที่ “ใกล้แต่ผิด” ก็ยิ่งเยอะ ของถูกเลยถูกเบียดอันดับ ทางแก้คือใช้ scope ตัด search space ให้แคบลงก่อนค้น (เช่นค้นเฉพาะโฟลเดอร์ เครื่อง หรือหมวดที่เกี่ยว) แล้วค่อยให้ rerank คัดอีกชั้น
- “ลิสต์ทั้งหมด” พัง — พอมีหมื่นเล่ม คำถามอย่าง “มีคู่มืออะไรบ้าง” จะตอบด้วย embed ไม่ได้ ต้องใช้การนับและแบ่งหน้าบนตารางเอกสารแทน นี่เป็นงานของ catalog ไม่ใช่ของ vector
- นำเข้าทีละ chunk ช้า — การ embed ทีละท่อนกลายเป็นคอขวดเมื่อนำเข้าทีละมาก ๆ เราจึงต้องจัดคิวเป็น batch แล้วทำเบื้องหลัง ไม่บล็อกผู้ใช้ คือรับเอกสารเข้าระบบทันที แล้วค่อย ๆ ทยอยทำดัชนีพร้อมรายงานสถานะให้เห็น
จะสังเกตว่าไม่มีปัญหาข้อไหนแก้ได้ด้วย “embedding ที่ฉลาดขึ้น” เลย เพราะการสเกลเป็นเรื่องของ สถาปัตยกรรม — แยกที่เก็บ จัดคิว ตัด scope — ไม่ใช่เรื่องของตัวโมเดล
provenance · เส้นด้ายที่ทำให้เชื่อได้
ทีนี้ก็ถึงเวลาเก็บเกี่ยว provenance ที่เราติดมาตั้งแต่ขั้นแรก ป้าย (เอกสาร, หน้า, ลำดับ) ที่ติดอยู่กับทุก chunk ทำให้ทุกประโยคที่ระบบตอบ ชี้กลับได้ว่ามาจากหน้าไหนของเล่มไหน ผู้ใช้คลิกแล้วกระโดดไปดูตรงนั้นได้จริง
บทที่ 5 บอกไว้ว่า แก่นของการกัน AI หลอนคือการคุมว่า ใครเป็นคนเขียนส่วนที่ต้องเป็นความจริง provenance คืออีกครึ่งของเรื่องเดียวกัน เพราะถึงแม้ LLM จะเป็นคนเรียบเรียงคำตอบ แต่เมื่อทุกข้อความผูกกับแหล่งที่เปิดดูได้ การหลอนก็จะถูกจับได้ทันที
และสิ่งสำคัญคือ provenance เติมทีหลังไม่ได้ ถ้าตอนแตกข้อความเราทิ้งเลขหน้า หรือตอนตัด chunk เราทิ้งลำดับ ข้อมูลนั้นก็หายไปถาวร เราจึงต้องร้อยมันมาตั้งแต่ชิ้นแรก ไม่ใช่ไปคิดเอาทีหลัง
ร้อยเข้าด้วยกัน
ฝั่งเขียนทั้งหมดมีอยู่เพื่อรับใช้ฝั่งอ่าน ตัด chunk ตรงไหน เลือก embed อันไหน แยกข้อมูลไปทางไหน เก็บที่มาหรือไม่ — ทุกการตัดสินใจตอนสร้าง index ล้วนกำหนดไว้ล่วงหน้าแล้วว่า ตอนถาม recall กับ rerank จะทำงานได้ดีแค่ไหน บทที่ 5 ถึงจะดึงของที่ใช่ออกมาได้ ก็เพราะบทนี้สร้างของที่ใช่เอาไว้ให้ดึง
และนี่ก็คือแกนเดียวกับทั้งซีรีส์ คือเข้าใจรูปของข้อมูลก่อน แล้วโครงสร้างที่เหมาะจะปรากฏขึ้นเอง สุดท้ายแล้ว คลังความรู้ที่ดีไม่ได้วัดกันที่ว่า embed ได้กี่ล้านชิ้น แต่วัดกันที่ว่า ถามแล้วได้คำตอบที่ใช่ พร้อมที่มาที่เปิดตรวจได้ และยังตอบได้ดีอยู่แม้คลังจะโตขึ้นอีกสิบเท่า
ขอบเขต — บทนี้พาเข้าใจ “จะเปลี่ยนเอกสารดิบปริมาณมากให้กลายเป็นคลังที่ค้นได้อย่างไร” · เป็นคู่ฝั่งเขียนของ บทที่ 5 · แนวคิดหลัก: per-page extraction + provenance, encrypted/image-only PDF, overlapping-window chunking, embed-time vs query-time, sticky embedding (re-index cost), structured-vs-unstructured routing (catalog/fact-store vs vector), HNSW + idempotent re-index + content-hash dedup, two-stage recall→rerank (bi-encoder กว้าง · cross-encoder แม่น), การสเกล (RAM · scope · count/paginate · batched background ingestion), citation-grade provenance · ชั้นความทรงจำข้ามบทสนทนาเป็นบทถัดไป · เครื่องมือเปลี่ยนตามยุค แต่ “เข้าใจรูปของข้อมูลก่อนแล้วเลือกที่เก็บให้พอดี” อยู่นาน