الدرس 11 من 23

استراتيجيات التقطيع المتقدمة

تصميم القطع الأمثل

3 دقيقة للقراءة

حجم القطعة، التداخل، والتنسيق يؤثرون بشكل كبير على جودة الاسترجاع. إليك كيفية تحسين هذه المعلمات لحالة استخدامك.

إرشادات حجم القطعة

الحجم (رموز) حالة الاستخدام المقايضات
128-256 أسئلة وأجوبة، إجابات دقيقة دقة عالية، قد يفتقد السياق
256-512 RAG عام (موصى به) دقة/سياق متوازن
512-1024 طويل، مواضيع معقدة سياق أكثر، دقة أقل
1024+ سياق المستند الكامل خطر الضوضاء، مكلف

إيجاد الحجم الأمثل

اختبر أحجام مختلفة تجريبياً:

def evaluate_chunk_size(documents, queries, ground_truth, sizes=[256, 512, 768, 1024]):
    """تقييم جودة الاسترجاع عبر أحجام القطع."""
    results = {}

    for size in sizes:
        # إنشاء القطع
        splitter = RecursiveCharacterTextSplitter(
            chunk_size=size,
            chunk_overlap=int(size * 0.1)
        )
        chunks = splitter.split_documents(documents)

        # الفهرسة
        vectorstore = Chroma.from_documents(chunks, embeddings)

        # التقييم
        recall = calculate_recall(vectorstore, queries, ground_truth)
        precision = calculate_precision(vectorstore, queries, ground_truth)

        results[size] = {
            "recall": recall,
            "precision": precision,
            "f1": 2 * (recall * precision) / (recall + precision),
            "num_chunks": len(chunks)
        }

    return results

# مثال الإخراج:
# 256: {"recall": 0.82, "precision": 0.71, "f1": 0.76}
# 512: {"recall": 0.78, "precision": 0.79, "f1": 0.78}  <- أفضل F1
# 768: {"recall": 0.74, "precision": 0.82, "f1": 0.78}
# 1024: {"recall": 0.68, "precision": 0.85, "f1": 0.75}

استراتيجية التداخل

التداخل يمنع فقدان المعلومات عند حدود القطع:

# قاعدة عامة: 10-20% تداخل
chunk_size = 512
chunk_overlap = 50  # ~10%

# للمحتوى التقني مع مراجع متقاطعة
chunk_overlap = 100  # ~20%

# تمثيل بصري:
# القطعة 1: [===================]
# القطعة 2:              [===================]
# القطعة 3:                       [===================]
#                       ^تداخل^

متى تزيد التداخل:

  • التوثيق التقني مع مراجع متقاطعة
  • النص القانوني مع عبارات تابعة
  • الكود مع عبارات متعددة الأسطر

الحفاظ على التنسيق

الحفاظ على هيكل المستند في القطع:

class FormatPreservingChunker:
    """الحفاظ على تنسيق المستند في القطع."""

    def chunk(self, document: str) -> list[dict]:
        chunks = []

        # اكتشاف تنسيق المستند
        if self._is_markdown(document):
            chunks = self._chunk_markdown(document)
        elif self._is_code(document):
            chunks = self._chunk_code(document)
        else:
            chunks = self._chunk_plain(document)

        return chunks

    def _chunk_markdown(self, document: str) -> list[dict]:
        """حافظ على عناوين markdown مع محتواها."""
        sections = re.split(r'(^#{1,3}\s.+$)', document, flags=re.MULTILINE)
        chunks = []
        current_header = ""

        for section in sections:
            if re.match(r'^#{1,3}\s', section):
                current_header = section.strip()
            elif section.strip():
                chunks.append({
                    "content": f"{current_header}\n\n{section.strip()}",
                    "header": current_header,
                    "type": "markdown"
                })

        return chunks

    def _chunk_code(self, document: str) -> list[dict]:
        """قسم الكود حسب الدوال/الفئات."""
        chunks = []

        # نمط دالة Python
        functions = re.findall(
            r'((?:def|class)\s+\w+.*?(?=\n(?:def|class)|\Z))',
            document,
            re.DOTALL
        )

        for func in functions:
            chunks.append({
                "content": func.strip(),
                "type": "code",
                "language": "python"
            })

        return chunks

استراتيجيات خاصة بالمحتوى

الجداول

حافظ على الجداول سليمة:

