تصور کنید دستیار هوش مصنوعی شما در پاسخ به سؤال کاربر درباره صورتحساب، دادههای مربوط به ارسال کالا را بازیابی میکند. این اتفاق به دلیل یک خطای کوچک در بازتلاش (retry) خط لوله تولید بردارها رخ داده که بهطور خاموش کل ایندکس برداری شما را فاسد کرده است؛ شکست در اینجا نه به دلیل خرابی API، بلکه به دلیل موفقیت کد در مسیر اشتباه است.
ساخت یک سامانه تولید بازیابیافزا (RAG) — شبیه دانشآموزی که قبل از جواب دادن، اول کتاب درسی را باز میکند و از آن نقل میآورد — در مقیاس تولیدی، نیازمند عبور از حلقههای سادهی for است. همانطور که در تحلیل قبلی ما دربارهی محدودیتهای نرخ API مدلهای زبانی اشاره کردیم، مدیریت محدودیتهای ارائهدهنده تنها گام نخست است. چالش مهندسی واقعی در مرحله «جذب داده» (ingest) نهفته است؛ جایی که تبدیل یک مجموعه داده بزرگ به بردار، کمتر شبیه به یک اسکریپت و بیشتر شبیه به یک سامانه توزیعشده است که باید با شکستهای جزئی و ناپایداری شبکه مقابله کند. این فرآیند ممکن است یک ساعت به طول بیانجامد، با یک API دارای محدودیت نرخ ارتباط برقرار کند، دادهها را در پایگاهداده بنویسد و در میانه راه به گونهای شکست بخورد که یک حلقه for ساده هیچ پاسخی برای آن ندارد.
شکاف کارایی: دستهبندی و همروندی
پیادهسازیهای ابتدایی که هر تکه متن را در یک فراخوانی API جداگانه ارسال میکنند، بهشدت کند هستند. یک حلقه را در نظر بگیرید که برای هر تکه متن، تابع openai.embeddings.create را فراخوانی میکند. طبق مستندات OpenAI، برای مجموعهای با ۵۰,۰۰۰ تکه، اگر زمان رفتوبرگشت هر درخواست ۱۵۰ میلیثانیه باشد، بیش از دو ساعت زمان صرف انتظار در حالت بیکاری (idle) میشود. برای حل این مشکل، توسعهدهندگان باید از ورودیهای آرایهای در مدلهایی مثل text-embedding-3-small استفاده کنند.
با این حال، دستهبندی (Batching) — که مثل بستهبندی چندین سفارش در یک جعبه برای کاهش هزینه ارسال است — محدودیت جدیدی به نام سقف توکن ایجاد میکند. در حالی که دستهای با ۵۱۲ تکه کوتاه ممکن است پذیرفته شود، ۶۴ تکه بلند احتمالاً خطای ۴۰۰ ایجاد میکنند، زیرا وقتی تکهها بلند هستند، محدودیت توکن زودتر از تعداد آیتمها فعال میشود. رویکرد درست این است که اندازه دستهها بر اساس تخمین توکن تعیین شود، نه تعداد ثابت. یک تابع دستهبندی مقاوم باید یک «بودجه توکن» (مثلاً ۶۰,۰۰۰ توکن) را دنبال کند و به محض اینکه هزینه تخمینی تکه بعدی از این بودجه فراتر رفت، دسته فعلی را به آرایه خروجی منتقل کند.
برای به حداکثر رساندن توان عملیاتی بدون برخورد با محدودیت نرخ، باید همروندی محدود (bounded concurrency) پیاده شود. استفاده از ابزاری مثل p-limit اجازه میدهد تعداد مشخصی از درخواستها بهطور همزمان در جریان (in-flight) باشند. برای شروع، عدد ۴ نقطه مناسبی است. این کار مانع از مسدود شدن سریع حساب کاربری میشود که معمولاً در اثر استفاده از Promise.all روی کل مجموعه داده رخ میدهد. عدد ایدهآل برای همروندی به محدودیتهای حساب شما و این بستگی دارد که در صورت کرش کردن، حاضر هستید چه مقدار از پیشرفت کار را از دست بدهید؛ همروندی بالاتر کار را سریعتر تمام میکند اما ریسک از دست رفتن پیشرفت بیشتر است.
مکانیزمهای بازتلاش هوشمند
بازتلاش با تأخیر ثابت در صورت بروز هر خطایی، اغلب بدتر از عدم بازتلاش است، زیرا محدودیت موقت نرخ را به یک فشار مستمر تبدیل میکند. بر اساس بررسیهای فنی، یک تابع withRetry استاندارد باید موارد زیر را داشته باشد:
- فیلتر کردن خطاهای قابل بازتلاش: استفاده از یک بررسی
isRetryableبرای تفکیک خطاهای ۴۲۹ (درخواستهای زیاد) و ۵xx (خطای سرور) از خطای ۴۰۰ (درخواست نادرست). بازتلاش برای یک درخواست بدساخت، تنها اتلاف تلاشها و منابع است. - زمانبندی مبتنی بر پاسخ: اولویت دادن به هدر
Retry-Afterارسالی از سرور نسبت به منحنیهای تأخیر محلی، زیرا سرور دقیقاً میداند چه زمانی آماده پذیرش درخواست جدید است. - عقبنشینی نمایی با نویز (Jitter): در صورت نبود هدر، استفاده از فرمولی مثل
Math.min(2 ** i, 30)به همراه یک مقدار تصادفی کوچک (مثلاًMath.random() * 0.3 * backoff). این کار مانع از آن میشود که چندین Worker بهطور همزمان و هماهنگ بازتلاش کنند و به API حمله کنند.
خطر نگاشت موقعیتی
بحرانیترین باگ در زمان بازتلاشها رخ میدهد. اکثر توسعهدهندگان بردار معنایی (Embedding) — که مثل کارت معرفی عددی برای هر واژه است و میگوید این کلمه همسایه چه کلمات دیگری است — را بر اساس موقعیت آنها در آرایه بازگشتی به تکههای متن متصل میکنند. این روش تا زمانی کار میکند که یک دسته بهطور کامل موفق شود. اما اگر دستهای به دلیل تایماوت، قطع اتصال یا فراتر رفتن یکی از ورودیها از حد توکن شکست بخورد و منطق بازتلاش، آیتم خطادار را حذف کند، فاجعه آغاز میشود.
اگر بازتلاش لیستی کوتاهتر از تکهها را ارسال کند (مثلاً با فیلتر کردن failedId) اما کد همچنان بر اساس طول دسته اصلی پیمایش کند، هر تکه متن بردار همسایهاش را دریافت میکند. این موضوع باعث کاهش نامحسوس در بازیابی (Recall) میشود که هیچ تست استانداردی آن را شناسایی نمیکند، زیرا درج در پایگاهداده بدون خطا انجام میشود. در واقع حلقه اصلی روی دسته اولیه حرکت میکند در حالی که دارد از یک آرایه پاسخ کوتاهتر داده میخواند.

