هذا المقال يتطلب مستوى: متوسط
تقطيع المستندات (Chunking) في RAG: ليه إجابات نموذجك بتطلع ناقصة والسبب مش في الـ Embedding
لو نظام RAG بتاعك بيرجّع إجابات ناقصة أو غلط رغم إن المعلومة موجودة في مستنداتك، المشكلة غالبًا مش في النموذج ولا في الـ embedding، المشكلة في طريقة تقطيع النص. ركز في المقال ده هتعرف بالظبط إزاي تقطّع مستنداتك بحيث كل قطعة تحمل معنى كامل، وتقيس الفرق بالأرقام.
المشكلة باختصار
نظام RAG بياخد سؤال المستخدم، يدوّر على أقرب القطع (chunks) من مستنداتك في قاعدة بيانات المتجهات، ويحقنها في البرومبت قبل التوليد. القطعة دي مش بتيجي من السما: انت اللي بتقطّع المستند لقطع قبل ما تخزّنه. لو قطّعت غلط، النموذج بيستقبل نصًا مبتور المعنى، فيجاوب غلط وهو واثق.
مثال بسيط الأول
تخيّل إنك بتقص كتاب طبخ بالمسطرة كل 3 سطور بالظبط، من غير ما تبص على المحتوى. النتيجة إن المقادير هتقع في ورقة، وخطوات التنفيذ في ورقة تانية. لو حد سألك "الوصفة دي محتاجة كام بيضة؟" وانت ماسك ورقة الخطوات بس، مش هتلاقي الإجابة رغم إنها موجودة في الكتاب. ده بالظبط اللي بيحصل للنموذج لما تقطّع بالحجم الثابت.
الشرح الدقيق
التقطيع ثابت الحجم (fixed-size chunking) بيقص النص كل عدد محدد من الحروف أو التوكنز، بغض النظر عن حدود الجملة أو الفكرة. النتيجة إن الشرط بتاع سياسة معيّنة يقع في قطعة، والاستثناء بتاعه في قطعة تانية. لما تحسب التشابه الدلالي، القطعة اللي فيها نص الشرط بس بيبقى تشابهها مع السؤال أضعف، فمحرك البحث ممكن ميجيبهاش أصلًا ضمن أفضل النتائج.
الحل: قطّع على الحدود المنطقية مع تداخل
بدل ما تقص كل 500 حرف بالسيف، استخدم مقسّمًا يحترم حدود الفقرات والجمل، وحُط تداخلًا (overlap) بسيطًا بين كل قطعة والتانية عشان السياق ميضيعش عند الحدود. أشهر أداة عملية هي RecursiveCharacterTextSplitter في LangChain: بتحاول تقسّم على الفقرة الأول، وبعدين السطر، وبعدين الجملة، لحد ما توصل للحجم المطلوب.
from langchain_text_splitters import RecursiveCharacterTextSplitter
# chunk_size بالتوكنز أو الحروف حسب length_function
splitter = RecursiveCharacterTextSplitter(
chunk_size=800, # حجم القطعة المستهدف
chunk_overlap=120, # تداخل ~15% يحافظ على السياق عند الحدود
separators=["\n\n", "\n", ". ", "، ", " ", ""], # يحترم حدود الفقرة ثم الجملة
)
with open("policy.md", encoding="utf-8") as f:
text = f.read()
chunks = splitter.split_text(text)
print(f"عدد القطع: {len(chunks)}")
print(f"أطول قطعة: {max(len(c) for c in chunks)} حرف")