پرش به محتوای اصلی
پرش به محتوای مقاله

سرویس‌ورکرها اتلاف توکن‌های هوش مصنوعی را با کشینگ پاسخ‌های استریم کاهش دادند

·۲۷ مرداد ۱۴۰۵۶ دقیقه مطالعه۲ بازدید
راهنما
ذخیره پاسخ‌های چت در سرویس ورکر برای جلوگیری از مصرف دوباره توکن‌های رایگان
ذخیره پاسخ‌های چت در سرویس ورکر برای جلوگیری از مصرف دوباره توکن‌های رایگان
اشتراک‌گذاری
واقعاً چه چیز جدید است؟

استفاده از قابلیت Stream Teeing در سرویس‌ورکرها برای ذخیره‌سازی پاسخ‌های استریم در لحظه تولید، بدون ایجاد تأخیر برای کاربر نهایی.

تصور کنید یک اپلیکیشن چت در یک بعدازظهر نزدیک به ۶ میلیون توکن مصرف کند، بدون اینکه حتی یک پاسخ جدید تولید کرده باشد. این اتلاف عظیم زمانی رخ می‌دهد که تسترها و کاربران به طور مکرر فرم‌ها را رفرش می‌کنند یا روی پرامپت‌های قبلی کلیک می‌کنند و هر بار، یک درخواست استریم (Streaming) کامل و یکسان به سرور ارسال می‌شود.

به نقل از گزارشی که در ۱۸ آگوست ۲۰۲۶ در dev.to منتشر شد، سرور در هر بار درخواست وضعیت ۲۰۰ (OK) را برمی‌گرداند و این ناکارآمدی تا زمانی که نمودارهای سهمیه توکن‌ها جهش شدیدی نکردند، پنهان ماند. این مشکل دقیقاً همان‌جایی است که معماری‌های فرانت‌اند با یک چالش جدی روبرو می‌شوند: اکثر آن‌ها هر پاسخ ۲۰۰ را به عنوان داده‌ای تازه می‌بینند و هیچ تفاوتی بین یک درخواست تکراری و یک درخواست واقعاً جدید قائل نمی‌شوند. این نوع اتلاف منابع در مقیاس بزرگتر، یادآور تحلیل ما درباره تله‌های سیستم‌های خودکار است که در آن یک نشست تنها در یک شب ۱۳۶ میلیون توکن را می‌سوزاند.

وقتی کاربر یک پرامپت یکسان را دوباره می‌فرستد، در تب Network مرورگر درخواست‌های POST مکرر به نقطه اتصال /api/chat دیده می‌شود. هر درخواست، بدنه JSON و هدرهای یکسانی دارد و مدل را مجبور می‌کند توالی توکن‌ها را از ابتدا تولید کند. هر پاسخ، اتصال را برای مدت زمانی تقریباً یکسان باز نگه می‌دارد. در مدل‌های رایگان با سهمیه محدود (Finite Token Pool)، این رفتار به شدت هزینه‌بر و در برخی موارد غیرقابل تحمل است.

همان‌طور که در تحلیل‌های قبلی ما درباره‌ی بهینه‌سازی هزینه‌های استنتاج اشاره کردیم، انتقال بار پردازشی از سرور به کلاینت کلید کاهش هزینه‌هاست. برای حل این مشکل، توسعه‌دهندگان می‌توانند از یک سرویس‌ورکر (Service Worker) — شبیه به یک نگهبان در ورودی مرورگر که درخواست‌ها را قبل از خروج بررسی می‌کند — استفاده کنند تا به جای متن پرامپت، کل جریان پاسخ (Completed Stream) را کش (Cache) کند. سرویس‌ورکر به عنوان یک پروکسی عمل می‌کند که درخواست‌ها را پیش از خروج از مرورگر رهگیری می‌کند. اگر یک تکرار دقیق شناسایی شود، ورکر بلافاصله پاسخ ذخیره‌شده را برمی‌گرداند. این سازوکار فراخوانی مدل را برای پرامپت‌های جدید تغییر نمی‌دهد، بلکه فقط مسیر درخواست‌های تکراری را کوتاه (Short-circuit) می‌کند. این رویکرد مشابه استفاده از Sidecar در پایپ‌لاین‌های CI است که با کشینگ پاسخ‌ها از سوختن سهمیه‌های مدل‌های زبانی جلوگیری می‌کند.

