تخطّى إلى المحتوى الرئيسي
بثّ استجابات الذكاء الاصطناعي في Next.js: ما الذي يتعطّل فعلًا في الإنتاج

بثّ استجابات الذكاء الاصطناعي في Next.js: ما الذي يتعطّل فعلًا في الإنتاج

July 21, 2026
الذكاء الاصطناعي وأدوات المطورين
6 دقيقة قراءة

في ديفيا، كل ميزة ذكاء اصطناعي شحنّاها خلال العام الماضي — مساعدات محادثة، وملخّصات، ومولّدات تقارير — تبثّ مخرجاتها توكن بتوكن. المستخدم يتحمّل توليدًا طويلًا عندما يظهر النص فورًا، لكنه يهجر مؤشر تحميل صامتًا بسرعة أكبر بكثير. كود البثّ نفسه أصبح قصيرًا؛ أنماط الفشل المحيطة به هي ما يملأ ملاحظاتنا الهندسية، وهذا المقال يجمعها.

أهم الخلاصات

• البثّ في Next.js App Router هو Route Handler يعيد Response متدفّقة. دالة streamText من AI SDK مع toUIMessageStreamResponse() تُصدران بثّ رسائل مبنيًا على SSE، وخطاف useChat من @ai-sdk/react يستهلكه على العميل.

• واجهات المحادثة لا تستخدم واجهة EventSource في المتصفح. EventSource يدعم طلبات GET فقط بلا جسم، لذا يرسل العميل POST عبر fetch ويقرأ الاستجابة بصيغة SSE من خلال قارئ ReadableStream.

• البثّ الذي يعمل محليًا ويصل كتلة واحدة في الإنتاج سببه غالبًا تخزين مؤقت وسيط: وسيط ضغط، أو proxy_buffering في nginx، أو بروكسي شركات. الترويسات Cache-Control: no-transform و X-Accel-Buffering: no تعالج معظم الحالات.

• تمرير AbortSignal الخاص بالطلب الوارد إلى streamText يعني أن إغلاق التبويب يلغي استدعاء النموذج في المنبع ويوقف إنفاق التوكنز فورًا.

• تحديث الصفحة يقتل بثّ fetch الافتراضي. حفظ الرسائل المكتملة في onFinish على الخادم هو الأساس؛ أما resumable streams فلا تستحق بنيتها الإضافية إلا عندما تكون استمرارية التوليد متطلبًا حقيقيًا في المنتج.

ماذا يتطلب بثّ استجابة ذكاء اصطناعي في Next.js؟

‏Route Handler واحد وخطاف واحد. على الخادم، تبدأ streamText من AI SDK — عدة Vercel بلغة TypeScript لاستدعاء النماذج اللغوية — استدعاء النموذج وتعود فورًا، وتحوّل toUIMessageStreamResponse() النتيجة إلى Response متدفّقة تتحدث بروتوكول UI message stream المبني على SSE. على العميل، يرسل useChat من @ai-sdk/react المحادثة، ويحلّل البثّ، ويعيد الرسم مع وصول الأجزاء.

تفصيلتان مهمتان على serverless. يجب أن تعيش الدالة طوال مدة التوليد، لذا نصدّر maxDuration من المسار — وعلى Vercel يبثّ Fluid Compute الاستجابات وسقف التنفيذ الافتراضي 300 ثانية. ونمرّر النموذج كسلسلة provider/model مثل anthropic/claude-sonnet-4-6 عبر AI Gateway، ما يجعل تبديل المزوّد تغيير سطر واحد بدل استبدال اعتمادية.

هل نستخدم SSE أم WebSockets أم بثّ fetch العادي؟

لبثّ التوكنز من الخادم إلى العميل، صيغة SSE المقروءة عبر fetch هي الخيار الافتراضي، وهي ما يطبّقه AI SDK. تقنية Server-Sent Events هي صيغة HTTP بسيطة يكتب فيها الخادم أسطر data: عبر استجابة واحدة طويلة العمر. أما WebSockets فتستحق مكانها فقط عندما تكون الحركة ثنائية الاتجاه فعلًا: محادثات صوتية، مؤشرات متعددة المستخدمين، تحرير تشاركي. لصندوق محادثة عادي تضيف حالة اتصال وبنية تحتية دون مقابل.

النقطة الدقيقة: واجهة EventSource المدمجة في المتصفح ليست ما تستخدمه تطبيقات المحادثة. EventSource لا يصدر سوى طلبات GET ولا يمكنه إرفاق جسم أو ترويسات مخصصة، بينما يحتاج طلب المحادثة إلى POST بتاريخ الرسائل. لذلك يرسل AI SDK طلب fetch POST عاديًا ويحلّل صيغة SSE من جسم الاستجابة عبر قارئ ReadableStream — نحصل على صيغة SSE دون قيود EventSource، مقابل خسارة إعادة الاتصال التلقائية.

• fetch مع صيغة SSE: اتجاه واحد من الخادم إلى العميل، أي HTTP method مع جسم، يعمل على serverless — الافتراضي لمحادثات الذكاء الاصطناعي.

• EventSource: اتجاه واحد، GET فقط بلا جسم، إعادة اتصال تلقائية عبر Last-Event-ID — يناسب المؤشرات الحية والإشعارات، لا المحادثة.

