تبدو معظم عروض RAG التوضيحية رائعة على أجهزة الكمبيوتر المحمولة. قم بتغذية نص برمجي بملف PDF مكون من عشرين صفحة، واطرح سؤالاً، وشاهد النظام وهو يستشهد بالفقرة الصحيحة. لكن نقل نفس مسار المعالجة إلى مرحلة الإنتاج هو المكان الذي ينتهي فيه هذا الانبهار. تُقطع الوثائق القانونية إلى نصفين عند مستوى الجملة. وتغرق مراجع واجهة برمجة التطبيقات (API) الكثيفة الإشارات المهمة في ضجيج النصوص الروتينية. يرتفع زمن الاستجابة بشكل هائل. ينتظر المستخدمون، ويشعرون بالتوتر، ثم يغادرون. لقد واجهنا هذا الجدار بقوة. لذا، قمنا بتفكيك طبقة الاسترجاع بالكامل وأعدنا بناءها كنظام مدروس وقابل للضبط. وكانت النتيجة مسار معالجة يحقق نسبة استدعاء (recall) تصل إلى 95% دون تحويل تجربة المستخدم إلى عرض شرائح بطيء.

لماذا تفشل عروض RAG التوضيحية في مرحلة الإنتاج

إن المكونات القياسية (stack) موحدة بشكل مفاجئ عبر المشاريع الهواة والمنتجات في مراحلها الأولى: أجزاء (chunks) ذات عدد رموز (tokens) ثابت، وتضمينات (embeddings) جاهزة، واستدعاء بحث شعاعي واحد. هذه البساطة مغرية، وهي تعمل عندما تكون مجموعة البيانات (corpus) نظيفة وصغيرة ويمكن التنبؤ ببنيتها النحوية. لكن بيانات الإنتاج ليست كذلك على الإطلاق. فجزء ثابت مكون من 512 رمزاً سيقطع بكل سهولة منتصف بند تعويض في عقد SaaS. وفجأة، تقوم طبقة الاسترجاع بتغذية النموذج اللغوي بنصف التزام قانوني وتطلب منه الإجابة على سؤال يتعلق بالمسؤولية. يبدأ النموذج في "الهلوسة" لأن السياق مقطوع.

وتزيد الوثائق التقنية الكبيرة من تفاقم المشكلة. فتوثيق واجهة برمجة التطبيقات (API) مليء بتواقيع الدوال، والجداول، وكتل الأكواد. قد تلتقط النافذة الثابتة منتصف واجهة TypeScript ولكنها قد تفقد اسم الدالة الموجود فوقها ومثال الاستخدام الموجود تحتها. ينتهي الأمر بمتجه التضمين (embedding vector) بتمثيل أجزاء من البنية النحوية وضجيج داخلي بدلاً من القدرة الفعلية التي يستفسر عنها المستخدم. مدخلات سيئة، مخرجات هلوسة.

التجزئة بناءً على البنية، وليس بناءً على عدد الرموز

كان التغيير الأول الذي أجريناه هو التوقف عن التفكير في الأجزاء (chunks) كمجرد حقائب من الرموز. الأجزاء هي وحدات دلالية. وتعتمد الاستراتيجية الصحيحة تماماً على ما تقوم بفهرسته.

بالنسبة للوثائق القانونية، انتقلنا إلى التجزئة المتكررة (recursive chunking) التي تحترم التسلسل الهرمي للوثيقة. فهي تعامل الأقسام، والأقسام الفرعية، والبنود كحدود فاصلة. يظل البند سليماً لأن البند هو وحدة معنى؛ وإذا قمت بتقطيعه، فسوف يتسرب المنطق القانوني.

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

تذاكر الدعم الفني أكثر تعقيداً؛ فهي حوارية، ومتسلسلة، وغير خطية. ستلتقط الأجزاء الثابتة تحديث حالة من مهندس وشكوى من عميل من نفس السلسلة وتتظاهر بأنهما يشكلان وحدة متماسكة. لذا انتقلنا إلى التجزئة الدلالية (semantic chunking)، حيث يتم التقسيم عند تغير الموضوع أو المتحدث بدلاً من انتهاء ميزانية الرموز.

غالباً ما تكون الويكي الداخلية (Internal wikis) هي البيانات الأكثر عشوائية في المؤسسة. التنسيق غير متسق، والعناوين مفقودة، والأقسام متداخلة. بالنسبة لهذه البيانات، نستخدم التجزئة القائمة على النماذج اللغوية الكبيرة (LLM-based chunking). حيث يقرأ نموذج صغير النص مسبقاً ويحدد الحدود المنطقية قبل أن نقوم بإنشاء أي تضمين. هذا يكلف أكثر في البداية مقارنة بالتقسيم بناءً على عدد الحروف، لكن جودة الاسترجاع تعوض هذه التكلفة فوراً.

الاسترجاع الهجين: دمج الإشارات

البحث الشعاعي (Vector search) قوي ولكنه يعاني من نقاط عمياء. إذا قمت بلصق رمز خطأ دقيق مثل ERR_CONNECTION_REFUSED_0x800 فقد يعيد بحث التشابه دليل استكشاف أخطاء لوحدة غير ذات صلة لأن مساحة التضمين وضعتهم قريبين من بعضهم البعض. التطابقات الدقيقة مهمة، والبحث الشعاعي وحده قد يمحوها.

يحل البحث بالكلمات المفتاحية باستخدام BM25 مشكلة التطابق الدقيق بشكل رائع، لكنه يتعثر أمام المسافات المفاهيمية. إذا سأل المستخدم عن "تدهور الأداء تحت الحمل الثقيل"، فسيفتقد BM25 ملاحظة تشخيصية تصف "بطء الإنتاجية أثناء ذروة حركة المرور" لعدم وجود تداخل كافٍ في الكلمات المفتاحية.

توقفنا عن اختيار جانب واحد وبدأنا في تشغيل كليهما بالتوازي. يعيد كل من البحث الشعاعي والبحث بالكلمات المفتاحية قوائم مرتبة خاصة به. نقوم بدمجهما باستخدام دمج الرتب المتبادل (Reciprocal Rank Fusion - RRF). يتميز RRF بالبساطة والفعالية الشديدة؛ فهو يقيم كل وثيقة بناءً على موقعها في كل قائمة. الوثائق التي تقع في أعلى القائمتين تحصل على دفعة هائلة، أما الوثائق التي يفضلها محرك واحد فقط، فلا تزال تحجز مكاناً لها في مجموعة المرشحين النهائية.

بعد عملية الدمج (fusion)، نقوم بتمرير أفضل المرشحين عبر مُعيد ترتيب من نوع cross-encoder. هذه العملية ليست مجانية؛ فهي تضيف حوالي 50 مللي ثانية من وقت الحوسبة. كما أنها تزيد من نسبة الاستدعاء (recall) بمقدار 15%. يقوم الـ cross-encoder بتقييم الاستعلام الكامل وكل قطعة (chunk) مرشحة معاً، مما ينتج عنه درجة صلة أكثر دقة بكثير مما يمكن أن يحققه أي embedding من نوع bi-encoder. تلك الـ 50 مللي ثانية الإضافية هي صفقة رابحة؛ فهي تمنعك من إرسال نافذة سياق (context window) مليئة بالبيانات غير المفيدة إلى الـ LLM، ومن ثم قضاء ثانيتين في انتظار إجابة مشوشة أو مبنية على "هلوسة" (hallucination).

أصلح الاستعلام قبل البحث

لا يكتب المستخدمون الاستعلامات مثل مهندسي البحث؛ فهم يكتبون "التطبيق معطل"، أو يلصقون أجزاءً غامضة من سجلات النظام (logs)، أو يطرحون أسئلة مبهمة وغير واضحة. إذا أرسلت هذه النصوص الخام مباشرة إلى الفهرس (index)، فستحصل على نتائج غير مفيدة.

نحن نقوم بتحويل كل استعلام قبل أن يصل إلى محرك الاسترجاع (retrieval engine).

أولاً، توسيع الاستعلام (query expansion). يقوم النظام بتوليد مصطلحات بحث متعددة من سؤال قصير واحد. فإذا سأل المستخدم: "كيف أصلح مشكلة انتهاء المهلة (timeout)؟"، يقوم المحرك بتوسيع ذلك ليشمل انتهاء مهلة الاتصال، وانتهاء مهلة القراءة، وانتهاء مهلة البوابة (gateway)، ومنطق إعادة المحاولة. هذا النهج وحده رفع نسبة الاستدعاء (recall) لدينا من 78% إلى 96%.

ثانياً، تفكيك الاستعلام (query decomposition). يتم تقسيم الأسئلة المعقدة إلى أسئلة فرعية أصغر. فاستعلام مثل "ما هي سياسة الاسترداد لعملاء المؤسسات الذين تجاوزوا 90 يوماً، وكيف تختلف عن الخطط الشهرية؟" يتحول إلى عمليتي بحث مركزتين بدلاً من عملية بحث واحدة ضخمة عبر الـ embedding. يتم استهداف كل سؤال فرعي للفهرس بشكل مستقل، ثم تُجمع النتائج معاً في المراحل اللاحقة. هذا يحافظ على دقة وتركيز عملية الاسترجاع، مما يمنع تشتت النتائج الذي يحدث عندما يحاول embedding واحد مطابقة عشرات المفاهيم في وقت واحد.

دع البحث البايزي (Bayesian Search) يضبط مسار عملك (Pipeline)

إذا كنت لا تزال تقوم بضبط حجم القطع (chunk size)، ونسب التداخل (overlap ratios)، وأوزان الاسترجاع يدوياً، فأنت تضيع الكثير من الأداء الممكن. لقد توقفنا عن التخمين.

لقد حددنا مساحة بحث تكون فيها أحجام القطع، ونسبة التداخل، وأوزان المتجهات مقابل BM25، وعتبات مُعيد الترتيب (reranker thresholds) جميعها متغيرات. ثم طبقنا التحسين البايزي (Bayesian optimization). فبدلاً من البحث الشبكي (grid-searching) عبر مئات التكوينات العشوائية، يقوم البحث البايزي ببناء نموذج احتمالي لما ينجح. فهو يقترح تكويناً، ثم يراقب نسبة الاستدعاء وزمن الاستجابة (latency)، ويحدث معتقداته، ثم يقترح التكوين التالي. ومع مرور الوقت، يصل إلى توازنات لا يمكن للإنسان الوصول إليها يدوياً أبداً.

لقد وجد تركيبات لم نكن لنجربها أبداً: قطع أصغر مع تداخل أكبر، أو وزن أقل قليلاً للبحث باستخدام المتجهات الكثيفة (dense vector search) مقترناً بعتبة أكثر صرامة لمُعيد الترتيب. هذه المقايضات غير البديهية منحتنا نسبة استدعاء أعلى وزمن استجابة أقل في آن واحد.

هذه ليست مهمة إعداد تُنفذ لمرة واحدة. نحن نعيد تشغيل تحسين المعلمات الفائقة (hyperparameter optimization) شهرياً. فمجموعة البيانات (corpus) تتغير، وسلوك المستخدمين يتبدل، لذا يجب أن يتكيف مسار عملك بدلاً من أن يظل جامداً في مكانه.

النتيجة النهائية

من الصعب الجدال في النتائج المباشرة لعملية إعادة البناء تلك.

ارتفعت نسبة الاستدعاء عند المركز العاشر من 78% إلى 95%. وعندما تكون الإجابة الصحيحة موجودة في قاعدة معرفتنا، فإننا نظهرها 19 مرة من أصل 20. كما انخفض زمن الاستجابة عند المئين الـ 95 (95th percentile) من 850 مللي ثانية إلى 320 مللي ثانية. أصبح الدردشة تبدو فورية بدلاً من كونها بطيئة ومثقلة.

أدى الاسترجاع الأفضل إلى توفير أساس (grounding) أفضل للنموذج اللغوي. وانخفض معدل الهلوسة من 12% إلى 3%. فعندما يكون السياق الصحيح أمام النموذج، يتوقف عن اختراع الحقائق. كما انخفضت التكلفة لكل استعلام بنسبة 38%؛ فعملية الاسترجاع الأسرع والأكثر دقة تعني هدر عدد أقل من الرموز (tokens) في سياقات غير ذات صلة، أو حلقات إعادة المحاولة، أو المطالبات (prompts) المطولة وغير المفيدة.

ابنِهِ كبنية تحتية

إذا كنت تنتقل من مرحلة النموذج الأولي إلى مرحلة الإنتاج، فتعامل مع الاسترجاع ككود للبنية التحتية (infrastructure code) وليس مجرد إعدادات (configuration).