اگر برای هر تماس با API مدلهای زبانی یک حلقهٔ بازخوانی ساده نوشتهاید، احتمالاً در حال ساختن یک بمب ساعتی مالی هستید. باید بدانید که منطقِ متداول در درخواستهای HTTP معمولی، در دنیای هوش مصنوعی زاینده نهتنها ناکارآمد است، بلکه میتواند وضعیت دادههای شما را تخریب کند. یک ابزار کمکی بازخوانی عمومی (Generic Retry Helper) زمانی که روی فراخوانهای مدلهای زبانی بزرگ (LLM) اعمال شود، تبدیل به یک «تله مالی» یا همان Footgun میشود.
در حالی که بستن یک درخواست HTTP ناپایدار در یک حلقه بازخوانی یک رویه استاندارد است، انجام این کار برای یک ویژگی مبتنی بر هوش مصنوعی اغلب منجر به صورتحسابهای بیحدومرز و وضعیتهای فاسد (Corrupted State) میشود. توسعهدهنده xgabriel، نویسنده مجموعه «AI That Answers» و سازنده Hermes IDE، با جزئیات توضیح داده است که چرا مفروضات اصلی بازخوانیهای سنتی REST در متن هوش مصنوعی زاینده شکست میخورند.
اکثر توسعهدهندگان از یک Helper استفاده میکنند که فراخوانها را برای مدیریت نوسانات شبکه میپوشاند. احتمالاً در کد شما هم همین حالا یک Helper بازخوانی وجود دارد که فراخوانهای HTTP ناپایدار را پوشش داده و با استراتژی Backoff عمل میکند. این رویکرد برای درخواستهای GET تکرارپذیر (Idempotent) که هزینه آنها تنها یک رفتوبرگشت ساده است، به خوبی کار میکند. با این حال، اعمال الگویی مانند const res = await withRetry(() => client.messages.create({ model, max_tokens: 4096, messages })) خطرناک است، زیرا فراخوانهای مدل نه ارزان هستند و نه تکرارپذیر.
سه فرض مرگبار در بازخوانیهای سنتی
اول، توسعهدهندگان تصور میکنند بازخوانیها ارزان هستند. در یک تماس REST استاندارد، بازخوانی صرفاً یک درخواست دیگر است. اما در یک فراخوان مدل، بازخوانی باعث میشود کل فرآیند تولید متن (Generation) دوباره از ابتدا اجرا شود. در پاسخهای طولانی، تلاش دوم ممکن است گرانترین درخواستی باشد که سرویس شما در آن ساعت ارسال میکند.
موردی که بیشترین آسیب را میزند، «زمان انتظار برای خواندن» (Read Timeout) است. اگر کلاینت شما پس از ۳۰ ثانیه تسلیم شود اما ارائهدهنده مدل همچنان در حال تولید پاسخ باشد، شما هزینه آن تولید را پرداخت میکنید، فارغ از اینکه آیا پاسخ را خواندهاید یا خیر. اگر سپس اقدام به بازخوانی کنید، برای بار دوم هزینه یک تولید کامل را میپردازید. این یعنی شما هزینه دو تولید کامل را میدهید در حالی که تنها از یکی از آنها با موفقیت استفاده کردهاید.
استریمینگ (Streaming) راهکار اصلی برای رفع مشکلات Timeout است. با دریافت مداوم توکنها به جای انتظار برای پاسخ کامل، مسئله Timeout اساساً ناپدید میشود. اگر در فراخوانهای غیر استریمینگ روی خروجیهای طولانی دچار Timeout میشوید، راهکار درست تغییر وضعیت به استریمینگ است، نه صرفاً تنظیم مجدد مدتزمان Timeout.
دوم، این فرض که درخواست یکسان لزوماً پاسخ یکسانی میدهد، غلط است. یک درخواست GET تکرارپذیر، بدنه یکسانی را دو بار برمیگرداند، اما یک فراخوان مدل اینگونه نیست. این موضوع زمانی اهمیت مییابد که بازخوانی توسط چیزی در پاییندستِ یک تولید موفق تحریک شود؛ مثلاً شکست در تجزیه (Parse failure)، خطای اعتبارسنجی، یا یک نوسان شبکه هنگام خواندن پاسخ.
نتیجه، یک خروجی غیرقطعی (Non-deterministic) است. بازخوانی یک پاسخ متفاوت تولید میکند و لاگهای شما را با یک شناسه درخواست (Request ID) مواجه میکند که با دو خروجی متفاوت مرتبط شده است، در حالی که تنها یکی از آنها واقعاً مورد عمل قرار گرفته است.
برای حل این مشکل، توسعهدهندگان باید در مورد آنچه بازخوانی میکنند دقیق باشند و «فراخوان» (Call) را از «تجزیه» (Parse) جدا کنند. خطاهای انتقال (مانند ۴۲۹ یا ۵xx) بازخوانی میشوند. اما خطاهای محتوایی — جایی که JSON نامعتبر است — باید به عنوان یک «نوبت تکمیلی» (Follow-up turn) مدیریت شوند. این نوبت جدید شامل پاسخ قبلی و خطای اعتبارسنجی است تا مدل ببیند چه چیزی اشتباه بوده است. این رویکرد هم ارزانتر است و هم احتمال موفقیت بیشتری نسبت به یک بازخوانی کورکورانه دارد. این لایه کنترل محتوایی مشابه رویکردهای اعتبارسنجی معنایی برای جلوگیری از حلقههای شکست در ابزارهاست که ثبات سیستم را تضمین میکند.
سوم، این فرض که بازخوانی ایمن است، در گردشهای کاری «عاملمحور» (Agentic) شکست میخورد. اگر یک فراخوان مدل یک تکمیل ساده (Plain Completion) باشد، تکرار آن صرفاً اتلاف هزینه است. اما اگر فراخوان بخشی از یک نوبت عامل باشد که قبلاً ابزاری را اجرا کرده است، تکرار آن یک «اثر جانبی» (Side Effect) دوم ایجاد میکند.
به عنوان مثال، در «نوبت N»، مدل درخواست ارسال یک ایمیل را میدهد و سیستم آن را ارسال میکند. اگر خواندن پاسخ در این مرحله شکست بخورد و سیستم بازخوانی کند، همان آرایه پیامها را دوباره ارسال میکند. مدل ممکن است دوباره درخواست ارسال ایمیل را بدهد و منجر به ارسال ایمیل تکراری برای کاربر شود. Helper بازخوانی نمیتواند بداند که آرایهای که دوباره ارسال میکند، توصیفکننده کاری است که قبلاً تکمیل شده است. هر عملیاتی که دارای اثر جانبی است، به یک کلید یکتاییساز (Idempotency Key) نیاز دارد که از خودِ عملیات مشتق شده باشد، نه از تلاش مجدد. در معماریهای پیچیدهتر، این ریسکها با تهدیداتی چون تزریق پرامپت ترکیب میشوند که برای مهار آنها باید لایههای دفاعی چندگانه در عاملهای TypeScript پیادهسازی شود.
مهندسی سیاست بازخوانی اختصاصی برای مدلها
پیادهسازی یک تابع callModel قدرتمند در TypeScript نیازمند تغییرات معماری مشخص است. یک پیادهسازی درست باید از حلقهای استفاده کند که تعداد تلاشها (n) و آخرین خطای مواجه شده را ردیابی کند، در حالی که یک بررسی بودجه را از طریق opts.budget?.assertCanSpend(estimateCost(params)) ادغام میکند.
تفاوتهای کلیدی بین یک Helper تخصصی مدل و یک Helper عمومی عبارت است از:
فیلتر دقیق خطا (Narrow Error Filtering): بازخوانیها باید محدود به خطاهای انتقال باشند. بررسی
isRetryableباید تنها نمونههایAPIErrorرا هدف قرار دهد که وضعیت آنها ۴۲۹ (Too Many Requests)، ۴۰۸ (Request Timeout) یا هر وضعیتی برابر یا بیشتر از ۵۰۰ باشد. خطای ۴۰۰ به این معناست که درخواست بدساخت (Malformed) است و دوباره هم بدساخت خواهد بود؛ همچنین رد درخواست به دلیل سیاستهای محتوایی یک خطای گذرا نیست. بازخوانی این موارد فقط تعداد تلاشها را میسوزاند و نمایش خطای واقعی را به تأخیر میاندازد.احترام به Retry-After: بهجای تکیه صرف بر منحنی Backoff، Helper باید از هدر
Retry-Afterاستفاده کند. سرور دقیقاً میداند چه زمانی دوباره درخواست را میپذیرد، در حالی که یک منحنی Backoff عمومی صرفاً در حال حدس زدن است.پیادهسازی Jitter: جیتر (Jitter) حیاتی است زیرا محدودیتهای نرخ (Rate Limits) معمولاً در سطح حساب کاربری هستند. بدون جیتر، هر درخواست همزمان در سرویسی که در یک لحظه با خطای ۴۲۹ مواجه شود، همزمان بازخوانی خواهد شد و یک «هجوم گلهای» (Thundering Herd) ایجاد میکند که دوباره محدودیت را فعال میکند. تأخیر پیشنهادی شامل پایهای برابر با
Math.min(1000 * 2 ** (n - 1), 20_000)به همراه یک جیتر تصادفی تا ۳۰٪ است.اجرای بودجه (Budget Enforcement): Helper باید بودجه را قبل و بعد از هر تلاش ثبت و بررسی کند. از آنجایی که سه تلاش برای یک درخواست بزرگ میتواند هزینه را سه برابر کند، حلقه بازخوانی دقیقاً همان جایی است که صورتحسابهای بیحدومرز متولد میشوند.
مدیریت بیشباری و مشاهدهپذیری
خطاهای بیشباری (Overload) یا ظرفیت نادر نیستند؛ ارائهدهندگان در زمانهای پیک بار زیاد، مکرراً آنها را برمیگردانند. چون این خطاها اغلب شبیه به خطاهای ۵xx هستند، یک Helper بازخوانی عمومی سرویسی را که همین حالا هم اشباع شده است، بیشتر بمباران میکند. رویکرد درست این است که بیشباری را به عنوان یک مورد خاص با کفِ زمان خواب (Sleep Floor) طولانیتر در نظر بگیرید، مثلاً 5_000ms + Math.random() * 5_000.
اگر یک مدل جایگزین (Fallback) وجود دارد، این ایدهآلترین لحظه برای استفاده از آن است. تنزل سطح (Degrading) به یک مدل کوچکتر و سریعتر، برای اکثر ویژگیها بهتر از شکست کامل سیستم است. وقتی حد maxAttempts (که معمولاً ۳ است) تکمیل شود، سیستم باید یک خطای تایپشده مانند ModelUnavailable صادر کند که شامل تعداد تلاشها و علت آخرین شکست باشد. این به فراخوانکننده اجازه میدهد تا بین نمایش پاسخ کششده، تغییر مدل یا نمایش صفحه خطا یکی را انتخاب کند.