• WebSockets: ثنائي الاتجاه بعد مصافحة ترقية، يحتاج اتصالًا دائمًا ولا يناسب serverless — الصحيح للصوت والتعاون اللحظي.

لماذا يعمل البثّ محليًا ثم يصل دفعة واحدة في الإنتاج؟

لأن شيئًا بين الدالة والمتصفح يخزّن مؤقتًا. وسيط الضغط هو المتهم الأكثر شيوعًا: gzip و brotli يريدان مقاطع كاملة للضغط، فتبقى دلتات التوكنز الصغيرة في مخزن الضاغط حتى تنتهي الاستجابة. البروكسيات العكسية ثانيًا: proxy_buffering في nginx يجمع استجابة المنبع كاملة افتراضيًا. بروكسيات الشركات وبعض برامج الحماية تفعل الشيء نفسه وهي خارج سيطرتنا تمامًا.

الحل هو وسم الاستجابة بأنها غير قابلة للتحويل أو التخزين المؤقت: Content-Type: text/event-stream و Cache-Control: no-cache, no-transform و X-Accel-Buffering: no لأجل nginx، مع استثناء مسار البثّ من أي وسيط ضغط. على Vercel تمرّ الاستجابات المتدفّقة دون تخزين مؤقت تلقائيًا — والحالة المعطوبة الكلاسيكية في خبرتنا هي Next.js مستضاف ذاتيًا خلف إعداد nginx لم يلمسه أحد منذ سنة.

كيف نتوقف عن الدفع مقابل توكنز لا يقرأها أحد؟

بربط الإلغاء من الطرف إلى الطرف. كائن Request الوارد يحمل AbortSignal يشتعل عندما يغلق المستخدم التبويب أو يغادر الصفحة أو يضغط إيقاف. تمريره كخيار abortSignal في streamText ينشر الإلغاء إلى مزوّد النموذج، فيتوقف التوليد — وتتوقف الفوترة.

هنا مقايضة حقيقية. دالة consumeStream() في SDK تفعل العكس: تفصل التوليد عن اتصال العميل ليكمل النموذج حتى النهاية وتتمكن onFinish من حفظ الرسالة كاملة حتى بعد الانقطاع. الإلغاء عند الانقطاع يوفر التوكنز لكنه يفقد الاستجابة الجزئية؛ والاستهلاك حتى الاكتمال يحفظ الاستجابة لكنه يدفع ثمن كل توكن. نحن نربط الإلغاء للمحادثة الرخيصة، والاستهلاك مع الحفظ للتوليدات الطويلة المكلفة. الاختيار لكل ميزة على حدة — لا عالميًا — هو الفكرة.

ماذا يحدث عندما يحدّث المستخدم الصفحة أثناء البثّ؟

في الإعداد الافتراضي، يموت بثّ fetch مع الصفحة، وإذا كان الإلغاء مربوطًا يموت التوليد على الخادم معه. بعد إعادة التحميل تعرض المحادثة ما حفظته onFinish — الرسائل المكتملة تنجو، والرسالة الجارية تختفي. لمعظم منتجات المحادثة هذا مقبول وصادق.

وحين لا يكون كذلك — تقارير طويلة يطلقها المستخدم ويعود إليها لاحقًا — تسدّ resumable streams الفجوة: تُنشر المقاطع إلى مخزن مثل Redis فور وصولها، ويعيد العميل المتصل حديثًا الاشتراك من حيث توقف. يدعم AI SDK هذا النمط، لكنه يضيف بنية تحتية وحالة تشغيلية حقيقية. قاعدتنا: احفظ الرسائل المكتملة دائمًا؛ وأضف قابلية الاستئناف فقط عندما يسمّيها متطلب في المنتج.

أسئلة شائعة

س: هل نحتاج WebSockets لبثّ استجابات على طريقة ChatGPT؟

ج: لا. بثّ التوكنز أحادي الاتجاه من الخادم إلى العميل، وصيغة SSE عبر fetch POST تتكفل به على HTTP عادي. تُترك WebSockets للحركة ثنائية الاتجاه مثل الصوت والتحرير التشاركي.

س: لماذا لا يعمل بثّ الذكاء الاصطناعي خلف nginx؟

ج: يخزّن nginx استجابات المنبع افتراضيًا. أرسل X-Accel-Buffering: no مع الاستجابة (أو عطّل proxy_buffering للمسار) وتأكد أن لا طبقة ضغط تعيد تخزين البثّ.

س: هل يعمل البثّ على منصات serverless مثل Vercel؟

ج: نعم. دوال Vercel تبثّ الاستجابات، ويبقي Fluid Compute الدالة حية طوال التوليد — سقف التنفيذ الافتراضي 300 ثانية، ويُضبط لكل مسار عبر maxDuration.

س: كيف نحفظ رسالة الذكاء الاصطناعي إذا أغلق المستخدم التبويب أثناء البثّ؟

ج: استدعِ consumeStream() على الخادم ليكمل التوليد حتى النهاية وتحفظ onFinish الرسالة كاملة. لاحظ أن هذا يتعارض مع الإلغاء لتوفير التوكنز — كل مسار يختار سلوكًا واحدًا.

س: هل يستطيع EventSource إرسال جسم POST؟

ج: لا. EventSource يصدر طلبات GET فقط دون جسم أو ترويسات مخصصة، ولهذا يقرأ عملاء المحادثة صيغة SSE من fetch POST عبر قارئ ReadableStream.