AI Engineer › Embeddings และ RAG

Chunking: ตัดเอกสารให้ค้นหาได้

ประมาณ 35 นาที

เป้าหมายของบท

  • ตัดเอกสารเป็น chunk แบบ sliding window (size/overlap) และคำนวณจำนวน chunk ได้
  • ประเมินขนาด chunk ด้วย hit@k คู่กับปริมาณข้อความที่ส่งเข้า prompt
  • ระวังตัวเลขที่หลอกตาเมื่อ chunk ใหญ่ครอบคลุมหลายเอกสาร

ทำไม RAG ต้องตัดเอกสาร

RAG (retrieval-augmented generation) = ดึงเฉพาะส่วนของเอกสารที่เกี่ยวกับคำถามมาใส่ใน prompt แล้วให้โมเดลตอบจากส่วนนั้น (จากบทแรก: ดึง 3 ส่วนแทนยัดทั้งฐาน ประหยัด token ราว 7 เท่า) เอกสารจริงยาวหลายหน้า จึงต้องตัดเป็น chunk ก่อน แล้วค้นหาระดับ chunk — chunk ที่ตัดไม่ดีทำให้คำตอบขาด (ตัดกลางประโยคสำคัญ) หรือรก (chunk ใหญ่จนมีเรื่องปน)

ชุดข้อมูลของเรา: 24 เอกสาร ~40 คำ รวม 973 คำ (ทั้งหมดเป็นข้อความสั้นอยู่แล้ว) — เพื่อฝึก chunking เราต่อทั้งหมดเป็น “คู่มือ” เล่มเดียวแล้วจดว่าแต่ละเอกสารอยู่ช่วงคำไหน (spans)

Chunk แบบหน้าต่างเลื่อน (sliding window)

def chunk(words, size, overlap):
    step = size - overlap
    out = []
    for start in range(0, len(words), step):
        out.append((start, " ".join(words[start:start + size])))
        if start + size >= len(words):
            break
    return out
  • size = ความยาว chunk (คำ), overlap = จำนวนคำที่ซ้อนกับ chunk ก่อนหน้า (กันตัดกลางข้อมูลสำคัญ)
  • 973 คำ ขนาด 100 overlap 20 → เลื่อนทีละ 80 คำ ได้ 12 chunk (chunk สุดท้ายสั้นกว่า: 93 คำ)
  • ของจริงมักตัดตามโครงสร้าง (หัวข้อ ย่อหน้า ประโยค) ก่อน แล้วค่อยจำกัดความยาว — หลักการคือ 1 chunk ควรเป็น “หน่วยความหมาย” ที่อ่านเดี่ยวๆ รู้เรื่อง

วัดผลของขนาด chunk: ระวังตัวเลขที่หลอกตา

เราวัด hit@k = สัดส่วนคำถามที่ chunk อันดับ 1..k มีเอกสารเฉลยอยู่ด้วย (ค้นด้วย TF-IDF บน 30 คำถามของเรา):

ขนาด (คำ) overlap จำนวน chunk hit@1 hit@3
20 0 49 0.5667 0.8000
40 0 25 0.6333 0.9000
60 0 17 0.5667 0.9333
100 20 12 0.6333 0.8667
200 40 6 0.7000 1.0000
400 80 3 0.8333 1.0000
973 (ทั้งเล่ม) – 1 1.0000 1.0000

hit@k ดูดีขึ้นเรื่อยๆ เมื่อ chunk ใหญ่ขึ้น — แต่นั่นเพราะ chunk ใหญ่ครอบคลุมหลายเอกสาร โอกาสทับเฉลยจึงสูง ทั้งเล่มได้ 1.0 โดยไม่ได้ “ค้นหา” อะไรเลย ต้องดูควบคู่กับ ปริมาณข้อความที่ส่งเข้า prompt (k × ขนาด chunk): ขนาด 40 ส่ง 3 × 40 = 120 คำ ได้ hit@3 0.90 เทียบกับขนาด 200 (overlap 40) ส่ง 600 คำ ได้ 1.00 — ต่อ 100 คำที่ส่ง ประสิทธิภาพ 0.75 เทียบ 0.1667 (ถ้า overlap 0 ทั้งคู่: ขนาด 200 ได้ hit@3 0.9667 หรือ 0.1611 ต่อ 100 คำ ข้อสรุปเหมือนเดิม)

สรุปหลักคิด: ไม่มีขนาดที่ดีที่สุดสากล — เลือกจากการวัด hit@k และ ต้นทุน/สิ่งรบกวนของ context บนชุดคำถามของงานจริงของคุณ, และใช้ overlap เมื่อกลัวข้อมูลสำคัญถูกตัดกลาง

ตัวอย่างแก้โจทย์

กด "ขั้นต่อไป" ทีละขั้น ลองรันและแก้โค้ดในแต่ละขั้นได้

ตัดคู่มือเป็น chunk

ต่อ 24 เอกสารเป็นคู่มือเล่มเดียวแล้วตัดด้วยหน้าต่างเลื่อน

  1. คู่มือมี 973 คำ; เอกสาร d05 อยู่ที่ช่วงคำ (ค่า start, end) ใด

    Python

ขนาด chunk กับ hit@k และปริมาณที่ส่ง

วัดด้วย evaluate (ให้ไว้ ค้นด้วย TF-IDF)

  1. ขนาด 40 ไม่ overlap: 25 chunk, hit@1 0.6333, hit@3 0.9

    Python

เติมช่องว่าง

แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ

ยังไม่ได้ทำ

ตัดข้อความ 10 คำเป็น chunk ยาว 4 คำ ไม่ overlap แล้วนับจำนวน chunk ในตัวแปร n_chunks เติม ___ ให้ครบ

