กลยุทธ์การแบ่งส่วน: ขนาดคงที่ เทียบกับประโยค เทียบกับแบบเรียกซ้ำ
นำตัวแบ่งข้อความตามขนาดคงที่ ตามขอบเขตประโยค และแบบอักขระเรียกซ้ำมาใช้และเปรียบเทียบกัน พร้อมทำความเข้าใจว่าขนาดส่วนและการซ้อนทับส่งผลต่อคุณภาพการค้นคืนอย่างไร
กลยุทธ์การแบ่งส่วน: ขนาดคงที่ เทียบกับประโยค เทียบกับแบบเรียกซ้ำ เป็นบทเรียน AI Engineering Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน AI Engineering Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส AI Engineering Academy มีบทเรียนทั้งหมด 4 บทเรียน
เหตุใดคุณภาพการแบ่งชิ้นข้อความจึงสำคัญ
การแบ่งชิ้นข้อความ คือกระบวนการแบ่งเอกสารที่โหลดแล้วออกเป็นส่วนย่อย ๆ ซึ่งพอดีกับหน้าต่างบริบทของ LLM และสามารถสร้างดัชนีและค้นคืนแยกกันได้ วิธีแบ่งชิ้นข้อความส่งผลต่อคุณภาพการค้นคืนมากกว่าปัจจัยอื่นเกือบทั้งหมด หากชิ้นข้อความแบ่งคำตอบออกเป็นสองส่วน จะไม่มีชิ้นใดชิ้นหนึ่งเพียงลำพังที่มีข้อมูลเพียงพอสำหรับตอบคำถาม และหากชิ้นข้อความผสมหัวข้อที่ไม่เกี่ยวข้องกันสองหัวข้อ ชิ้นนั้นจะถูกค้นคืนสำหรับคำถามเกี่ยวกับทั้งสองหัวข้อ แต่กลับไม่มีประโยชน์เพียงพอสำหรับหัวข้อใดเลย
การแบ่งชิ้นข้อความขนาดคงที่
การแบ่งชิ้นข้อความขนาดคงที่ จะแบ่งข้อความเป็นชิ้นที่มีอักขระหรือโทเค็นจำนวน N ตัวพอดี โดยไม่สนใจขอบเขตของประโยคหรือย่อหน้า นี่เป็นกลยุทธ์ที่ง่ายที่สุดและนำไปใช้งานได้รวดเร็ว ข้อเสียสำคัญคือมัก ตัดประโยคออกเป็นสองส่วน ทำให้ชิ้นข้อความเริ่มต้นหรือสิ้นสุดกลางความคิด วิธีนี้ยอมรับได้สำหรับข้อความหนาแน่นที่มีรูปแบบสม่ำเสมอ เช่น ข้อมูลส่งออกจากฐานข้อมูล แต่ให้คุณภาพการค้นคืนต่ำสำหรับร้อยแก้วเชิงบรรยายหรือเอกสารทางเทคนิค
def fixed_size_chunks(text, chunk_size=500, overlap=50):
'''Split text into fixed-size character chunks with overlap'''
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end]
chunks.append(chunk)
start += chunk_size - overlap # overlap keeps context at boundaries
return chunks
example = 'This is a long document. ' * 100
chunks = fixed_size_chunks(example, chunk_size=200, overlap=20)
print(f'Produced {len(chunks)} chunks, first: {chunks[0][:80]}...')การเพิ่มส่วนซ้อนทับให้ชิ้นข้อความคงที่
การปรับปรุงสำคัญของการแบ่งชิ้นข้อความขนาดคงที่คือ ส่วนซ้อนทับ โดยให้แต่ละชิ้นมีอักขระ N ตัวร่วมกับชิ้นก่อนหน้า วิธีนี้ช่วยให้ข้อมูลบริเวณรอยต่อของชิ้นข้อความปรากฏอยู่ในชิ้นที่ติดกันทั้งสองชิ้น หากซ้อนทับกัน 50-100 โทเค็น ประโยคที่ข้ามรอยต่อจะถูกเก็บไว้อย่างครบถ้วนในอย่างน้อยหนึ่งชิ้นจากสองชิ้นนั้น การซ้อนทับที่มากขึ้นช่วยให้ครอบคลุมข้อมูลได้ดีขึ้น แต่ทำให้ดัชนีมีขนาดใหญ่ขึ้นและมีเนื้อหาซ้ำซ้อนในผลการค้นคืน
# Overlap example with a sentence at the boundary
text = 'A B C D E F G H I J'
chunk_size = 6 # characters
overlap = 2
i = 0
while i < len(text):
print(repr(text[i:i+chunk_size]))
i += chunk_size - overlap
# Output shows overlapping content:
# 'A B C D'
# 'C D E F'
# 'E F G H'
# Each adjacent pair shares 2 charsการแบ่งชิ้นข้อความตามขอบเขตประโยค
การแบ่งชิ้นข้อความตามขอบเขตประโยค ใช้ไลบรารี NLP เช่น nltk หรือ spacy เพื่อตรวจจับจุดสิ้นสุดของประโยคก่อนแบ่งข้อความ วิธีนี้รับประกันว่าจะไม่มีประโยคใดถูกตัดออกเป็นสองส่วน ระบบจะสะสมประโยคไปเรื่อย ๆ จนกว่าการเพิ่มประโยคถัดไปจะทำให้เกินขนาดเป้าหมาย จากนั้นจึงเริ่มชิ้นข้อความใหม่ ผลลัพธ์คือชิ้นข้อความที่มีความคิดสมบูรณ์เสมอ ซึ่งให้คุณภาพการฝังเวกเตอร์ดีกว่าการแบ่งตามจำนวนอักขระแบบคงที่อย่างมาก
import nltk
nltk.download('punkt', quiet=True)
def sentence_chunks(text, max_tokens=300):
sentences = nltk.sent_tokenize(text)
chunks = []
current = []
current_len = 0
for sent in sentences:
sent_tokens = len(sent.split())
if current_len + sent_tokens > max_tokens and current:
chunks.append(' '.join(current))
current = []
current_len = 0
current.append(sent)
current_len += sent_tokens
if current:
chunks.append(' '.join(current))
return chunksการแบ่งข้อความด้วยอักขระแบบเรียกซ้ำ
การแบ่งข้อความด้วยอักขระแบบเรียกซ้ำ เป็นกลยุทธ์ที่ใช้กันแพร่หลายที่สุดในระบบ RAG สำหรับใช้งานจริง โดยจะลองใช้ตัวคั่นตามลำดับชั้น ได้แก่ การแบ่งย่อหน้า (\n\n) ก่อน ตามด้วยการขึ้นบรรทัดใหม่ (\n) จุดสิ้นสุดประโยค ช่องว่าง และสุดท้ายคืออักขระแต่ละตัว ระบบจะแบ่งตรงขอบเขตที่มีความหมายมากที่สุดและยังคงรักษาชิ้นข้อความให้มีขนาดต่ำกว่าเป้าหมาย ส่งผลให้ชิ้นข้อความเคารพ โครงสร้างเอกสารได้มากที่สุดเท่าที่จะเป็นไปได้
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # target size in characters
chunk_overlap=200, # overlap between consecutive chunks
separators=['\n\n', '\n', '. ', '! ', '? ', ' ', ''],
length_function=len
)
with open('document.txt') as f:
text = f.read()
chunks = splitter.split_text(text)
print(f'Split into {len(chunks)} chunks')
for i, chunk in enumerate(chunks[:3]):
print(f'Chunk {i}: {len(chunk)} chars — {chunk[:60]}...')การแบ่งข้อความโดยคำนึงถึงโทเค็น
จำนวนอักขระเป็นตัวแทนจำนวนโทเค็นที่ไม่แม่นยำ จำนวน 1000 อักขระอาจเท่ากับ 200 หรือ 400 โทเค็นก็ได้ ขึ้นอยู่กับความยาวของคำและภาษา หากต้องการควบคุมอย่างแม่นยำ ให้แบ่งตาม จำนวนโทเค็นโดยใช้ tiktoken วิธีนี้สำคัญต่อการทำให้ชิ้นข้อความพอดีกับหน้าต่างบริบทของโมเดลและการประเมินต้นทุนอย่างแม่นยำ TokenTextSplitter ของ LangChain เป็นตัวห่อหุ้ม tiktoken สำหรับการแบ่งชิ้นข้อความโดยคำนึงถึงโทเค็น
from langchain_text_splitters import TokenTextSplitter
import tiktoken
# Split by actual token count, not characters
splitter = TokenTextSplitter(
encoding_name='cl100k_base', # GPT-4 tokenizer
chunk_size=256, # target tokens per chunk
chunk_overlap=32 # overlap in tokens
)
chunks = splitter.split_text(text)
# Verify token count
enc = tiktoken.get_encoding('cl100k_base')
for chunk in chunks[:3]:
tokens = len(enc.encode(chunk))
print(f'Chunk: {tokens} tokens')การเลือกขนาดชิ้นข้อความที่เหมาะสม
ขนาดชิ้นข้อความเป็นไฮเปอร์พารามิเตอร์ที่สำคัญ ชิ้นข้อความขนาดเล็ก (100-200 โทเค็น) มีความแม่นยำ โดยบรรจุข้อมูลที่เน้นเฉพาะเรื่องและเหมาะกับการฝังเวกเตอร์ แต่จะสูญเสียบริบทโดยรอบ ทำให้ LLM อาจมีข้อมูลไม่เพียงพอสำหรับตอบคำถาม ชิ้นข้อความขนาดใหญ่ (500-1000 โทเค็น) ให้บริบทมากกว่า แต่เวกเตอร์ฝังของชิ้นข้อความจะเฉลี่ยครอบคลุมหัวข้อที่กว้างขึ้น จึงค้นคืนได้ยากขึ้นสำหรับคำถามเฉพาะเจาะจง ระบบสำหรับใช้งานจริงส่วนใหญ่ใช้ 256-512 โทเค็น โดยทดสอบกับข้อมูลของตนเองจากผลการทดลอง
การเพิ่มบริบทให้ชิ้นข้อความ
การปรับปรุงที่ทรงพลังคือ ส่วนหัวบริบทของชิ้นข้อความ โดยเติมชื่อเอกสารและหัวข้อส่วนไว้ด้านหน้าข้อความของแต่ละชิ้นก่อนการฝังเวกเตอร์ วิธีนี้ทำให้เวกเตอร์ฝังเก็บทั้งเนื้อหาของชิ้นข้อความและตำแหน่งที่มาของชิ้นนั้นในเอกสาร ตัวอย่างเช่น ชิ้นข้อความที่อ่านว่า สิทธิประโยชน์และนโยบายวันหยุด: พนักงานได้รับวันหยุดสะสม 15 วันต่อปี... จะถูกค้นคืนได้แม่นยำกว่าสำหรับคำถามเกี่ยวกับนโยบายวันหยุด เมื่อเทียบกับข้อความเดียวกันที่ไม่มีส่วนหัว
def create_contextual_chunks(doc, splitter):
title = doc['metadata'].get('title', '')
section = doc['metadata'].get('section', '')
text = doc['text']
raw_chunks = splitter.split_text(text)
contextual_chunks = []
for chunk in raw_chunks:
# Prepend document context to each chunk
context_header = f'{title}\n{section}\n\n' if title else ''
contextual_chunks.append({
'text': context_header + chunk,
'metadata': doc['metadata']
})
return contextual_chunksการเปรียบเทียบกลยุทธ์กับข้อมูลของคุณ
ไม่มีกลยุทธ์การแบ่งชิ้นข้อความใดดีที่สุดสำหรับทุกกรณี ให้สร้างการประเมินอย่างรวดเร็ว โดยเลือกคำถาม 20 ข้อที่คุณทราบคำตอบอยู่แล้ว เรียกใช้การค้นคืนด้วยแต่ละกลยุทธ์ แล้ววัดว่าชิ้นข้อความที่ถูกต้องอยู่ในผลลัพธ์ 5 อันดับแรกบ่อยเพียงใด (อัตราการพบ@5) การตั้งค่านี้ใช้เวลาประมาณหนึ่งชั่วโมงและช่วยประหยัดเวลาหลายสัปดาห์ที่อาจต้องคาดเดา คุณมักจะพบว่ารูปแบบเอกสารเฉพาะของคุณ (ร้อยแก้วกฎหมายที่หนาแน่นเทียบกับเอกสารทางเทคนิคที่มีโครงสร้าง) เหมาะกับกลยุทธ์หนึ่งมากกว่ากลยุทธ์อื่นอย่างชัดเจน
def evaluate_chunking_strategy(questions_and_answers, retriever):
hits = 0
for qa in questions_and_answers:
results = retriever.retrieve(qa['question'], top_k=5)
result_texts = [r['text'] for r in results]
# Check if answer text appears in any retrieved chunk
if any(qa['answer'] in text for text in result_texts):
hits += 1
hit_rate = hits / len(questions_and_answers)
print(f'Hit rate@5: {hit_rate:.1%} ({hits}/{len(questions_and_answers)})')
return hit_rateการจัดการเอกสารประเภทพิเศษ
เอกสารบางประเภทต้องใช้การแบ่งชิ้นข้อความเฉพาะทาง ไฟล์โค้ดควรแบ่งตามขอบเขตของฟังก์ชันหรือคลาส ไม่ใช่ตามจำนวนอักขระ ไฟล์ Markdownควรแบ่งตามขอบเขตของหัวข้อ เพื่อให้แต่ละชิ้นสอดคล้องกับหนึ่งส่วน ตารางควรเก็บไว้เป็นชิ้นเดียวโดยไม่แบ่ง (การแบ่งกลางตารางทำให้ไม่สามารถตีความเนื้อหาได้) ให้วางแผนกลยุทธ์การแบ่งชิ้นข้อความตามประเภทเอกสารในคลังของคุณ แทนการใช้ตัวแบ่งแบบเดียวกับทุกกรณี
from langchain_text_splitters import MarkdownHeaderTextSplitter
# Split Markdown by header hierarchy
md_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[
('#', 'h1'),
('##', 'h2'),
('###', 'h3')
]
)
with open('documentation.md') as f:
md_text = f.read()
# Each chunk gets metadata from its heading hierarchy
chunks = md_splitter.split_text(md_text)
for chunk in chunks[:3]:
print('Section:', chunk.metadata)
print('Text:', chunk.page_content[:80])
print()การติดตามสายที่มาของชิ้นข้อความ
ชิ้นข้อความทุกชิ้นต้องมี ID ที่คงที่และไม่ซ้ำกัน ซึ่งสร้างจากเอกสารต้นทางและตำแหน่งของชิ้นข้อความ ใช้ ID นี้เพื่ออัปเดตชิ้นข้อความเฉพาะรายการเมื่อเอกสารเปลี่ยนแปลง โดยไม่ต้องสร้างดัชนีคลังเอกสารทั้งหมดใหม่ แฮชแบบกำหนดแน่นอนของเส้นทางต้นทางรวมกับดัชนีชิ้นข้อความใช้งานได้ดี นอกจากนี้ให้บันทึกตำแหน่งของชิ้นข้อความไว้ในเมทาดาทาด้วย (ชิ้นที่ 3 จาก 12 ของเอกสาร X) เพราะจะช่วยประกอบส่วนเต็มกลับคืนมาเมื่อมีการค้นคืนชิ้นข้อความที่อยู่ติดกันหลายชิ้น
import hashlib
def assign_chunk_ids(chunks, source_path):
for i, chunk in enumerate(chunks):
key = f'{source_path}::chunk_{i}'
chunk_id = hashlib.sha256(key.encode()).hexdigest()[:16]
chunk['id'] = chunk_id
chunk['metadata']['chunk_index'] = i
chunk['metadata']['total_chunks'] = len(chunks)
return chunks
# IDs are stable across re-runs if source and position match
chunks = sentence_chunks(text)
chunks = [{'text': c, 'metadata': {}} for c in chunks]
assign_chunk_ids(chunks, 'docs/policy_v3.pdf')ตรวจสอบความเข้าใจอย่างรวดเร็ว
ทดสอบความเข้าใจแนวคิดวิศวกรรม AI จากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า การแบ่งชิ้นข้อความขนาดคงที่ทำได้ง่ายแต่ตัดประโยคออกเป็นสองส่วน การแบ่งชิ้นข้อความตามขอบเขตประโยคช่วยรักษาความคิดให้สมบูรณ์ การแบ่งข้อความด้วยอักขระแบบเรียกซ้ำเคารพโครงสร้างเอกสารและเป็นตัวเลือกที่ใช้กันมากที่สุดในระบบจริง รวมถึง เทคนิคขั้นสูง เช่น การแบ่งโดยคำนึงถึงโทเค็น ส่วนหัวบริบท ตัวแบ่งเฉพาะทางสำหรับ Markdown และโค้ด และ ID ชิ้นข้อความที่คงที่สำหรับการอัปเดตแบบเพิ่มทีละส่วน บทถัดไปเราจะสร้างเวกเตอร์ฝังและจัดเก็บชิ้นข้อความเหล่านี้ในฐานข้อมูลเวกเตอร์
คำถามที่พบบ่อย
บทเรียน “กลยุทธ์การแบ่งส่วน: ขนาดคงที่ เทียบกับประโยค เทียบกับแบบเรียกซ้ำ” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “กลยุทธ์การแบ่งส่วน: ขนาดคงที่ เทียบกับประโยค เทียบกับแบบเรียกซ้ำ” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส AI Engineering Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส AI Engineering Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “กลยุทธ์การแบ่งส่วน: ขนาดคงที่ เทียบกับประโยค เทียบกับแบบเรียกซ้ำ”
นำตัวแบ่งข้อความตามขนาดคงที่ ตามขอบเขตประโยค และแบบอักขระเรียกซ้ำมาใช้และเปรียบเทียบกัน พร้อมทำความเข้าใจว่าขนาดส่วนและการซ้อนทับส่งผลต่อคุณภาพการค้นคืนอย่างไร คุณปฏิบัติ AI Engineering Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน AI Engineering Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน AI Engineering Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน
บทเรียน “กลยุทธ์การแบ่งส่วน: ขนาดคงที่ เทียบกับประโยค เทียบกับแบบเรียกซ้ำ” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน AI Engineering Academy นี้ได้ไหม
ได้ บทเรียน AI Engineering Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การโหลดเอกสารและการดึงข้อความ
- กลยุทธ์การแบ่งส่วน: ขนาดคงที่ เทียบกับประโยค เทียบกับแบบเรียกซ้ำ
- การทำดัชนี: การสร้างและจัดเก็บส่วนข้อความ
- ค้นคำค้น คืนข้อมูล และสร้างคำตอบ