0Pricing
AI Engineering Academy · درس

الضغط السياقي وتصفية الصلة

طبّقوا الضغط السياقي لإزالة الجمل غير ذات الصلة من المقاطع المسترجعة قبل تمريرها إلى LLM، مما يقلل الضوضاء ويوفر الرموز.

الضغط السياقي وتصفية الصلة درس مجاني في AI Engineering Academy على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في AI Engineering Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة AI Engineering Academy 4 دروس في المجموع.

مشكلة المقاطع المسترجعة المليئة بالضوضاء

غالبًا ما تحتوي المقاطع المسترجعة على محتوى متفاوت الصلة. فقد يجيب مقطع من 500 رمزًا مميزًا حول فهرسة قواعد البيانات عن أول جملتين من الاستعلام، لكنه يحتوي على ست جمل غير مرتبطة تتناول إجراءات النسخ الاحتياطي. يؤدي إرسال المقطع بأكمله إلى LLM إلى هدر الرموز المميزة، وخفض نسبة الإشارة إلى الضوضاء، وقد يتسبب في إنشاء النموذج إجابة تستند إلى الجزء غير ذي الصلة بدلًا من الجمل ذات الصلة.

ما هو الضغط السياقي؟

إن الضغط السياقي خطوة لاحقة للاسترجاع، تأخذ كل مقطع مسترجع وتستخرج الجمل ذات الصلة بالاستعلام فقط قبل تمرير المقطع إلى LLM. ويُضغط المقطع الأصلي إلى أجزائه الأكثر صلة، مما يقلل استخدام الرموز المميزة ويحسّن جودة الإجابة. وتغلف ContextualCompressionRetriever في LangChain أي مسترجع بمكوّن ضغط ينفذ عملية الاستخراج هذه.

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain_openai import ChatOpenAI

# LLMChainExtractor uses an LLM to extract the relevant portion
llm = ChatOpenAI(model='gpt-4o-mini', temperature=0)
compressor = LLMChainExtractor.from_llm(llm)

compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=base_retriever,
)

results = compression_retriever.invoke('how does HNSW indexing work?')
for doc in results:
    print(len(doc.page_content), 'chars:', doc.page_content[:150])

LLMChainFilter: تصفية الصلة

بدلًا من استخراج الجمل من المقاطع، يتخذ LLMChainFilter قرارًا ثنائيًا: هل هذا المقطع ذو صلة بالاستعلام أم لا؟ تُحذف المقاطع غير ذات الصلة بالكامل قبل وصولها إلى LLM. وتُعد هذه العملية أقل تكلفة من الاستخراج، نظرًا إلى قِصر استدعاء LLM، كما تفيد عندما تكون المقاطع قصيرة ومترابطة بما يكفي بحيث لا يحقق الاستخراج الجزئي فائدة. وعادةً ما تُصفّى نسبة تتراوح بين 20 و40 بالمئة من المقاطع المسترجعة مبدئيًا.

from langchain.retrievers.document_compressors import LLMChainFilter

filter_compressor = LLMChainFilter.from_llm(
    llm=ChatOpenAI(model='gpt-4o-mini', temperature=0)
)

filtering_retriever = ContextualCompressionRetriever(
    base_compressor=filter_compressor,
    base_retriever=base_retriever,
)

# Fetch 10 docs, filter drops irrelevant ones
results = filtering_retriever.invoke('what are the pgvector distance operators?')
print(f'{len(results)} chunks passed the relevance filter')

تصفية الصلة بالاعتماد على Embeddings

يؤدي استخدام LLM للتصفية إلى إضافة زمن استجابة وتكلفة. والبديل الأقل تكلفة هو التصفية المعتمدة على Embeddings، التي تحسب تشابه جيب التمام بين تضمين الاستعلام وتضمين كل مقطع، ثم تحذف المقاطع التي تقل عن عتبة التشابه. وهذه العملية حتمية وسريعة ومجانية — إذ تعيد استخدام التضمينات التي حُسبت أثناء الاسترجاع من دون استدعاء إضافي لـ LLM.

