الـ المقال السابق تناول كيفية جعل وكيل واحد موثوقًا: حلقة التشغيل، وتوجيه الأدوات، وضغط السياق، والجلسات التي تصمد أمام الأعطال. تجيب تلك الطبقة عن سؤال «كيف يُنجز وكيل واحد مهمة من دون أن يتعطل؟». أما هذا المقال فيتناول الطبقة التي تعلوها: ما الذي يحدث عندما لا يكفي وكيل واحد، ويجب تقسيم العمل على فريق.
هذا ما يقصده الناس عادةً بعبارة تنسيق الوكلاء المتعددين: يقسّم وكيل قائد يتولى المحادثة الطلب إلى أجزاء، ويسلّم كل جزء إلى وكيل فرعي متخصص، وينتظر انتهاء الوكلاء المعنيين قبل بدء الخطوة التالية، ويمرر النتائج إلى المراحل اللاحقة، ويحافظ على سير العملية عند فشل خطوة. يشغّل Orkas هذا كله على جهاز المستخدم نفسه. وفيما يلي كيفية بناء طبقة التنسيق هذه؛ نُقّحت الشيفرة وعُمّمت، لكن البنية حقيقية.
الوكيل القائد والوكلاء الفرعيون
التصور هو فريق صغير بسلسلة قيادة واضحة. يتولى وكيل قائد (نسميه القائد) المحادثة والسياق العام. لا ينجز كل العمل بنفسه؛ بل تتمثل مهمته في تحديد ما الذي يجب إنجازه، وبأي ترتيب، وعلى يد من. أما الوكلاء الفرعيون فهم مختصون، ولكل منهم إعداداته الخاصة من تعليمات النظام والأدوات المسموح بها والمهارات. يتقن الوكيل الفرعي جزءًا محددًا من العمل، ويُستدعى عندما يحين دور ذلك الجزء.
هناك أمران يجعلان هذا أكثر من مجرد مصطلح رائج. أولًا، الوكلاء الفرعيون والمهارات وحدات أساسية قائمة بذاتها، وليست حيلًا في صياغة التعليمات: الوكيل الفرعي وكيل حقيقي له إعداد مستقل، وإسناد العمل إليه تسليم فعلي بسياقه الخاص. ثانيًا، لا يُترك التنسيق لحسن نوايا النموذج؛ بل تقوده وثيقة صريحة يضمن النظام، لا النموذج، مطابقتها للواقع. وهذه الوثيقة هي الخطة.
الخطة رسم بياني، لا نص متتابع
عندما يقرر الوكيل القائد أن الطلب يحتاج إلى أكثر من خطوة، يكتب خطة. الخطة ليست نصًا حرًا ولا قائمة تحقق خطية، بل رسمًا بيانيًا صغيرًا للاعتماديات، موجّهًا وخاليًا من الدورات (DAG). كل عقدة خطوة، وتحمل الخطوة كل ما يحتاج إليه المنسّق لإسنادها:
interface PlanStep {
index: number; // 1-based, stable, never renumbered
title: string; // human-readable, shown in the UI
assignee: string; // who runs it: "user" | "commander" | a sub-agent
input?: string; // the dispatch payload — a template (see below)
wait_for?: number[]; // upstream step indexes; defaults to [index - 1]
on_failure?: "abort_plan" | "continue" | "ask_commander";
// --- runtime state, owned by the orchestrator, NOT written by the model ---
status: "pending" | "in_progress" | "done" | "failed" | "skipped" | "blocked";
output_summary?: string; // short summary of what the step produced
output_files?: string[]; // files the step produced
failure_reason?: string;
}الـ wait_for هو الحقل الذي يحوّل القائمة إلى رسم بياني. افتراضيًا، تنتظر الخطوة سابقتها، في سلسلة بسيطة، لكن يمكنها أن تصرّح باعتمادها على عدة خطوات سابقة؛ فقد تنتظر خطوة «التلخيص» كلًا من «بحث السوق» و«استطلاع المنافسين». هذا شكل مُعيَّن، لا خط، ويتعامل معه المنسّق على هذا الأساس.
من يملك الحقيقة: المنسّق، لا النموذج
انظر مجددًا إلى التقسيم في تلك البنية. يملأ النموذج النية — العناوين والمكلّفين والمدخلات والاعتماديات — عند كتابة الخطة أول مرة. لكن كل ما يتعلق بـ حالة التنفيذ — status, output_summary, failure_reason — يخضع حصريًا للمنسّق. يقترح النموذج الخطة مرة واحدة؛ ولا يُسمح له أبدًا بوضع علامة «مكتملة» على خطواته بنفسه.
هذا الفصل مقصود، وهو أهم قرار في التصميم كله. يستطيع نموذج لغوي أن يعلن بكل ارتياح «اكتملت الخطوة 3» بينما أخفقت الخطوة 3، أو أن يفقد تتبّع الخطوات التي لم تُنجز بعد في منتصف محادثة طويلة. لو كانت الحالة محفوظة في ذهن النموذج، لانحرفت الخطة عن الواقع. وبجعل الحالة سجلًا منظّمًا لا يكتب فيه إلا المنفّذ، ولا يكتب إلا استجابةً لانتهاء خطوة فعلًا، تظل الخطة مرآة دقيقة لما حدث حقًا. يحدد النموذج شكل العمل؛ وتحدد بيئة التشغيل الحقائق المتعلقة بتقدّمه.
إسناد الخطوات الجاهزة
بمجرد وجود خطة، يدفعها محرك صغير، هو المنفّذ، إلى الأمام. العملية الأساسية هي «العثور على الخطوات الجاهزة وإسنادها». تكون الخطوة جاهزة عندما تظل في حالة pending وتكون كل خطوة تنتظرها قد بلغت حالة نهائية تُعد ناجحة بما يكفي:
function findReadySteps(plan): PlanStep[] {
return plan.steps.filter((s) => {
if (s.status !== "pending") return false;
const deps = s.wait_for ?? (s.index > 1 ? [s.index - 1] : []);
return deps.every((d) => isTerminal(plan.step(d).status)); // done or skipped
});
}لا يعمل هذا بمؤقّت، بل استجابةً للأحداث. كلما انتهت خطوة، سواء عاد وكيل فرعي بالنتيجة أو أنهى القائد جولة دمج النتائج، يجري المنفّذ مطابقة للحالة: يسجّل حصيلة الخطوة التي انتهت للتو، ثم يعيد الفحص بحثًا عما أصبح جاهزًا نتيجةً لذلك ويسنده. التنسيق هو حلقة مطابقة الحالة هذه، تتكرر حتى لا تبقى أي خطوات.
يمر الإسناد عبر المحادثة نفسها، لا قناة جانبية
إليك خيارًا يحافظ على صدق النظام: لا يستخدم إسناد الخطوة قناة RPC مخفية. بل ينشر رسالة في المحادثة الجماعية نفسها التي يشاهدها المستخدم، من الوكيل القائد، مع الإشارة إلى الوكيل الفرعي بعلامة @. بالنسبة إلى الوكيل الفرعي، لا يختلف إسناد مهمة إليه عن مخاطبته في المحادثة؛ فهو ينفّذ جولته المعتادة فحسب. لا يوجد مسار تنفيذ ثانٍ يجب إبقاؤه متزامنًا مع الأول.
يمكن أن يكون المكلّف واحدًا من ثلاثة أنواع، ولكل منها طريقة إسناد مختلفة قليلًا:
- وكيل فرعي — وهي الحالة الشائعة. يحوّل المنفّذ اسم الوكيل إلى معرّفه وينشر
@<agent> <rendered input>باسم القائد. يتلقاها الوكيل الفرعي وينفّذ جولة وكيل كاملة. - المستخدم — عندما تحتاج خطوة فعلًا إلى مدخلات بشرية، تتحول إلى نموذج وتوقف الخطة مؤقتًا، وسنتناول ذلك لاحقًا. لا تتقدم أي خطوة لاحقة حتى يجيب المستخدم.
- القائد نفسه — لخطوات دمج النتائج أو اتخاذ القرار، مثل «اقرأ كل ما سبق واكتب الملخص». هذا تنبيه داخلي لا ينشر رسالة زائدة يراها المستخدم؛ بل يبدأ الوكيل القائد جولةً والسياق المجمّع بين يديه.
تمرير السياق من خطوة إلى التالية
لا يفيد الفريق إلا إذا انتقل العمل بين أعضائه. والآلية هي قالب input . عندما يكتب الوكيل القائد خطوة، لا يكون المُدخل نصًا ثابتًا؛ إذ يمكنه الإشارة إلى نتائج سابقة، ويستبدل المنفّذ تلك الإشارات بمحتواها عند الإسناد:
// step 3.input, as written by the lead agent
"Using the findings below, draft the launch note.\n\n{{step_1.output_summary}}"
// what the sub-agent actually receives at dispatch time
"Using the findings below, draft the launch note.\n\n- Market is growing ~20% YoY; two incumbents…"لاحظ ما يُمرّر إلى الخطوات اللاحقة: output_summary، أو موجز لكل خطوة مكتملة، لا سجلها الكامل. هذا قرار يتعلق بالميزانية، وينطلق من المنطق نفسه وراء ضغط السياق في إطار التشغيل. لو ورثت كل خطوة لاحقة التاريخ الكامل، رمزًا برمز، لكل ما سبقها، لتضخّم السياق وارتفعت التكلفة بشدة بعد انتقالات قليلة فقط. تُبقي الملخصات كل عملية تسليم قليلة التكلفة، وتحافظ على تركيز كل وكيل فرعي على ما يحتاج إليه فعلًا من الخطوات السابقة، بدلًا من الخوض في كيفية وصولها إلى نتائجها. وتُمرّر أيضًا رسالة المستخدم الأصلية وأي مرفقات، بحيث تظل الخطوة التي تبعد ثلاث انتقالات في السلسلة على علم بالطلب الأصلي.
التتابع افتراضيًا، ولماذا
قد تتوقع أنه عندما تصبح عدة خطوات جاهزة في آن واحد، مثل فرعين في رسم مُعيَّن، يشغّلها المنسّق جميعًا بالتوازي. يمكنه ذلك؛ لكنه اليوم يسند خطوة جاهزة واحدة في كل مرة، وهي الأسبق حسب الفهرس، ويترك البقية تنتظر جولة مطابقة الحالة التالية. تشغيل فريق كامل خطوةً واحدة في كل مرة اختيار متحفظ ومقصود، ومن الجدير توضيح سببه بصراحة.
السبب هو ضمان صحة التنفيذ مع التزامن. تخيّل وكيلين فرعيين ينتهيان في اللحظة نفسها تقريبًا. يطلق كل اكتمال عملية مطابقة للحالة؛ وتقرأ كلتاهما الخطة؛ وترى كلتاهما الخطوة اللاحقة نفسها لا تزال في حالة pending — فتسندانها معًا. وهكذا تُنفّذ الخطوة نفسها مرتين. ولمنع ذلك، تُنفّذ كل دورة قراءة وتعديل وإسناد لمحادثة معيّنة بالتتابع تحت قفل خاص بتلك المحادثة:
// all state-changing paths for one conversation run under one mutex
planLock(uid, cid).runExclusive(async () => {
const plan = await readPlan(uid, cid);
applyOutcomeOfFinishedStep(plan); // mark done / failed / skipped
await dispatchReady(plan); // dispatch the next ready step
});يضمن القفل أن يحدث «تسجيل ما انتهى» و«تحديد التالي» كوحدة واحدة غير قابلة للتجزئة، فلا يمكن إسناد خطوة لاحقة مرتين أبدًا. وبوجود هذا القفل، يصبح إسناد خطوة واحدة في كل مرة أبسط نهج تتضح صحته. ويمكن توسيع هذا الأساس ليشمل توزيعًا متوازيًا حقيقيًا، لكن الأساس هو منفّذ متتابع خالٍ من حالات التسابق، وترتيب الأولويات هذا، الصحة أولًا والسرعة لاحقًا، هو المقصود.
عندما تسوء خطوة
عند التشغيل على جهاز حقيقي والتعامل مع واجهات API خارجية للنماذج، يكون الفشل أمرًا معتادًا، ويصنّفه المنسّق إلى حالات عدة بدلًا من التعامل مع كل خطأ بالطريقة نفسها.
يسأل أولًا عما إذا كان الفشل مجرد حالة عابرة — انقطاع اتصال، أو حد لمعدل الطلبات، أو خلل عابر. إذا كان الأمر كذلك، ولم تكن الخطوة قد استنفدت بالفعل العدد المحدود المخصص لإعادة المحاولة، تُعاد حالة الخطوة بهدوء إلى pending كي تعيد دورة المطابقة التالية إسنادها. (تعمل هذه الآلية فوق محاولات الإعادة الخاصة بإطار التشغيل داخل الجولة نفسها؛ ولا تعيد الخطة إسناد الخطوة إلا بعد استنفاد محاولات الوكيل نفسه، مع حد أقصى صارم يمنع الخطوة المعطلة فعليًا من الدوران إلى الأبد.)
إذا كان الإخفاق فعليًا، فإن سياسة الخطوة المعلنة on_failure تحدد ما يفعله الفريق بعد ذلك:
abort_plan— هذه الخطوة مهمة إلى حد أن كل ما يليها يصبح بلا معنى من دونها. تُوسم بأنها فشلت، ويمتد الأثر إلى بقية الخطوات: تُوسم كل خطوة لا تزال معلقة بالحالةskipped. تتوقف الخطة بصورة سليمة بدلًا من البناء على أساس مفقود.continue— هذه الخطوة اختيارية. تُوسم بالحالةskippedوتُترك الخطوات اللاحقة تتابع كما لو أنها لم تنتج شيئًا فحسب.ask_commander(الخيار الافتراضي) — لا إيقاف أعمى ولا استمرار أعمى. تُوسم الخطوة بأنها فشلت، ويُستدعى الوكيل القائد ليفحص ما حدث ويقرر: إعادة المحاولة بطريقة مختلفة، أو إيجاد مسار بديل، أو التوقف وسؤال المستخدم.
ثمة حالة سادسة تستحق الذكر: blocked. قد يدرك وكيل فرعي أثناء تنفيذ خطوته أنه يحتاج إلى شيء لا يستطيع توفيره إلا المستخدم، فيعرض نموذجًا أو سؤالًا. لا تفشل الخطوة، بل تنتقل إلى الحالة blocked، وتتوقف الخطة بأكملها مؤقتًا. وبمجرد إجابة المستخدم، يجري المنفذ المطابقة ويستأنف الفريق من النقطة نفسها التي توقف عندها. الخطة المتوقفة انتظارًا لمدخلات هي خطة معلقة مؤقتًا، وليست معطلة.
كل خطوة هي تشغيل كامل لوكيل
يجدر هنا ربط الفكرة بالمقال السابق. عندما يسند المنسق خطوة إلى وكيل فرعي، لا ينفذ ذلك الوكيل إجراءً مختزلًا، بل يشغّل حلقة إطار التشغيل كاملة: حلقة التشغيل المتدفقة الخاصة به، واستدعاءات أدواته، ونافذة سياقه مع ضغط المحتوى، وجلسته المحمية من الأعطال. يقع التنسيق بوضوح فوق بيئة تشغيل الوكيل الواحد؛ ولا يتدخل في داخلها مطلقًا. يحدد الوكيل القائد شكل العمل وترتيب تسليمه؛ وما إن يتسلم كل وكيل فرعي حصته حتى يكون وكيلًا كاملًا قائمًا بذاته.
هذا التدرج في الطبقات هو ما يجعل المقالين متكاملين. يجعل إطار التشغيل الوكيل الواحد موثوقًا لمهمة واحدة. ويجمع المنسق عدة وكلاء موثوقين في فريق يستطيع تولي مهمة أكبر أو أكثر تنوعًا من أن يتولاها أي منهم بمفرده.
بعض القرارات التي أحدثت فرقًا
المنسق مسؤول عن حالة التنفيذ، والنموذج مسؤول عن القصد. يقترح النموذج الخطة؛ ولا يوسم الخطوات بأنها مكتملة أو فاشلة أو متجاوزة إلا بيئة التشغيل، وفقط استجابةً لشيء حدث بالفعل. هذا الحد الفاصل هو ما يبقي الخطة مرآة صادقة للواقع بدلًا من أن تكون تخمينًا متفائلًا من النموذج.
إسناد العمل عبر المحادثة نفسها، لا عبر قناة جانبية. الخطوة المسندة ليست سوى رسالة من الوكيل القائد إلى وكيل فرعي. مسار تنفيذ واحد، ولا شيء خفي يمكن أن يخرج عن التزامن، ويستطيع المستخدم مشاهدة الفريق يعمل في سلسلة المحادثة نفسها التي يقرؤها أصلًا.
ملخصات بين الخطوات، لا سجلات محادثة كاملة. تحمل كل عملية تسليم ملخصًا قصيرًا لمخرجات الخطوات السابقة. يحافظ ذلك على استهلاك معقول للسياق عبر السلاسل الطويلة، ويبقي كل وكيل فرعي مركزًا على ما يحتاج إليه، لا على كيفية وصول الوكيل السابق إلى نتيجته.
الصحة قبل التوازي. يجعل قفل خاص بكل محادثة كل دورة قراءة وتعديل وإسناد متسلسلة، وتُسند الخطوات واحدة تلو الأخرى. التنفيذ المتسلسل تمامًا هو النسخة التي يتضح خلوها من تعارضات الإسناد المزدوج؛ أما توزيع العمل بالتوازي فهو تحسين يُضاف فوق أساس ثبتت صحته.
ختامًا
لا تقوم طبقة التنسيق في Orkas على خوارزمية غريبة. تكمن قيمتها في بضعة حدود ثابتة: خطة هي رسم بياني للاعتماديات وليست نصًا تنفيذيًا؛ وحالة تنفيذ تملكها بيئة التشغيل لا النموذج؛ وإسناد يمر عبر المحادثة نفسها التي يشاهدها المستخدم؛ وسياق ينتقل بين الخطوات في صورة ملخصات؛ ومنفذ يقدم الصحة على التزامن. كل منها بسيط بمفرده. لكنها معًا تحول وكيلًا واحدًا موثوقًا إلى فريق يقسم العمل، ويسلمه إلى المرحلة التالية، ويتعافى عندما يتعثر جزء منه.
إذا أردت التعرف إلى الطبقة التي تقع تحت هذه الطبقة، فاقرأ كيف يُصمم وكيل واحد ليعمل بموثوقية. وإذا أردت التعرف إلى الطبقة التي تجعل كل وكيل يتحسن مع الاستخدام، فاقرأ كيف يتعلم وكلاء Orkas من عملهم. وإذا كنت تفضل توجيه هذه الطبقة بدلًا من بنائها، فإن Orkas يوفرها في صورة تنسيق مفتوح المصدر لوكلاء الذكاء الاصطناعي يعمل على جهازك.