طبق مستندات فنی این پیاده‌سازی، سازوکار دقیق آن بر چهار محور استوار است تا دقت و عملکرد سیستم تضمین شود:

  • اثر انگشت بدنه (Body Fingerprinting): سیستم با استفاده از crypto.subtle.digest (SHA-256)، ترکیبی از URL کامل درخواست و بدنه JSON را هش می‌کند. این کار برای جلوگیری از تداخل (Collision) در مواردی است که پرامپت‌های مختلف از یک URL مشترک استفاده می‌کنند اما محتوای بدنه آن‌ها متفاوت است. در کد پیاده‌سازی شده، از فرمول request.url + '|' + body برای ایجاد این اثر انگشت استفاده شده است.
  • فیلتر کردن هدرها: برای جلوگیری از آلودگی کش (Cache Pollution)، فقط هدرهایی که بر محتوا اثر می‌گذارند، مانند accept و content-type در نظر گرفته می‌شوند. هدرهای متغیر احراز هویت (Authentication Headers) که در هر درخواست تغییر می‌کنند، از کلید کش حذف شده‌اند تا باعث ایجاد ورودی‌های تکراری و بی‌مورد نشوند.
  • دوگانه کردن استریم (Stream Teeing): چون response.body یک ReadableStream است، سرویس‌ورکر از متد cacheable.clone() استفاده می‌کند. این کار باعث «دوگانه شدن» استریم می‌شود؛ به این معنا که مرورگر می‌تواند تکه‌های داده را در لحظه برای کلاینت ارسال کند و هم‌زمان، شاخه دیگر استریم را در حافظه Cache Storage تخلیه و ذخیره کند. در نتیجه، کاربر همچنان داده‌ها را به صورت زنده دریافت می‌کند و کش در پس‌زمینه پر می‌شود.
  • مدیریت حافظه: برای جلوگیری از اتمام فضای دیسک کاربر، تابعی به نام trimCache پیاده‌سازی شده است. این تابع از یک حد مجاز به نام MAX_CACHE_ENTRIES با مقدار ۴۰ ورودی استفاده می‌کند و با استفاده از keys.shift()، قدیمی‌ترین رکوردها را ابتدا حذف می‌کند تا حجم ذخیره‌سازی در محدوده مجاز مرورگر باقی بماند.

در لایه اجرایی، منطق سرویس‌ورکر در فایل sw.js با نام کش chat-cache-v1 و ریشه ذخیره‌سازی /__chat_cache/ تعریف شده است. این کد به طور خاص برای رویداد fetch گوش می‌دهد و درخواست‌های POST را که به /chat ختم می‌شوند هدف قرار می‌دهد.

وقتی یک درخواست رهگیری می‌شود، تابع handleChatRequest بررسی می‌کند که آیا تطابقی در کش وجود دارد یا خیر. اگر تطابقی پیدا شود، هدر X-Chat-Cache: HIT به پاسخ اضافه می‌شود. در غیر این صورت، پاسخ از شبکه دریافت شده، با هدر X-Chat-Cache: MISS علامت‌گذاری می‌شود و با استفاده از event.waitUntil به صورت غیرهمزمان در حافظه ذخیره می‌گردد؛ البته این ذخیره‌سازی تنها در صورتی انجام می‌شود که وضعیت پاسخ OK باشد و از نوع 206 (Partial Content) نباشد.

