حواجز حماية LLM أثناء البث: الرمز المُرسَل لا يُسترد
حواجز حماية LLM أثناء البث: الرمز المُرسَل لا يُسترد. كيف تُفاضل NeMo وGuardrails AI وBedrock بين السرعة والأمان، وكيف تفحص بوابة الجمل كل ادعاء.
الرمز الذي بثّه نموذج LLM لا يمكن استرداده. فحين يصل النص إلى القارئ، لا يملك حاجز الحماية (guardrail) الذي يعترض عليه إلا أن يوقف ما يليه. لذلك لا يتخذ حاجز الحماية على البث إلا قرارًا حقيقيًا واحدًا: أن يتحقق قبل الإرسال ويُبقي القارئ منتظرًا، أو أن يرسل أولًا ويقبل أن الإجابة المحجوبة قد قُرئت بالفعل. وثمة ثلاثة أشكال عملية: التحقق بعد الإرسال، أو بوابة تحتجز مقاطع ثابتة الطول، أو بوابة تحتجز الجمل كاملةً. والمساعد في هذا الموقع، وقد بنيتُه، يحتجز الجمل. كلفة ذلك انتظار بطول جملة واحدة. وحدّه أنه يتحقق من أن الادعاء مدعوم بالمصادر، لا من أنه صحيح.
تتناول هذه المقالة آخر فحص قبل إرسال إجابة مستندة إلى المصادر. أما الطبقات التي تسبقه، من رفض الأسئلة الخارجة عن النطاق إلى حدّ أدنى لدرجة الاسترجاع، فتجدها في حواجز الحماية من هلوسة RAG: تصميم من خمس طبقات.
لماذا لا يمكن استرداد الرمز بعد بثّه؟
المدقِّق اللاحق (post-hoc validator) لا يرى الإجابة كاملة إلا بعد أن يكون القارئ قد قرأها. فإذا اختلقت الجملة الثالثة تاريخًا، فإن حذفها بعد ذلك يغيّر السجل، لا ما قُرئ. وتصوغ ورقة بحثية أولية (preprint) حديثة على arXiv كتبها Christopher M. Frost هذا الحدّ بدقة: «آلية التحكم التي تكتشف نمطًا محظورًا بعد إرسال المقطع الذي يكتمل به لا تستطيع استرداده».
ويوثّق المزوّدون الحدّ نفسه. فالمرشِّح غير المتزامن (asynchronous filter) في Azure OpenAI، وهو ميزة لا تعمل إلا بعد تفعيلها صراحةً، يعيد النص فورًا، ولا يضمن إشارة المخالفة إلا ضمن نحو ١٬٠٠٠ حرف من المحتوى المخالف. ونصيحة Azure في قسم استكشاف الأخطاء وإصلاحها هي «تنفيذ تنقيح المحتوى في تطبيقك لإزالة المحتوى المعروض بالفعل». ويوصي مدخل LLM09:2025 لدى OWASP بأدوات وعمليات «للتحقق تلقائيًا من المخرجات الرئيسية». والسؤال المفتوح هو متى يجري التحقق بالنسبة إلى لحظة الإرسال.
ثلاث طرق لحماية البث: تحقق لاحق، أو بوابة مقاطع، أو بوابة جمل
النهج | متى يرى القارئ النص | الإجابة المحجوبة | أمثلة |
|---|---|---|---|
التحقق اللاحق (الإرسال أولًا) | قبل فحصه (في NeMo: كل مقطع فور اكتمال تخزينه مؤقتًا؛ وفي الوضع غير المتزامن في Azure وBedrock: أولًا بأول مع توليده) | قُرئ جزء منها قبل التوقف | NeMo مع تفعيل |
بوابة المقاطع | بعد اجتياز كل مقطع للفحص | لا يُعرض المقطع المحجوب أبدًا | الإعداد الافتراضي في Azure وBedrock؛ NeMo مع تعطيل |
بوابة الجمل | بعد اجتياز كل جملة للفحص | لا تُعرض الجملة المحجوبة أبدًا | مساعد هذا الموقع |
ما المفاضلة التي يفرضها stream_first في NeMo Guardrails؟
تشغيل حواجز الإخراج (output rails) على البث معطَّل افتراضيًا في NeMo Guardrails. وعند تفعيله، تعمل حواجز الإخراج، وهي فحوص NeMo على النص المولَّد، على مقاطع طولها الافتراضي ٢٠٠ رمز، ويُرحَّل ٥٠ رمزًا من السياق إلى المقطع التالي. والقيمة الافتراضية للخيار stream_first هي True: «يتلقى العميل كل مقطع من الرموز قبل أن تعالج حواجز الإخراج ذلك المقطع. وهذا يتيح زمنًا أقصر حتى أول رمز، لكن إذا حجب أحد الحواجز المحتوى، فإن المستخدم يكون قد تلقى الرموز بالفعل». ثم ينتهي البث بخطأ بصيغة JSON.
ومع False، «لا يرى المستخدم المحتوى المحجوب أبدًا، لكن الزمن حتى أول رمز يزيد بمقدار زمن تنفيذ الحاجز لكل مقطع». هذه هي المفاضلة: الزمن حتى أول رمز (TTFT) في مقابل وصول النص المحجوب إلى القارئ. فمتى فُعِّل تشغيل حواجز الإخراج على البث، يرسل NeMo أولًا افتراضيًا، أما المرشِّحان السحابيان الواردان أدناه فيتحققان أولًا. وإذا كان الحاجز موجودًا ليمنع شيئًا يجب ألا يُرى أبدًا، فغيّر القيمة الافتراضية.
ما المفاضلة في مدقِّقات الجمل لدى Guardrails AI؟
أما Guardrails AI فتختار الجملة: «افتراضيًا، تنتظر المدقِّقات (validators) حتى تجمع من LLM محتوى بقدر جملة قبل تشغيل التحقق». والتحقق من جمل منفردة بدلًا من الإجابة كاملة «يساعد على توفير التكاليف في المدقِّقات التي تتطلب استدلالًا مكلفًا».
وليست كل أنواع on_fail مدعومة مع البث، والإصلاحات الصادرة عن عدة مدقِّقات تنتظر «حتى تجمع كل المدقِّقات مقاطع كافية» قبل دمجها. ولا تقول الوثائق صراحةً ما إذا كانت الرموز الخام تصل إلى المستخدم قبل التحقق، فتحقّق من ذلك بنفسك عند دمج المكتبة في نظامك.
تخزين مؤقت افتراضيًا: Azure OpenAI وAmazon Bedrock
يخزّن Azure OpenAI نص الإكمال مؤقتًا افتراضيًا، ويرشِّح محتوى المخزن المؤقت، ويعيد «مقاطع محتوى» لا رموزًا منفردة. ويفحص Amazon Bedrock Guardrails المقاطع قبل إرسالها في وضعه المتزامن الافتراضي، الذي «يضيف بعض التأخير» لكنه «يوفّر دقة أفضل». أما في الوضع غير المتزامن في Bedrock، فإن المقاطع «قد تحتوي على محتوى غير لائق إلى أن يكتمل فحص حواجز الحماية»، ولا يمكن إخفاء المعلومات الحساسة.
كيف تتحقق بوابة الجمل من البث؟
لا يجيب مساعد هذا الموقع إلا من محتوى الموقع نفسه، والسؤال الذي لا يخصّني ولا يدعمه أي مقطع مسترجَع لا يصل إلى النموذج أصلًا. وهو يتحدث عني بضمير الغائب. وفي موقع يحمل اسمي، أسوأ إخفاق ممكن هو مؤهل مختلَق، والمؤهل ادعاء. والجملة أصغر وحدة تحمل ادعاءً، لذلك تخزّن بوابة الجمل (sentence gate) النص مؤقتًا حتى حدّ الجملة، ثم تفحصه، ولا ترسله إلا بعد ذلك. ويستدعي عرض «RAG مقابل بلا استرجاع» في المختبر نقطة نهاية البث نفسها.
أين تنتهي الجملة والرموز ما زالت تصل؟
علامات الإنهاء:
.و!و?و…، إضافةً إلى علامتين من الخط العربي: علامة الاستفهام؟والنقطة۔، وللصينية والهندية。و!و?و«الداندا»।و«الداندا المزدوجة»॥. وتُعدّ سلسلة مثل?!حدًّا واحدًا. أما الفاصلة المنقوطة العربية؛فتصل بين أجزاء الجملة، ولذلك استُبعدت عمدًا.يجب أن تليها مسافة بيضاء. هذه القاعدة وحدها تُبقي
3.14وexample.comدون تقسيم. ويُستثنى منها النقطة وعلامتا التعجب والاستفهام في الصينية، إذ لا تليها مسافة. وإذا وقعت داخل اقتباس فإنها تنهي الجملة حيث يُغلق الاقتباس، ولا تنهيها أصلًا إذا تلته فاصلة.ليست كل نقطة نهايةً لجملة. فالنقطة التي يليها حرف لاتيني صغير لا تنهيها (
et al. and)، ولا نقطة اختصار من قائمة قصيرة، مثلe.g.أوbzw.الألمانية، ولا النقطة التي تلي اليوم في التاريخ الألماني (am 17. September).الحرف التالي هو الذي يحسم. لا يصبح الحدّ نهائيًا إلا حين يصل حرف ليس مسافةً ولا علامةَ إسناد، لأن علامات الإسناد التي تلي علامة الإنهاء تنتمي إلى الجملة السابقة: كلها،
[S1][S2]أو[S1] [S2]، لا الأولى وحدها.رقم البند بعد الحدّ ليس جملة. وإلا لعُدّ
1.في بداية الإجابة أو بداية جملة جديدة ادعاءً بلا إسناد، ولأوقف إجابةً مستندة إلى المصادر عند أول بند فيها.سقف. إذا تجاوز المخزن المؤقت ألف حرف دون حدّ، يُقطع عند آخر مسافة ويُفحص على أي حال.
السطر الفارغ يبقى. الجملة التي سبقها سطر فارغ تُرسَل وقبلها سطر فارغ أيضًا، فتبقى الإجابة المكتوبة في فقرتين فقرتين.
ويكلّف انتظارُ الحرف التالي تأخيرًا بمقدار حرف واحد عادةً، وأكثر قليلًا حين تلي النقطةَ علاماتُ إسناد. وتُفحص الجملة الأخيرة عند انتهاء البث. وتوحِّد المقارنات الأرقامَ الفارسية وفواصلها (۳٫۱۴ يطابق 3.14)، والأشكالَ العربية للحروف مع نظائرها الفارسية، وتحذف علامات الحركات القصيرة في العربية، وتعامل محرف عدم الوصل صفري العرض (ZWNJ) معاملة المسافة: وهي الاختلافات نفسها التي يعالجها البحث الهجين في RAG للغة الفارسية في جانب الاسترجاع.
الفحوص الحتمية الأربعة: الإسناد والقيم وصيغة المتكلم
نطاق الإسناد. علامة مثل
[S7]حين لم يُقدَّم سوى ستة مصادر تشير إلى مصدر مختلَق.وجوب الإسناد. الجملة المؤلفة من ثماني كلمات أو أكثر، أو التي فيها أي رقم أو عنوان URL أو عنوان بريد إلكتروني، تحتاج إلى علامة. أما الجمل الرابطة القصيرة فتمرّ.
الدعم الحرفي. يجب أن يظهر كل رقم وعنوان URL وعنوان بريد إلكتروني في مصدر تستشهد به الجملة. وتراعي المطابقة حدود الأرقام، فلا يمكن لسنة واردة في مصدر أن تشهد لنسبة مئوية تشاركها أرقامها الأخيرة، والأعداد العشرية التي لا تختلف إلا بأصفار في آخرها قيمة واحدة:
20.00هي20. والفاصلة التي لا تفصل الآلاف فاصلةٌ عشرية:2,5هي2.5لا25.صيغة المتكلم. أي كلمة بصيغة المتكلم توقف البث. القائمة الإنجليزية قصيرة وثابتة: ضمائر المتكلم وصيغها المختصرة. أما القائمة الفارسية فمنتقاة: الضمائر إضافةً إلى بضع صيغ فعلية عالية الخطورة، مثل «هستم» و«دارم»، وهما مقابلا «أنا» و«لديّ» بالفارسية. فالفارسية تدل على الشخص بلواحق في آخر الفعل قد تنتهي بها أسماء عادية أيضًا، لذا فإن قاعدة تتسع لالتقاط كل هذه اللواحق ستلتقط الأسماء كذلك. أما قوائم لغات الموقع الأخرى فتضم الضمائر وضمائر الملكية، وتضيف قوائم الإسبانية والبرتغالية والروسية والعربية والهندية بضعة أفعال، لأن هذه اللغات كثيرًا ما تعبّر عن المتكلم بالفعل وحده؛ وفي الصينية يُبحث عن 我 في أي موضع من الجملة، إلا في مركّبٍ يبدأ ب 自我 بمعنى «ذاتي»، مثل 自我修正 («التصحيح الذاتي»). وتتوقف القوائم المطبَّقة على اللغة التي طُلبت بها الإجابة. فقوائم الإنجليزية والفارسية والعربية والروسية والهندية والصينية تُطبَّق على كل إجابة: فالإنجليزية لغة التعليمات الموجَّهة إلى النموذج، ولذا فهي أرجح لغة تنزلق إليها الإجابة، أما الخمس الأخرى فمكتوبة بخطوط لا يمكن لأي اسم أو اختصار لاتيني أن يطابق كلماتها. والاستثناء هو الإجابة المطلوبة بالعربية: فلا يُبحث فيها عن «من» و«ما» الفارسيتين، أي «أنا» و«نحن»، لأن «من» في العربية حرف جر، و«ما» كلمة شائعة فيها. أما قوائم الألمانية والفرنسية والإسبانية والبرتغالية فلا تُطبَّق إلا على الإجابات المطلوبة بلغتها: فكلماتها القصيرة تكون أيضًا أسماءً وكلماتٍ عادية في لغات أخرى، مثل «Ma» في «Ma et al.». وقبل قراءة أي قائمة تُستبعد الاختصارات المكتوبة بالأحرف الكبيرة، واسم العائلة الذي يسبق «et al.»، والرقم الروماني في «Annex I».
ماذا يحدث حين تخفق جملة في الفحص؟
تُرسل كل جملة اجتازت الفحص قبل الإخفاق، بما فيها الجمل السابقة في الدفعة نفسها.
تتوقف البوابة وتبقى متوقفة: جملة واحدة غير مدعومة تُسقط الثقة بما بقي من الإجابة.
يحمل حدث
guardأسباب المخالفة ونصًا ثابتًا بلغة القارئ يفيد بأن بقية الإجابة حُجبت، ويسجّل حدثdoneالأخير هذا التوقف. ولا تُرسل الجملة المحجوبة أبدًا؛ ولا يسجّل الخادم إلا طولها.يُغلق بث المزوّد، وبهذا يلغي التصميم الطلب إلى المنبع (upstream). وما وُلِّد قبل التوقف يبقى على الفاتورة، فيُحتسب ضمن سقف الإنفاق الشهري أيضًا. وكلا بروتوكولَي المزوّد يُبلغ عن استهلاك الإخراج في حدث ختامي لا يبلغه أبدًا بثٌّ قُطع في منتصفه، لذلك تحتفظ الخدمة بالنص الذي تلقّته، وتملأ ما لم يُبلغ عنه المزوّد برمز واحد لكل بايت في ترميز UTF-8، وهو عدٌّ إن أخطأ فإلى الزيادة. أما الرموز التي يصرفها نموذج الاستدلال (reasoning) في التفكير فلا تصل نصًّا أبدًا، فيغفلها هذا التقدير. وتتناول مقالة الطبقات الخمس هذا السقف.
وتَسِم لوحة الدردشة هذا الرد بأنه محجوب، وتُبقي نصه المرسَل في سجل المحادثة، لأن ذلك النص مدعوم حتى نقطة التوقف.
أما الاقتطاع (truncation) فإخفاق من نوع آخر، وقد خلطت نسخة سابقة من هذا المساعد بين الاثنين. فحين كانت تنفد مساحة الإخراج المتاحة للنموذج، كانت البوابة تعامل الجزء المقطوع بوصفه جملة مكتملة، وتحجبه لأنه بلا إسناد. فكان القارئ يُبلَّغ بأن المصادر لا تدعم إجابةً لم يصبها سوى الانقطاع. وحين لم يَعُد أي نص على الإطلاق، كان الرد يختفي دون أي رسالة. أما الآن، فيُسقَط الجزء غير المكتمل دون الحكم عليه، ويُعرض إشعار بأن الإجابة انقطعت. فالجملة المنقطعة ليست جملة غير مدعومة. وإن وقع الأمران معًا، فالأولوية لتوقف البوابة.
علامات الإسناد داخلية
يطلب الموجّه (prompt) علامة [Sn] على كل جملة تذكر معلومة واقعية، وتطبّق البوابة قاعدة الإسناد على كل جملة جوهرية. وحين تجتاز جملة الفحص، تُزال علاماتها وتُصلَح المسافات، بما في ذلك ما قبل علامات الترقيم الفارسية، فيقرأ القارئ نصًا متصلًا لا علامات. أما حذف العلامات من الموجّه بدلًا من ذلك، فسيُسقط كل جملة جوهرية في قاعدة الإسناد.
حين لا يجد النموذج ما يقوله
حين لا تغطي المصادر السؤال، يطلب الموجّه من النموذج رمزًا ثابتًا لا جملة. فالرفض الذي يصوغه النموذج بنفسه ادعاء كأي ادعاء آخر، بل ادعاء بلا إسناد، ستوقفه البوابة بإشعار يُلقي اللوم على المصادر في سؤال لم يكن مطلوبًا منها أن تجيب عنه أصلًا.
ولا ترسل البوابة هذا الرمز أبدًا. فما دام النص الوارد حتى الآن قد يتحول إلى هذا الرمز، يبقى محجوزًا؛ وأول حرف يستبعد ذلك يعيد النص إلى مساره المعتاد، فلا تنتظر الإجابة العادية شيئًا إضافيًا. وإذا بدأت الإجابة بهذا الرمز، وضعت الخدمة مكانها نص الرفض الثابت في الموقع، وهو النص نفسه الذي يظهر في أي رفض آخر. وإذا جاء الرمز بعد جمل أُرسلت بالفعل، بقيت تلك الجمل وانتهت الإجابة عندها.
التحقق من أن الادعاء مدعوم ليس تحققًا من صحته
في دراسة بقيادة باحثين من جامعة ستانفورد، أجرى Magesh وزملاؤه ما يصفونه بأنه أول تقييم مسجَّل مسبقًا (preregistered) لأدوات البحث القانوني المعتمدة على الذكاء الاصطناعي. وقد تراوحت نسبة الهلوسة في كلٍّ من أدوات البحث بالذكاء الاصطناعي لدى LexisNexis وThomson Reuters بين ١٧٪ و٣٣٪ من الحالات. وعُدّت الإجابة هلوسةً إذا كانت «إما غير صحيحة وإما خاطئة الإسناد (misgrounded)»، والإسناد الخاطئ يعني أن «المقولات الوقائعية الأساسية مُسندة إلى مصادر، لكنها تسيء تفسير المصدر أو تحيل إلى مصدر لا ينطبق». فالإجابات خاطئة الإسناد تحمل استشهادات. أما ما يعنيه ذلك لمنتجات الذكاء الاصطناعي القانونية فتتناوله مقالة منفصلة.
البوابة القائمة على مطابقة النصوص لا ترى الإسناد الخاطئ، ولهذه البوابة تحديدًا نقاط عمياء معروفة:
الجملة التي تستشهد بمصدرين يمكن أن تأخذ قيمة من أيٍّ منهما. فالفحص يجري على مستوى الجملة، لا على مستوى أجزائها.
النقطة العشرية تُعدّ حدًّا. فوجود
14.52في مصدر يمكن أن يشهد للقيمة52%أو14، وفاصل الآلاف يفعل الشيء نفسه:1,200يشهد للقيمة200.الكلمات لا تُفحص. الجملة الخالية من الأرقام التي تستبدل دورًا بآخر أو مؤسسة بأخرى تمرّ مع أي علامة ضمن النطاق.
الجمل القصيرة غير المسندة تمرّ. الجملة التي تقل عن ثماني كلمات وليس فيها رقم أو URL أو عنوان بريد لا تحتاج إلى علامة. والصينية لا تضع مسافات بين الكلمات، فتُعدّ فيها الحروف بدل الكلمات، وكل ثلاثة حروف تُحسب كلمتين: فالجملة المؤلفة من حروف صينية وحدها لا تحتاج إلى علامة ما دامت دون اثني عشر حرفًا.
في قوائم الكلمات الثابتة ثغرات. في الإنجليزية تمرّ
we'dوlet's، وكذلك الضمير المكتوب كله بالأحرف الكبيرة، مثلWEأوUS، لأنه يُقرأ اختصارًا. أما تغطية صيغة المتكلم في الفارسية فتعتمد على الموجّه: فأي فعل بصيغة المتكلم غير مدرج في القائمة المنتقاة يمرّ. وقوائم اللغات الأخرى منتقاة هي أيضًا، أما صيغة المتكلم بالألمانية أو الفرنسية أو الإسبانية أو البرتغالية في إجابة مطلوبة بلغة أخرى فمتروكة للموجّه.علامات الترقيم الختامية قد تدمج الجمل. بعد علامة اقتباس تُغلَق على علامة الإنهاء، كما في
?»أو."، لا ينهي الجملةَ إلا حرف كبير أو سطر جديد، وبعد القوس، كما في.)، لا ينهيها إلا سطر جديد أو علامة إسناد. لذلك قد تُقيَّم جملتان كأنهما جملة واحدة: بعد.)قبل حرف كبير، وبعد؟»في العربية والفارسية والهندية، إذ ليس فيها حروف كبيرة، وبعد علامتَي الاقتباس الألمانيتين„…?“و»…?«، اللتين لا يقرؤهما المقسِّم علامتَي إغلاق أصلًا. أما علامات الإنهاء الصينية فلها قاعدتها الخاصة المذكورة أعلاه.
ويقع الخطأ في الاتجاه المعاكس أيضًا. فالاختصار الذي ليس في القائمة، إذا تلاه حرف كبير، ينهي «الجملة» قبل أوانها، كما يفعل Mr. قبل اسم، وكذلك سؤالٌ بين علامتي اقتباس يليه اسم علم، أو أي اسم في الألمانية، وإذا كان ذلك الجزء من ثماني كلمات أو أكثر وبلا علامة، تُحجب إجابة مستندة إلى المصادر. وإذا مهّد لقائمةٍ سطرٌ ينتهي بنقطتين رأسيتين، فلا حدَّ قبل رقم بندها الأول: يقطع المقسِّم النص مباشرةً بعد 1.، فيخفق ذلك الجزء في قاعدة الإسناد. وفي الحالتين تعمل البوابة بمبدأ الإغلاق عند الإخفاق (fail closed): فتحجب إجابةً مستندة إلى المصادر بدلًا من أن ترسل إجابة لم تُفحص.
والمدوّنة النصية نفسها ليست ثابتة. فالمقالات المنشورة تُفهرس للمساعد، فتصبح أرقامها، بما فيها ١٧٪ و٣٣٪ الواردتان في هذه المقالة، قيمًا ستشهد لها البوابة. ف«مدعوم» يعني «موجود في مكان ما ضمن ما تستشهد به الجملة»، لا «يخص هذه المسألة».
البوابة آخر فحص قبل الإرسال، لا الفحص الوحيد. فقد وجد Joren وزملاؤه أن النماذج الأكبر «كثيرًا ما تُخرج إجابات غير صحيحة بدلًا من الامتناع» حين لا يكون السياق كافيًا، وتشير Google Research إلى أن RAG، وإن كان يحسّن الأداء العام في الغالب، «يقلّل، على نحو مفارِق، قدرة النموذج على الامتناع عن الإجابة حين يكون ذلك مناسبًا». لذلك يأتي الرفض والحدّ الأدنى للدرجة أولًا، كما يبيّن التصميم ذو الطبقات الخمس. وتصوغ ورقة Frost الأولية حدود نتيجتها بعبارة تنطبق على هذه البوابة أيضًا: «امتثال للسياسة في خط دفاع أخير حتمي، لا دليل على الأمان الدلالي أو على اكتمال السياسة».
بث استجابات LLM عبر SSE: الطلب عبر fetch، والبروكسي، والإلغاء
لا تنفع بوابة الجمل إلا إذا وصلت كل جملة تُرسلها إلى القارئ فورًا. وتحسم ذلك أربعة تفاصيل في طبقة النقل.
لماذا fetch وReadableStream وليس EventSource؟
لا تمنح مواصفة WHATWG مُنشئ EventSource إلا عنوان URL وخيارًا واحدًا هو withCredentials: فلا يمكن تحديد طريقة الطلب (method)، ولا إرسال جسم له. والسؤال، مع لغته وسجل المحادثة، جسمُ طلب، لذلك تستقبل نقطة النهاية طلب POST وتعيد أحداثًا مُرسَلة من الخادم (Server-Sent Events). ويقرؤها المتصفح باستخدام fetch وقارئ ReadableStream وAbortController، الذي تستطيع دالّته abort()، بعبارة MDN، «إلغاء طلبات fetch، واستهلاك أي أجسام استجابة، والتدفقات».
ويحتاج المحلِّل (parser) المكتوب يدويًا إلى هذه القواعد:
خزِّن البيانات مؤقتًا بين عمليات القراءة. المقطع ليس رسالة.
أطلِق الحدث عند سطر فارغ. وإلى أن يصل، يبقى الحدث ناقصًا.
تجاهَل أسطر التعليق، أي الأسطر التي تبدأ ب
:.اجمع البيانات متعددة الأسطر: كل أسطر
data:في الحدث الواحد تُضمّ بمحرف سطر جديد (line feed).اقبل CRLF وLF وCR نهاياتٍ للأسطر.
احذف مسافة واحدة بعد النقطتين الرأسيتين، وواحدة فقط.
وفُكّ الترميز باستخدام TextDecoder مع {stream: true}، وهو الإعداد الذي يذكره MDN «عند معالجة البيانات على شكل مقاطع». وإلا فإن حرفًا فارسيًا انقسم بين مقطعين سيتحول عند فك ترميزه إلى محارف بديلة (replacement characters).
التخزين المؤقت في البروكسي: nginx وCaddy
يخزّن nginx الاستجابات التي يمرّرها بصفته بروكسي تخزينًا مؤقتًا (buffering) بشكل افتراضي، ويمكن لترويسة الاستجابة X-Accel-Buffering أن تعطّل ذلك. ويرسل بث المساعد X-Accel-Buffering: no وCache-Control: no-cache, no-store, no-transform؛ ووجود no-transform سببه أن البروكسي الذي يعيد ضغط جسم الاستجابة قد يدمج المقاطع. ويدفع Caddy استجابات text/event-stream إلى العميل فورًا، كما يضبط إعداد Caddy في الموقع flush_interval -1 على مسار الدردشة. ولا يسجّل عامل الخدمة (service worker) أي مسار (route) للبث، لأن أي استراتيجية تخزين مؤقت (caching) كانت ستحتجز الاستجابة حتى تنتهي.
رسائل إبقاء الاتصال والأخطاء داخل بث مفتوح
تقترح مواصفة SSE سطر تعليق «كل ١٥ ثانية تقريبًا» للحماية من خوادم البروكسي القديمة التي تقطع الاتصالات بعد مهلة قصيرة، ويرسل الخادم ping بهذا الفاصل الزمني. وتكون حالة الاستجابة 200 بالفعل حين يغادر أول بايت، لذلك تنتقل الإخفاقات في صورة حدث error داخل البث.
إلغاء يوقف الفاتورة أيضًا
يُلغي الإيقافُ في المتصفح طلبَ fetch. ويتحقق الخادم من انقطاع الاتصال مع وصول كل رمز، فإذا اكتشفه خرج من الحلقة وأغلق بث المزوّد، وهو ما يلغي، في هذا التصميم، الطلب إلى المنبع. وثمة أمران يستحقان اختبارًا لا افتراضًا. الأول: أن كل بروكسي وشبكة CDN بين المتصفح وواجهة API لديك يمرّر انقطاع اتصال العميل. اختبر هذا المسار، ولا تثق بالإعداد. والثاني: متى يبلّغ مزوّدك عن الاستهلاك؛ فإذا كان استهلاك الإخراج لا يصل إلا مع المقطع الأخير، فإن البث الذي تغلقه مبكرًا لن يتلقاه أبدًا، وسقف الإنفاق الذي يعدّ الاستهلاك المبلَّغ عنه سيحتسب أقل مما وُلِّد فعلًا.
كيف تختبر حاجز حماية على البث؟
تغطي الاختبارات البوابة على ثلاثة مستويات:
اختبارات وحدة للبوابة على مصادر ثابتة. القيم المختلَقة، والعلامات خارج النطاق، والادعاءات غير المسندة، وكلمات صيغة المتكلم (بكل لغة من لغات الموقع) توقف البث؛ والقيم المدعومة والجمل الرابطة القصيرة تمرّ؛ والجمل السليمة السابقة تبقى بعد إخفاق جملة لاحقة.
اختبارات مقسِّم الجمل. الأعداد العشرية وأسماء النطاقات ليست حدودًا، والعلامات اللاحقة تبقى مع جملتها، و
؟تنهي الجملة، والنص الصيني يُقسَّم إلى جمل دون مسافات. والاختبار الجدير بأن تنقله إلى مشروعك: النص الذي يُغذّى على قطع من ثلاثة أحرف يجب أن يُقسَّم تمامًا كما يُقسَّم النص كاملًا.اختبارات البث مع مزوّد مبرمَج مسبقًا (scripted provider) يعيد بث إجابة جاهزة على قطع صغيرة: القيمة المختلَقة لا تخرج أبدًا إلى الشبكة، وتوقف البوابة أو انقطاع الاتصال يغلق البث إلى المنبع، والاقتطاع يُبلَّغ عنه بوصفه اقتطاعًا.
وهذه مسودة مبسّطة (sketch) للاختبارين المحوريين، كُتبت لهذه المقالة ولم تُؤخذ من الموقع:
# Sketch only: SentenceSplitter and SentenceGate stand for your own classes.
import pytest
ANSWER = "The reading room opens early [S1]. Is the café open? Yes, on weekdays [S2]."
def pieces(text, size):
return [text[i:i + size] for i in range(0, len(text), size)]
def split_streamed(text, size):
splitter = SentenceSplitter()
sentences = [s for piece in pieces(text, size) for s in splitter.feed(piece)]
tail = splitter.flush()
return sentences + ([tail] if tail else [])
@pytest.mark.parametrize("size", [1, 2, 3, 7])
def test_piece_size_does_not_change_sentences(size):
assert split_streamed(ANSWER, size) == split_streamed(ANSWER, len(ANSWER))
def test_unsupported_value_never_reaches_the_reader():
gate = SentenceGate(sources=["The library opens at nine."])
released = ""
for piece in pieces("It opens at nine [S1]. Late fees ended in 2019 [S1]. More", 7):
outcome = gate.feed(piece)
released += outcome.text
if outcome.stopped:
break
assert "2019" not in released and gate.stoppedلا شيء من هذا يُخضع نموذجًا حيًّا لاختبار الفريق الأحمر (red-teaming). وما قسته هو أخطاء البوابة نفسها: إجاباتٌ سُجّلت في بيئة الإنتاج ثم أُعيد تمريرها عليها دون اتصال، لمعرفة أين أوقفت إجابةً مستندة إلى المصادر. وهذا عدٌّ للتوقفات الخاطئة مع نموذج واحد ومجموعة واحدة من الأسئلة، لا لعدد المرات التي يختلق فيها نموذج حقيقي، لذلك لا أنشره معدّلًا.
قائمة تحقق: إطلاق حاجز حماية على إجابة مبثوثة
حدِّد ما يجب ألا يُرى أبدًا. فهذا ما يحسم الاختيار بين التحقق قبل الإرسال والإرسال أولًا، أيًّا كان الإعداد الافتراضي في إطار العمل لديك.
اختر الوحدة وحدِّد مقدار الانتظار: مقطع ثابت أو جملة.
استخدم فحوصًا حتمية حيث تكفي مطابقة النصوص، بعد توحيد الأرقام وأشكال الحروف أولًا. فالحاجز القائم على نموذج يضيف زمن تنفيذه إلى كل وحدة.
لا تَعُدّ الحدّ مؤكدًا إلا حين يصل الحرف التالي، وضع سقفًا للمخزن المؤقت.
عند المخالفة، أرسِل ما اجتاز الفحص، وتوقف، واعرض نصًا ثابتًا، ولا تُعِد عرض النص المحجوب أبدًا، وأغلق البث إلى المنبع.
افصل الاقتطاع عن المخالفة، واحتسب ما وُلِّد، لا ما عُرض.
النقل: طلب POST عبر
fetchمع قارئ بث وAbortController؛ ومفكّك ترميز يعمل بنمط البث؛ ورسائل ping في أسطر تعليق؛ والأخطاء في صورة أحداث.البروكسي:
X-Accel-Buffering: no، وno-transform، ودفع استجابات event-stream فورًا، ومسار مُختبَر لانقطاع الاتصال، ولا عامل خدمة على مسار البث.الاختبارات: أعِد بث الإجابات على قطع صغيرة، وتحقق من أن القيمة المحجوبة لا تخرج أبدًا إلى الشبكة.
إذا كنت تصمم مساعدًا مستندًا إلى المصادر وتريد رأيًا ثانيًا في مسار إرسال إجاباته، فمن صفحة تشخيص المشروع يبدأ هذا الحديث.
الأسئلة الشائعة
هل يمكن التحقق من مخرجات LLM المبثوثة دون التخلي عن البث؟
نعم. خزّن النص مؤقتًا حتى تكتمل وحدة يمكنك فحصها، وأرسل كل وحدة حين تجتاز الفحص. فيرى القارئ النص يصل جملةً بجملة أو مقطعًا بمقطع، لا رمزًا برمز.
هل يؤخر التخزين المؤقت حتى نهاية الجملة زمن أول رمز؟
يؤخّر الإخراج المرئي بمقدار جملة واحدة: يظهر النص حين تكتمل الجملة الأولى وتُفحص. وفحوص مطابقة النصوص زهيدة الكلفة مقارنةً بالتوليد، لكنني لم أقِس الفرق في هذا الموقع، لذلك لا أذكر رقمًا.
هل ينبغي تعطيل stream_first في NeMo Guardrails؟
عطّله إذا كان يجب ألا يُرى المقطع المحجوب أبدًا. وتذكر وثائق NeMo الكلفة: يزيد الزمن حتى أول رمز بمقدار زمن تنفيذ الحاجز لكل مقطع. ولا تُبقِ على الإعداد الافتراضي إلا إذا كان منتجك يحتمل أن يرى القارئ رموزًا يحجبها حاجز لاحقًا.
لماذا لا يُستخدم EventSource لبث استجابات LLM؟
يقبل EventSource عنوان URL وخيارًا لبيانات الاعتماد، لذا لا يستطيع إرسال جسم للطلب. وطلب الدردشة المصحوب بسجل المحادثة طلب POST، فاستخدم fetch مع قارئ بث، وهذا يمنحك أيضًا الإلغاء عبر AbortController.
هل يوقف فحص الإسناد الهلوسة؟
لا. إنه يوقف أرقام المصادر المختلَقة، والجمل الجوهرية غير المسندة، والقيم الغائبة عن المصادر التي تستشهد بها الجملة. لكنه لا يستطيع أن يحكم ما إذا كان المصدر المستشهَد به يدعم الادعاء، وهذا هو إخفاق الإسناد الخاطئ. فتعامل معه بوصفه خط دفاع أخير بعد الرفض والحدّ الأدنى لدرجة الاسترجاع.