في الآونة الأخيرة، اكتسبت تقنية RAG سمعة سيئة أحيانًا؛ فالبعض يراها الآن بسيطة تمامًا (البدء بها بسيط، أما توسيع نطاقها فأقل بساطة)، والبعض الآخر يعتقد أن «الأنظمة الوكيلة» تجاوزتها (مع أن التعمق قليلًا في كثير من هذه الأنظمة يكشف سريعًا عن شبه كبير بينها وبين RAG...)
تعرض هذه التدوينة مثالًا عمليًا أو اثنين لتوضيح كيفية تعاملنا مع بعض التحديات الشائعة، وهي:
التعامل مع البيانات المختلطة من نصوص وأرقام، ولماذا تفشل معها تطبيقات RAG الساذجة: تتداخل الكلمات المفتاحية، ولا تحمل الأرقام أي معنى دلالي.
لماذا يفيد تصميم التضمينات بالبدء بالملخص: أنشئ ملخصًا وصفيًا قصيرًا لكل مقطع، ثم نفّذ التضمين والاستعلام اعتمادًا عليه.
كيفية إنشاء ملخصات سياقية: أدرج سياق المستند الأصلي حتى يظل من الممكن التمييز بين الإحصاءات المتشابهة في بنيتها.
متى تعتمد على الشفرة البرمجية ونماذج Pydantic: عندما يلزم نقل المحتوى حرفيًا، اجمع بين شفرة مخصصة و/أو نموذج Pydantic واستدعاءات النماذج اللغوية الكبيرة (LLM) لضمان الموثوقية.
الأساسيات
تدعم أنظمة RAG تطبيقات متنوعة، من روبوتات الدعم إلى مساعدي المعرفة الداخليين.
وعادةً ما تجري العمليات التالية في الخلفية:
قسّم مستنداتك المصدر إلى مقاطع
ضمّن كل مقطع في فضاء متجهي
استرجع أفضل K مقاطع عند الاستعلام
أنشئ إجابة تستند إلى تلك المقاطع
تجعل مجموعات الأدوات الشائعة، مثل LangChain وLlamaIndex وFilestore من OpenAI، هذه الخطوات شبه بديهية. لكن مسارات العمل الفعلية ستتضمن بيانات لا تقتصر على النصوص الكثيفة، وقد تواجه تطبيقات RAG الأساسية صعوبة في التعامل معها. سنعرض في الأقسام التالية أمثلة ملموسة لتحديات البيانات، ونبني الحل تدريجيًا مع ازدياد التعقيد.
عندما لا تقتصر بياناتك على النصوص (وهو أمر شائع في الواقع)
تأمل مقطع البيانات التالي في سياق إحدى الألعاب:
JSON
تنجح التضمينات بفضل العلاقات المتعلَّمة بين الكلمات من خلال المعنى الدلالي والقواعد اللغوية. تتضمن البيانات أعلاه مزيجًا من النصوص والأرقام، ولا تربط الأرقام بالكلمات أي علاقات خارج هذا السياق المحدد. لذا يمكن القول إن مقطع البيانات هذا ليس سوى مزيج من كلمات وصفية إلى حد ما، تليها بعض الأرقام العشوائية.
ولن يمثل ذلك مشكلة لو كان هذا النوع الوحيد من البيانات لدينا، إذ سيظل بإمكاننا الاسترجاع اعتمادًا على تضمينات الكلمات الوصفية القليلة المتاحة (أو استخدام تحويل النص إلى SQL ببساطة). ولكن ماذا لو كان هذا المقطع مدفونًا بين الكثير من المقاطع النصية الكثيفة التي تتكرر فيها هذه الكلمات أيضًا؟ على سبيل المثال:
JSON
تخيل الآن أننا نريد استرجاع “What is the attack range with Draconic Ascension?” يُرجح بشدة ألا نتمكن من استرجاع المقطع المطلوب، لأنه غارق في ضجيج المقاطع الأخرى التي تحتوي على الكلمات المفتاحية نفسها.
تكمن المشكلة الأساسية في تعذر التمييز الجيد بين مقاطع البيانات هذه، رغم أنها تتضمن أنواعًا مختلفة من المعلومات حول الموضوع نفسه. فهل يمكننا إثراء ذلك أو تحسينه بطريقة ما؟ بالطبع يمكننا ذلك :smile:
أثرِ بياناتك بتلخيصها، نعم، لقد قرأت ذلك بشكل صحيح
بدلًا من تضمين المقطع نفسه مباشرةً، يمكننا أولًا إنشاء ملخص يصف محتوى البيانات، ثم تنفيذ التضمين والاسترجاع اعتمادًا على الملخص. وفي مرحلة التوليد، سنظل نستخدم البيانات الأصلية المرتبطة بالملخص.
لذلك، سننشئ لمثالي المقاطع الموضحين أعلاه ملخصات من قبيل:
إحصاءات مدى الهجوم وسرعته وضرره (افتراضيًا ومع Draconic Ascension).
وصف Draconic Ascension وتفاصيله، بما يشمل شروط التفعيل والمؤثرات المرئية والخلفية القصصية.
ثم نعزز الاستعلام أيضًا كي «يتوافق» مع الملخص. فعلى سبيل المثال، سنحوّل “What is the attack range with Draconic Ascension?” إلى “What is the statistics of attack range with Draconic Ascension?” وهذا مهم خصوصًا عندما يأتي استعلام الاسترجاع من مستخدمين غير متخصصين تقنيًا، يطرحون أسئلتهم بلغة بشرية ~~«عفوية»~~ طبيعية؛ فهم في النهاية لا يعرفون كيفية عمل RAG ولا يعنيهم ذلك، حتى يحققوا أفضل دقة واستدعاء ممكنين.


لا تخرج الأمور من سياقها (نصيحة تصلح للحياة عمومًا)
لنتناول الآن سيناريو لاحقًا يتضمن التعامل مع عدد هائل من مقاطع البيانات المتشابهة، كما يلي:
Plain Text
إذا واصلنا اتباع النهج نفسه، فتخيل أن نسأل: “what is character X’s attack range?” عندها سنخوض لعبة تخمين تعتمد على الحظ مع الملخصات التي أنشأناها للتو، لأنها ستبدو متشابهة جدًا أيضًا. فكيف يمكننا التمييز بينها؟
الإجابة بسيطة: أضف السياق. يمكننا ببساطة أن ندرج في مقطع البيانات مرجعًا إلى المستند الأصلي للمقطع، مثل {”character”: “X”} في هذه الحالة. وهكذا يمكننا الآن استرجاع البيانات الصحيحة للشخصية X بدقة، حتى عند توفر البيانات نفسها للشخصيتين Y وZ.
لكن النهج الأفضل والأكثر قابلية للتعميم هو إنشاء ملخص سياقي للمقطع. أي بدلًا من إنشاء ملخص لمقطع البيانات وحده، يمكننا تمرير المستند الأصلي والمقطع معًا لإنشاء ملخص سياقي عام يوضح كيفية ارتباط المقطع بالمستند الأصلي، مثل:
يقدم هذا المقطع إحصاءات تفصيلية عن ... للشخصية X. يندرج المقطع ضمن المستند الكامل من خلال إظهار قوة X في سرعة الهجوم...
يقدم هذا المقطع إحصاءات تفصيلية عن ... للشخصية Y. يندرج المقطع ضمن المستند الكامل من خلال إظهار الإحصاءات المعززة للشخصية Y عند استخدام قدرتها الخاصة...
يقدم هذا المقطع إحصاءات تفصيلية عن ... للشخصية Z. يندرج المقطع ضمن المستند الكامل من خلال إظهار إحصاءات Z التي تلائم دور المدافع في المباريات الجماعية...
قد تبدو هذه الطريقة (المستوحاة جزئيًا من Anthropic) مبالغًا فيها للمثال الموضح أعلاه، لكنها فعالة جدًا مع المقاطع التي قد يُساء تفسيرها «خارج سياقها». كما أنها تقدم نهجًا موحدًا يصلح لجميع المقاطع ويحافظ على مسار هندسي منظم.


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


كانت محاولتنا الأولى مع هذه البيانات هي إدخالها كلها في استدعاء للنماذج اللغوية الكبيرة (LLM)، وطلب تجميعها بالطريقة التي يراها مناسبة، ثم إرجاع المحتوى المجمّع. يُفترض أن تكون النماذج اللغوية الكبيرة (LLM) بارعة جدًا في ذلك، أليس كذلك؟ حسنًا، نعم ولا.
اكتشفنا، في مناسبات عديدة أخرى أيضًا، أن النماذج اللغوية الكبيرة (LLM) تميل إلى التكاسل ولا يمكن الوثوق بها عند طلب المحتوى كاملًا وحرفيًا، لا سيما عندما يكون السياق طويلًا. وهذا منطقي تمامًا. لكن ذلك كان عائقًا حاسمًا في حالة الاستخدام هذه تحديدًا، لأننا نحتاج إلى المحتوى الحرفي كلمة بكلمة: بلا تلخيص ولا إسقاط لأي جزء من المحتوى الأصلي. لا يمكننا إغفال أي تفاصيل.
أما جانب «نعم»، فتمثل بالطبع في أدائها الممتاز في فهم دلالات المقاطع المجزأة وبنيتها. هذا إن لم ترفض إعادة المحتوى الحرفي. تبًا لها :/
فكيف نستفيد مما تتقنه النماذج اللغوية الكبيرة (LLM)، ونتجنب في الوقت نفسه ما لا يمكن الوثوق بها فيه؟ لجأنا إلى صديقنا القديم الموثوق: الشفرة البرمجية (أي دالة Python مخصصة). وإلى نموذج Pydantic «بالغ البساطة». إليك الحل:
مرّ على الأقسام بالتتابع مع الاحتفاظ بمقطع منطقي حالي
عند كل قسم، اسأل النماذج اللغوية الكبيرة (LLM): هل ينتمي هذا القسم إلى المقطع المنطقي الحالي؟ أجب بنعم أو لا (وفقًا لنموذج Pydantic).
إذا كانت الإجابة نعم، فألحق القسم بالمقطع؛ وإن كانت لا، فأخرج المقطع المنطقي الحالي بعد اكتماله، ثم ابدأ مقطعًا جديدًا بهذا القسم.


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