اما کش کردن پاسخ‌های استریم یک چالش جدید در تجربه کاربری (UX) و دسترسی‌پذیری (Accessibility) ایجاد می‌کند: سرعت. پاسخ‌های کش‌شده چنان سریع می‌رسند که صفحه‌خوان‌ها (Screen Readers) ممکن است انتقال وضعیت از حالت «در حال تولید...» (Streaming) به «پاسخ نهایی» را تشخیص ندهند. اگر رابط کاربری ابتدا متن status.textContent = 'Streaming...' را نمایش دهد و بلافاصله آن را با پاسخ جایگزین کند، صفحه‌خوان ممکن است هرگز این تغییر وضعیت را برای کاربر نگوید.

برای حل این مشکل، توسعه‌دهنده از هدر سفارشی X-Chat-Cache استفاده کرد تا فرانت‌اند بتواند صراحتاً ناحیه وضعیت را به‌روزرسانی کند:

const cacheState = response.headers.get('X-Chat-Cache');
const status = document.getElementById('stream-status');
status.textContent = cacheState === 'HIT' ? 'Cached response loaded' : 'Streaming new response';
status.setAttribute('role', 'status');

این به‌روزرسانی کوچک، یک ترفند عملکردی نامرئی را به یک تغییر وضعیت تبدیل می‌کند که کاربران کیبورد و صفحه‌خوان می‌توانند آن را بشنوند. بدون این تغییر، یک پاسخ کش‌شده ممکن است به گونه‌ای به نظر برسد که صفحه به طور ناگهانی پریده است، که این یکی از خطاهای رایج در ممیزی‌های دسترسی‌پذیری (Accessibility Audits) است.

این الگو با استفاده از MonkeyCode تست شد که یک لایه سرور رایگان و سهمیه ۳۰ میلیون توکن ارائه می‌دهد. توسعه‌دهنده از این سرور برای میزبانی صفحه و سرویس‌ورکر استفاده کرد و دمو را از طریق یک متغیر محیطی (AI_UPSTREAM_URL) به یک نقطه اتصال بالادستی متصل نمود. در حالی که این روش بر لایه کلاینت تمرکز دارد، ابزارهایی مانند Claude Code با پیاده‌سازی کشینگ در لایه‌های عمیق‌تر، پتانسیل کاهش هزینه‌های API تا ۸۴ درصد را دارند.

برنامه تست شامل مراحل زیر بود:

  1. اجرای دمو به صورت محلی با دستور npx serve . و تایید ثبت سرویس‌ورکر در Chrome DevTools در مسیر Application > Service Workers.
  2. تایید دریافت X-Chat-Cache: MISS در اولین فراخوانی و X-Chat-Cache: HIT در فراخوانی دوم.
  3. اطمینان از اینکه در فراخوانی دوم، هیچ درخواست جدیدی به ارائه‌دهنده مدل در لایه بالادستی نمی‌رسد.
  4. تایید وجود ورودی در مسیر Application > Cache Storage > chat-cache-v1.
  5. پاک کردن کش از طریق caches.delete('chat-cache-v1') برای اطمینان از اینکه مدل دوباره فراخوانی می‌شود.
  6. تست با VoiceOver (در macOS) و NVDA (در ویندوز) برای اطمینان از اینکه ناحیه وضعیت عبارت «Cached response loaded» را اعلام می‌کند.

با این حال، توسعه‌دهندگان باید مراقب باشند و از کش کردن پاسخ‌هایی که حاوی داده‌های شخصی، اطلاعات حساب کاربری یا هر چیزی است که توسط هدر Cache-Control: no-store کنترل می‌شود، خودداری کنند.

سایر ملاحظات فنی عبارتند از:

  • تأخیر فراخوانی اول: استریم‌های طولانی باعث افزایش تأخیر در اولین فراخوانی می‌شوند تا زمانی که ورودی کش در دسترس قرار گیرد. کلاینت استریم اصلی را می‌بیند، اما کش تا زمان تکمیل کامل پاسخ قابل استفاده نیست.
  • بار پردازشی CPU: ایجاد هش اثر انگشت، هزینه پردازشی بسیار اندکی به هر درخواست اضافه می‌کند، هرچند این مقدار در مقایسه با هزینه یک فراخوانی مدل، ناچیز است.
  • بودجه توکن: پرامپت‌های اول-بار همچنان توکن مصرف می‌کنند. این سیستم تنها زمانی کمک می‌کند که کاربران درخواست‌های دقیقاً یکسانی را تکرار کنند.

