في شتاء العام الماضي ساعدت فريقًا يعمل على خدمة عملاء ذكية في تشخيص مشكلة إنتاجية. كان خادمهم الخلفي متصلًا في الوقت نفسه بواجهات برمجة تطبيقات أربع شركات للنماذج اللغوية الكبيرة، بينما تستخدم الواجهة الأمامية SSE لإخراج البث المباشر. بدءًا من اليوم الثالث للإطلاق، بدأ المستخدمون يشتكون من أن "الإجابات تُخرج كما لو كانت معجون أسنان يُضغط من الأنبوب" — أحيانًا تتوقف فقرة كاملة من النص بلا حركة ثم تتدفق فجأة كتلة كبيرة دفعة واحدة؛ وأحيانًا تُقطع الجملة إلى نصفين مع كتل فارغة غريبة في المنتصف. لم تكن هناك أخطاء 5xx في سجلات الخادم الخلفي، وكانت مراقبة توفر واجهات برمجة التطبيقات لدى كل الشركات خضراء.
هذا النوع من المشكلات هو الأصعب في التشخيص، لأن كل حلقة تبدو طبيعية. تابعت معهم البدء من لوحة Network في المتصفح، وتقشير الطبقات واحدة تلو الأخرى، حتى اكتشفنا في النهاية أن المشكلة ليست في واجهة برمجة تطبيقات شركة بعينها، بل في مسألة "الخلط بين الأربع" نفسها.
أولًا الاستنتاج: عدم توحيد بروتوكول البث المباشر هو الفخ الخفي للربط المجمّع
إخراج البث المباشر في واجهات برمجة تطبيقات النماذج اللغوية الكبيرة يعتمد في الغالب على SSE (Server-Sent Events). لكن "الجميع يستخدم SSE" لا يعني "السلوك متطابق". لكل من الشركات الأربع تنفيذها الخاص في حجم التقطيع وعلامة النهاية واستراتيجية نبضات القلب، وتبدو تدفقات البايتات التي تتلقاها الواجهة الأمامية وكأنها تبدأ جميعها بـ data:، لكن منطق التحليل لا يمكن إعادة استخدامه إطلاقًا. هذا هو السبب الجذري للتقطيع والشذوذ في حدود الكتل.
التشخيص طبقة تلو الأخرى: من الواجهة الأمامية إلى البوابة، أين تكمن المشكلة
الطبقة الأولى، الواجهة الأمامية. كان تحليل SSE لديهم يستخدم EventSource القياسي، ويقسم الأحداث عند \n\n. هذا في حد ذاته ليس مشكلة.
الطبقة الثانية، إعادة التوجيه في الخادم الخلفي. استخدم الخادم الخلفي طبقة تمرير شفاف بـ Node، حيث يتم تمرير ReadableStream من المنبع مباشرةً إلى الاستجابة عبر pipe. هنا كان يكمن أول لغم: أحجام الكتل تختلف اختلافًا كبيرًا بين الشركات، فبعضها يرسل حسب الرمز (token) والبعض الآخر يجمع عدة رموز ثم يرسلها. إذا تم استخدام Buffer.concat أثناء الـ pipe دون معالجة صحيحة لعبور حدود الكتل للأحرف متعددة البايتات في UTF-8، فسيتم قطع حرف صيني إلى نصفين، وتُعرض الواجهة الأمامية مربعات مشوشة.
الطبقة الثالثة، المهلة وإعادة المحاولة. كان الخادم الخلفي قد ضبط مهلة موحدة قدرها 30 ثانية للشركات الأربع. لكن في سيناريو البث المباشر، هل تعني "مهلة 30 ثانية" وقت البايت الأول أم وقت التدفق بأكمله؟ لم يميزوا بينهما، مما أدى إلى قطع الإجابات الطويلة في منتصفها، وظهر ذلك في الواجهة الأمامية على شكل "توقف مفاجئ في منتصف الكلام".
الطبقة الرابعة، رموز الأخطاء. تختلف رموز حالة HTTP وبنية body التي تعيدها الشركات الأربع عند تحديد المعدل وفشل المصادقة واعتراض مراجعة المحتوى. كان الخادم الخلفي يقرأها جميعًا عبر error.message، فنجح مع شركتين وفشل مع شركتين حيث حصل على undefined، وعرضت الواجهة الأمامية رسالة خطأ فارغة.
عند هذه النقطة اتضح الأمر: المشكلة الحقيقية ليست وجود خطأ في واجهة برمجة تطبيقات شركة ما، بل تعرّض دلالات أربعة بروتوكولات مباشرةً لطبقة الأعمال.
الاختلافات الجوهرية في تنفيذ البث المباشر لدى الشركات الأربع
أعددت جدول مقارنة، والبيانات مصدرها الوثائق الرسمية لكل شركة والحزم الفعلية الملتقطة، وما لم تفصح عنه الشركات الرسمي فقد وضعت عليه علامة:
البُعدالشركة أالشركة بالشركة جالشركة د
علامة النهايةdata: [DONE]حقل finish_reasondata: [DONE]حدث event: done
نبضات القلبلا شيءسطر تعليق كل 15 ثانية تقريبًالا شيءغير معلن رسميًا
حجم التقطيعحسب الرمزجمع 2-4 رموزحسب الرمزغير ثابت
موضع رمز الخطأرمز حالة HTTPbody.error.codeرمز حالة HTTPbody.code
من هذا الجدول يتضح لماذا لا يمكن إعادة استخدام منطق تحليل الواجهة الأمامية. ثلاث طرق لعلامة النهاية، وموضعان لرمز الخطأ، واستراتيجية نبضات القلب موجودة لدى شركتين وغائبة لدى شركتين. إذا كتبت في كود الأعمال مباشرةً if (chunk.includes('[DONE]'))، فلن تنتهي أبدًا مع الشركة ب، وستظل الاتصال معلقًا حتى انتهاء المهلة.
أربع نقاط تصميمية لإحكام الإغلاق في طبقة البوابة
أولًا، توحيد صيغة أحداث SSE. تُطبّع البوابة جميع أحداث المنبع من الشركات المختلفة في بنية واحدة: {type: "delta" | "done" | "error", content, code}. لا تعرف طبقة الأعمال سوى هذه المجموعة، ولم تعد تهتم بمن هو المنبع. في مشروعنا استخدمنا منصة تجميع واجهات برمجة تطبيقات النماذج اللغوية الكبيرة token8341 التي تعمل بهذه الطريقة، فهي لا تعرض للخارج سوى بروتوكول بث مباشر واحد، ولا يحتاج كود الأعمال إلى تعديل عند تبديل النموذج.
ثانيًا، تجميع المصادقة في البوابة. أربع مفاتيح لأربع شركات متناثرة في إعدادات الأعمال، وتدويرها يتطلب تعديل أربعة أماكن. بجمع المفاتيح في البوابة، تحصل طبقة الأعمال على رمز داخلي واحد فقط. قيمة هذه الطبقة التجميعية مثل منصة تجميع واجهات برمجة تطبيقات النماذج اللغوية الكبيرة token8341 تكمن هنا: مفتاح واحد يستدعي نماذج متعددة، وتدوير المفاتيح والتحكم في الحصص يتمان في جانب البوابة.
ثالثًا، يجب تقسيم المهلة إلى جزأين. مهلة البايت الأول (TTFB) ومهلة خمول التدفق منفصلتان. تُضبط TTFB قصيرة، مثلًا 10 ثوانٍ، للفشل السريع؛ وتُضبط مهلة خمول التدفق أطول، مثلًا 60 ثانية، للسماح للنموذج بالتفكير. استخدام 30 ثانية لكليهما سيسبب مشاكل حتمًا.
رابعًا، إعادة المحاولة للأخطاء المتطابقة فقط. بمجرد أن يبدأ طلب البث المباشر في الإخراج، ستؤدي إعادة المحاولة إلى محتوى مكرر. يجب أن تميز البوابة: الفشل في مرحلة إنشاء الاتصال يمكن إعادة المحاولة فيه، أما الفشل بعد بدء إخراج التدفق فلا يمكن سوى الإبلاغ عنه للواجهة الأمامية ليعيد المستخدم الإرسال يدويًا. إذا لم تُرسم هذه الحدود بوضوح، فستظهر مشكلة "تكرار الجملة نفسها مرتين".
جدول تعيين رموز الأخطاء يجب صيانته بنفسك
رموز تحديد المعدل لدى الشركات الأربع: اثنتان تعيدان 429، وواحدة تعيد 200 لكن مع خطأ في body، وواحدة تعيد رمزًا مخصصًا. يجب على البوابة بناء جدول تعيين لتوحيد الأخطاء المختلفة من المنبع في رموز أخطاء داخلية، مثل RATE_LIMIT وAUTH_FAILED وCONTENT_BLOCKED. تجري طبقة الأعمال التخفيض بناءً على الرموز الداخلية، ولم تعد تخمّن دلالات المنبع. لا يوجد طريق مختصر لهذا الجدول، فلا بد من التقاط الحزم شركةً شركة ومقارنتها بالوثائق، وقد قدّمت منصة تجميع واجهات برمجة تطبيقات النماذج اللغوية الكبيرة token8341 في وثائقها مرجع تعيين جاهزًا، ما وفّر الكثير من الجهد.
متى يكون الاتصال المباشر أبسط فعليًا
يجب الاعتراف بأنه ليس كل السيناريوهات تستحق بوابة. إذا كان المشروع يتصل بنموذج واحد فقط، أو كان يقوم فقط باستدلال دفعي غير بثي، فإن الاتصال المباشر بـ SDK الرسمي يعني كودًا أقل ومسارًا أقصر وتشخيصًا أسهل. ما تحله طبقة البوابة هو التعقيد الناجم عن "الربط الموحد لنماذج متعددة"، أما في سيناريو النموذج الواحد فإن إدخال البوابة يعني طبقة تمرير إضافية ونقطة فشل إضافية. معيار الحكم مباشر: عندما يتجاوز عدد النماذج المتصلة شركتين، أو عند الحاجة إلى تبديل النماذج أثناء التشغيل، عندها فقط تتجاوز فائدة البوابة تكلفتها.
خاتمة
حُلّ ذلك العطل في النهاية بإضافة طبقة بوابة للإغلاق، فتقليص منطق تحليل SSE في الواجهة الأمامية من أربعة فروع إلى فرع واحد، واختفى التقطيع وشذوذ الكتل تبعًا لذلك. بالنظر إلى الوراء، لم تكن المشكلة يومًا في واجهة برمجة تطبيقات نموذج لغوي كبير بعينه، بل في "تمرير الاختلافات البروتوكولية بين الشركات المختلفة مباشرةً إلى طبقة الأعمال". إذا كنت تعمل أيضًا على ربط نماذج متعددة، فقم أولًا بمحاذاة الأمور الأربعة — بروتوكول البث المباشر، ورموز الأخطاء، والمصادقة، والمهلات — في طبقة البوابة، ثم تحدث عن الوظائف الأعلى.
المؤلف: صن هاوَران
تاريخ النشر: 25 أكتوبر 2026