مشروع RAG مصغر: سكربت Python يتيح لك “الدردشة” مع كتاب PDF

دقائق القراءة: 6

مشروع RAG مصغر: كيف تجعل سكربت Python “يتحدث” مع كتاب PDF

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

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

المعمارية المختصرة للتطبيق

التطبيق يتكون من أربع طبقات مترابطة، وكل طبقة لها وظيفة محددة:

  • طبقة الاستخراج: قراءة محتوى PDF وتحويله إلى نص خام.
  • طبقة التقسيم: استخدام Text Splitters لتقطيع المستند إلى أجزاء مناسبة.
  • طبقة الفهرسة: تحويل كل جزء إلى Vector وتخزينه داخل قاعدة مثل ChromaDB.
  • طبقة التوليد: استرجاع المقاطع الأقرب عبر Retriever ودمجها مع السؤال لإرسالها إلى النموذج.

هذا الفصل مهم جداً من زاوية هندسية: النموذج نفسه لا “يقرأ الكتاب” بشكل دائم، بل يعتمد على سياق لحظي يتم بناؤه وقت السؤال. لذلك فدقة النتائج لا تعتمد فقط على جودة LLM، بل أيضاً على جودة التقسيم والاسترجاع والصياغة.

المكتبات المطلوبة وخط التجهيز

قبل تشغيل المشروع، جهّز البيئة كما شرحنا في إعداد بيئة العمل الذكية، ثم ثبّت المكتبات المناسبة لهذا السيناريو:

pip install langchain langchain-openai langchain-community chromadb pypdf

وسنحتاج أيضاً إلى مفتاح API صالح، ويمكنك العودة إلى مقال جلب مفاتيح API وكتابة أول سكربت اتصال إذا أردت ترتيب الإعدادات بشكل سليم.

بناء السكربت الكامل خطوة بخطوة

1) تحميل الكتاب واستخراج الصفحات

هنا نستخدم محمّل مستندات يقرأ صفحات الملف ويحوّل كل صفحة إلى كائن قابل للمعالجة. هذه الخطوة ترتبط مباشرة بما شرحناه في استخراج النصوص من ملفات PDF.

2) تقسيم الصفحات إلى مقاطع متداخلة

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

3) إنشاء التضمينات وتخزينها

بعد التقسيم، نمرر المقاطع إلى نموذج Embeddings، ثم نحفظ الناتج داخل قاعدة متجهة محلية. إذا أردت فهماً أعمق لهذا المستوى، راجع تحويل قطع النصوص إلى Vectors وتخزينها.

4) تنفيذ حلقة محادثة تفاعلية

في كل سؤال، يبحث Retriever عن أفضل المقاطع، ثم نصوغ Prompt يجبر النموذج على الاعتماد على سياق الكتاب فقط، وهي نقطة جوهرية في الخطوة النهائية من RAG.

import os
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma

PDF_PATH = "book.pdf"
CHROMA_DIR = "chroma_store"

def build_vectorstore(pdf_path: str, persist_dir: str):
    loader = PyPDFLoader(pdf_path)
    documents = loader.load()

    splitter = RecursiveCharacterTextSplitter(
        chunk_size=900,
        chunk_overlap=150
    )
    chunks = splitter.split_documents(documents)

    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

    vectorstore = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        persist_directory=persist_dir
    )
    return vectorstore

def load_vectorstore(persist_dir: str):
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    return Chroma(
        persist_directory=persist_dir,
        embedding_function=embeddings
    )

def ask_book(vectorstore, question: str):
    retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
    docs = retriever.get_relevant_documents(question)

    context = "\n\n".join([doc.page_content for doc in docs])

    prompt = f"""
You are a helpful assistant answering questions about a PDF book.
Use only the context below.
If the answer is not found in the context, say clearly that the information is not available in the book.

Context:
{context}

Question:
{question}
"""

    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
    response = llm.invoke(prompt)
    return response.content, docs

def main():
    if not os.path.exists(CHROMA_DIR):
        print("Building vector database from PDF...")
        vectorstore = build_vectorstore(PDF_PATH, CHROMA_DIR)
    else:
        print("Loading existing vector database...")
        vectorstore = load_vectorstore(CHROMA_DIR)

    print("Chat with your PDF. Type 'exit' to quit.")

    while True:
        question = input("\nYour question: ").strip()
        if question.lower() == "exit":
            break

        answer, docs = ask_book(vectorstore, question)

        print("\nAnswer:\n")
        print(answer)

        print("\nSources:\n")
        for i, doc in enumerate(docs, 1):
            page = doc.metadata.get("page", "unknown")
            print(f"{i}. Page: {page}")

if __name__ == "__main__":
    main()

كيف يتدفق السؤال داخل النظام؟

عند تشغيل السكربت لأول مرة، يتم إنشاء فهرس كامل للكتاب. هذه العملية قد تكون مكلفة زمنياً، لكنها تحدث مرة واحدة فقط. بعد ذلك تصبح بنية التطبيق كالتالي:

  • المستخدم يكتب سؤالاً.
  • السؤال يتحول ضمنياً إلى تمثيل دلالي عبر نموذج Embeddings.
  • قاعدة Chroma تبحث عن أقرب المقاطع دلالياً.
  • المقاطع المسترجعة تُحقن داخل Prompt.
  • النموذج ينتج جواباً مستنداً إلى السياق، لا إلى ذاكرته العامة فقط.

هذا التدفق يعالج مشكلتين شائعتين: حدود السياق Context Window، ومشكلة الهلوسة. كما أنه يقلل عدد Tokens لأنك لا ترسل إلا المقاطع المرجحة، لا الكتاب كاملاً.

صياغة البرومبت بشكل هندسي صحيح

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

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

هذا النمط قريب من مبادئ هندسة الأوامر داخل الكود، خصوصاً عندما نبني النص بشكل ديناميكي باستخدام F-Strings.

تحسينات عملية على النسخة المصغرة

السكربت السابق تعليمي وعملي، لكنه قابل للتطوير سريعاً:

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

أخطاء شائعة يجب الانتباه لها

  • اختيار chunk_size كبير جداً، فيضعف دقة الاسترجاع.
  • اختيار حجم صغير جداً، فيؤدي إلى فقدان السياق المعنوي.
  • استخدام temperature مرتفع عند الإجابة على أسئلة معرفية؛ والأفضل غالباً القيمة 0 أو قيمة منخفضة، كما ناقشنا في إعداد معاملات الإبداع.
  • نسيان تنظيف النصوص أو التعامل مع ملفات PDF الرديئة التي تحتوي على كسر في السطور أو ترميز غير مستقر.

الخلاصة الهندسية

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

هذا السكربت الصغير هو نقطة انطلاق ممتازة لبناء مساعد بحث داخلي، قارئ عقود، أو محرك أسئلة وأجوبة تقني فوق مكتبة وثائق كاملة. والأهم أنه يضعك عملياً في قلب هندسة تطبيقات Generative AI الحديثة.

5 comments

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *