ما هو RAG؟ بناء بحث دلالي في Laravel مع pgvector

RAG (التوليد المعزّز بالاسترجاع) يمنح نموذج الذكاء الاصطناعي معلومات تطبيقك قبل الإجابة. إليك فكرته وكيف تبنيه في Laravel خطوة بخطوة.

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

ما هو RAG ومن أين جاء؟

الاسم يعود إلى ورقة بحثية بعنوان Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (باتريك لويس وزملاؤه، 12 مؤلفًا)، نُشرت على arXiv في 22 مايو 2020 وقُبلت في مؤتمر NeurIPS 2020. اقترح الباحثون دمج نموذج توليد مدرَّب مسبقًا ("ذاكرة بارامترية") مع فهرس متجهات لويكيبيديا يُستخدم عبر مُسترجِع عصبي ("ذاكرة غير بارامترية").

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

باختصار، RAG له ثلاث خطوات:

  1. الفهرسة: تقسيم مستنداتك إلى مقاطع وتحويل كل مقطع إلى متجه (embedding) وتخزينه.
  2. الاسترجاع: تحويل سؤال المستخدم إلى متجه وجلب أقرب المقاطع إليه.
  3. التوليد: إرسال السؤال مع المقاطع المسترجعة إلى النموذج ليجيب اعتمادًا عليها.

ما هو الـ Embedding والبحث الدلالي؟

الـ embedding قائمة أرقام تمثّل معنى النص. النصوص المتقاربة في المعنى تنتج متجهات متقاربة، حتى لو اختلفت الكلمات. لذلك يجد البحث الدلالي عبارة "كيف أستعيد أموالي" عند بحثك في مستند عنوانه "سياسة الاسترجاع"، وهو ما يعجز عنه LIKE '%...%'. القرب بين متجهين يُقاس عادةً بالتشابه الكوسيني (cosine similarity).

تجهيز قاعدة البيانات في Laravel

بحسب توثيق Laravel 13، تُدعم استعلامات التشابه الشعاعي على PostgreSQL مع إضافة pgvector، وعلى MariaDB 11.7 أو أحدث. المهاجرة التالية تنشئ جدول المقاطع:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::ensureVectorExtensionExists();

        Schema::create('documents', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            $table->vector('embedding', dimensions: 1536)->index();
            $table->timestamps();
        });
    }
};

انتبه إلى أن dimensions يجب أن يطابق عدد أبعاد نموذج الـ embeddings الذي تستخدمه. استدعاء index() على عمود المتجه ينشئ فهرسًا يستخدم المسافة الكوسينية لتسريع الاستعلامات.

في الموديل، يتيح Laravel cast مخصصًا للمتجهات:

use Illuminate\Database\Eloquent\Casts\AsVector;

protected function casts(): array
{
    return [
        'embedding' => AsVector::class,
    ];
}

RAG في Laravel — الفهرسة: توليد الـ Embeddings وتخزينها

توفّر حزمة Laravel AI SDK الدالة toEmbeddings على Stringable، وكلاس Embeddings لتوليد عدة متجهات دفعة واحدة. بما أن توليد الـ embeddings يستهلك طلبات مدفوعة، يُفضَّل تنفيذه داخل Job في الطابور:

use App\Models\Document;
use Laravel\Ai\Embeddings;

$chunks = ['نص المقطع الأول...', 'نص المقطع الثاني...'];

$response = Embeddings::for($chunks)->generate();

foreach ($chunks as $i => $chunk) {
    Document::create([
        'title' => 'دليل الاستخدام',
        'content' => $chunk,
        'embedding' => $response->embeddings[$i],
    ]);
}

تقسيم النص إلى مقاطع قرار مهم: مقاطع كبيرة جدًا تُشتّت المعنى، وصغيرة جدًا تفقد السياق. لا توجد قاعدة ثابتة؛ ابدأ بفقرات قصيرة متماسكة وجرّب.

الاسترجاع: البحث بالتشابه

الجزء الأجمل في Laravel أن الاسترجاع سطر واحد. الدالة whereVectorSimilarTo تفلتر بالتشابه الكوسيني وترتّب النتائج الأكثر صلة أولًا، وإذا مرّرت نصًا عاديًا فسيولّد Laravel الـ embedding له تلقائيًا عبر AI SDK:

$docs = Document::query()
    ->whereVectorSimilarTo('embedding', 'كيف أسترجع المبلغ المدفوع؟', minSimilarity: 0.4)
    ->limit(5)
    ->get();

القيمة minSimilarity بين 0.0 و1.0 حيث 1.0 تعني التطابق التام. إن احتجت تحكمًا أدق فهناك selectVectorDistance وwhereVectorDistanceLessThan وorderByVectorDistance.

التوليد: إعطاء النموذج السياق

بعد الاسترجاع، تُدخل المقاطع في تعليمات النموذج مع السؤال، وتطلب منه صراحةً أن يجيب فقط من هذا السياق وأن يقول "لا أعرف" إذا لم تكفِ المعلومات. هذه التعليمة الأخيرة هي ما يقلّل الهلوسة فعليًا.

بدلًا من أن تنفّذ الاسترجاع بنفسك في كل مرة، يوفّر AI SDK أداة SimilaritySearch تُعطى لـ Agent فيبحث بنفسه حين يحتاج:

use App\Models\Document;
use Laravel\Ai\Tools\SimilaritySearch;

public function tools(): iterable
{
    return [
        SimilaritySearch::usingModel(Document::class, 'embedding', minSimilarity: 0.7, limit: 10),
    ];
}

وللتحكم الكامل (مثل حصر النتائج بمستندات المستخدم الحالي) تقبل الأداة closure تُرجع النتائج بنفسك.

أخطاء شائعة عند بناء RAG

  • تجاهل الصلاحيات: إن كان لكل مستخدم مستنداته، فقيّد الاسترجاع بمعرّف المستخدم (where('user_id', ...)) قبل التشابه، وإلا قد يرى النموذج بيانات الآخرين.
  • تغيير نموذج الـ embeddings: المتجهات من نماذج مختلفة غير قابلة للمقارنة. إن غيّرت النموذج فأعد فهرسة كل شيء.
  • ضبط minSimilarity عشوائيًا: عتبة منخفضة تجلب ضجيجًا، وعالية تُرجع نتائج فارغة. اضبطها بتجربة أسئلة حقيقية.
  • الاختبار ضد API حقيقي: يوفّر AI SDK دالة Embeddings::fake() لتوليد متجهات وهمية بالأبعاد الصحيحة في الاختبارات.
  • الاعتقاد أن RAG يلغي الهلوسة: هو يقلّلها ولا يلغيها؛ تحقق من جودة الاسترجاع وأظهر المصادر للمستخدم.

متى لا تحتاج RAG؟

ليس كل مشروع يحتاج إلى متجهات. اسأل نفسك أولًا:

  • بياناتك قليلة وثابتة (صفحة أسئلة شائعة واحدة مثلًا): ضعها كاملة في تعليمات النموذج مباشرة، فهذا أبسط وأدق.
  • السؤال يحتاج مطابقة حرفية (رقم طلب، اسم منتج، كود خطأ): البحث النصي العادي أو الـ full-text أفضل، لأن البحث الدلالي قد يُرجع شيئًا "قريبًا في المعنى" وهو ليس ما طلبه المستخدم. وفي كثير من الأنظمة يُجمع النوعان معًا.
  • الإجابة تحتاج حسابًا أو تجميعًا (كم طلبًا هذا الشهر؟): هذا استعلام SQL وليس استرجاع نصوص.

الاختبار العملي: إن كان الجواب موجودًا داخل فقرة أو فقرتين من مستنداتك، فـ RAG مناسب. وإن كان يتطلب تجميع آلاف الصفوف، فلا.

شكل التعليمة التي تُرسل للنموذج

مهما كانت الأداة، تبقى البنية متشابهة: دور واضح، ثم السياق المسترجع، ثم السؤال، ثم قاعدة صريحة للحالة التي لا يكفي فيها السياق. مثال على النص (وليس كودًا خاصًا بمكتبة):

أنت مساعد دعم فني. أجب فقط اعتمادًا على المقاطع التالية.
إذا لم تجد الإجابة فيها فقل: "لا أملك معلومة كافية".
اذكر عنوان المقطع الذي استندت إليه.

المقاطع:
[1] سياسة الاسترجاع: يمكن استرجاع المبلغ خلال 14 يومًا...
[2] ...

السؤال: كيف أسترجع المبلغ المدفوع؟

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

الخلاصة

RAG ليس تقنية معقدة: فهرسة، ثم استرجاع بالتشابه، ثم توليد من السياق المسترجع. وفي Laravel 13 صار لديك عمود vector وفهرس واستعلام whereVectorSimilarTo جاهزة. ابدأ صغيرًا: مهاجرة واحدة، وجدول مقاطع من وثائقك، وسؤال تجريبي، ثم قِس جودة النتائج قبل أن تضيف أي تعقيد.

المصادر