from langchain.retrievers.document_compressors import EmbeddingsFilter
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings()
embeddings_filter = EmbeddingsFilter(
    embeddings=embeddings,
    similarity_threshold=0.76,  # cosine similarity threshold
)

embedding_retriever = ContextualCompressionRetriever(
    base_compressor=embeddings_filter,
    base_retriever=base_retriever,
)

results = embedding_retriever.invoke('BM25 hyperparameter tuning')
print(f'Filtered to {len(results)} relevant chunks')

تسلسل عدة ضواغط

يمكنك تسلسل عدة ضواغط بالترتيب باستخدام DocumentCompressorPipeline. ومن الأنماط الشائعة تطبيق مرشح سريع قائم على Embeddings أولًا لإزالة المقاطع غير ذات الصلة بوضوح، ثم تقسيم الجمل لتجزئة المقاطع الطويلة إلى جمل، وأخيرًا تطبيق استخراج قائم على LLM لجلب الجمل الأكثر صلة. ويوازن هذا النهج متعدد الطبقات بين التكلفة والدقة.

from langchain.retrievers.document_compressors import DocumentCompressorPipeline
from langchain_community.document_transformers import EmbeddingsRedundantFilter
from langchain_text_splitters import CharacterTextSplitter

# Step 1: split chunks into individual sentences
sentence_splitter = CharacterTextSplitter(
    chunk_size=200,
    chunk_overlap=0,
    separator='. ',
)

# Step 2: remove redundant sentences via embeddings
redundant_filter = EmbeddingsRedundantFilter(embeddings=OpenAIEmbeddings())

# Step 3: keep only relevant sentences
relevance_filter = EmbeddingsFilter(embeddings=OpenAIEmbeddings(), similarity_threshold=0.76)

pipeline = DocumentCompressorPipeline(
    transformers=[sentence_splitter, redundant_filter, relevance_filter]
)

piped_retriever = ContextualCompressionRetriever(
    base_compressor=pipeline,
    base_retriever=base_retriever,
)

إزالة المقاطع المكررة

عندما تقول عدة مقاطع مسترجعة الشيء نفسه تقريبًا، فإن إرسالها جميعًا إلى LLM يهدر الرموز المميزة من دون إضافة معلومات. يزيل EmbeddingsRedundantFilter المقاطع شبه المكررة عبر حساب تشابه جيب التمام بين كل زوج، ثم حذف المقاطع شديدة التشابه مع المقاطع المحددة مسبقًا. ويكتسب هذا أهمية خاصة عندما تحتوي مجموعة المستندات على مقاطع متداخلة كثيرة بسبب إعدادات التداخل المرتفعة أثناء الإدخال.

from langchain_community.document_transformers import EmbeddingsRedundantFilter
from langchain_core.documents import Document

redundant_filter = EmbeddingsRedundantFilter(
    embeddings=OpenAIEmbeddings(),
    similarity_threshold=0.95,  # treat docs with >95% cosine sim as duplicates
)

# Simulate near-duplicate documents
docs = [
    Document(page_content='pgvector is a PostgreSQL extension for vector similarity search'),
    Document(page_content='pgvector extends PostgreSQL to support vector similarity search'),  # near duplicate
    Document(page_content='HNSW and IVFFlat are the two index types in pgvector'),
]

filtered = redundant_filter.transform_documents(docs, query='')
print(f'Kept {len(filtered)} of {len(docs)} documents after deduplication')

استخراج الصلة على مستوى الجملة

بالنسبة إلى المستندات الطويلة التي يتوزع فيها المحتوى ذي الصلة، يوفر الاستخراج على مستوى الجملة باستخدام cross-encoder أعلى دقة. قيّم كل جملة في المقطع المسترجع مقابل الاستعلام، واحتفظ فقط بالجمل التي تتجاوز عتبة معينة، ثم أعد بناء مستند مضغوط. ويقلل هذا النهج حجم السياق عادةً بنسبة تتراوح بين 40 و70 بالمئة، مع الحفاظ على جميع الجمل ذات الصلة.