برای جلوگیری از این اتفاق، هرگز نباید از موقعیت (position) برای نگاشت در مرز بازتلاشها استفاده کرد. در عوض، باید از فیلد index بازگشتی توسط ارائهدهنده استفاده کرد تا بردارها از طریق یک شیء Map به شناسههای (ID) خاص متصل شوند. با ایجاد یک Map از res.data که کلید آن d.index است، میتوانید تضمین کنید که بردار بهدرستی با تکه متن جفت شده است. اگر برداری برای یک ایندکس خاص گم شد، سامانه باید خطای MissingEmbedding صادر کند، نه اینکه با دادههای جابهجا شده ادامه دهد.
تضمین سلامت دادهها
برای اینکه فرآیند جذب داده واقعاً مقاوم باشد، عملیات نوشتن باید idempotent (تکرارپذیر بدون تغییر نتیجه) باشد. با استفاده از هش SHA-256 از متن نرمالشده به عنوان content_hash و بهکارگیری عبارت ON CONFLICT در SQL، میتوان تضمین کرد که تولید مجدد بردار برای یک محتوای تکراری، منجر به ایجاد ردیف جدید نشود و صرفاً یک عملیات بدون اثر (no-op) باشد.
بهطور مشخص، SQL باید ستونهای content_hash و embed_model را هدف قرار دهد: INSERT INTO chunks ... ON CONFLICT (content_hash, embed_model) DO UPDATE SET embedding = EXCLUDED.embedding;.
این معماری اجازه میدهد تا یک جوب پس از کرش کردن، بدون نیاز به ردیابی مکاننما (cursor)، از ابتدا شروع شود. این ساختار تضمین میکند که:
- محتوای تغییرنیافته نادیده گرفته شود.
- محتوای تغییریافته بهروزرسانی شود.
- هیچ ردیف تکراری ایجاد نشود.