مشاهدهپذیری (Observability) تکه نهایی پازل است. لاگها باید شامل موارد زیر باشند:
- شماره تلاش (
n) - کد وضعیت HTTP (مثلاً از
APIError) - نام سازنده خطا (Constructor name of the error)
- تأخیر به میلیثانیه
- نسخه پرامپت (
PROMPT.version) - کل هزینه تاکنون به دلار (
costSoFarUsd) از بودجه
مانیتورینگ این تعداد بازخوانی به عنوان یک شاخص پیشرو (Leading Indicator) عمل میکند. افزایش تدریجی در نرخ موفقیتهای «تلاش-۲» نشان میدهد که شما در حال نزدیک شدن به سقف نرخ درخواستهای خود هستید. این موضوع یک فرصت ایجاد میکند تا محدودیتهای همزمانی را به عنوان یک تغییر برنامهریزیشده افزایش دهید، به جای اینکه به یک حادثه در محیط عملیاتی واکنش نشان دهید. این دقت در ردیابی وضعیت، مشابه اهمیت جلوگیری از جابهجاییهای خاموش در بردارهای معنایی است تا از تخریب تدریجی کیفیت پاسخها جلوگیری شود.
خلاصه استراتژی بازخوانی مدل
برای اجتناب از «شوک صورتحساب» و تخریب وضعیت که در اولین اپلیکیشنهای LLM رایج است، این قوانین را دنبال کنید:
- فقط خطاهای انتقال (۴۲۹، ۴۰۸، ۵xx) را بازخوانی کنید.
- از جیتر استفاده کنید و به هدرهای
Retry-Afterاحترام بگذارید. - خطاهای محتوایی را به عنوان یک نوبت اصلاحی ارسال کنید، نه به عنوان بازخوانی.
- بودجه را قبل از هر تک تلاش بررسی کنید.
- هرگز اجازه ندهید یک Helper عمومی فراخوانی را که قبلاً باعث ایجاد اثر جانبی شده است، بپوشاند.
این تغییر نگرش، توسعهدهنده را از treating AI به عنوان یک API جعبهسیاه، به treating آن به عنوان یک سیستم توزیعشده پیچیده منتقل میکند. برای کسانی که میخواهند عمیقتر در این حالتهای شکست — شامل Timeoutها، استریمینگ و مرز تجزیه — غوطهور شوند، مجموعه «AI That Answers» یک راهنمای جامع در xgabriel.com/ai-in-typescript ارائه میدهد.




گفتگو