from sentence_transformers import CrossEncoder
import re

sentence_reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')

def compress_with_cross_encoder(
    query: str, chunk: str, min_score: float = 0.0
) -> str:
    sentences = re.split(r'(?<=[.!?]) +', chunk)
    if len(sentences) <= 1:
        return chunk

    pairs = [[query, s] for s in sentences]
    scores = sentence_reranker.predict(pairs)

    relevant = [
        sentence
        for sentence, score in zip(sentences, scores)
        if float(score) >= min_score
    ]
    return ' '.join(relevant) if relevant else chunk

إدارة ميزانية الرموز المميزة عبر الضغط

يمثل الضغط السياقي أيضًا أداة فعالة لـإدارة ميزانية الرموز المميزة. فإذا أردت تمرير 5 مستندات إلى نموذج ذي نافذة سياق محدودة، فإن ضغط كل مستند من 500 رمز مميز إلى 100 رمز مميز ذي صلة يتيح لك ملاءمة معلومات أكثر بخمسة أضعاف. ويكتسب هذا أهمية خاصة عند العمل مع نماذج أصغر وأسرع مثل GPT-4o-mini، التي تمتلك نوافذ سياق أقصر ومتطلبات أكثر صرامة لزمن الاستجابة.

def compress_to_budget(
    query: str,
    docs: list[str],
    total_token_budget: int = 2000,
    tokens_per_char: float = 0.25,
) -> list[str]:
    compressed = []
    used_tokens = 0

    for doc in docs:
        compressed_doc = compress_with_cross_encoder(query, doc)
        doc_tokens = int(len(compressed_doc) * tokens_per_char)

        if used_tokens + doc_tokens <= total_token_budget:
            compressed.append(compressed_doc)
            used_tokens += doc_tokens
        else:
            break  # stop when budget is exhausted

    print(f'Using {used_tokens} tokens across {len(compressed)} docs')
    return compressed

قياس جودة الضغط

ينطوي الضغط على خطر: فقد تحذف عن طريق الخطأ جملة مهمة للإجابة عن الاستعلام. قِس جودة الضغط بمقارنة درجات الأمانة قبل الضغط وبعده باستخدام RAGAS أو مُقيّم مخصص قائم على LLM يتحقق مما إذا كان السياق المضغوط لا يزال يدعم الإجابة الصحيحة. وإذا انخفضت الأمانة بعد الضغط، فاخفض العتبة أو انتقل إلى الاستخراج بدلًا من التصفية.

def measure_compression_faithfulness(query, original_docs, compressed_docs, llm):
    # Use LLM to check if answer from compressed context matches
    # answer from full context
    def generate_answer(docs, q):
        context = '\n'.join(docs)
        resp = llm.invoke(f'Answer from context:\n{context}\nQ: {q}')
        return resp.content

    full_answer = generate_answer([d.page_content for d in original_docs], query)
    comp_answer = generate_answer(compressed_docs, query)

    # Check semantic similarity between answers
    vecs = [embed(full_answer), embed(comp_answer)]
    sim = cosine_similarity(vecs[0], vecs[1])
    print(f'Answer similarity after compression: {sim:.3f}')
    return sim

متى تتجاوز الضغط؟

لا يكون الضغط السياقي مفيدًا دائمًا. ففي حالة المقاطع القصيرة والمترابطة (الأقل من 200 رمز مميز)، يكون المقطع بأكمله ذا صلة عادةً، ولا يضيف الضغط سوى زمن استجابة. أما في الوثائق التقنية التي تعتمد فيها الإجابة على جميع أجزاء الإجراء، مثل تسلسل الخطوات المرقمة، فقد تؤدي تصفية الجمل الفردية إلى حذف خطوات مهمة. طبّق الضغط بحذر، وتحقق دائمًا من أنه يحسّن جودة الإجابة في حالة الاستخدام الخاصة بك.