در نهایت، سامانه باید «بلند» شکست بخورد. جوبی که ساعتها اجرا میشود و خطاها را فقط در یک لیست لاگ میکند اما در نهایت کد خروجی ۰ (موفقیت) میدهد، فریبدهنده است، حتی اگر ۶٪ از مجموعه داده گم شده باشد. با ردیابی شکستها در یک آرایه و تنظیم process.exitCode = 1 در صورت غیرخالی بودن آن، مطمئن میشوید که زمانبند (scheduler) شما متوجه شکست شده است. بدون این کار، ممکن است کاربر شکایت کند که دستیار «درباره سندی چیزی نمیداند» در حالی که آن سند فیزیکی در پایگاهداده موجود است اما بردارش گم شده است. این نوع عدم قطعیت در بازیابی دادهها میتواند منجر به رفتارهای پیشبینیناپذیر در لایههای بالاتر شود؛ به همین دلیل است که استفاده از لایههای اعتبارسنجی معنایی برای جلوگیری از لوپهای خطا در فراخوانی ابزارها حیاتی است.
تأیید ایندکس
از آنجا که فساد موقعیتی نامرئی است، تنها راه مطمئن، نمونهبرداری پس از اجراست. با انتخاب چند ده ردیف و تولید مجدد بردار آنها بهصورت تازه، میتوان شباهت کسینوسی (Cosine Similarity) بین بردار ذخیرهشده و بردار جدید را محاسبه کرد. هر مقداری بهطور محسوس کمتر از ۱.۰ نشان میدهد که بردار به متن اشتباهی متصل شده است.
این تغییر در رویکرد، تبدیل Embedding را از یک فراخوانی ساده API به یک خط لوله مهندسی دقیق میکند. در این مدل، قابلیت ازسرگیری (resumability) و تأییدیه بر سرعت خام اولویت دارند تا تضمین شود دادههای زمینهای برای LLM واقعاً درست هستند. در برخی معماریهای پیشرفتهتر، برای کاهش وابستگی به این ایندکسهای حجیم و کاهش مصرف توکن، از روشهایی مانند افشای تدریجی اطلاعات استفاده میشود تا نیاز به بازیابیهای گسترده برداری کاهش یابد.
توسعهدهندگانی که به دنبال پیادهسازی این الگوها هستند، میتوانند جزئیات بیشتر و بررسی عمیقتری از این مکانیزمها را در سری مقالات AI in TypeScript در وبسایت xgabriel.com بیابند که همه چیز را از اولین فراخوانی LLM تا عاملها (agents) در محیط تولید پوشش میدهد. برای کسانی که در مرحله تولید هستند، امنیت این عاملها نیز به اندازه پایداری دادهها اهمیت دارد و باید لایههای دفاعی در برابر تزریق پرامپت را در معماری خود لحاظ کنند.
گام بعدی شما
- بررسی مجدد منطق
mapدر خط لولههای Embedding برای جایگزیینی موقعیت باindex. - پیادهسازی
content_hashدر پایگاهداده برای جلوگیری از دادههای تکراری در بازتلاشها. - اجرای یک اسکریپت تأییدیه (Verification) برای محاسبه شباهت کسینوسی روی نمونههای تصادفی ایندکس.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell مراجعه کنید.




گفتگو