سرور MCP من قبلاً خیلی ساده از کار میافتاد. بدون هیچ گزارش خطای سیستم (crash dump). بدون هیچ ردپای پشتهای (stack trace) در لاگها. کلاینتها بدون هیچ شکایتی متصل میشدند، اما بعد از چند ساعت، کل سیستم ساکت میشد. درخواستها ناپدید میشدند و عامل هوش مصنوعی در سمت دیگر، چیزی جز فضای خالی دریافت نمیکرد.
این یک داستان بهشدت رایج و کلافهکننده در اکوسیستم پروتکل زمینه مدل (MCP) است. این پروتکل تعیین میکند که عاملهای هوش مصنوعی چگونه ابزارهای خارجی را کشف و فراخوانی کنند، اما در مشخصات فنی فرض بر این است که شما خودتان خطاها را مدیریت خواهید کرد. بیشتر آموزشها و پیادهسازیهای اولیه از این بخش عبور میکنند. آنها بر «مسیر ایدهآل» (happy path) تمرکز میکنند: یک تابع را مشخص کنید، آن را از طریق سرور ارائه دهید و یک نتیجه تمیز برگردانید. آنها بهندرت نشان میدهند که وقتی یک اختلال لحظهای در شبکه به API خارجی شما برخورد میکند، یا وقتی مدل نام یک پارامتر را توهم میزند و ورودی نامعتبر میفرستد، چه اتفاقی میافتد. نتیجه، یک سرور شکننده است که سالم به نظر میرسد اما در واقع ساعتهاست که از کار افتاده است.
چرا پاسخهای خالی بدتر از کرش کردن هستند
وقتی یک استثنای (exception) مدیریتنشده در هندلرِ ابزار MCP نشت میکند، لایه انتقال (transport layer) اغلب آن را میبلعد. فرآیند سرور زنده میماند، سوکت باز میماند، اما کلاینت یک پاسخ خالی دریافت میکند. این وضعیت خطرناکتر از یک کرشِ پرسرصدا است، زیرا سیستم مانیتورینگ شما ممکن است متوجه آن نشود. فرآیند همچنان در حال اجراست. پورت همچنان در حال گوش دادن است. با این حال، هر فراخوانیِ ابزار، هیچ چیزی بر نمیگرداند.
مدل هوش مصنوعی سکوت را به عنوان شکست تفسیر نمیکند. بلکه سکوت را به عنوان یک فراخوانی موفقیتآمیز که هیچ دادهای تولید نکرده، تفسیر میکند. آن پاسخ خالی باعث میشود مدل یاد بگیرد که بداههپردازی کند. مدل شروع به توهم زدن درباره واقعیتها میکند تا جای خالی را پر کند، یا وارد حلقهای از تلاش مجدد برای همان فراخوانی خراب میشود. مشکلات کوچکی مانند اتمام زمان شبکه (timeout) یا آرگومان نامعتبر برای ابزار، هرگز نباید اجازه داده شوند که باعث چنین رفتاری شوند.
الگوی Wrapper: سه خط دفاعی
من این مشکل را با قرار دادن هر هندلرِ ابزار در یک لایه نازکِ بازیابی خطا (error-recovery layer) حل کردم. این Wrapper سعی نمیکند هر شکست احتمالی را پیشبینی کند؛ بلکه آنها را دستهبندی کرده و بر همان اساس پاسخ میدهد.
ConnectionError و TimeoutError
این خطاها زمانی رخ میدهند که سرور شما با یک API خارجی صحبت میکند و شبکه دچار نوسان میشود. راه حل غریزی، راهاندازی مجدد کل فرآیند سرور MCP است. این کار را نکنید. ریبوت کردن باعث قطع اتصالات فعال کلاینت، پاک شدن هرگونه وضعیت در حافظه (in-memory state) و اجبار به بازراهاندازی کامل میشود. در عوض، خطای اتصال را بگیرید و فقط لایه انتقال یا کلاینت HTTP را که ابزار شما از آن استفاده میکند، دوباره متصل کنید. سرور گرم و بلافاصله برای درخواست بعدی آماده میماند.
ValueError
این چیزی است که وقتی کلاینت هوش مصنوعی آرگومانهای بدشکل (malformed) میفرستد، مشاهده میکنید. شاید مدل یک پارامتر را از خودش درآورده باشد، یک رشته (string) را در جایی که عدد صحیح (integer) لازم بوده فرستاده باشد، یا یک فیلد اجباری را فراموش کرده باشد. اگر اجازه دهید این خطا بدون مدیریت بالا برود، کلاینت یا با کرش مواجه میشود یا یک پاسخ خالی دریافت میکند. آن را داخل Wrapper بگیرید، سپس یک پیام واضح و مشخص بسازید که دقیقاً به مدل بگوید چه چیزی اشتباه بوده است. توضیح دهید کدام پارامتر با خطا مواجه شده و چه چیزی انتظار میرفته است. اکثر مدلهای مدرن هوش مصنوعی آن پیام را میخوانند و در همان مرحله بعد، خود را اصلاح میکنند. یک خطای مبهم، یک چرخه استدلال را هدر میدهد؛ اما یک خطای دقیق، مشکل را بلافاصله حل میکند.
General Exceptions
یک شبکه ایمنی داشته باشید. اگر خطایی خارج از دستههای بالا رخ داد، جزئیات را برای خودتان لاگ کنید و یک پاسخ شکستِ عمومی و تمیز به کلاینت برگردانید. این کار از اینکه یک مورد خاص و عجیب، کل نشست (session) را برای همه از کار بیندازد، جلوگیری میکند. سرور زنده میماند، کلاینت سیگنالی دریافت میکند که چیزی با شکست مواجه شده است، و شما بافت (context) کافی را در لاگهای خود برای عیبیابی در آینده نگه میدارید.
پرچم isError غیرقابل مذاکره است
این جزئیاتی است که در واقع تعیین میکند آیا راه حل شما کار میکند یا خیر. پاسخهای MCP شامل یک فیلد بولین isError هستند. اگر استثنایی رخ دهد و شما یک پیام خطا برگردانید بدون اینکه isError را روی true تنظیم کنید، کلاینت آن متن خطا را به عنوان یک نتیجه موفق از ابزار در نظر میگیرد.
تصور کنید API خارجی شما به محدودیت نرخ درخواست (rate limit) برخورد میکند. شما استثنا را میگیرید و رشته "API rate limit exceeded" را برمیگردانید اما isError را روی false باقی میگذارید. کلاینت آن رشته را طوری به پنجره زمینه (context window) مدل میفرستد که انگار خروجی واقعی ابزار است. سپس مدل سعی میکند بر اساس آن متن استدلال کند، گویی که آن متن یک داده است. مدل ممکن است آن خطا را در یک خلاصه نقل قول کند، یا بدتر از آن، ممکن است بین آن متن خطا و حقایق دیگر، روابطی را توهم بزند. شما یک مشکل زیرساختی موقت را به منبعی از اطلاعات نادرست تبدیل کردهاید.
همیشه وقتی یک پِیلود (payload) خطا برمیگردانید، isError را برابر با true قرار دهید. این کار سیگنال واضحی به کلاینت میدهد که فراخوانی ابزار (tool call) با شکست مواجه شده است، که به مدل اجازه میدهد تصمیم بگیرد آیا دوباره تلاش کند، درخواست شفافسازی کند یا کلاً ابزار دیگری را امتحان کند.
بدانید چه چیزی را باید مدیریت (catch) کنید و چه چیزی را باید متوقف (kill) کنید
کل سرور خود را در یک بلوک try-catch کورکورانه که همه چیز را میبلعد، محصور نکنید. برخی خطاها به این معنا هستند که سرور باید فوراً متوقف شود. اگر یک متغیر محیطی (environment variable) ضروری در هنگام راهاندازی موجود نباشد، یا فایل پیکربندی شما خراب باشد، هیچ میزان مدیریت خطا در سطح درخواست (request-level catching) کمکی نخواهد کرد. برای خطاهای مهلک (fatal errors) از این دست، یک کلاس استثنای (exception class) اختصاصی ایجاد کنید و اجازه دهید فرآیند (process) متوقف شود.
قانون ساده است. اگر خطا موقتی است یا فقط به یک درخواست خاص محدود میشود، آن را مدیریت کرده و بازیابی کنید. اگر خطا به این معناست که هر درخواست بعدی قطعاً با شکست مواجه خواهد شد، اجازه دهید سرور با صدای بلند (به صورت واضح) از کار بیفتد. یک شکست سریع در هنگام راهاندازی، بینهایت بهتر از سروری است که روزها در وضعیتی خراب به سختی به کار خود ادامه میدهد.
پیش از آنکه به آن نیاز پیدا کنید، قابلیت مشاهدهپذیری (Observability) را اضافه کنید
هنگامی که این پوششدهنده (wrapper) را پیادهسازی کردید، آن را با لاگگذاری ساختاریافته (structured logging) ترکیب کنید. هر فراخوانی ابزار و نتیجه آن را در قالب JSON ثبت کنید. نام ابزار، آرگومانهای خام، تأخیر (latency) و اینکه آیا با موفقیت انجام شده، با شکست مواجه شده یا دوباره تلاش شده است را نیز شامل شود.
این نظم به سرعت نتیجه میدهد. وقتی متوجه افزایش ناگهانی خطاها میشوید، میتوانید بر اساس ابزار فیلتر کنید و در عرض چند دقیقه الگوها را شناسایی کنید. شاید یک API خارجی خاص، هر روز در یک زمان مشخص با خطای تایماوت (timeout) مواجه میشود که نشاندهنده یک بازه تعمیر و نگهداری برنامهریزیشده است که از آن بیخبر بودید. شاید یک ابزار بهطور مداوم آرگومانهای بدشکل (malformed) دریافت میکند که نشاندهنده یک نقص در مهندسی پرامپت (prompt engineering) در مراحل بالادستی است. لاگهای متنی ساده که در میان ردپای خطاها (stack traces) دفن شدهاند، این کار کارآگاهی را دردناک میکنند؛ اما JSON ساختاریافته آن را بسیار ساده میکند.
نتیجه در محیط عملیاتی (Production)
من این الگوی پوششدهنده (wrapper pattern) را در سه هفته گذشته روی دو سرور MCP در محیط عملیاتی اجرا کردهام. در این بازه زمانی، هیچ شکست خاموشی (silent failure) مشاهده نکردهام. قبل از اضافه کردن این پوششدهنده، بهطور متوسط روزانه تقریباً یک شکست توضیحناپذیر داشتم. این الگو پیچیده نیست، اما تأثیر آن بسیار زیاد است زیرا نویزهای قابل تحمل را از مشکلات واقعی جدا میکند.
شکستهای خاموش هزینهای بیشتر از کرش کردن (crash) دارند. یک کرش، سیستم هشداردهنده شما را فعال میکند، اما سکوت فقط اعتماد را از بین میبرد. یک روز عامل هوش مصنوعی شما دادههای مفیدی از ابزار برمیگرداند و روز بعد شروع به ساختن اطلاعات از خودش میکند، چون سرور از ساعتها قبل دیگر پاسخگو نیست. الگوی پوششدهنده این شکاف را پر میکند. این الگو سرور شما را در میان تلاطمهای جزئی فعال نگه میدارد، بافت (context) کافی برای اصلاح اشتباهات به مدل میدهد و تضمین میکند که وقتی اتفاق واقعاً مهلکی میافتد، بلافاصله مطلع شوید.
اگر امروز در حال ساخت ابزارهای MCP هستید، با این پوششدهنده و پرچم isError شروع کنید. بقیه موارد صرفاً مرتبسازی و پاکسازی است.
