هل تعبت من نماذج اللغة التي تخترع حقائق أو تفشل في الإجابة عن أسئلتك الدقيقة؟ RAG هو الحل العملي الذي يجمع بين قوة البحث وقوة التوليد. هذا الدليل يشرح كيف يعمل خلف الكواليس، ومتى تستخدمه، وكيف تتجنب الفخاخ التي يقع فيها حتى المطورون المحترفون.
تخيل أنك تبني chatbot للشركة، وبعد شهرين من التطوير تكتشف أن النموذج يخترع سياسات داخلية لم توجد أصلاً، أو يعطي إجابات متضاربة عن نفس السؤال. المشكلة ليست في النموذج نفسه، بل في أنه لا يملك سياقاً كافياً. هنا يأتي دور RAG (Retrieval Augmented Generation) كحل عملي يجمع بين قوة البحث في قواعد البيانات وقوة توليد النصوص. لكن RAG ليس حلاً سحرياً - إذا استخدمته بشكل خاطئ، ستجد نفسك أمام نظام أبطأ وأكثر تعقيداً من اللازم، وربما أقل دقة مما كنت تأمل.
في هذا الدليل، سنشرح RAG من منظور المطور الذي يريد نتائج فعلية، وليس من منظور الباحث الذي ينشر أوراقاً علمية. سنغطي كيف يعمل RAG خلف الكواليس (بما في ذلك ما يحدث في الذاكرة والمعالج)، متى يجب استخدامه ومتى يجب تجنبه، وكيفية بناء نظام RAG فعال دون الوقوع في الفخاخ الشائعة مثل الـ latency الزائد أو الـ hallucinations المستمرة.
عندما يرسل المستخدم سؤالاً إلى نظام RAG، لا يذهب السؤال مباشرة إلى نموذج اللغة. بدلاً من ذلك، يمر بثلاث مراحل رئيسية: الـ retrieval، الـ augmentation، ثم الـ generation. المرحلة الأولى هي الأكثر أهمية والأكثر تعقيداً - هنا يبحث النظام في قاعدة البيانات عن السياق المناسب للإجابة عن السؤال. لكن هذا البحث ليس بسيطاً مثل SELECT * FROM documents WHERE text LIKE '%سؤال%'. في الواقع، يستخدم RAG تقنيات متقدمة مثل الـ vector embeddings والـ similarity search للعثور على السياق الأكثر صلة.
لفهم ما يحدث خلف الكواليس، تخيل أن قاعدة البيانات الخاصة بك تحتوي على آلاف المستندات، كل منها يتم تحويله إلى متجه رقمي (vector) باستخدام نموذج مثل sentence-transformers/all-MiniLM-L6-v2. عندما يأتي سؤال جديد، يتم تحويله أيضاً إلى متجه، ثم يقارن النظام هذا المتجه مع جميع متجهات المستندات باستخدام خوارزمية مثل cosine similarity. النتيجة هي قائمة بالمستندات الأكثر صلة بالسؤال، مرتبة حسب درجة التشابه. هذه العملية تحدث في جزء من الثانية، لكنها تتطلب موارد معالجة كبيرة إذا كانت قاعدة البيانات ضخمة.
# مثال عملي: تحويل نص إلى متجه باستخدام مكتبة sentence-transformers
from sentence_transformers import SentenceTransformer
import numpy as np
# تحميل النموذج مسبقاً لتجنب التحميل في كل طلب
model = SentenceTransformer('all-MiniLM-L6-v2')
def get_embedding(text: str) -> np.ndarray:
"""تحويل نص إلى متجه رقمي"""
return model.encode(text, cFalse)
# مثال على حساب التشابه بين سؤال ومستند
question = "ما هي سياسة الإجازات في الشركة؟"
document = "سياسة الإجازات: يحق للموظف 21 يوماً إجازة مدفوعة الأجر سنوياً."
question_embedding = get_embedding(question)
document_embedding = get_embedding(document)
# حساب التشابه باستخدام cosine similarity
similarity = np.dot(question_embedding, document_embedding) / (
np.linalg.norm(question_embedding) * np.linalg.norm(document_embedding)
)
print(f"درجة التشابه: {similarity:.4f}") # الناتج عادة بين 0 و1المرحلة الثانية هي الـ augmentation، حيث يتم دمج السياق المستخرج من قاعدة البيانات مع السؤال الأصلي. لكن هذا الدمج ليس مجرد إضافة نص عشوائي - يجب أن يكون بطريقة تحافظ على تماسك السياق ولا تربك نموذج اللغة. عادةً ما يتم استخدام قالب (template) محدد مسبقاً لإدراج السياق في مكان مناسب داخل الـ prompt. على سبيل المثال، قد يكون القالب كالتالي: "استخدم المعلومات التالية للإجابة عن السؤال: [السياق]. السؤال: [السؤال]. الإجابة:". هذا القالب يوجه نموذج اللغة لاستخدام السياق المقدم بدلاً من الاعتماد على معرفته العامة.
أخيراً، تأتي مرحلة الـ generation، حيث يتم إرسال الـ prompt المعدل إلى نموذج اللغة لتوليد الإجابة. لكن حتى هنا، هناك تفاصيل مهمة يجب مراعاتها. مثلاً، يجب تحديد معلمات النموذج بعناية - إذا جعلت الـ temperature عالياً جداً، قد يبدأ النموذج في اختلاق معلومات رغم وجود السياق. وإذا جعلتها منخفضاً جداً، قد تصبح الإجابات جافة وغير طبيعية. أيضاً، يجب مراقبة طول الـ prompt - إذا كان السياق طويلاً جداً، قد يتجاوز الحد الأقصى للـ tokens المسموح به للنموذج، مما يؤدي إلى قطع السياق أو فشل الطلب تماماً.
RAG ليس حلاً مناسباً لكل مشكلة. في الواقع، استخدامه في الحالات الخاطئة قد يؤدي إلى نظام أبطأ وأكثر تعقيداً دون فائدة حقيقية. القاعدة الأساسية هي: استخدم RAG عندما يكون لديك بيانات ديناميكية أو متخصصة تحتاج إلى دمجها مع قدرات نموذج اللغة، وتجنب استخدامه عندما تكون البيانات ثابتة أو عندما يمكن لنموذج اللغة الإجابة عن الأسئلة بشكل جيد دون سياق إضافي.
من تجربتي، RAG مفيد جداً في الحالات التالية: أنظمة دعم العملاء التي تعتمد على وثائق داخلية متغيرة باستمرار، الأدوات التعليمية التي تحتاج إلى الرجوع إلى كتب أو مصادر محددة، وأنظمة التحليل المالي التي تعتمد على تقارير دورية. مثلاً، في شركة ناشئة عملت معها، استخدمنا RAG لبناء نظام يجيب عن أسئلة العملاء حول سياسات الشحن والضرائب، والتي كانت تتغير شهرياً. بدون RAG، كنا سنضطر إلى إعادة تدريب النموذج باستمرار، وهو أمر مكلف ويستغرق وقتاً طويلاً.
من ناحية أخرى، تجنب استخدام RAG في الحالات التالية: عندما تكون الأسئلة عامة ويمكن لنموذج اللغة الإجابة عنها دون سياق إضافي (مثل "ما هي عاصمة فرنسا؟")، عندما تكون البيانات حساسة ولا يمكن تخزينها في قاعدة بيانات خارجية، أو عندما يكون الأداء هو الأولوية القصوى ولا يمكنك تحمل الـ latency الإضافي الذي يضيفه RAG. مثلاً، في نظام تداول آلي عالي التردد، لا يمكنك تحمل الانتظار حتى يتم استرجاع السياق ثم توليد الإجابة - هنا يجب الاعتماد على نماذج أصغر وأكثر تخصصاً تعمل محلياً.
بناء نظام RAG فعال يتطلب أكثر من مجرد توصيل قاعدة بيانات بنموذج لغة. هناك العديد من التفاصيل التي يجب مراعاتها لضمان أن النظام يعمل بكفاءة ودقة. أولاً، يجب اختيار قاعدة البيانات المناسبة للـ retrieval. ليست كل قواعد البيانات متساوية - بعضها مصمم خصيصاً للبحث الدلالي، بينما البعض الآخر أفضل للبحث النصي التقليدي. مثلاً، قواعد البيانات مثل Pinecone أو Weaviate مصممة خصيصاً للبحث باستخدام الـ vector embeddings، بينما قواعد البيانات التقليدية مثل PostgreSQL يمكن توسيعها باستخدام إضافات مثل pgvector.
ثانياً، يجب الانتباه إلى كيفية تقسيم المستندات قبل تخزينها في قاعدة البيانات. إذا قمت بتخزين مستندات كاملة بدون تقسيم، قد تحصل على نتائج بحث غير دقيقة أو سياقات طويلة جداً. القاعدة العامة هي تقسيم المستندات إلى أجزاء صغيرة (chunks) بحجم مناسب - مثلاً، 500 إلى 1000 حرف لكل chunk. لكن هذا الحجم قد يختلف حسب نوع البيانات - في المستندات القانونية، قد تحتاج إلى chunks أكبر للحفاظ على سياق الفقرة، بينما في الأسئلة والأجوبة القصيرة، قد تكون chunks أصغر كافية.
# مثال على تقسيم المستندات إلى chunks باستخدام مكتبة langchain
from langchain.text_splitter import RecursiveCharacterTextSplitter
# نص طويل يمثل مستنداً كاملاً
document = """
سياسة الإجازات:
- يحق للموظف 21 يوماً إجازة مدفوعة الأجر سنوياً.
- يمكن ترحيل 5 أيام كحد أقصى إلى العام التالي.
- الإجازات المرضية تتطلب شهادة طبية بعد اليوم الثالث.
سياسة العمل عن بعد:
- يسمح بالعمل عن بعد يومين في الأسبوع.
- يجب الحصول على موافقة المدير مسبقاً.
- يجب استخدام VPN للاتصال بالأنظمة الداخلية.
"""
# تقسيم النص إلى chunks بحجم 200 حرف مع تداخل 20 حرف
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=200,
chunk_overlap=20,
length_function=len,
separators=["\n\n", "\n", " ", ""]
)
chunks = text_splitter.create_documents([document])
for i, chunk in enumerate(chunks):
print(f"Chunk {i+1}: {chunk.page_content}\n")ثالثاً، يجب تحسين عملية الـ retrieval نفسها. استخدام الـ vector embeddings وحده قد لا يكون كافياً للحصول على أفضل النتائج. في كثير من الحالات، يمكن تحسين النتائج باستخدام مزيج من البحث الدلالي والبحث النصي التقليدي. مثلاً، يمكنك استخدام الـ vector search للعثور على المستندات الأكثر صلة، ثم تصفية النتائج باستخدام بحث نصي تقليدي للتأكد من أن المستندات تحتوي على كلمات مفتاحية محددة. هذه التقنية تسمى hybrid search وهي مدعومة في العديد من قواعد البيانات مثل Weaviate وElasticsearch.
أخيراً، يجب مراقبة أداء النظام باستمرار وتحسينه بناءً على البيانات الحقيقية. لا تعتمد فقط على الاختبارات الأولية - قم بتسجيل الأسئلة والإجابات الفعلية التي يرسلها المستخدمون، وقم بمراجعتها بانتظام لتحديد الأنماط التي يفشل فيها النظام. مثلاً، قد تكتشف أن النظام يفشل في الإجابة عن الأسئلة التي تحتوي على أرقام أو تواريخ، مما يشير إلى الحاجة إلى تحسين طريقة تقسيم المستندات أو طريقة البحث. أيضاً، راقب الـ latency باستمرار - إذا لاحظت أن الوقت المستغرق في الـ retrieval يزداد بمرور الوقت، قد تحتاج إلى تحسين فهرسة قاعدة البيانات أو تقليل حجم chunks.
أول فخ شائع هو الاعتماد على الـ retrieval وحده دون التحقق من جودة السياق. حتى إذا حصلت على مستندات ذات تشابه عالي مع السؤال، قد لا تكون هذه المستندات مفيدة في الواقع. مثلاً، قد يكون لديك مستند يتحدث عن "سياسة الإجازات" لكن السؤال يتعلق بـ "إجازة الأمومة" - هنا قد يكون التشابه عالياً لكن السياق غير ذي صلة. الحل هو إضافة طبقة تحقق إضافية بعد الـ retrieval، مثل استخدام نموذج صغير لتصنيف مدى صلة السياق بالسؤال قبل إرساله إلى نموذج اللغة الرئيسي.
الفخ الثاني هو تجاهل مشكلة الـ context window للنموذج. إذا قمت بإرسال سياق طويل جداً، قد يتجاوز الحد الأقصى للـ tokens المسموح به، مما يؤدي إلى قطع السياق أو فشل الطلب. حتى إذا كان السياق ضمن الحد الأقصى، قد يؤثر طول السياق على جودة الإجابة - النماذج تميل إلى التركيز أكثر على بداية ونهاية الـ prompt، مما قد يؤدي إلى تجاهل المعلومات الهامة في الوسط. الحل هو ترتيب السياق بعناية، ووضع المعلومات الأكثر أهمية في البداية والنهاية، أو استخدام تقنيات مثل summary للمستندات الطويلة قبل إدراجها في الـ prompt.
الفخ الثالث هو إهمال تحديث قاعدة البيانات. إذا كانت بياناتك تتغير باستمرار (مثل الوثائق الداخلية للشركة)، يجب أن يكون لديك نظام لتحديث قاعدة البيانات بانتظام. إذا لم تفعل ذلك، ستجد أن نظام RAG يعطي إجابات قديمة أو غير دقيقة. الحل هو بناء pipeline لتحديث قاعدة البيانات تلقائياً عند تغيير المستندات، أو على الأقل جدولة تحديثات منتظمة. أيضاً، يجب أن يكون لديك آلية لإزالة المستندات القديمة أو غير ذات الصلة للحفاظ على كفاءة قاعدة البيانات.
لإعطائك فكرة عملية عن كيفية بناء نظام RAG، سنقوم ببناء مثال بسيط باستخدام LangChain وOpenAI. هذا المثال سيستخدم قاعدة بيانات في الذاكرة للتبسيط، لكن المفهوم نفسه ينطبق على قواعد البيانات الحقيقية مثل Pinecone أو Weaviate. سنستخدم نموذج OpenAI للحصول على embeddings ولتوليد الإجابات، لكن يمكنك استبداله بأي نموذج آخر حسب احتياجاتك.
# بناء نظام RAG بسيط باستخدام LangChain وOpenAI
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS
from langchain.chains import RetrievalQA
from langchain.llms import OpenAI
from langchain.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter
import os
# تحميل المستندات (في المثال سنستخدم نصاً بسيطاً)
documents = [
"سياسة الإجازات: يحق للموظف 21 يوماً إجازة مدفوعة الأجر سنوياً.",
"سياسة العمل عن بعد: يسمح بالعمل عن بعد يومين في الأسبوع.",
"سياسة الإجازات المرضية: تتطلب شهادة طبية بعد اليوم الثالث."
]
# تقسيم المستندات إلى chunks
text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=0)
texts = text_splitter.create_documents(documents)
# تحميل embeddings واستخدام FAISS كقاعدة بيانات في الذاكرة
embeddings = OpenAIEmbeddings(openai_api_key=os.getenv("OPENAI_API_KEY"))
db = FAISS.from_documents(texts, embeddings)
# إنشاء سلسلة RAG باستخدام نموذج OpenAI
qa = RetrievalQA.from_chain_type(
llm=OpenAI(openai_api_key=os.getenv("OPENAI_API_KEY")),
chain_type="stuff",
retriever=db.as_retriever(search_kwargs={"k": 2})
)
# طرح سؤال على النظام
question = "كم عدد أيام الإجازة المدفوعة الأجر سنوياً؟"
result = qa.run(question)
print(f"السؤال: {question}")
print(f"الإجابة: {result}")في هذا المثال، استخدمنا FAISS كقاعدة بيانات في الذاكرة، وهي مناسبة للاختبار والتطوير، لكنها ليست مناسبة للإنتاج. في نظام حقيقي، ستستخدم قاعدة بيانات مخصصة مثل Pinecone أو Weaviate. أيضاً، استخدمنا نموذج OpenAI للحصول على embeddings ولتوليد الإجابات، لكن يمكنك استبداله بنماذج مفتوحة المصدر مثل sentence-transformers للمتجهات وLlama أو Mistral لتوليد الإجابات. لاحظ أننا استخدمنا chain_type="stuff"، وهذا يعني أن السياق سيتم إدخاله مباشرة في الـ prompt دون معالجة إضافية. هناك أنواع أخرى من السلاسل مثل "map_reduce" و"refine" التي قد تكون أكثر ملاءمة للمستندات الطويلة.
من المهم أيضاً ملاحظة أننا قمنا بتحديد k=2 في الـ retriever، مما يعني أن النظام سيسترجع مستندين فقط لكل سؤال. هذا الرقم يجب تعديله بناءً على حجم قاعدة البيانات ونوع الأسئلة. إذا كان لديك قاعدة بيانات صغيرة، قد يكون k=1 كافياً. أما إذا كانت قاعدة البيانات كبيرة وتحتوي على معلومات متداخلة، قد تحتاج إلى زيادة k للحصول على سياقات أكثر شمولية.
قبل أن تبدأ في بناء نظام RAG، اسأل نفسك سؤالين: أولاً، هل حقاً تحتاج إلى RAG؟ إذا كانت أسئلتك عامة ويمكن لنموذج اللغة الإجابة عنها بدون سياق إضافي، قد يكون الحل الأبسط هو الأفضل. ثانياً، هل لديك البيانات المناسبة؟ RAG يعتمد على جودة البيانات المتاحة - إذا كانت بياناتك قديمة أو غير منظمة، لن تحصل على نتائج جيدة مهما كانت التقنية المستخدمة متقدمة.
إذا قررت المضي قدماً في بناء نظام RAG، ابدأ صغيراً. قم ببناء نموذج أولي باستخدام قاعدة بيانات في الذاكرة واختبره مع مجموعة صغيرة من الأسئلة والبيانات. راقب الأداء والدقة، وقم بتحسين النظام بناءً على النتائج الفعلية. لا تحاول بناء نظام مثالي من المحاولة الأولى - RAG هو عملية تكرارية تتطلب الكثير من الضبط والتحسين. وأخيراً، لا تنسَ مراقبة النظام بعد النشر - قم بتسجيل الأسئلة والإجابات، وراجعها بانتظام لتحديد الأنماط التي يمكن تحسينها.
RAG ليس حلاً سحرياً، لكنه أداة قوية عندما تستخدم في المكان الصحيح وبالطريقة الصحيحة. المفتاح هو فهم متى وكيف تستخدمه، وليس مجرد اتباع الموضة.
— تجربة شخصية بعد بناء 3 أنظمة RAG مختلفة