هذا المقال يتطلب مستوى: متوسط
تقطيع المستندات (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)} حرف")
القاعدة العملية: خلي chunk_overlap بين 10% و20% من chunk_size. أقل من كده بتفقد السياق عند الحدود، وأكتر من كده بتكرّر النص وتكبّر الفهرس بلا داعٍ.
سيناريو واقعي بالأرقام
الافتراض إن عندك قاعدة معرفة دعم فني فيها 2,000 صفحة، وبتستخدم نموذج embedding بسقف 512 توكن للقطعة. لو قطّعت بحجم ثابت 1,000 توكن من غير تداخل، القطعة هتتقص عند سقف النموذج وتفقد آخرها، وهتلاقي دقة الاسترجاع (recall@5) بتدور حوالي 60% في الأسئلة اللي إجابتها بتقع على حدود قطعتين.
لما تنزّل الحجم لـ 800 توكن مع تداخل 15% وفصل يحترم الفقرات، الـ recall@5 بيقفز لحوالي 85% على نفس مجموعة الأسئلة. مفيش تغيير في النموذج ولا في الـ embedding، التغيير كله في التقطيع. الرقم ده متوافق مع تقييمات Chroma وPinecone اللي بتوضّح إن ضبط حجم القطعة والتداخل بيفرق عشرات النقاط المئوية في الدقة.
الـ trade-offs اللي لازم تعرفها
- قطع صغيرة (200–400 توكن): تشابه دلالي أدق لكل قطعة، بس بتخسر السياق المحيط، والنموذج ممكن يجاوب من غير خلفية كافية.
- قطع كبيرة (1000+ توكن): سياق أغنى، بس التشابه بيبقى "مخفّف" لأن القطعة بتخلط أكتر من فكرة، فبتنزل دقة الترتيب.
- التداخل: بيكسب سياق عند الحدود، بيخسر مساحة تخزين وتكلفة embedding أعلى (كل توكن متكرر بيتحسب مرتين). التداخل 15% نقطة بداية متوازنة.
الـ trade-off هنا: مفيش حجم مثالي واحد. الحجم الأنسب بيعتمد على طبيعة مستنداتك وطول الأسئلة، فقيس ولا تخمّن.
متى لا تستخدم هذه الطريقة
لو مستنداتك أصلًا منظّمة في وحدات قصيرة مستقلة المعنى، زي أسئلة شائعة (FAQ) كل سؤال وجوابه في فقرة، فالتقطيع ثابت الحجم أو حتى قطعة لكل عنصر بيكفي، والتقطيع الذكي بيبقى تعقيد زيادة. كمان لو حجم بياناتك صغير جدًا (أقل من بضع مئات من القطع) وبتقدر تحقن المستند كله في نافذة السياق، ساعتها RAG نفسه ممكن يبقى مبالغة، وتكتفي بحقن النص مباشرة.
الخطوة التالية
افتح خط RAG بتاعك دلوقتي وشوف قيمة chunk_size وchunk_overlap. لو مش موجود تداخل خالص، حُط 15% وأعد بناء الفهرس، وجرّب نفس 10 أسئلة اللي كانت بتفشل قبل كده. لو الإجابات بقت أكمل، ده معناه إن مشكلتك كانت في التقطيع من الأول.
المصادر
- LangChain — Text Splitters & RecursiveCharacterTextSplitter (توثيق رسمي): https://python.langchain.com/docs/concepts/text_splitters/
- Pinecone — Chunking Strategies for LLM Applications: https://www.pinecone.io/learn/chunking-strategies/
- Chroma — Evaluating Chunking Strategies for Retrieval (تقرير تقني): https://research.trychroma.com/evaluating-chunking
- Anthropic — Introducing Contextual Retrieval (2024): https://www.anthropic.com/news/contextual-retrieval