این رویکرد، بار تکرار را از API گران‌قیمت مدل به حافظه ارزان‌قیمت مرورگر منتقل می‌کند. با کوتاه کردن مسیر درخواست‌های تکراری، توسعه‌دهندگان می‌توانند بدون تغییر در تجربه کاربر برای پرامپت‌های جدید، از بودجه توکن‌های خود محافظت کنند.

گام بعدی شما

  • اگر از APIهای گران‌قیمت استفاده می‌کنید، لایه سرویس‌ورکر را برای شناسایی درخواست‌های تکراری در فرانت‌اند پیاده کنید.
  • برای بهبود دسترسی‌پذیری (Accessibility)، حتماً وضعیت بارگذاری از کش را برای صفحه‌خوان‌ها اعلان کنید.
  • سقف تعداد ورودی‌های کش (MAX_CACHE_ENTRIES) را بر اساس حجم داده‌های اپلیکیشن خود تنظیم کنید.

اما داستان سخت‌افزاری این تحول حتی شگفت‌انگیزتر است — به تحلیل ما درباره‌ی تراشه‌های Blackwell مراجعه کنید.

چرا این موضوع مهم است؟

این روش با حذف درخواست‌های تکراری، هزینه‌های عملیاتی توسعه‌دهندگان را کاهش داده و فشار روی زیرساخت‌های استنتاج مدل را کم می‌کند. اعتبار این رویکرد از تجربه عملی در محیط‌های با سهمیه محدود (Quota) تأیید شده است.

تأثیر برای ایران

برای توسعه‌دهندگان ایرانی که با محدودیت‌های پرداخت ارزی و سهمیه‌های محدود APIهای خارجی دست‌وپنجه نرم می‌کنند، این روش راهکاری حیاتی برای بهینه‌سازی هزینه‌ها و جلوگیری از اتلاف توکن‌هاست.

·نگاه ما
تحریریه دات‌هوش

انتقال مدیریت تکرار از لایه مدل به لایه مرورگر، یک چرخش هوشمندانه در کاهش هزینه‌های استنتاج است. این رویکرد نشان می‌دهد که بسیاری از هزینه‌های APIهای هوش مصنوعی نه به دلیل پیچیدگی مدل، بلکه به دلیل ناکارآمدی‌های ساده در معماری فرانت‌اند است. در واقع، پیاده‌سازی یک لایه میان‌افزار (Middleware) در کلاینت می‌تواند بدون تغییر در مدل، نرخ مصرف توکن را در سناریوهای خاص به شدت کاهش دهد.

منابع

این گزارش با خط‌لولهٔ خودکار دات‌هوش از منابع معتبر جهانی تدوین و زیر نظر تحریریه منتشر شده است. روش کار ما

گفتگو

پنج‌شنبه‌های هوش‌محور

بسته‌ی هفتگی دات‌هوش

۵ خبر، ۲ ابزار، ۱ پرامپت در هر شماره. به‌زودی راه‌اندازی می‌شود — هر پنج‌شنبه صبح.

خبر کلیدی
ابزار کاربردی
پرامپت حرفه‌ای
تحلیل پژوهش
به‌زودی
زاویه‌ی ایرانی
به‌زودی
تمرین این هفته
به‌زودی

راهنماهای دات‌هوش

راهنماهای کاربردیِ دات‌هوش برای کار با هوش مصنوعی — از همین‌جا شروع کنید:

دات‌هوش

راهنمای فارسی هوش مصنوعی — با نگاه به ایران

اخبار روزانه، معرفی ابزارها و مدل‌ها، و آموزشِ کار با هوش مصنوعی؛ همیشه با این پرسش که از ایران چه چیزی کار می‌کند و چه چیزی نه.