هل سئمت من ردود نماذج اللغة التي تبدو ذكية لكنها في الحقيقة تخترع حقائق من العدم؟ اكتشف كيف يحل RAG هذه المشكلة بتقنية استرجاع المعلومات الذكية، وكيف تطبقه بخطوات عملية دون تعقيدات أكاديمية.
في أحد المشاريع التي عملت عليها العام الماضي، كنا نبني نظاماً للإجابة عن أسئلة العملاء حول منتجات شركة أدوية عملاقة. استخدمنا نموذج لغة متطور، لكن الكارثة حدثت عندما بدأ النموذج يوصي بمركب دوائي غير موجود أصلاً في قاعدة بيانات الشركة. لم يكن خطأً بسيطاً - كان النموذج "يخترع" معلومات كاملة، بما في ذلك الجرعات والآثار الجانبية. هذه الظاهرة، المعروفة بـ Hallucinations، ليست مجرد مشكلة تقنية، بل هي كابوس حقيقي لأي فريق يعتمد على الذكاء الاصطناعي في إنتاج محتوى دقيق. هنا يأتي دور RAG (Retrieval Augmented Generation)، التقنية التي لا تحل مشكلة الاختراعات فحسب، بل تحول نماذج اللغة من آلات تخمين ذكية إلى أنظمة معلوماتية دقيقة تعتمد على مصادر موثوقة.
المشكلة ليست في قدرة النماذج على توليد نص متماسك - فمعظمها يتقن ذلك ببراعة. المشكلة الحقيقية تكمن في أن هذه النماذج تعمل كصناديق سوداء لا تعرف شيئاً عن العالم الحقيقي بعد تاريخ قطع بياناتها التدريبية. تخيل أنك تطلب من مطور جونيور كتابة تقرير عن تقنية جديدة بعد قراءته لمئات المقالات عنها، لكنه لا يملك القدرة على البحث في الإنترنت أو الرجوع إلى المصادر الأصلية. النتيجة؟ سينتج نصاً يبدو مقنعاً، لكنه قد يحتوي على معلومات قديمة أو غير دقيقة أو حتى مختلقة تماماً. هذا بالضبط ما يحدث مع نماذج اللغة التقليدية، حيث تعتمد فقط على ما حفظته خلال التدريب. RAG يغير هذه المعادلة تماماً عبر إضافة طبقة استرجاع ذكية تسمح للنموذج بالوصول إلى معلومات محدثة ودقيقة قبل توليد الرد.
عندما يرسل المستخدم استعلاماً إلى نظام RAG، يبدأ رحلة معقدة عبر عدة مراحل لا يراها المستخدم النهائي. أولاً، يدخل الاستعلام إلى مكون يسمى Query Encoder، الذي يحوله إلى تمثيل رقمي (embedding) باستخدام نموذج متخصص مثل sentence-transformers/all-MiniLM-L6-v2. هذه التمثيلات ليست مجرد تحويل للكلمات إلى أرقام، بل هي تمثيلات دلالية تحافظ على معنى الجملة في فضاء رياضي عالي الأبعاد (عادة 384 أو 768 بُعداً). بعد ذلك، ينتقل هذا التمثيل إلى مرحلة الاسترجاع، حيث يقارن مع ملايين التمثيلات المخزنة مسبقاً في قاعدة بيانات متجهية مثل FAISS أو Pinecone.
المرحلة التالية هي الأكثر إثارة للاهتمام من الناحية التقنية. بدلاً من مقارنة الاستعلام بكل وثيقة في قاعدة البيانات (وهو أمر مستحيل عملياً مع ملايين الوثائق)، تستخدم قواعد البيانات المتجهية خوارزميات بحث تقريبية مثل HNSW (Hierarchical Navigable Small World) للعثور على أقرب الجيران في الفضاء عالي الأبعاد. هذه الخوارزميات تعمل بكفاءة مذهلة، حيث يمكنها العثور على أقرب 5 أو 10 وثائق من بين ملايين الوثائق في أجزاء من الثانية. لكن هنا تكمن المعضلة: كيف نحدد أي الوثائق المسترجعة هي الأكثر صلة؟ هذا هو المكان الذي تلعب فيه تقنيات مثل reranking دوراً حاسماً، حيث تعيد ترتيب النتائج بناءً على معايير أكثر تعقيداً من مجرد المسافة الإقليدية في الفضاء المتجهي.
# مثال عملي لكيفية عمل مرحلة الاسترجاع في RAG باستخدام مكتبات حقيقية
from sentence_transformers import SentenceTransformer
from langchain.vectorstores import FAISS
from langchain.embeddings import HuggingFaceEmbeddings
import numpy as np
# تحميل نموذج التحويل إلى متجهات
model = SentenceTransformer('all-MiniLM-L6-v2')
# تمثيل الاستعلام
query = "ما هي أحدث علاجات مرض السكري من النوع الثاني؟"
query_embedding = model.encode(query)
# تحميل قاعدة البيانات المتجهية (مفترض أنها موجودة مسبقاً)
embedding_model = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
db = FAISS.load_local("diabetes_treatments_db", embedding_model)
# استرجاع الوثائق الأكثر صلة (k=5 تعني استرجاع 5 وثائق)
docs = db.similarity_search_with_score(query, k=5)
# إعادة ترتيب النتائج باستخدام نموذج reranker
# لاحظ أن هذا مثال مبسط - في الإنتاج قد تستخدم مكتبات مثل FlagEmbedding
reranked_docs = sorted(docs, key=lambda x: x[1], reverse=True) # هنا نستخدم المسافة فقط للتوضيح
# عرض النتائج
for i, (doc, score) in enumerate(reranked_docs, 1):
print(f"\nالوثيقة {i} (درجة الصلة: {score:.4f}):")
print(doc.page_content[:200] + "...") # عرض أول 200 حرف فقطبعد استرجاع الوثائق ذات الصلة، تأتي المرحلة الأكثر أهمية: دمج المعلومات المسترجعة مع قدرات توليد اللغة للنموذج. هنا يحدث السحر الحقيقي لـ RAG. بدلاً من تقديم الوثائق مباشرة للمستخدم (كما تفعل محركات البحث التقليدية)، يقوم النظام بحقن المعلومات المسترجعة في السياق الذي يستخدمه نموذج اللغة لتوليد الرد. هذا الحقن ليس مجرد إضافة نص عشوائي في بداية الـ prompt، بل هو عملية دقيقة تتطلب هندسة prompt متقنة لضمان أن النموذج يستخدم المعلومات المسترجعة بشكل فعال دون تجاهلها أو تشويهها.
في أحد المشاريع التي عملت عليها، اكتشفنا أن مجرد إضافة الوثائق المسترجعة إلى الـ prompt دون هيكل محدد يؤدي إلى نتائج كارثية. كان النموذج إما يتجاهل المعلومات تماماً، أو يستخدمها بشكل سطحي دون دمجها بشكل منطقي مع معرفته العامة. الحل كان في هندسة prompt متقنة تستخدم تقنيات مثل "few-shot prompting" و"chain-of-thought" لضمان استخدام المعلومات المسترجعة بشكل فعال. على سبيل المثال، بدلاً من كتابة prompt بسيط مثل "استخدم المعلومات التالية للإجابة عن السؤال: [المعلومات المسترجعة] السؤال: [السؤال]"، استخدمنا هيكلاً أكثر تعقيداً يتضمن أمثلة على كيفية استخدام المعلومات بشكل صحيح.
# مثال على هندسة prompt فعالة في RAG
RAG_PROMPT_TEMPLATE = """
أنت مساعد طبي متخصص في تقديم معلومات دقيقة ومحدثة عن علاجات الأمراض.
استخدم المعلومات التالية للإجابة عن السؤال في الأسفل:
المعلومات المسترجعة:
{context}
أمثلة على الإجابات الجيدة:
السؤال: ما هي أحدث علاجات مرض السكري من النوع الثاني؟
المعلومات: [مستخرج من وثيقة علمية عن دواء جديد]
الإجابة: وفقاً للدراسات السريرية المنشورة في مجلة JAMA في مارس 2024، أظهر دواء Tirzepatide فعالية أكبر بنسبة 22% مقارنة بـ Semaglutide في خفض مستويات HbA1c لدى المرضى الذين لم يستجيبوا للعلاج التقليدي. يجب ملاحظة أن هذا الدواء قد يسبب آثاراً جانبية مثل الغثيان والقيء لدى 15% من المرضى.
السؤال: هل يمكن استخدام Metformin لمرضى الكلى؟
المعلومات: [مستخرج من وثيقة عن موانع استخدام Metformin]
الإجابة: وفقاً لإرشادات الجمعية الأمريكية للسكري (ADA) لعام 2023، لا ينصح باستخدام Metformin لدى المرضى الذين يعانون من قصور كلوي شديد (eGFR < 30 مل/دقيقة/1.73م²) بسبب خطر تراكم حمض اللبن (Lactic Acidosis). يجب تقييم وظائف الكلى قبل بدء العلاج وبشكل دوري خلاله.
السؤال: {question}
الإجابة:
"""
# استخدام الـ prompt في توليد الرد
from langchain.llms import OpenAI
from langchain.chains import LLMChain
llm = OpenAI(temperature=0.3)
prompt = RAG_PROMPT_TEMPLATE.format(
c"\n".join([doc.page_content for doc, _ in reranked_docs]),
question=query
)
chain = LLMChain(llm=llm, prompt=prompt)
response = chain.run({})
print(response)قرار استخدام RAG ليس قراراً سهلاً، وهو ليس الحل السحري لكل مشاكل نماذج اللغة. من تجربتي في بناء أنظمة ذكاء اصطناعي لعدة شركات، وجدت أن RAG يكون فعالاً بشكل استثنائي في حالات محددة جداً. أولاً، عندما تحتاج إلى معلومات دقيقة ومحدثة باستمرار، مثل الأنظمة الطبية أو القانونية التي تعتمد على بيانات متغيرة باستمرار. ثانياً، عندما يكون لديك مصدر موثوق للمعلومات يمكن الاعتماد عليه، سواء كان قاعدة بيانات داخلية أو مجموعة من الوثائق الموثوقة. ثالثاً، عندما يكون السياق مهماً جداً ولا يمكن الاعتماد على المعرفة العامة للنموذج وحده، مثل أنظمة الدعم الفني التي تحتاج إلى الرجوع إلى وثائق المنتجات المحددة.
على الجانب الآخر، هناك حالات يكون فيها استخدام RAG فكرة سيئة أو حتى ضارة. أولاً، عندما تكون السرعة هي الأولوية القصوى، حيث أن إضافة طبقة الاسترجاع قد تزيد زمن الاستجابة من مئات المللي ثانية إلى عدة ثوان. ثانياً، عندما تكون البيانات حساسة للغاية ولا يمكن تخزينها في قواعد بيانات خارجية، خاصة مع الخدمات السحابية. ثالثاً، عندما يكون لديك نموذج صغير نسبياً لا يمكنه الاستفادة بشكل فعال من المعلومات الإضافية، حيث قد يؤدي حقن المعلومات إلى تشويش النموذج بدلاً من مساعدته. وأخيراً، عندما تكون المشكلة تتطلب تفكيراً إبداعياً أو توليد محتوى أصلي بدلاً من استرجاع معلومات، مثل كتابة قصص أو توليد أفكار جديدة.
في ورشة عمل نظمتها مؤخراً لمطورين في شركة تقنية كبيرة، سألني أحد المشاركين: "إذا كان RAG بهذه الفعالية، فلماذا لا يستخدمه الجميع؟" الحقيقة هي أن RAG يأتي مع مجموعة من التحديات الحقيقية التي نادراً ما يتم مناقشتها في الدورات التدريبية أو المقالات السطحية. أول هذه التحديات هو مشكلة "الضوضاء في البيانات" - فعندما تسترجع وثائق متعددة حول موضوع معين، قد تحتوي هذه الوثائق على معلومات متضاربة أو قديمة. كيف يقرر النموذج أي المعلومات هي الصحيحة؟ هذا تحدي حقيقي يتطلب حلولاً متقدمة مثل استخدام نماذج reranking متخصصة أو تطبيق تقنيات تصفية ذكية قبل حقن المعلومات في الـ prompt.
التحدي الثاني هو مشكلة "الحجم الأمثل للسياق" - فإذا قمت بحقن كمية كبيرة جداً من المعلومات في الـ prompt، قد يعاني النموذج من "فقدان السياق" حيث يتجاهل أجزاء من المعلومات أو يخلط بينها. وإذا قمت بحقن كمية قليلة جداً، قد لا تحصل على الفائدة المرجوة من RAG. في أحد المشاريع التي عملت عليها، اكتشفنا أن الحجم الأمثل للسياق يختلف بشكل كبير بين النماذج المختلفة - فبينما كان نموذج Llama2 يعمل بشكل جيد مع 2000 رمز (token)، كان نموذج GPT-3.5 يحتاج إلى 4000 رمز لتحقيق نفس مستوى الدقة. هذا يعني أنك بحاجة إلى إجراء الكثير من التجارب والاختبارات لتحديد الحجم الأمثل لكل نموذج ولكل حالة استخدام.
أحد أسوأ الكوابيس التي واجهتها مع RAG هو ما أسميه "Hallucinations الجزئية" - وهي الحالة التي يستخدم فيها النموذج بعض المعلومات المسترجعة بشكل صحيح، لكنه يخترع معلومات أخرى لتكملة الإجابة. هذه المشكلة أخطر بكثير من عدم استخدام RAG على الإطلاق، لأنها تعطي انطباعاً زائفاً بالدقة. على سبيل المثال، في نظام طبي قمنا ببنائه، استخدم النموذج معلومات صحيحة عن دواء معين من الوثائق المسترجعة، لكنه اخترع آثاراً جانبية غير موجودة في أي مصدر موثوق. المشكلة هنا أن المستخدم قد يثق في المعلومات لأنها تبدو مدعومة بمصادر، بينما في الواقع جزء منها مختلق تماماً.
الحل لهذه المشكلة يتطلب نهجاً متعدد الجوانب. أولاً، يجب استخدام تقنيات مثل "attribution" حيث يشير النموذج بشكل صريح إلى المصدر الذي استقى منه كل معلومة. ثانياً، يمكن استخدام نماذج أصغر متخصصة في التحقق من الحقائق بعد توليد الرد الأولي. ثالثاً، يجب تطبيق تقنيات مثل "self-consistency" حيث يتم توليد عدة ردود مختلفة لنفس الاستعلام ثم اختيار الرد الأكثر اتساقاً بينها. وأخيراً، يجب دائماً تذكير المستخدمين بأن النظام ليس معصوماً من الخطأ وأن عليهم التحقق من المعلومات المهمة مع مصادر موثوقة.
# مثال على تطبيق attribution في RAG باستخدام LangChain
from langchain.chains import RetrievalQAWithSourcesChain
from langchain.llms import OpenAI
# إنشاء سلسلة RAG مع مصادر
chain = RetrievalQAWithSourcesChain.from_chain_type(
llm=OpenAI(temperature=0.3),
chain_type="stuff",
retriever=db.as_retriever(search_kwargs={"k": 3}),
return_source_documents=True
)
# تنفيذ الاستعلام
result = chain({"question": query})
# عرض النتيجة مع المصادر
print("الإجابة:", result["answer"])
print("\nالمصادر:")
for i, doc in enumerate(result["source_documents"], 1):
print(f"{i}. {doc.metadata.get('source', 'مصدر غير معروف')}")
print(f" (درجة الصلة: {doc.metadata.get('score', 'غير متوفر')})")
print(f" {doc.page_content[:100]}...")إذا قررت أن RAG هو الحل المناسب لمشكلتك، فإليك الخطوات العملية لبناء نظام جاهز للإنتاج، بناءً على تجربتي في بناء أنظمة مشابهة لعدة شركات. أولاً، ابدأ بتحديد مصادر البيانات الموثوقة التي ستعتمد عليها. هذه قد تكون قواعد بيانات داخلية، وثائق PDF، صفحات ويب، أو حتى قواعد بيانات علائقية. تذكر أن جودة نظام RAG تعتمد بشكل كامل على جودة البيانات التي تغذيه بها - فالنظام لن يكون أفضل من أسوأ مصدر بيانات تستخدمه.
الخطوة الثانية هي تجهيز قاعدة البيانات المتجهية. استخدم مكتبات مثل FAISS (من فيسبوك) أو Pinecone (خدمة سحابية) لتخزين تمثيلات المتجهات للوثائق. عملية التحويل إلى متجهات (embedding) تتطلب اختيار نموذج مناسب - فأنا شخصياً أفضل استخدام نماذج مثل all-MiniLM-L6-v2 للغة الإنجليزية أو paraphrase-multilingual-MiniLM-L12-v2 للغات متعددة بما في ذلك العربية. بعد ذلك، قم بتقسيم الوثائق إلى أجزاء صغيرة (chunks) بحجم مناسب - عادةً بين 200 و500 رمز لكل جزء - مع تداخل بسيط لضمان استمرارية السياق.
# مثال كامل لبناء قاعدة بيانات متجهية من الصفر
from langchain.document_loaders import WebBaseLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import FAISS
import os
# 1. تحميل البيانات من مصادر مختلفة
urls = [
"https://ar.wikipedia.org/wiki/%D8%A7%D9%84%D8%B3%D9%83%D8%B1%D9%8A" # صفحة عن مرض السكري
]
loader = WebBaseLoader(urls)
documents = loader.load()
# 2. تقسيم الوثائق إلى chunks
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=300,
chunk_overlap=50,
length_function=len,
separators=["\n\n", "\n", " ", ""]
)
chunks = text_splitter.split_documents(documents)
# 3. تحويل chunks إلى متجهات وتخزينها
embedding_model = HuggingFaceEmbeddings(
model_name="paraphrase-multilingual-MiniLM-L12-v2",
model_kwargs={"device": "cpu"} # استخدم "cuda" إذا كان لديك GPU
)
# إنشاء قاعدة البيانات المتجهية
vector_db = FAISS.from_documents(chunks, embedding_model)
# حفظ قاعدة البيانات للاستخدام لاحقاً
vector_db.save_local("diabetes_db")
print(f"تم تخزين {len(chunks)} chunk في قاعدة البيانات المتجهية")بعد إعداد قاعدة البيانات، يأتي الجزء الأكثر أهمية: تصميم سلسلة المعالجة (processing pipeline). هنا يجب عليك اتخاذ عدة قرارات هندسية مهمة. أولاً، حدد عدد الوثائق التي ستسترجعها لكل استعلام - عادةً بين 3 و5 وثائق. ثانياً، قرر ما إذا كنت ستستخدم نموذج reranking لإعادة ترتيب النتائج قبل حقنها في الـ prompt. ثالثاً، صمّم هندسة prompt المناسبة لحالة الاستخدام الخاصة بك، مع الأخذ في الاعتبار أن prompts المختلفة قد تكون مطلوبة لأنواع مختلفة من الاستعلامات. وأخيراً، قرر ما إذا كنت ستستخدم نموذج لغة كبير مثل GPT-4 أو نموذج أصغر وأكثر كفاءة مثل Llama2، مع الأخذ في الاعتبار أن النماذج الأكبر قد تكون أفضل في التعامل مع المعلومات المعقدة ولكنها أبطأ وأكثر تكلفة.
إحدى الأخطاء الشائعة التي أراها في مشاريع RAG هي الاعتقاد بأن النظام سيعمل بشكل مثالي بمجرد بنائه. الحقيقة هي أن أنظمة RAG تحتاج إلى مراقبة وتحسين مستمرين. أولاً، يجب عليك إنشاء مجموعة اختبارات تحتوي على أسئلة معروفة الإجابات، بحيث يمكنك قياس دقة النظام بشكل دوري. ثانياً، يجب مراقبة أداء النظام في الإنتاج باستخدام مقاييس مثل زمن الاستجابة، ودقة الإجابات، ورضا المستخدمين. ثالثاً، يجب تحديث قاعدة البيانات المتجهية بشكل دوري لضمان أن المعلومات محدثة - فهذه ليست عملية لمرة واحدة، بل تتطلب صيانة مستمرة.
من الأدوات المفيدة في هذا المجال هي LangSmith من LangChain، التي تسمح لك بتتبع ومراقبة أداء نظام RAG الخاص بك. يمكنك استخدامها لتسجيل كل استعلام وإجابته، ثم مراجعة الإجابات غير الدقيقة لتحديد الأنماط الشائعة للمشكلات. على سبيل المثال، قد تكتشف أن النظام يعاني بشكل خاص مع الاستعلامات التي تتطلب مقارنة بين عدة مصادر، أو مع الأسئلة التي تتطلب تفكيراً منطقياً معقداً. هذه الرؤى ستساعدك على تحسين النظام بشكل مستمر، سواء من خلال تعديل هندسة prompt، أو تحسين عملية الاسترجاع، أو حتى تغيير النموذج المستخدم.
إذا نظرنا إلى المستقبل، فمن الواضح أن RAG لن يكون مجرد تقنية عابرة، بل سيصبح جزءاً أساسياً من معظم أنظمة الذكاء الاصطناعي التي تتطلب دقة وموثوقية. أحد الاتجاهات الواعدة هو دمج RAG مع تقنيات أخرى مثل Graph Neural Networks (GNNs) لتمثيل العلاقات المعقدة بين المفاهيم. بدلاً من مجرد استرجاع وثائق منفصلة، يمكن لهذه الأنظمة فهم العلاقات بين المفاهيم المختلفة والاستفادة منها في توليد الإجابات. على سبيل المثال، في مجال الطب، يمكن لنظام RAG متقدم أن يفهم العلاقة بين مرض معين ودواء معين وآثار جانبية معينة، بدلاً من مجرد استرجاع وثائق تحتوي على هذه المصطلحات.
اتجاه آخر مثير هو تطوير نماذج متخصصة في RAG، بدلاً من الاعتماد على نماذج اللغة العامة. هذه النماذج ستتميز بقدرتها على التعامل بشكل أفضل مع المعلومات المسترجعة، وفهم العلاقات بين الوثائق المختلفة، وتوليد إجابات أكثر دقة وموثوقية. كما نتوقع رؤية المزيد من التحسينات في تقنيات الاسترجاع نفسها، مثل استخدام نماذج متعددة للاسترجاع (multi-stage retrieval) حيث يتم استخدام نماذج مختلفة في مراحل مختلفة من عملية الاسترجاع لتحسين الدقة والكفاءة.
إذا أخذت شيئاً واحداً فقط من هذا المقال، فليكن هذا: لا تبدأ مشروع RAG قبل أن تجيب بوضوح على سؤال واحد فقط - "هل حقاً أحتاج إلى RAG؟" الكثير من الفرق تبدأ في بناء أنظمة RAG لأنها تبدو التقنية الرائجة، لكن في كثير من الحالات تكون حلول أبسط وأكثر كفاءة متاحة. إذا كانت مشكلتك يمكن حلها باستخدام قاعدة بيانات تقليدية مع واجهة بحث جيدة، أو حتى باستخدام نموذج لغة بسيط بدون RAG، فلا تضيع وقتك ومواردك في بناء نظام معقد لا تحتاجه. ولكن إذا قررت أن RAG هو الحل المناسب، فتذكر أن الجودة تأتي من البيانات أولاً، ثم من هندسة النظام ثانياً، وليس من النموذج نفسه. استثمر وقتك في بناء قاعدة بيانات متجهية عالية الجودة، وفي تصميم سلسلة معالجة فعالة، وفي إنشاء نظام مراقبة وتحسين مستمر. بهذه الطريقة، ستحصل على نظام RAG لا يعمل فقط، بل يعمل بشكل ممتاز ويوفر قيمة حقيقية لمستخدميك.