تصور کنید یک برنامهنویس ابزاری ساخته که به مدل زبانی اجازه میدهد دادههای زنده یک کشتی را بخواند، اما مدل به دلیل چند حدس اشتباه درباره نام سنسورها، بهطور کامل «کور» میشود و دیگر هیچ دادهای دریافت نمیکند. این اتفاق نه به دلیل خرابی سرور، بلکه به دلیل یک اشتباه کوچک در نحوه تفسیر کدهای HTTP رخ میدهد. چرا یک الگوی استاندارد «قطعکننده» (Circuit Breaker) در محیطهای اجرای عامل (Agent Runtimes)، پاسخهای بیضرر «یافت نشد» را به قطعی کامل ابزارها تبدیل میکند؟
به نقل از یک تحلیل فنی در وبسایت dev.to در ۲۴ ژوئن ۲۰۲۶، یک خطای HTTP 404 که بهاشتباه طبقهبندی شده باشد، میتواند باعث ایجاد یک شکست آبشاری شود و بهطور مؤثر عامل هوش مصنوعی را نسبت به ابزارهای خودش کور کند. این مسئله زمانی رخ میدهد که توسعهدهندگان، مدلهای زبانی بزرگ (LLM) — مثل کتابخانهداری که میلیاردها صفحه را خوانده و حالا با همان لحن کتابها جواب میدهد — را به APIهای REST متصل میکنند. در بسیاری از سیستمها، وضعیت 404 یک «کرش» یا خرابی نیست، بلکه یک پاسخ مشروع است؛ به این معنا که یک نقطه داده خاص صرفاً منتشر نشده است. وقتی یک عامل (Agent) مسیری را حدس میزند که وجود ندارد، در واقع شکست نخورده است، بلکه در حال کاوش (Probing) در محیط است.
برای درک بهتر، یک داشبورد دیجیتال برای کشتی را تصور کنید. اگر شناور فاقد قطبنمای الکترونیکی باشد، درخواست برای «جهت مغناطیسی» خطای 404 برمیگرداند. این یک وضعیت واقعی از دنیای فیزیکی است، نه یک خطای سرور. اما اکثر کلاینتهای HTTP سادهانگار از متدی به نام raise_for_status() استفاده میکنند که هر کد غیر از 2xx را به عنوان یک استثنای (Exception) حیاتی تلقی کرده و برنامه را متوقف میکند.
جزئیات فنی: کالبدشکافی یک شکست
این اختلال در یک زنجیره معماری خاص رخ داده است:
- مدل: یک مدل محلی با حدود ۳۰ میلیارد پارامتر.
- محیط اجرا: یک رانتایم عامل که فراخوانی ابزارها را بهصورت موازی (Fan-out) ارسال میکند.
- ابزار: یک سرور پروتکل زمینه مدل (MCP) که به عنوان یک پوشش (Wrapper) برای یک HTTP API عمل میکند.
- منبع داده بالادستی: SignalK، که یک سرور دادههای دریایی است.
در این سناریو، ابزار مورد استفاده read_sensor(path) است. اگرچه این مورد خاص به حوزه دریایی مربوط است، اما مشکل در هر ابزاری که یک API با HTTP را پوشش میدهد و در آن نبودِ یک کلید (Key) منجر به بازگشت کد 404 میشود، جهانی و مشترک است. این نوع عدم پایداری در تعامل با ابزارها مشابه مواردی است که خطاهای پنهان در طرحوارههای JSON باعث غیرفعال شدن ابزارهای MCP در کلود میشوند و عیبیابی را دشوار میکنند. علائم این مشکل در متن گفتگوهای عامل (Transcript) ظاهر شد: کاربر از عامل درباره سرعت و مسیر کشتی پرسید. عامل بهدرستی گزارش داد: «سرعت روی زمین ۶.۱ گره است»، اما سپس ادعا کرد «مسیر در حال حاضر در دسترس نیست».
نکته حیاتی این است که دادهها در واقع در دسترس بودند. سرور بالادستی در تمام مدت مقدار navigation.courseOverGroundTrue = 205° را ارائه میداد و این موضوع در مرورگر دادههای خود سرور قابل مشاهده بود. شکست عامل بهصورت متناوب رخ میداد و پس از ریاستارت کردن جلسه (Session)، مشکل بهطور خودکار حل میشد؛ و این دقیقاً سختترین نوع باگ برای تشخیص و عیبیابی است.
تلهی قطعکننده (Circuit Breaker)
محیطهای اجرای عامل معمولاً از یک «قطعکننده» استفاده میکنند تا از ایجاد حلقههای بینهایت و فشار بیش از حد (Hammering) به سرور جلوگیری کنند. طبق این الگوی شناختهشده در MCP، اگر ابزاری N بار متوالی شکست بخورد، قطعکننده «باز» (Open) شده و رانتایم ارسال هرگونه درخواست به آن ابزار را برای یک دوره استراحت (Cooldown) بهطور کامل متوقف میکند.
در مورد گزارش شده، آستانه شکست روی عدد N = 3 تنظیم شده بود. پویایی سیستم به این شکل پیش رفت:
- محرک: مدل ۳۰ میلیارد پارامتری مسیرهای دقیق را نمیدانست و شروع به حدس زدن کرد. او درخواستهای موازی متعددی مانند
read_sensor("navigation.headingTrue")،read_sensor("navigation.headingMagnetic")وread_sensor("sensors.depth")را ارسال کرد. - نتیجه: هر سه حدس با خطای 404 پاسخ داده شدند (چون کشتی یا قطبنما نداشت یا فضای نام (Namespace) اشتباه بود).
- استثنا: کلاینت سادهانگار — که تقریباً همه توسعهدهندگان در ابتدا به همین شکل مینویسند — ساختاری شبیه به این داشت:
async def get_value(self, path: str) -> dict:url = f"{self.base_url}/api/vessels/self/{path.replace('.', '/')}"resp = await self._http.get(url)resp.raise_for_status() # <-- هر کد غیر از 2xx تبدیل به استثنا میشودreturn resp.json() - قطع ارتباط: این سه خطای 404 در واقع سه استثنای
HTTPStatusErrorبودند که رانتایم آنها را به عنوان سه شکست متوالی ابزار ثبت کرد.
قطعی نامریی
به دلیل اینکه آستانه دقیقاً روی عدد ۳ بود، قطعکننده فوراً فعال شد. درخواست چهارم که کاملاً درست بود و مسیر واقعی داده (navigation.courseOverGroundTrue) را هدف گرفته بود، درست پشت سر حدسهای اشتباه در صف قرار داشت. اما چون قطعکننده اکنون «باز» بود، رانتایم بدون اینکه حتی تلاشی برای تماس با سرور کند، پیام فوری «ابزار در دسترس نیست» را بازگرداند.
این وضعیت یک «قطعی شبحوار» (Phantom Outage) ایجاد میکند. از دید کاربر، عامل ادعا میکند دادهها در دسترس نیستند. اما از دید توسعهدهندهای که لاگهای سرور را بررسی میکند، سرور کاملاً سالم است و دادهها را ارسال میکند. در لاگهای دسترسی بالادستی، سرور با خوشحالی به درخواستها تا آخرین خطای 404 پاسخ داده و سپس هیچ درخواستی دریافت نمیکند؛ زیرا درخواستهای بعدی هرگز از سمت کلاینت خارج نشدند چون قطعکننده در سمت کلاینت باز شده بود. حدسهای اشتباه مدل «محرک» بودند، اما استفاده از raise_for_status() روی کد 404 بود که این اتفاق را «کشنده» کرد.
تلاشهای شکستخورده برای اصلاح
نویسنده مقاله در dev.to چهار روش رایج اما نادرست را بررسی کرد که توسعهدهندگان معمولاً برای رفع این مشکل به سراغ آنها میروند:
۱. استثناهای کلی (Blanket Exceptions): استفاده بیقاعده از raise_for_status() ریشه اصلی باگ است. این کار هیچ تفاوتی بین «منبع وجود ندارد» (404) و «سرور خراب است» (500) قائل نمیشود. در نتیجه هر مسیر حدسزده شده به یک شکست ابزار تبدیل میشود. در واقع قرارداد سیستم از همان ابتدا غلط است: سیستم «نبودِ داده» را به عنوان «شکست» گزارش میکند.
۲. بالا بردن آستانه خطا: افزایش حد شکست از ۳ به ۸ (same_tool_failure: 8) منطق سیستم را اصلاح نمیکند، بلکه فقط «قیمت ورود» را بالا میبرد. یک مدل پرحرفتر یا پرسشی که منجر به حدسهای بیشتری شود، در نهایت باز هم به عدد ۸ خواهد رسید. علاوه بر این، هدف واقعی قطعکننده را کند میکند؛ زیرا وقتی سرور بالادستی واقعاً از دسترس خارج شود، رانتایم باید ۸ تماس شکستخورده را تحمل کند تا بتواند از خود محافظت کند.
۳. تلاش مجدد (Retrying Calls): قرار دادن فراخوانی در یک حلقه تکرار (مثلاً سه تلاش با وقفهای معادل 0.2 * attempt) ابزار اشتباهی برای خطاهای 404 است. مسیری که در میلیثانیه صفر منتشر نشده است، در میلیثانیه ۶۰۰ هم منتشر نخواهد شد. این کار سه خطای 404 سریع را به نه خطای 404 کند تبدیل میکند که باعث باز شدن سریعتر قطعکننده و افزایش تأخیر (Latency) میشود. تکرار درخواستها برای Time-outها و خطاهای 5xx است، نه برای منابع موجود نیست.
۴. مهندسی پرامپت: دادن راهنماییهای دقیق درباره مسیرها به مدل (مثلاً «وقتی مسیر پرسیده شد، از navigation.courseOverGroundTrue بخوان») حدس زدن را کاهش میدهد اما همچنان شکننده است. چنین راهنماییهایی وابسته به مدل هستند و ممکن است توسط نسخههای جدیدتر مدل نادیده گرفته شوند. ایمنی قطعکننده باید در لایه قطعی (Deterministic) ابزار باشد، نه در یک پرامپت.
راهکار قطعی: تفکیک نبود از خرابی
راه حل درست این است که «نبودِ داده» از «خرابی» در مرز HTTP جدا شود. توسعهدهنده کدی را پیاده کرد که پیش از ایجاد استثنا، وضعیت 404 را بررسی میکند. اگر 404 رخ دهد، ابزار اکنون یک دیکشنری با مقادیر تهی برمیگرداند:
async def get_value(self, path: str) -> dict:
"""Fetch a value object from the upstream API. A 404 means the upstream simply doesn't publish that path — a normal "not available" result, not a failure. We return a null-valued dict rather than raising, so missing/guessed paths don't register as tool failures (which can trip the client's consecutive-failure circuit breaker). Any other HTTP error (5xx, etc.) is a real fault and still raises. """
url = f"{self.base_url}/api/vessels/self/{path.replace('.', '/')}"
resp = await self._http.get(url)
if resp.status_code == 404:
return {"value": None, "timestamp": None}
resp.raise_for_status() # 5xx / other faults still raise — breaker still works
return resp.json()
با بازگرداندن یک نتیجه به جای ایجاد خطا، کد 404 دیگر در شمارنده شکستهای قطعکننده محاسبه نمیشود. اما خطاهای واقعی — مانند خطاهای 5xx، رد درخواست اتصال (Connection Refusal) یا Time-outها — همچنان استثنا ایجاد میکنند و تضمین میکنند که قطعکننده همچنان از سیستم در برابر کرشهای واقعی سرور محافظت میکند. همین منطق را میتوان برای سایر نقاط انتهایی (Endpoints) مانند پیمایش درخت برای کشف مسیر (Tree-walks) به کار برد: اگر get_subtree با 404 مواجه شد، باید یک دیکشنری خالی {} برگرداند.
این رویکرد یک مفهوم ثابت از «عدم دسترسی» برای استدلال عامل فراهم میکند. چه مقدار موجود باشد اما تهی باشد (مثلاً کشتی لنگر انداخته و مسیری ندارد) و چه مسیر کاملاً غایب باشد (404)، عامل مقدار value=None را میبیند. این کار هزینه «طوفان حدس زدن» مدل را از یک قطعی سیستمی به یک بازگشت بیضررِ مقدار تهی تبدیل میکند.
چرا این موضوع اهمیت دارد و نکات نهایی
این الگو به هر عامل یا ابزار MCP که روی یک HTTP API قرار دارد و در آن «غایب بودن» یک نتیجه مشروع است، تعمیم مییابد. نکات کلیدی عبارتند از:
- تمایز بین نبود و خرابی: کدهای 404 و 200های خالی، «پاسخ» هستند؛ اما 5xx و Time-outها «شکست» هستند.
- قطعکنندهها ضریب تقویتکننده هستند: قطعکنندهها خطاهای اشتباه برچسبگذاری شده را تقویت میکنند. یک پاسخ عادی که بهاشتباه طبقهبندی شده باشد، میتواند چند حدس ساده را به یک پنجره توقف کامل برای تمام فراخوانیهای بعدی تبدیل کند.
- فرض بر حدس زدن مدل: مدلهای محلی کلیدهای اشتباه را جستجو میکنند. «نبودِ داده» را به عنوان یک نتیجه درجهاول و غیر شکستخورده در نظر بگیرید تا حدس زدن «ارزان» شود.
- تست تکخطی: هنگام تصمیمگیری در مورد اینکه آیا یک کد وضعیت باید استثنا ایجاد کند یا خیر، بپرسید: «آیا یک سیستم سالم که بهدرستی استفاده شده، هرگز این کد را برمیگرداند؟» برای یک منبع گمشده، پاسخ «بله» است، پس نباید استثنا ایجاد کند.
برای کسانی که لایههای عملیاتی هوش مصنوعی (AI Ops) را میسازند، مانند ابزار متنباز signalk-mcp که در این پروژه کاتماران چارتر تمام-الکتریک استفاده شد، این الگو حیاتی است. این ابزار در github.com/sailingnaturali/signalk-mcp در دسترس است. هر ابزاری که یک API را پوشش میدهد و در آن نبودِ یک کلید وضعیتی معتبر است، باید از استثناهای کلی 404 اجتناب کند تا پایداری عامل حفظ شود.
گام بعدی شما
- اگر از MCP یا هر ابزاری برای اتصال LLM به API استفاده میکنید، متد
raise_for_status()را حذف کرده و برای کدهای ۴۰۴ مدیریت دستی بنویسید. - آستانه قطعکننده (Circuit Breaker) را با توجه به نرخ حدس زنی مدل خود بهینهسازی کنید.
- در لایه ابزار، تفاوت بین
Value=None(داده موجود نیست) وError(ارتباط قطع است) را به مدل منتقل کنید تا بتواند استدلال بهتری کند.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell مراجعه کنید.




گفتگو