مسار ضغط جاهز للإنتاج

يجمع مسار الضغط المتين في بيئة الإنتاج بين التصفية المعتمدة على Embeddings (سريعة ومنخفضة التكلفة)، وإزالة التكرار (لتجنب تكرار المعلومات نفسها)، واستخراج الجمل اختياريًا باستخدام cross-encoder (دقيق لكنه أبطأ). شغّل المراحل السريعة أولًا، ولا تطبّق المرحلة المكلفة المعتمدة على LLM إلا على الاستعلامات عالية القيمة أو عندما تترك المراحل السريعة عددًا كبيرًا جدًا من المقاطع. يحسّن هذا النهج التكيفي التكلفة والجودة معًا.

class AdaptiveCompressor:
    def __init__(self, embeddings, cross_encoder=None, threshold=0.76):
        self.emb_filter = EmbeddingsFilter(embeddings=embeddings,
                                           similarity_threshold=threshold)
        self.dedup = EmbeddingsRedundantFilter(embeddings=embeddings)
        self.cross_encoder = cross_encoder

    def compress(self, query: str, docs, use_cross_encoder: bool = False):
        # Stage 1: embedding filter
        docs = self.emb_filter.compress_documents(docs, query=query)
        # Stage 2: deduplication
        docs = self.dedup.transform_documents(docs, query=query)
        # Stage 3: optional sentence-level extraction
        if use_cross_encoder and self.cross_encoder:
            docs = [
                type(d)(page_content=compress_with_cross_encoder(
                    query, d.page_content
                ), metadata=d.metadata)
                for d in docs
            ]
        return docs

اختبار سريع

اختبر مدى فهمك للضغط السياقي مما تعلّمته في هذا الدرس.

مراجعة الدرس

في هذا الدرس تعلّمتم أن الضغط السياقي يقلّل الضوضاء من خلال استخراج المحتوى غير ذي الصلة أو تصفيته من المقاطع المسترجعة قبل وصولها إلى LLM، وأن DocumentCompressorPipeline يربط عدة ضواغط (التصفية وإزالة التكرار والاستخراج) بالتتابع، وأن التصفية القائمة على التضمينات توفّر بديلاً سريعاً ومنخفض التكلفة للضغط القائم على LLM في معظم السيناريوهات. يساعد الضغط في إدارة ميزانية الرموز ويحسّن جودة الإجابات. بعد ذلك سنقيس التأثير الفعلي لإعادة الترتيب في جودة الاسترجاع.

الأسئلة الشائعة

هل درس «الضغط السياقي وتصفية الصلة» مجاني؟

نعم — نص درس «الضغط السياقي وتصفية الصلة» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة AI Engineering Academy، انتقل إلى CoddyKit PRO. تتضمن دورة AI Engineering Academy 4 دروس في المجموع.

ماذا ستتعلم في «الضغط السياقي وتصفية الصلة»؟

طبّقوا الضغط السياقي لإزالة الجمل غير ذات الصلة من المقاطع المسترجعة قبل تمريرها إلى LLM، مما يقلل الضوضاء ويوفر الرموز. تتمرن على AI Engineering Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ AI Engineering Academy؟

لا تُشترط خبرة سابقة. AI Engineering Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.

كم من الوقت يستغرق درس «الضغط السياقي وتصفية الصلة»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس AI Engineering Academy هذا؟

نعم. كل درس في AI Engineering Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. لماذا ينجح الاسترجاع على مرحلتين
  2. إعادة الترتيب باستخدام Cross-Encoder مع Cohere وBGE
  3. الضغط السياقي وتصفية الصلة
  4. قياس أثر إعادة الترتيب
← العودة إلى AI Engineering Academy