استراتيجيات تقسيم RAG المتقدمة: الدليل الشامل
استراتيجيات تقسيم RAG المتقدمة: الدليل الشامل
تفشل معظم الفرق في مشاريع التوليد المعزز بالاسترجاع (Retrieval-Augmented Generation - RAG) لأنهم يعاملون عملية تحليل المستندات كأمر ثانوي. لا يمكنك ببساطة تقسيم ملف PDF مكون من 100 صفحة بناءً على عدد أحرف ثابت وتوقع أن يجيب نموذج لغة كبير (LLM) بشكل موثوق على الأسئلة المعقدة. لتحقيق موثوقية جاهزة للإنتاج، تحتاج إلى استراتيجيات تقسيم RAG المتقدمة (Advanced RAG Chunking Strategies).
إذا كنت تعتمد على التقسيم العشوائي البسيط للأحرف (recursive character splitting)، فإن نافذة السياق (context window) الخاصة بك ستمتلئ حتماً بشظايا مفككة. يغطي هذا الدليل كيفية تنفيذ استراتيجيات تقسيم RAG المتقدمة باستخدام Python 3.11 وإطار العمل LangChain. سأوضح لك البنية البرمجية الدقيقة والكود المطلوب لاحترام حدود المستند، والحفاظ على المعنى الدلالي، ومنع فشل الاسترجاع.
المشكلة في التقسيم البسيط
عند بناء نظام RAG، فإن المسار الافتراضي الذي يسلكه معظم المطورين هو استخدام أداة RecursiveCharacterTextSplitter القياسية من LangChain، وتحديد حجم الجزء (chunk size) بـ 1000 والتداخل (overlap) بـ 200، ثم إنهاء المهمة. هذا خطأ فادح.
يعامل التقسيم البسيط النص غير المنظم ككتلة موحدة من الأحرف، ويتجاهل التسلسل الهرمي الهيكلي للمادة المصدر. فملفات PDF التي تحتوي على تقارير مالية، أو عقود قانونية، أو وثائق تقنية تعتمد بشكل كبير على التخطيط، والعناوين، والجداول، والفقرات لإيصال المعنى. عندما تقوم بقطع النص بشكل أعمى كل 1000 حرف، فإنك تقطع هذه العلاقات الدلالية.
تخيل عقداً قانونياً حيث تم تقسيم بند مسؤولية هام من المنتصف تماماً. ينتهي الأمر بنصف البند في الجزء أ (Chunk A)، وتصنف معايير الاستبعاد في الجزء ب (Chunk B). عندما يسأل المستخدم "تحت أي شروط تكون الشركة مسؤولة؟"، قد يقوم محرك الاسترجاع بجلب الجزء ب فقط بناءً على تشابه المتجهات (vector similarity)، مما يترك نموذج LLM مع فرضية غير مكتملة أو معيبة أساساً. سيقوم النموذج بالهلوسة بثقة وإعطاء إجابة بناءً على بيانات جزئية.
يدمر هذا العمى الهيكلي دقة أنبوب RAG الخاص بك. إذا كانت خطوة الاسترجاع تسترجع بيانات غير دقيقة، فإن خطوة التوليد ستنتج مخرجات غير دقيقة. وينتهي بك الأمر بإضاعة الوقت في تعديل أمر الـ LLM أو الانتقال من GPT-4o إلى Claude 3.5 Sonnet، على أمل الحصول على نتائج أفضل، في حين أن السبب الجذري يكمن بالكامل في كيفية تقسيم البيانات في مرحلة مبكرة.
يجب أن تتوقف عن معاملة المستندات كمصفوفات مسطحة من الأحرف. المستندات هي عبارة عن رسوم بيانية (graphs) لبيانات هرمية، ويجب أن تحترم استراتيجية التقسيم الخاصة بك هذه الحقيقة.
لماذا يصعب بناء استراتيجيات تقسيم RAG المتقدمة؟
إن تطبيق استراتيجيات تقسيم RAG المتقدمة أمر شاق. وتنشأ الصعوبة من الطبيعة الفوضوية لتنسيقات البيانات غير المنظمة. ملفات PDFs، ومستندات DOCX، وصفحات HTML لا تلتزم بمعيار واحد يمكن التنبؤ به.
ملف PDF، على سبيل المثال، هو في الأساس مجموعة من تعليمات الرسم. وهو لا يفهم بطبيعته معنى "الفقرة" أو "العنوان". كل ما يعرفه هو أن سلسلة نصية معينة موضوعة عند الإحداثيات (x: 120, y: 350) بحجم خط 14pt. وتتطلب إعادة بناء التدفق المنطقي للمستند من هذه التعليمات المستندة إلى الإحداثيات استخدام قواعد استدلالية (heuristics). يتعين عليك كتابة منطق يستنتج: "إذا كان حجم الخط 14pt وعريضاً (bold)، والنص الذي تحته هو 11pt، فهذا على الأرجح عنوان H2".
يصبح هذا الأمر أصعب بكثير عند التعامل مع التخطيطات متعددة الأعمدة، والجداول المدمجة، والرؤوس، والتذييلات، والصور المضمنة. غالباً ما تعيد مكتبات التحليل القياسية خليطاً فوضوياً من النصوص. وإذا قمت بتغذية هذا النص الخام غير المرتب في نموذج تضمين (embedding model)، فإن المتجهات الناتجة ستشير إلى مساحة دلالية لا معنى لها.
علاوة على ذلك، فإن الحفاظ على السياق عبر حدود الأجزاء يتطلب هندسة برمجية متطورة. فحتى لو قمت بتحديد فقرة بشكل صحيح، فقد تعتمد تلك الفقرة على سياق تم تأسيسه قبل ثلاث صفحات. على سبيل المثال، قد يذكر دليل تقني أن "هذا المعامل يجب تعيينه على true". إذا قمت بتقسيم هذه الفقرة بمعزل عن غيرها، فإن التضمين يفقد سياق ما يشير إليه "هذا المعامل".
يتطلب حل هذا التحدي حقن بيانات وصفية سياقية (contextual metadata) في كل جزء. يجب عليك الحفاظ على حالة تشغيلية للتسلسل الهرمي للمستند أثناء تحليله. إذا كنت داخل الفصل 2، القسم 3.1، يجب أن يحمل كل جزء يتم إنشاؤه داخل هذا القسم البيانات الوصفية {"chapter": "2", "section": "3.1"}. يسمح هذا لقاعدة بيانات المتجهات (vector database) بإجراء تصفية البيانات الوصفية (metadata filtering)، مما يمنع تداخل السياقات أثناء الاسترجاع.
بنية الحدود الدلالية
تتخلى البنية القوية لتقسيم RAG عن مفهوم حدود الأحرف الثابتة. بدلاً من ذلك، تعتمد على الحدود الدلالية والتحليل الهرمي. تتكون هذه البنية من ثلاث طبقات رئيسية: المحلل (parser)، والموجه المنطقي (logical router)، والمقسم السياقي (contextual chunker).
-
المحلل (The Parser): المحلل مسؤول عن تحويل الملفات غير المنظمة إلى تنسيق وسيط نظيف - عادة ما يكون Markdown. يعد Markdown التنسيق الأمثل لنماذج LLMs ونماذج التضمين لأنه يمثل الهيكل (العناوين، القوائم، كتل الكود) بشكل طبيعي باستخدام الحد الأدنى من الرموز (tokens). نحن نعتمد على أدوات متخصصة مثل Unstructured أو نماذج رؤية متخصصة لتحويل ملفات PDF إلى Markdown بدقة.
-
الموجه المنطقي (The Logical Router): بمجرد الحصول على تمثيل Markdown، يقوم الموجه بتحليل شجرة المستند. ويحدد الأقسام الرئيسية (H1)، والأقسام الفرعية (H2)، والوحدات الأساسية مثل الفقرات والقوائم والجداول. يحدد الموجه الاستراتيجية المثلى لكل نوع عقدة. فجدول ضخم يتطلب استراتيجية معالجة مختلفة عن كتلة من النص السردي.
-
المقسم السياقي (The Contextual Chunker): يقوم المقسم بعملية التقسيم الفعلية. حيث يقوم بتفكيك النص بناءً على الحدود التي حددها الموجه. والأهم من ذلك، يقوم المقسم بإرفاق البيانات الوصفية الموروثة بكل جزء ناتج. ويقوم بإضافة نصوص سياقية مباشرة في بداية نص الجزء حتى يلتقط نموذج التضمين الثقل الدلالي الكامل.
بدلاً من توليد:
The maximum timeout is 30 seconds.
يقوم المقسم السياقي بتوليد:
Document: API Gateway Documentation | Section: Rate Limiting | The maximum timeout is 30 seconds.
يضمن هذا التحول المعماري أن يكون كل جزء مكتفياً ذاتياً ومكتملاً دلالياً. عندما تجري قاعدة بيانات المتجهات بحثاً عن تشابه جيب التمام (cosine similarity search), فإنها تطابق السياق الكامل، وليس فقط شظية معزولة.
التنفيذ باستخدام Python 3.11 و LangChain
دعنا نبني هذا. سنستخدم Python 3.11 وإصدارات محددة من نظام LangChain لضمان نتائج قابلة للتكرار.
أولاً، حدد التبعيات الخاصة بك في ملف requirements.txt:
langchain==0.2.14
langchain-text-splitters==0.2.2
unstructured==0.15.0
pydantic==2.8.2
سنقوم بتنفيذ مقسم عناوين Markdown مخصص يقوم بحقن السياق الهرمي في كل جزء. توفر LangChain أداة MarkdownHeaderTextSplitter، ولكننا بحاجة إلى تغليفها لضمان فرض البيانات الوصفية الصارمة ومعالجة الحالات الاحتياطية.
import logging
from typing import List, Dict, Any
from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
from langchain_core.documents import Document
from pydantic import BaseModel, Field
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class ChunkingConfig(BaseModel):
chunk_size: int = Field(default=1500, description="Max character size as a fallback")
chunk_overlap: int = Field(default=150, description="Overlap for fallback splitting")
headers_to_split_on: List[tuple[str, str]] = Field(
default_factory=lambda: [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
)
class AdvancedRAGChunker:
"""
Implements deterministic, semantic chunking based on Markdown headers,
falling back to recursive splitting for massive sections.
"""
def __init__(self, config: ChunkingConfig):
self.config = config
self.markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=self.config.headers_to_split_on,
strip_headers=False,
)
# Fallback splitter for sections that exceed the maximum size
self.fallback_splitter = RecursiveCharacterTextSplitter(
chunk_size=self.config.chunk_size,
chunk_overlap=self.config.chunk_overlap,
separators=["\n\n", "\n", ".", " ", ""],
keep_separator=True
)
def process_document(self, markdown_text: str, global_metadata: Dict[str, Any]) -> List[Document]:
"""
Splits markdown text based on headers and injects context.
"""
logger.info("Starting semantic chunking process.")
# Step 1: Split strictly by logical headers
header_splits = self.markdown_splitter.split_text(markdown_text)
final_chunks: List[Document] = []
for doc in header_splits:
# Inject global metadata
doc.metadata.update(global_metadata)
# Construct a context prefix based on the header hierarchy
context_prefix = self._build_context_prefix(doc.metadata)
# Step 2: Handle oversized sections
if len(doc.page_content) > self.config.chunk_size:
logger.warning(f"Oversized chunk detected. Falling back to recursive splitting.")
sub_chunks = self.fallback_splitter.split_documents([doc])
for sub_chunk in sub_chunks:
sub_chunk.page_content = f"{context_prefix}\n{sub_chunk.page_content}"
final_chunks.append(sub_chunk)
else:
doc.page_content = f"{context_prefix}\n{doc.page_content}"
final_chunks.append(doc)
logger.info(f"Generated {len(final_chunks)} contextual chunks.")
return final_chunks
def _build_context_prefix(self, metadata: Dict[str, Any]) -> str:
"""Constructs a dense semantic prefix string."""
parts = []
if "source" in metadata:
parts.append(f"Source: {metadata['source']}")
headers = [metadata.get(f"Header {i}") for i in range(1, 4) if metadata.get(f"Header {i}")]
if headers:
parts.append(f"Section Path: {' > '.join(headers)}")
return " | ".join(parts) if parts else "Context: General"
# Example Usage
if __name__ == "__main__":
raw_markdown = """
# Platform Authentication
This document outlines the authentication protocols.
## OAuth2 Flow
The OAuth2 flow requires a client ID and a secret.
Tokens expire after 3600 seconds.
## Single Sign-On (SSO)
We support SAML 2.0 and OpenID Connect for enterprise customers.
"""
config = ChunkingConfig()
chunker = AdvancedRAGChunker(config)
chunks = chunker.process_document(
markdown_text=raw_markdown,
global_metadata={"source": "engineering_docs.md", "version": "v1.2"}
)
for i, chunk in enumerate(chunks):
print(f"\n--- Chunk {i} ---")
print(chunk.page_content)
print("Metadata:", chunk.metadata)
يضمن كود Python 3.11 هذا أن تكون الأجزاء الخاصة بك محددة بمنطق دلالي. تحترم أداة MarkdownHeaderTextSplitter حدود H1/H2. نحافظ على خيار strip_headers=False ليبقى نص العنوان الفعلي ضمن المحتوى.
والأهم من ذلك، تقوم دالة _build_context_prefix بإضافة المسار الهيكلي مباشرة في النص نفسه. إذا تم عزل قسم "OAuth2 Flow"، فإن نموذج LLM سيظل يقرأ Source: engineering_docs.md | Section Path: Platform Authentication > OAuth2 Flow في الجزء العلوي من الجزء. يولد نموذج التضمين متجهاً يربط هذا النص صراحة بنطاق المصادقة، مما يمنع عزل السياق في قاعدة بيانات المتجهات الخاصة بك.
نقوم أيضاً بتنفيذ تراجع صارم (fallback) باستخدام RecursiveCharacterTextSplitter. إذا كان طول قسم واحد تحت وسم H2 هو 5000 حرف، فلا يمكننا تغذية نموذج التضمين به كاملاً. يعالج التراجع هذه الحالات الخاصة عن طريق تقسيم القسم الزائد عن الحد مع حقن بادئة السياق في كل جزء فرعي ناتج.
أخطاء فادحة يجب تجنبها
حتى مع وجود بنية قوية، تقع فرق الهندسة البرمجية عادةً في عدة فخاخ عند تحليل البيانات وتقسيمها.
أولاً، الاعتماد على استخراج نصوص PDF الخام يعد مساراً مسدوداً. لا تستخدم PyPDF2 لتفريغ سلاسل النصوص وتغذيتها مباشرة في LangChain. جودة الاستخراج رديئة للغاية؛ سينتهي بك الأمر بكلمات متداخلة، وجمل مكسورة، وأحرف سطر جديد غير مرئية. استخدم دائماً واجهة برمجة تطبيقات (API) مخصصة للتحليل أو أنبوب OCR لتحويل ملفات PDF إلى Markdown نظيف أولاً. تحدد خطوة التحليل الأولية سقف موثوقية تطبيق RAG بالكامل.
ثانياً، تجنب أحجام الأجزاء الصغيرة جداً. يقوم العديد من المطورين بتعيين chunk_size=250 رغبةً في استرجاع فائق الدقة. يؤدي هذا إلى نتائج عكسية؛ تفتقر الأجزاء الصغيرة إلى السياق الكافي لنموذج التضمين لاستيعاب المعنى الدلالي. وتنتج عنها كثافة عالية للكلمات الرئيسية ولكن كثافة دلالية منخفضة. قد يطابق الاستعلام الكلمات الدقيقة في جزء صغير، لكن هذا الجزء لن يحتوي على معلومات محيطة كافية لصياغة إجابة متماسكة. استهدف أحجام الأجزاء بين 800 و 1500 حرف، مع الاعتماد على نافذة السياق الواسعة لنموذج LLM لتصفية الضوضاء أثناء مرحلة التوليد.
ثالثاً، إهمال التداخل في أجزاء التراجع الاحتياطية. إذا فشل المقسم الدلالي الأساسي واعتمدت على تقسيم الأحرف، يجب عليك استخدام تداخل سخي (10% إلى 15%). بدون التداخل، فإنك تخاطر بقطع جملة حرجة أو كتلة كود برمجية من المنتصف، مما يجعل كلا الجزأين الناتجين بلا فائدة. يعمل التداخل كجسر يضمن استمرارية السياق.
رابعاً، إهمال استخراج الجداول. من المعروف أن الجداول صعبة التقسيم. سيقوم مقسم النصوص القياسي بتمزيق جدول Markdown سطراً بسطر، مما يؤدي إلى تدمير رؤوس الأعمدة والعلاقة الجدولية. إذا كان مستندك يحتوي على جداول ضخمة، فيجب عليك تنفيذ مسار تحليل منفصل يستخرج الجدول ككائن JSON منظم أو تلخيصه باستخدام استدعاء LLM خفيف قبل تضمينه. لا تعامل الجدول أبداً كفقرة عادية.
النتيجة النهائية: الدقة على نطاق واسع
يحول تطبيق استراتيجيات تقسيم RAG المتقدمة النموذج الأولي الهش والمعرض للهلوسة إلى نظام إنتاجي مرن.
من خلال الانتقال من حدود الأحرف العشوائية إلى الحدود الدلالية الحتمية، فإنك تضمن أن كل معلومة مخزنة في قاعدة بيانات المتجهات الخاصة بك سليمة هيكلياً وسياقياً. تصبح متجهات التضمين أكثر دقة، وتتوقف خطوة الاسترجاع عن إرجاع أجزاء غير ذات صلة، وتتلقى خطوة التوليد فرضية متماسكة.
يتطلب هذا النهج مزيداً من الهندسة المسبقة. فكتابة موجهات Python 3.11 مخصصة، وإدارة تحويل Markdown، ومعالجة حقن البيانات الوصفية أصعب بكثير من استدعاء دالة افتراضية واحدة. لكن النتائج تتحدث عن نفسها؛ حيث تلغي الحلقة اللانهائية من حيل هندسة الأوامر (prompt engineering) التي تهدف للتعويض عن الاسترجاع السيئ. وتبني نظاماً يمكنه تصفح 10,000 صفحة من البيانات غير المنظمة بدقة وإعادة إجابة دقيقة وقابلة للتحقق في كل مرة.
توقف عن تقطيع بياناتك إلى أجزاء عشوائية. ابدأ باحترام هيكل مستنداتك، وسيقدم تطبيق RAG الخاص بك أخيراً الموثوقية التي يطلبها مستخدموك.
خدمة سفن لابس
تطوير وكلاء الذكاء الاصطناعي ومسارات RAG