def preserve_tables(document: str) -> str:
    """علّم الجداول لمنع التقسيم."""
    # ابحث عن جداول markdown
    table_pattern = r'(\|.+\|(?:\n\|.+\|)+)'
    tables = re.findall(table_pattern, document)

    # استبدل بعناصر نائبة
    for i, table in enumerate(tables):
        document = document.replace(table, f"[[TABLE_{i}]]")

    return document, tables

def restore_tables(chunks: list[str], tables: list[str]) -> list[str]:
    """استعد الجداول في القطع."""
    restored = []
    for chunk in chunks:
        for i, table in enumerate(tables):
            chunk = chunk.replace(f"[[TABLE_{i}]]", table)
        restored.append(chunk)
    return restored

كتل الكود

لا تقسم كتل الكود أبداً:

def chunk_with_code_preservation(document: str, chunk_size: int) -> list[str]:
    """قطّع مع الحفاظ على كتل الكود."""
    # استخرج كتل الكود
    code_pattern = r'```[\s\S]*?```'
    code_blocks = re.findall(code_pattern, document)

    # استبدل بعناصر نائبة
    for i, block in enumerate(code_blocks):
        document = document.replace(block, f"[[CODE_{i}]]")

    # قسم النص
    splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size)
    chunks = splitter.split_text(document)

    # استعد كتل الكود
    restored = []
    for chunk in chunks:
        for i, block in enumerate(code_blocks):
            chunk = chunk.replace(f"[[CODE_{i}]]", block)
        restored.append(chunk)

    return restored

قائمة فحص الجودة

الفحص الهدف الاختبار
الاكتمال لا جمل مبتورة تحقق من نهايات القطع
السياق القطع مكتفية ذاتياً هل يمكنك الفهم بدون النص المحيط؟
الصلة محتوى مركّز موضوع واحد لكل قطعة
التنسيق الهيكل محفوظ الجداول، الكود، القوائم سليمة

أداة التشخيص

def diagnose_chunks(chunks: list[str]) -> dict:
    """تحليل جودة القطع."""
    diagnostics = {
        "total_chunks": len(chunks),
        "avg_length": sum(len(c) for c in chunks) / len(chunks),
        "issues": []
    }

    for i, chunk in enumerate(chunks):
        # تحقق من الجمل المبتورة
        if chunk[-1] not in '.!?"\'':
            diagnostics["issues"].append(f"القطعة {i}: قد تكون مبتورة")

        # تحقق من المراجع اليتيمة
        if re.search(r'\b(it|this|these|that)\b', chunk[:50]):
            diagnostics["issues"].append(f"القطعة {i}: تبدأ بمرجع ضمير")

        # تحقق من سلامة كتلة الكود
        if chunk.count('```') % 2 != 0:
            diagnostics["issues"].append(f"القطعة {i}: كتلة كود مكسورة")

        # تحقق من تباين الطول
        if len(chunk) < diagnostics["avg_length"] * 0.3:
            diagnostics["issues"].append(f"القطعة {i}: قصيرة جداً ({len(chunk)} حرف)")

    return diagnostics

الافتراضيات الموصى بها

# RAG للأغراض العامة
DEFAULT_CONFIG = {
    "chunk_size": 512,
    "chunk_overlap": 50,
    "separators": ["\n\n", "\n", ". ", " ", ""],
    "preserve_code_blocks": True,
    "preserve_tables": True
}

# أسئلة وأجوبة / FAQ
QA_CONFIG = {
    "chunk_size": 256,
    "chunk_overlap": 25,
    "separators": ["\n\n", "\n", "? ", ". ", " "],
    "keep_qa_pairs": True
}

# التوثيق التقني
TECHNICAL_CONFIG = {
    "chunk_size": 768,
    "chunk_overlap": 100,
    "separators": ["\n## ", "\n### ", "\n\n", "\n", ". "],
    "preserve_code_blocks": True,
    "include_headers": True
}

مبدأ رئيسي: تصميم القطع الأمثل يعتمد على استعلاماتك، وليس مستنداتك. صمم القطع لتطابق كيف سيبحث المستخدمون، وليس كيف يُنظم المحتوى.

في الوحدة التالية، سنستكشف البحث الهجين وإعادة الترتيب لتعظيم جودة الاسترجاع. :::

اختبار

الوحدة 3: استراتيجيات التقطيع المتقدمة

خذ الاختبار