زمستان سال گذشته به تیمی که در حال ساخت پشتیبانی مشتری هوشمند بود کمک کردم مشکلات آنلاینشان را بررسی کنند. بکاندشان بهطور همزمان به چهار API مدل بزرگ متصل بود و فرانتاند از SSE برای خروجی استریمینگ استفاده میکرد. از روز سوم پس از راهاندازی، کاربران شکایت کردند که «پاسخ مثل خمیردندان از لوله بیرون میآید» — گاهی یک پاراگراف کامل متن بهطور کامل متوقف میشد و سپس ناگهان یک توده بزرگ بیرون میریخت؛ گاهی یک جمله به دو نیم تقسیم میشد و در وسط آن بلوکهای خالی عجیب و غریب قرار داشت. در لاگهای بکاند هیچ 5xx وجود نداشت و مانیتورینگ دسترسپذیری APIهای هر شرکت سبز بود.
این نوع مشکلات سختترین برای بررسی هستند، چون هر بخش در ظاهر طبیعی به نظر میرسد. من همراه آنها از پنل Network مرورگر شروع کردم و لایه به لایه پایین رفتم، و در نهایت متوجه شدم که مشکل اصلاً در هیچیک از APIها نیست، بلکه در خودِ «استفاده مختلط از چهار شرکت» است.
اول نتیجهگیری: ناهمگونی پروتکل استریمینگ، تله پنهان اتصال تجمیعی است
خروجی استریمینگ APIهای مدل بزرگ، عمدتاً از SSE (Server-Sent Events) استفاده میکند. اما «همه از SSE استفاده میکنند» به معنای «رفتار یکسان» نیست. چهار شرکت در اندازه بلوکبندی، نشانگر پایان و استراتژی ضربان قلب هر کدام پیادهسازی خودشان را دارند. جریان بایتی که فرانتاند دریافت میکند در ظاهر همه با data: شروع میشوند، اما منطق تجزیه کاملاً قابل استفاده مجدد نیست. این ریشه لگ و ناهنجاری مرزهای بلوکبندی است.
بررسی لایه به لایه: از فرانتاند تا گیتوی، مشکل کجاست
لایه اول، فرانتاند. تجزیه SSE آنها از EventSource استاندارد استفاده میکند و رویدادها را با \n\n جدا میکند. این خودش مشکلی ندارد.
لایه دوم، فورواردینگ بکاند. بکاند با Node یک لایه عبوردهی ساخته و ReadableStream بالادست را مستقیماً به پاسخ pipe میکند. اینجا اولین مین کار گذاشته شده: اندازه chunk در شرکتهای مختلف تفاوت زیادی دارد؛ بعضی به ازای هر token ارسال میکنند، بعضی چند token جمع میکنند و بعد میفرستند. اگر در فرآیند pipe از Buffer.concat استفاده شده باشد و مرزهای چندبایتی UTF-8 در بین chunkها بهدرستی مدیریت نشود، نصف یک کاراکتر چینی بریده میشود و فرانتاند آن را بهصورت مربعهای خراب رندر میکند.
لایه سوم، تایماوت و تلاش مجدد. بکاند برای هر چهار شرکت تایماوت یکسان ۳۰ ثانیه تنظیم کرده بود. اما در سناریوی استریمینگ، «تایماوت ۳۰ ثانیه» دقیقاً یعنی زمان اولین بایت یا زمان کل جریان؟ آنها تفکیک نکرده بودند، که باعث میشد پاسخهای طولانی در میانه قطع شوند و فرانتاند «وسط حرف ناگهان متوقف شد» را نشان دهد.
لایه چهارم، کدهای خطا. کدهای وضعیت HTTP و ساختار body برای محدودیت نرخ، شکست احراز هویت و مسدودسازی بازبینی محتوا در چهار شرکت متفاوت است. بکاند بهطور یکسان از error.message استفاده میکرد، که نتیجهاش این بود که از دو شرکت مقدار دریافت میشد و از دو شرکت undefined، و فرانتاند پیام خطای خالی نشان میداد.
تا اینجا بررسی روشن شد: مشکل واقعی وجود باگ در یک API خاص نیست، بلکه این است که معناشناسی چهار پروتکل مستقیماً به لایه کسبوکار افشا شده است.
تفاوتهای کلیدی پیادهسازی استریمینگ چهار شرکت
یک مقایسه تهیه کردم، دادهها از مستندات رسمی هر شرکت و ضبط بستههای واقعی است، بخشهایی که رسماً افشا نشده را علامت زدهام:
بُعد / شرکت A / شرکت B / شرکت C / شرکت D
نشانگر پایان / data: [DONE] / فیلد finish_reason / data: [DONE] / رویداد event: done
ضربان قلب / ندارد / حدود ۱۵ ثانیه خط تفسیر / ندارد / رسماً افشا نشده
اندازه بلوکبندی / به ازای هر token / ۲-۴ token جمع میکند / به ازای هر token / نامشخص
محل کد خطا / کد وضعیت HTTP / body.error.code / کد وضعیت HTTP / body.code
با نگاه به این جدول مشخص میشود چرا منطق تجزیه فرانتاند قابل استفاده مجدد نیست. نشانگر پایان سه نوع نوشتار دارد، کد خطا دو محل، استراتژی ضربان قلب دو شرکت دارند و دو شرکت ندارند. اگر در کد کسبوکار مستقیماً if (chunk.includes('[DONE]')) بنویسید، با شرکت B هرگز پایان نمییابد و اتصال تا تایماوت آویزان میماند.
چهار نکته طراحی برای جمعبندی در لایه گیتوی
اول، یکسانسازی فرمت رویداد SSE. گیتوی همه رویدادهای شرکتهای بالادست را به یک ساختار واحد نرمال میکند: {type: "delta" | "done" | "error", content, code}. لایه کسبوکار فقط این یک مجموعه را میشناسد و دیگر نگران این نیست که بالادست کیست. پلتفرم تجمیعی API مدل بزرگ token8341 که در پروژهمان استفاده کردهایم دقیقاً همین کار را میکند؛ به بیرون فقط یک پروتکل استریمینگ ارائه میدهد و هنگام تعویض مدل کد کسبوکار نیازی به تغییر ندارد.
دوم، تمرکز احراز هویت در گیتوی. چهار شرکت چهار مجموعه Key دارند که در پیکربندی کسبوکار پراکندهاند و هر بار چرخش نیازمند تغییر چهار جاست. Keyها را در گیتوی جمع کنید و لایه کسبوکار فقط یک token داخلی بگیرد. ارزش چنین لایه تجمیعی مانند پلتفرم تجمیعی API مدل بزرگ token8341 همینجاست: یک Key برای فراخوانی چند مدل، و چرخش Key و کنترل سهمیه در سمت گیتوی انجام میشود.
سوم، تایماوت باید در دو بخش پیکربندی شود. تایماوت اولین بایت (TTFB) و تایماوت بیکاری جریان جدا باشند. TTFB را کوتاهتر تنظیم کنید، مثلاً ۱۰ ثانیه، برای شکست سریع؛ تایماوت بیکاری جریان را طولانیتر، مثلاً ۶۰ ثانیه، تا مدل فرصت فکر کردن داشته باشد. اشتراک ۳۰ ثانیه برای هر دو قطعاً مشکل ایجاد میکند.
چهارم، تلاش مجدد فقط برای خطاهای ایدمپوتنت. درخواست استریمینگ به محض شروع خروجی، تلاش مجدد باعث محتوای تکراری میشود. لایه گیتوی باید تفکیک کند: شکست در مرحله برقراری اتصال قابل تلاش مجدد است، شکست پس از شروع خروجی جریان فقط میتواند بهعنوان خطا به فرانتاند گزارش شود تا کاربر دستی دوباره ارسال کند. اگر این مرز بهدرستی ترسیم نشود، «یک جمله دو بار گفته میشود».
جدول نگاشت کدهای خطا را باید خودتان نگهداری کنید
کدهای محدودیت نرخ چهار شرکت: دو شرکت 429 برمیگردانند، یک شرکت 200 ولی با خطا در body، و یک شرکت کد سفارشی. لایه گیتوی باید یک جدول نگاشت بسازد و خطاهای مختلف بالادست را به کدهای خطای داخلی یکسان تبدیل کند، مثل RATE_LIMIT، AUTH_FAILED، CONTENT_BLOCKED. لایه کسبوکار بر اساس کدهای داخلی تنزل انجام میدهد و دیگر معناشناسی بالادست را حدس نمیزند. این جدول میانبری ندارد و فقط میتوان با ضبط بستههای هر شرکت و مقایسه با مستندات آن را تنظیم کرد؛ پلتفرم تجمیعی API مدل بزرگ token8341 یک مرجع نگاشت آماده در مستندات ارائه داده که کار زیادی را صرفهجویی کرد.
در چه شرایطی اتصال مستقیم سادهتر است
باید اعتراف کرد که همه سناریوها ارزش گذاشتن گیتوی را ندارند. اگر پروژه فقط به یک مدل متصل است، یا فقط استنتاج دستهای غیراستریمینگ انجام میدهد، اتصال مستقیم به SDK رسمی کمترین کد، کوتاهترین مسیر و سادهترین عیبیابی را دارد. لایه گیتوی پیچیدگی ناشی از «اتصال یکپارچه چند مدل» را حل میکند؛ در سناریوی تکمدل، افزودن گیتوی یک لایه فورواردینگ اضافه و یک نقطه خرابی بیشتر است. معیار قضاوت ساده است: وقتی تعداد مدلهای متصل از دو بیشتر شود، یا نیاز به تعویض مدل در زمان اجرا باشد، سود گیتوی از هزینهاش بیشتر میشود.
در پایان
آن خرابی در نهایت با افزودن لایه گیتوی برای جمعبندی حل شد؛ منطق تجزیه SSE فرانتاند از چهار شاخه به یک مجموعه کاهش یافت و لگ و ناهنجاری بلوکبندی همراه آن از بین رفت. با نگاه به گذشته، مشکل هرگز در هیچ API مدل بزرگی نبود، بلکه در «انتقال مستقیم تفاوتهای پروتکل چند شرکت به لایه کسبوکار» بود. اگر شما هم در حال اتصال چند مدل هستید، ابتدا چهار مورد پروتکل استریمینگ، کدهای خطا، احراز هویت و تایماوت را در لایه گیتوی هماهنگ کنید، سپس درباره قابلیتهای لایه بالاتر صحبت کنید.
نویسنده: سون هائوران
تاریخ انتشار: ۲۵ اکتبر ۲۰۲۶