แบบฝึกท้ายบท

บทนี้จะนับว่าเสร็จเมื่อผ่านข้อพื้นฐานและประยุกต์ครบ ข้อท้าทายได้ดาวเพิ่ม

🟢 พื้นฐาน

🟢 พื้นฐานยังไม่ได้ทำ

คู่มือ words มีกี่คำ (n_words) และเอกสาร d05 อยู่ที่ช่วงคำ start ถึง end (spans) เก็บไว้ในตัวแปร d05_start, d05_end

🟢 พื้นฐานยังไม่ได้ทำ

ใช้ chunk (ให้ไว้) ขนาด 50 ไม่ overlap ได้กี่ chunk เก็บไว้ในตัวแปร n_chunks_50

🟢 พื้นฐานยังไม่ได้ทำ

ขนาด 100 overlap 20: chunk สุดท้ายมีกี่คำ เก็บไว้ในตัวแปร last_chunk_words

🟢 พื้นฐานยังไม่ได้ทำ

ขนาด 100 overlap 20: ตำแหน่งเริ่มของ 4 chunk แรก (list ของ int) เก็บไว้ในตัวแปร first_starts

🟡 ประยุกต์

🟡 ประยุกต์ยังไม่ได้ทำ

ขนาด 100 overlap 20: chunk แต่ละอันทับกับกี่เอกสาร (ช่วงคำของ chunk คือ [start, start + จำนวนคำ) ทับกับ spans ถ้า s < g_end และ e > g_start) เก็บจำนวนเอกสารเฉลี่ยต่อ chunk (ปัด 2 ตำแหน่ง) ไว้ใน avg_docs_per_chunk

🟡 ประยุกต์ยังไม่ได้ทำ

ใช้ evaluate (ให้ไว้) เก็บ hit@3 ของ chunk ขนาด 30, 60 และ 200 (overlap 0) ไว้ในตัวแปร hit3_30, hit3_60, hit3_200

🟡 ประยุกต์ยังไม่ได้ทำ

ประสิทธิภาพต่อปริมาณที่ส่ง = hit@3 ÷ (3 × ขนาด ÷ 100) (ปัด 4 ตำแหน่ง, overlap 0) ของขนาด 40 และ 200 เก็บไว้ในตัวแปร eff_40, eff_200

🔴 ท้าทาย

🔴 ท้าทายยังไม่ได้ทำ

เขียนการประเมินเอง (chunk ให้ไว้): ขนาด 60 ไม่ overlap ค้นด้วย TfidfVectorizer(stop_words="english") บนข้อความของ chunk ทุกอัน จัดอันดับด้วย cosine_similarity กับคำถามใน qs (np.argsort(-sims[i], kind="stable")) — chunk ถูกต้องถ้าช่วงคำของมันทับ spans ของเอกสารเฉลย เก็บ hit@1 และ hit@3 (ปัด 4 ตำแหน่ง) ไว้ใน hit1_60, hit3_60

🔴 ท้าทายยังไม่ได้ทำ

เอกสารที่ถูกตัดข้ามหลาย chunk (ทับกับ chunk ตั้งแต่ 2 อันขึ้นไป) มีกี่เอกสาร เมื่อใช้ขนาด 60 ไม่ overlap (n_split_60) และขนาด 100 ไม่ overlap (n_split_100)

Quiz

1. overlap ระหว่าง chunk มีไว้ทำไม
2. ในตาราง chunk ขนาดทั้งเล่ม (973 คำ) ได้ hit@1 = 1.0 ข้อสรุปที่ถูกคือ
3. ควรเลือกขนาด chunk อย่างไร
4. ถ้าตั้ง overlap = size จะเกิดอะไรกับ chunk แบบ sliding window ในบทนี้

สรุป

  • RAG ค้นหาระดับ chunk: ตัดเอกสารด้วย size/overlap (973 คำ ขนาด 100 overlap 20 → 12 chunk) โดยให้ chunk เป็นหน่วยความหมายที่อ่านเดี่ยวๆ รู้เรื่อง
  • hit@k เพิ่มตามขนาด chunk (ขนาด 200 hit@3 = 1.0, ทั้งเล่ม = 1.0) แต่เป็นเพราะครอบคลุมหลายเอกสาร — ต้องดูปริมาณที่ส่งควบคู่ (ขนาด 40: 0.75 ต่อ 100 คำ vs ขนาด 200 overlap 40: 0.1667)
  • ไม่มีขนาดสากล เลือกจากการวัดบนคำถามจริง; overlap ต้องน้อยกว่า size

คำถามสัมภาษณ์

คุณจะเลือกขนาด chunk สำหรับระบบ RAG อย่างไร

เริ่มจากตัดตามโครงสร้างเอกสาร (หัวข้อ ย่อหน้า) แล้วจำกัดความยาว จากนั้นทดลองหลายขนาดและ overlap บนชุดคำถามที่มีเฉลย วัด hit@k/recall ของ retrieval และปริมาณ token ที่ส่งเข้า prompt พร้อมคุณภาพคำตอบ เลือกจุดที่คุ้ม ระวังตัวชี้วัดที่ดีขึ้นเพียงเพราะ chunk ใหญ่ครอบคลุมทุกอย่าง

chunk เล็กเกินไปและใหญ่เกินไปมีปัญหาอะไร

เล็กเกินไปตัดบริบทขาด ข้อมูลที่ต้องอ่านร่วมกันแยกไปอยู่คนละ chunk และมีจำนวน chunk มาก; ใหญ่เกินไปมีหลายเรื่องปน retrieval ไม่แม่นยำ สิ้นเปลือง token และสิ่งรบกวนทำให้โมเดลตอบแย่ลง