عندما تكذب عليك نماذج اللغة الكبيرة مثل ChatGPT وتخترع حقائق من العدم، يأتي RAG لإنقاذك. هذا الدليل العملي يفكك لك كيف يعمل RAG خلف الكواليس، متى تستخدمه، وكيف تنقذه من كارثة الهلوسة في الإنتاج.
في أحد المشاريع التي عملت عليها مع فريق ذكاء اصطناعي في شركة ناشئة، كان لدينا نموذج LLM متطور يُجيب على أسئلة العملاء حول الوثائق القانونية. المشكلة؟ النموذج كان يخترع قوانين كاملة من خياله. مثلاً، عندما سألناه عن "مدة صلاحية عقد الإيجار في دبي"، أجاب بثقة مطلقة: "ثلاث سنوات قابلة للتجديد تلقائياً بموجب القانون الاتحادي رقم 26 لسنة 2007". المشكلة أن هذا القانون لا وجود له أصلاً. كان النموذج ببساطة يهلوس (hallucinate) لأنه لا يملك المعرفة الحقيقية في ذاكرة السياق المحدودة. هنا جاء دور RAG (Retrieval Augmented Generation) ليغير قواعد اللعبة. لكن كيف يعمل بالضبط، ومتى يجب أن تفكر فيه بدلاً من مجرد توسيع السياق أو إعادة التدريب؟
الحقيقة المؤلمة هي أن نماذج اللغة الكبيرة ليست قواعد بيانات. حتى لو قمت بتدريبها على تيرابايتات من النصوص، فهي لا "تخزن" المعلومات كما تفعل قواعد البيانات التقليدية. بدلاً من ذلك، تعتمد على احتمالات التوليد بناءً على الأنماط التي رأتها أثناء التدريب. عندما تسأل نموذجاً مثل Llama أو GPT عن معلومة محددة، فهو لا "يبحث" عنها في ذاكرته، بل يولد إجابة بناءً على أقرب نمط إحصائي. هذا هو السبب في أن النماذج تخترع مصادر وأرقاماً وحقائق لم ترها من قبل. RAG يأتي هنا كحل عملي يجمع بين قوة البحث في قواعد البيانات الحقيقية وقوة توليد النصوص للنماذج الكبيرة.
RAG ليس مجرد إضافة قاعدة بيانات للنموذج. إنه نظام كامل يتكون من ثلاث طبقات مترابطة: طبقة البحث (Retrieval)، طبقة التوليد (Generation)، وطبقة الدمج (Augmentation). عندما يأتي سؤال من المستخدم، لا يذهب مباشرة للنموذج كما في الاستدعاءات التقليدية. بدلاً من ذلك، يمر بمراحل معالجة معقدة تبدأ بتحويل السؤال إلى تمثيل رياضي (embedding)، ثم البحث في قاعدة بيانات متجهية (vector database) عن المستندات الأكثر صلة، وأخيراً دمج هذه المستندات في سياق النموذج قبل توليد الإجابة.
الخطوة الأولى هي تحويل السؤال إلى embedding باستخدام نفس النموذج الذي تم استخدامه لتحويل المستندات أثناء الإعداد الأولي. مثلاً، إذا استخدمت نموذج all-MiniLM-L6-v2 لتحويل مستنداتك إلى embeddings، يجب استخدام نفس النموذج لتحويل السؤال. هذا يضمن أن السؤال والمستندات موجودان في نفس الفضاء المتجهي، مما يسمح بقياس التشابه بينهما باستخدام مقاييس مثل cosine similarity. المشكلة هنا أن معظم المطورين الجدد ينسون هذه النقطة الحيوية ويستخدمون نماذج مختلفة، مما يؤدي إلى نتائج بحث سيئة للغاية.
# مثال عملي لتحويل سؤال إلى embedding باستخدام Sentence Transformers
from sentence_transformers import SentenceTransformer
import numpy as np
# تحميل النموذج مسبقاً (يفضل تحميله مرة واحدة وتخزينه في الذاكرة)
model = SentenceTransformer('all-MiniLM-L6-v2')
# تحويل السؤال إلى embedding
query = "ما هي مدة صلاحية عقد الإيجار في دبي؟"
query_embedding = model.encode(query, cTrue)
# الآن يمكنك استخدام هذا Embedding للبحث في قاعدة البيانات المتجهية
# مثلاً باستخدام FAISS أو Pinecone أو Weaviateعندما نتحدث عن قواعد البيانات المتجهية مثل FAISS أو Pinecone، فإننا نتحدث عن أنظمة مصممة خصيصاً للبحث عن التشابه في فضاءات ذات أبعاد عالية (high-dimensional spaces). مثلاً، نموذج all-MiniLM-L6-v2 ينتج embeddings ذات 384 بُعداً. هذا يعني أن كل مستند أو سؤال يتم تحويله إلى نقطة في فضاء رياضي مكون من 384 محور. عندما تقوم بالبحث، فإن النظام يحسب المسافة بين نقطة السؤال ونقاط جميع المستندات المخزنة، ثم يعيد أقرب K نقطة (عادةً بين 3 و10 مستندات).
المشكلة هنا أن البحث في فضاءات عالية الأبعاد ليس سهلاً. حتى مع فهارس متقدمة مثل HNSW (Hierarchical Navigable Small World) المستخدمة في FAISS، فإن البحث قد يصبح بطيئاً جداً إذا كانت قاعدة البيانات كبيرة. مثلاً، في مشروع تعاملت معه، كان لدينا 5 ملايين مستند، وكان البحث يستغرق حوالي 200 مللي ثانية لكل استعلام. هذا قد يبدو سريعاً، لكنه يصبح كارثياً إذا كان لديك آلاف المستخدمين في نفس الوقت. الحل؟ استخدام تقنيات مثل quantization لتقليل حجم المتجهات، أو تقسيم قاعدة البيانات إلى شاردات (shards) موزعة.
# مثال على استخدام FAISS للبحث في قاعدة بيانات متجهية
import faiss
import numpy as np
# افترض أننا لدينا 1000 مستند، كل مستند له embedding بطول 384
# نقوم بإنشاء مصفوفة numpy تحتوي على جميع embeddings
embeddings = np.random.rand(1000, 384).astype('float32') # في الواقع تأتي من النموذج
# إنشاء فهرس FAISS
index = faiss.IndexFlatL2(384) # استخدام المسافة الإقليدية المربعة
index.add(embeddings)
# البحث عن أقرب 5 مستندات للسؤال
D, I = index.search(query_embedding.unsqueeze(0).numpy(), 5)
# D يحتوي على المسافات، I يحتوي على مؤشرات المستندات الأقرب
print("أقرب المستندات:", I)
print("المسافات:", D)بعد استرداد المستندات الأكثر صلة، تأتي مرحلة الدمج والتوليد. هنا تكمن المشكلة الأكبر في RAG: كيف ندمج هذه المستندات في سياق النموذج دون أن نفقد المعلومات المهمة أو نضيع في الضوضاء؟ معظم المطورين ببساطة يرمون المستندات في السياق كما هي، وهذا خطأ فادح. مثلاً، إذا كان لديك 5 مستندات كل منها مكون من 500 كلمة، فأنت تتحدث عن 2500 كلمة إضافية في السياق. هذا قد يتجاوز الحد الأقصى للسياق للنموذج، أو قد يجعل النموذج يركز على المعلومات غير المهمة.
الحل العملي هو استخدام تقنيات مثل reranking لإعادة ترتيب المستندات المستردة بناءً على أهميتها، ثم تلخيص كل مستند قبل إضافته للسياق. مثلاً، يمكنك استخدام نموذج مثل BERT أو Cross-Encoder لإعادة ترتيب المستندات، ثم استخدام نموذج تلخيص مثل BART أو T5 لتلخيص كل مستند إلى 100 كلمة فقط. بهذه الطريقة، تضمن أن السياق يحتوي على المعلومات الأكثر أهمية فقط دون ضوضاء. في أحد المشاريع، استخدمنا هذه التقنية وقمنا بتقليل حجم السياق بنسبة 70% مع الحفاظ على جودة الإجابات.
# مثال على استخدام Cross-Encoder لإعادة ترتيب المستندات
from sentence_transformers import CrossEncoder
# تحميل نموذج Cross-Encoder
cross_encoder = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
# قائمة الأسئلة والمستندات المستردة
query = "ما هي مدة صلاحية عقد الإيجار في دبي؟"
retrieved_docs = [
"وفقاً للقانون الاتحادي رقم 26 لسنة 2007، مدة عقد الإيجار في دبي هي سنتان قابلة للتجديد.",
"في أبوظبي، مدة الإيجار محددة بسنة واحدة بموجب القانون المحلي.",
"عقد الإيجار في دبي يمكن أن يكون لمدة سنة أو سنتين حسب الاتفاق بين الطرفين."
]
# إنشاء أزواج من السؤال وكل مستند
pairs = [[query, doc] for doc in retrieved_docs]
# إعادة ترتيب المستندات بناءً على درجة التشابه
scores = cross_encoder.predict(pairs)
# ترتيب المستندات من الأعلى إلى الأقل تشابهاً
ranked_docs = [doc for _, doc in sorted(zip(scores, retrieved_docs), reverse=True)]
print("المستندات المرتبة:", ranked_docs)بعد استرداد المستندات وإعادة ترتيبها، تأتي مرحلة بناء الـ Prompt الذي سيتم إرساله للنموذج. هذه المرحلة حاسمة جداً وغالباً ما يتم تجاهلها. معظم المطورين يستخدمون قوالب بسيطة مثل: "استخدم المستندات التالية للإجابة على السؤال: [المستندات]. السؤال: [السؤال]". هذا النهج كارثي لأنه لا يعطي أي توجيه للنموذج حول كيفية استخدام المستندات. بدلاً من ذلك، يجب استخدام تقنيات مثل Chain-of-Thought أو Few-Shot Prompting لتوجيه النموذج نحو الإجابة الصحيحة.
في أحد المشاريع، استخدمنا قالب prompt متطور يتضمن تعليمات واضحة للنموذج حول كيفية التعامل مع المعلومات المتعارضة وكيفية الاستشهاد بالمصادر. مثلاً: "أنت مساعد ذكي. استخدم المستندات التالية للإجابة على السؤال. إذا وجدت معلومات متعارضة، اذكر كل مصدر مع درجة الثقة. إذا لم تجد معلومات كافية، قل بصراحة أنك لا تعرف. المستندات: [المستندات]. السؤال: [السؤال]. فكر خطوة بخطوة قبل الإجابة." هذا النوع من الـ Prompts يقلل بشكل كبير من احتمالية الهلوسة ويجعل الإجابات أكثر شفافية.
# مثال على بناء prompt متطور لـ RAG
retrieved_docs = [
"المستند 1: وفقاً للقانون الاتحادي رقم 26 لسنة 2007، مدة عقد الإيجار في دبي هي سنتان.",
"المستند 2: وفقاً للقانون المحلي في دبي، مدة الإيجار يمكن أن تكون سنة أو سنتين حسب الاتفاق."
]
query = "ما هي مدة صلاحية عقد الإيجار في دبي؟"
# بناء prompt متطور
prompt = f"""
أنت مساعد قانوني ذكي. استخدم المستندات التالية للإجابة على السؤال.
قواعد الإجابة:
1. إذا وجدت معلومات متعارضة في المستندات، اذكر كل مصدر مع درجة ثقتك فيه.
2. إذا لم تجد معلومات كافية في المستندات، قل "لا أعرف الإجابة بناءً على المستندات المتاحة."
3. اذكر دائماً المصادر التي استندت إليها في الإجابة.
4. فكر خطوة بخطوة قبل الإجابة.
المستندات:
{chr(10).join([f"- {doc}" for doc in retrieved_docs])}
السؤال: {query}
الإجابة:
"""
print(prompt)RAG ليس حلاً سحرياً لكل المشاكل. هناك حالات يكون فيها RAG هو الخيار الأمثل، وحالات أخرى يكون فيها إعادة التدريب أو استخدام قاعدة بيانات تقليدية أفضل بكثير. القاعدة الذهبية هي: استخدم RAG عندما تكون لديك معلومات ديناميكية تحتاج للتحديث باستمرار، وعندما تكون هذه المعلومات كبيرة جداً بحيث لا يمكن وضعها في سياق النموذج. مثلاً، إذا كنت تبني نظام أسئلة وأجوبة لشركة لديها آلاف الوثائق القانونية التي تتغير يومياً، فإن RAG هو الخيار الوحيد العملي.
لكن إذا كانت معلوماتك ثابتة وصغيرة الحجم، فقد يكون من الأفضل إعادة تدريب النموذج عليها أو استخدام قاعدة بيانات تقليدية. مثلاً، إذا كنت تبني chatbot لشركة صغيرة ولديك فقط 50 صفحة من الأسئلة الشائعة، فإن إعادة تدريب نموذج صغير على هذه المعلومات قد يكون أسهل وأسرع من بناء نظام RAG كامل. أيضاً، إذا كانت أسئلتك تتطلب استدلالاً معقداً بدلاً من استرجاع معلومات، فقد يكون من الأفضل استخدام تقنيات مثل Chain-of-Thought أو نماذج أكبر بدون RAG.
في أحد المشاريع التي عملت عليها مع فريق ذكاء اصطناعي في شركة تقنية، كنا نبني نظاماً للإجابة على أسئلة العملاء حول المنتجات التقنية. كان لدينا أكثر من 100 ألف صفحة من الوثائق التقنية التي تتغير يومياً. كانت المشكلة الرئيسية هي أن النماذج التقليدية كانت تهلوس كثيراً، خاصة عندما يتعلق الأمر بمواصفات المنتجات الجديدة التي لم ترها من قبل أثناء التدريب. قررنا استخدام RAG مع قاعدة بيانات متجهية مبنية على FAISS، ونموذج embedding من Sentence Transformers، ونموذج توليد من عائلة Llama.
النتائج كانت مذهلة. قبل RAG، كان معدل الهلوسة في الإجابات حوالي 40%، وبعد RAG انخفض إلى أقل من 5%. كما أن الوقت اللازم لتحديث المعلومات انخفض من أسابيع (إعادة تدريب النموذج) إلى دقائق (تحديث قاعدة البيانات المتجهية). لكن التحدي الأكبر كان في ضبط النظام. مثلاً، وجدنا أن بعض الأسئلة تتطلب استرداد مستندات أكثر من غيرها، لذلك قمنا بتطبيق استراتيجية ديناميكية لاسترداد عدد متغير من المستندات بناءً على تعقيد السؤال. أيضاً، استخدمنا تقنيات مثل query expansion لتحسين نتائج البحث للأسئلة الغامضة.
RAG يبدو بسيطاً في النظرية، لكنه مليء بالفخاخ التي قد تدمر نظامك في الإنتاج. الفخ الأول هو استخدام نموذج embedding ضعيف. مثلاً، استخدام نموذج مثل word2vec بدلاً من نماذج أحدث مثل Sentence Transformers قد يؤدي إلى نتائج بحث سيئة للغاية. الفخ الثاني هو تجاهل إعادة ترتيب المستندات (reranking). بدون reranking، قد تحصل على مستندات غير ذات صلة في أعلى النتائج، مما يؤدي إلى إجابات خاطئة.
الفخ الثالث هو بناء prompt سيئ. كما ذكرت سابقاً، استخدام قالب بسيط مثل "استخدم المستندات التالية للإجابة على السؤال" قد يؤدي إلى إجابات غير دقيقة. الفخ الرابع هو تجاهل تحديث قاعدة البيانات المتجهية. إذا كانت معلوماتك تتغير باستمرار ولكنك لا تقوم بتحديث قاعدة البيانات، فإن نظامك سيصبح قديماً بسرعة. الفخ الخامس هو عدم مراقبة أداء النظام. بدون مراقبة دقيقة لمقاييس مثل معدل الهلوسة ووقت الاستجابة، قد لا تلاحظ تدهور الأداء حتى يصبح كارثياً.
إذا كنت تفكر في استخدام RAG، ابدأ دائماً بتجربة بسيطة: استخدم نموذج embedding قوي مثل all-MiniLM-L6-v2، وقاعدة بيانات متجهية مثل FAISS، وقم ببناء نظام بحث أولي. ثم اختبره بأسئلة صعبة تتضمن معلومات متعارضة أو غير موجودة في المستندات. إذا كان النظام قادراً على قول "لا أعرف" بدلاً من الهلوسة، فأنت على الطريق الصحيح. وإذا لم يكن كذلك، فابدأ بضبط الـ Prompt وإضافة تقنيات مثل reranking قبل أن تفكر في توسيع النظام. RAG ليس حلاً سحرياً، لكنه أداة قوية إذا استخدمت بالطريقة الصحيحة. تذكر دائماً: الجودة تأتي من التفاصيل، وليس من التعقيد.