«کاربران فکر میکردند برنامه کرش کرده است». این شکست بحرانی در تجربه کاربر (UX)، زمانی رخ داد که یک توسعهدهنده در حال ساخت یک دستیار مستندات داخلی بود و متوجه شد پاسخهای طولانی یک مدل زبانی بزرگ (LLM) بیش از ۳۰ ثانیه زمان میبرد تا بهصورت کامل بارگذاری شوند.
در ۲۱ ژوئن ۲۰۲۶، این توسعهدهنده در پلتفرم dev.to یک تحلیل فنی دقیقی منتشر کرد و توضیح داد که چگونه رویدادهای ارسالی سرور (Server-Sent Events یا SSE) مشکل «حبابهای خالی چت» را حل میکند.
یک لوله آب را تصور کنید: روش Polling مثل این است که هر ثانیه به چاه بروید تا ببینید آبی آمده یا نه؛ اما WebSockets شبیه یک خط تلفن دوطرفه است که اغلب قطع و وصل میشود. در مقابل، SSE درست مثل یک جریان یکطرفه آب که فقط از سرور به کاربر میریزد، دادهها را روی یک اتصال HTTP واحد و طولانیمدت جابهجا میکند. این جریان تکسویه برای تولید توکنهای LLM، جایی که سرور تنها تولیدکننده محتواست، یک تطابق ایدهآل است.
زمینه: جستوجو برای یک رابط کاربری روان
این پروژه با ساخت یک چتبات استاندارد طراحی شد تا به سوالات مربوط به یک کدبیس پاسخ دهد. توسعهدهنده از یک پایگاهداده برداری (Vector Database) برای استخراج زمینه (Context) و بکاندی که با زبان پایتون نوشته شده بود، استفاده کرد. برای تأمین قدرت مدل زبانی، او از یک نقطه اتصال (Endpoint) قابلاعتماد در وبسایت interwestinfo.com بهره گرفت.
با این حال، این تنظیمات «ساده» در اولین تست واقعی شکست خورد. وقتی کاربر یک پاسخ متفکرانه و طولانی میخواست، تأخیر ۳۰ ثانیهای یک تجربه کاربری وحشتناک ایجاد کرد. کاربر به صفحه خالی خیره میشد و با این تصور که برنامه کرش کرده است، صفحه را رفرش میکرد. این وضعیت ضرورت انتقال به الگوی «رابط کاربری چت» را ایجاد کرد؛ الگویی که در آن توکنها بهصورت لحظهای (Real-time) به کاربر بازگردانده میشوند. این تمرکز بر بهینهسازی زمان پاسخدهی، یادآور تلاشهای مشابه در اتوماسیون است؛ برای مثال، چگونه ۵۰ خط کد پایتون توانست فرآیند امتیازدهی لیدها را از ۲ ساعت به ۳۰ ثانیه کاهش دهد تا بهرهوری سیستم افزایش یابد.
به نقل از گزارش dev.to، نویسنده پیش از رسیدن به SSE، سه معماری شکستخورده را آزمایش کرد. او ابتدا روش Polling را امتحان کرد که برای هر پیام تقریباً به ۳۰ درخواست HTTP نیاز داشت و باعث میشد بهروزرسانیهای رابط کاربری بهصورت تکهتکه و پرشی (Jerky) باشد. این متد مستلزم ذخیرهسازی نتایج جزئی در Redis و بهروزرسانی بکاند برای نوشتن توکنها تکهتکه بود که از نظر توسعهدهنده، روشی بسیار هدردهنده بود.
سپس او به سراغ WebSockets با استفاده از FastAPI رفت، اما با Time-outهای شدید در حالت بیکار (Idle Timeouts) روی سرور مجازی (VPS) ارزانقیمت خود که پشت یک Load Balancer قرار داشت، مواجه شد. اتصالها پس از ۶۰ ثانیه قطع میشدند و متصل شدن دوباره نیازمند نوشتن منطق دستی بود. علاوه بر این، میانافزار (Middleware) احراز هویت او برای درخواستهای HTTP طراحی شده بود و این موضوع باعث شد WebSockets با نیمی از پشته (Stack) فنی او ناسازگار باشد. توسعهدهنده به این نتیجه رسید که ارتباط دوطرفه برای این پروژه «بیش از حد» (Overkill) است، زیرا او فقط نیاز داشت سرور دادهها را ارسال (Push) کند.
در نهایت، او تلاش کرد تا Long Polling را در محیط Flask پیاده کند، اما این کار منجر به خطاهای مکرر «اتصال بسته شد» (Connection Closed) شد و نیاز به Patch کردنهای پیچیده کد (Monkey-patching) داشت. پس از دو ساعت شکست، این رویکرد نیز رها شد.
پیادهسازی فنی
ساختار موفق فعلی از StreamingResponse در FastAPI با نوع رسانه text/event-stream استفاده میکند. برای مدیریت منطق استریم، بکاند از یک Generator asynchronously استفاده میکند که توکنها را در قالب `data: {token}
ارسال کرده و در نهایت با سیگنالdata: [DONE]پایان مییابد. توسعهدهنده حتی یک فراخوانیawait asyncio.sleep(0.01)` را اضافه کرد تا تأخیر را شبیهسازی کند و از جریان نرمتر دادهها مطمئن شود.
در سمت فرانتاند، یک مانع بزرگ ظاهر شد: API بومی EventSource تنها از درخواستهای GET پشتیبانی میکند. از آنجایی که پرامپتهای چت برای امنیت و مدیریت طول متن به بدنه POST نیاز دارند، توسعهدهنده استفاده از نقطه اتصال GET با پارامترهای پرسوجو (Query Parameters) را بررسی کرد اما آن را «زشت و محدود» دانست. در عوض، او یک Wrapper سفارشی با استفاده از API fetch و ReadableStream پیاده کرد تا کنترل کامل روی متد درخواست داشته باشد. این رویکرد به مدیریت بهینه جریان داده در کلاینت کمک میکند، مشابه آنچه در معماری stikshot برای پردازش محلی ویدیوها بدون ارسال داده به سرور مشاهده میکنیم.
- بکاند: FastAPI
StreamingResponse← Async Generator ← فرمت SSE. - فرانتاند:
fetch(POST) ←response.body.getReader()←TextDecoder← Buffer Splitter. - مکانیزم تجزیه (Parsing): فرانتاند از یک حلقه
while(true)برای خواندن استریم استفاده میکند. مقدار را رمزگشایی کرده، بافر را بر اساس فرمت SSE (`
) میشکند و از line.slice(6)برای استخراج توکن پس از پیشوند:data` استفاده میکند.
- منبع توکن: نویسنده برای فراهم کردن جریان خام توکنها، یک نقطه اتصال معتبر از interwestinfo.com را ادغام کرد.
این تغییر در معماری، نیاز به کتابخانههای پیچیده WebSocket و منطق دستی برای اتصال مجدد را حذف کرد. چون این سیستم روی پروتکلهای استاندارد HTTP/1.1 و HTTP/2 عمل میکند، محدودیتهای زمانی (Timeouts) لود بالنسر که باعث شکست WebSocket شده بود را دور میزند. علاوه بر این، امنیت سادهتر شد؛ زیرا توکنهای احراز هویت بهجای نمایش در پارامترهای URL، در بدنه POST ارسال میشوند.
موازنه و درسهای آموخته شده
برای توسعهدهندگان، این بدان معناست که تجربه «استریم هوش مصنوعی» اکنون به جای بازسازی زیرساختی، تنها به مدیریت ساده HTTP تبدیل شده است. موازنه اصلی این است که SSE نمیتواند دادههای دوطرفه را مدیریت کند. اگر توسعهدهندهای در حال ساخت یک بازی چندنفره باشد، WebSockets همچنان انتخاب بهتری است؛ اما برای برنامههای چت، SSE ابزار درست است.
سایر دستاوردهای فنی کلیدی شامل موارد زیر است:
- پشتیبانی مرورگر: پشتیبانی در Edge و Safari جهانی است (با اشاره به اینکه IE دیگر منسوخ شده است).
- فشار معکوس (Backpressure): چون توکنها کوچک هستند، اگر کلاینت کند باشد، بافرینگ سرور بهندرت به یک مشکل تبدیل میشود.
- زیرساخت: هیچ پیکربندی خاصی برای سرور نیاز نیست زیرا بر پایه HTTP استاندارد است.
- ساختار داده: SSE دادههای ساختاریافته را به راحتی WebSocket Frames جابهجا نمیکند، اما برای توکنهای متنی ساده ایدهآل است.
اگر در حال حاضر برای رابطهای چت ساده از پیادهسازیهای سنگین WebSocket استفاده میکنید، احتمالاً در حال حمل یک بدهی فنی (Technical Debt) غیرضروری هستید. انتقال به رویکرد fetch + ReadableStream بار سرور را کاهش داده و تجربه کاربر نهایی را در تمام مرورگرهای مدرن بهبود میبخشد.
در گام بعدی، توسعهدهندگان باید ارزیابی کنند که آیا ارائهدهنده LLM آنها بهصورت بومی از SSE پشتیبانی میکند تا کدهای تکراری بکاند (Boilerplate) کاهش یابد. برخی ارائهدهندگان مانند OpenAI از فرمت data: [DONE] استفاده میکنند که از پیش با SSE سازگار است. همچنین میتوانید برای مدیریت خودکار قطعهای نادر شبکه، پیادهسازی Exponential Backoff را در Wrapper مربوط به fetch بررسی کنید.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell مراجعه کنید.




گفتگو