تتمتع حلقات استدعاء الأدوات (tool-calling loops) الخاصة بـ Claude بسمعة في توليد أكواد promise متشابكة في Node.js.
تتيح ميزة Promise.withResolvers() الجديدة في Node.js 22 للمطورين استبدال نمط new Promise المثقل بالأكواد المتكررة (boilerplate) بسطر واحد يمنحك الـ promise ودالتي الـ resolve والـ reject الخاصتين به. والنتيجة هي عدد أقل من استدعاءات resolve المنسية، وعدم وجود تحذيرات "الرفض المزدوج" (double-reject)، وتدفق تحكم أكثر تسطحاً يسهل اختباره والحفاظ على استمراريته في بيئات الـ serverless.
لماذا يكسر النمط القديم تدفق العمل
عندما يطلب LLM مثل Claude أداة ما، يبدو تنفيذ Node التقليدي بهذا الشكل:
return new Promise((resolve, reject) => {
// launch the tool, attach callbacks, maybe fire another async call
});
تظهر ثلاث عثرات متكررة:
- نسيان الـ resolve – إذا لم يقم مسار الكود باستدعاء
resolveأبداً، فإن وظيفة Lambda أو أي معالج serverless آخر سيتعطل حتى تنتهي مهلة الانتظار (timeout)، مما يؤدي إلى زيادة التكلفة. - الرفض المزدوج (Double reject) – مسار الخطأ الذي يستدعي
rejectمرتين يؤدي إلى ظهور تحذيرات "رفض غير معالج" (unhandled rejection) والتي يمكن أن تتسبب في توقف العملية في وضع التشغيل الصارم (strict mode). - التداخل العميق (Deep nesting) – كل خطوة غير متزامنة (async) تضع callback أخرى داخل المُنشئ (constructor)، مما يشتت المنطق ويجعل اختبارات الوحدة (unit tests) هشة.
تنبع كل هذه المشكلات من حقيقة أن دوال التحكم في الـ promise محبوسة داخل نطاق (closure) المُنشئ، مما يضطر بقية الكود للرجوع إليها من الداخل.
Promise.withResolvers() في سطر واحد
أضاف Node 22 مساعداً ثابتاً (static helper) يعيد كائناً يحتوي على promise والدالتين اللتين تقومان بتسويته (settle it):
const { promise, resolve, reject } = Promise.withResolvers();
الآن يمكن تسليم الـ promise إلى أي جزء من النظام — سواء كان معالج HTTP، أو مستمع لقاعدة بيانات، أو عامل خلفية (background worker) — بينما يقوم المستدعي الأصلي ببساطة بانتظار (await) الـ promise. لا حاجة لتغليف كتلة تنفيذ الأداة بالكامل داخل مُنشئ new Promise.
تطبيق ذلك على حلقة أدوات Claude
سير عمل Claude هو:
- يُصدر الـ LLM طلباً لاستخدام أداة.
- يقوم الكود الخاص بك بتشغيل الأداة (على سبيل المثال، استدعاء API، أو قراءة ملف).
- يتم إرسال نتيجة الأداة مرة أخرى إلى Claude للدورة التالية.
باستخدام withResolvers يتقلص شكل الحلقة إلى:
async function runTool(request) {
const { promise, resolve, reject } = Promise.withResolvers();
// Kick off the tool; it can call resolve/reject from anywhere
executeTool(request, { resolve, reject });
// Optional timeout wrapper
const timeout = setTimeout(() => reject(new Error('Tool timed out')), 10_000);
try {
const result = await promise;
clearTimeout(timeout);
return result; // feed back to Claude
} finally {
// clean-up if needed
}
}
لم يعد تنفيذ الأداة بحاجة إلى التغليف في promise جديد؛ بل يستلم فقط resolve و reject. وهذا يقضي على حالات الفشل الثلاث المذكورة أعلاه.
عناصر التحكم في بيئة الإنتاج التي لا تزال مهمة
حتى مع وجود شكل أنظف للـ promise، تواجه الوكلاء (agents) في العالم الحقيقي قيوداً أخرى:
- مهلات الانتظار (Timeouts) – يوضح المقتطف أعلاه مؤقتاً بسيطاً يقوم بعمل
rejectإذا تجاوزت الأداة حداً معيناً. قم بضبط المدة بناءً على توقعات اتفاقية مستوى الخدمة (SLA). - التقنين (Throttling) – عندما تعيد الخدمة الأساسية خطأ تقنين (مثل
ThrottlingExceptionفي Bedrock)، قم بالإمساك بالخطأ، ثم انتظر، وأعد المحاولة باستخدام تراجع أسي (exponential back-off). يظل زوجresolve/rejectكما هو؛ فقط منطق إعادة المحاولة هو ما يتغير. - تكلفة Lambda – في AWS Lambda، قم بتعيين
callbackWaitsForEmptyEventLoop = false. هذا يخبر وقت التشغيل بإنهاء الوظيفة بمجرد إرجاع المعالج، حتى لو كانت التدفقات (streams) أو المعالجات الخلفية الأخرى لا تزال مفتوحة. هذا يمنع الوظيفة من البقاء عالقة بينما يتم تسوية الـpromiseفي مكان آخر.
متى لا يكون المساعد الجديد حلاً سحرياً
تتوفر Promise.withResolvers() فقط في Node 22 والإصدارات الأحدث. يجب على المشاريع المقيدة بإصدارات LTS الأقدم إما استخدام polyfill لهذا النمط أو الالتزام بالمُنشئ الكلاسيكي. يمكن للـ polyfills محاكاة الـ API ولكنها لن تحصل على فوائد الأداء الأصلية. علاوة على ذلك، لا يحل المساعد المشكلات المنطقية بشكل سحري: لا يزال يتعين على المطورين التأكد من استدعاء واحدة فقط من resolve أو reject لكل طلب، وإلا سيظل الـ promise معلقاً (pending) إلى أجل غير مسمى.
ما يجب مراقبته لاحقاً
- تبني أطر العمل – بدأت المكتبات التي تجرد حلقات وكلاء LLM (مثل أغلفة Claude مفتوحة المصدر) في إتاحة
withResolversكميزة اختيارية. راقب التحديثات التي تجعل هذا النمط هو الافتراضي. - نظام Node البيئي – مع انتقال المزيد من الخدمات إلى Node 22، سيصبح هذا المساعد معياراً فعلياً لأي نمط async من نوع "أطلق وانتظر" (fire-and-wait)، وليس فقط لوكلاء LLM.
- معايير استدعاء الأدوات – قد تفرض المواصفات الناشئة لاستدعاءات أدوات LLM عقداً يعتمد على "promise واحد"، وهو ما يتوافق تماماً مع نهج
withResolvers.
الخلاصة: من خلال استبدال غلاف new Promise المطول بسطر واحد Promise.withResolvers()، تكتسب الوكلاء القائمة على Claude تدفقاً أوضح، ومفاجآت أقل في وقت التشغيل، وتحكماً أدق في تكاليف الـ serverless — بشرط أن يدعم وقت التشغيل Node 22.
