كل من أطلق منتجًا قائمًا على الوكلاء يعرف هذا الشعور: تشغيل عرض تجريبي سريع، لكن تحويله إلى شيء يثق به المستخدم يوميًا، شيء يعمل طوال اليوم على جهازه دون أن يتعطل، أمر صعب. والجزء الصعب ليس ربط النموذج، بل الطبقة الكاملة المحيطة به.
تحمل هذه الطبقة عدة أسماء؛ وسأستخدم هنا اسم إطار تشغيل الوكيل. تقع بين «النموذج الكبير» و«ميزات المنتج»، وهي بيئة التشغيل الفعلية: تحوّل طلب مستخدم واحدًا إلى جولات متتابعة من المحادثة مع النموذج، وتتخللها استدعاءات الأدوات، وتُعيد النتائج إليه، وتضغط السياق قبل أن يتجاوز سعته، وتعيد المحاولة عند اضطراب الشبكة، وتستعيد المحادثة حتى بعد تعطل العملية. يتولى النموذج التفكير؛ ويحوّل إطار التشغيل ذلك التفكير إلى تسلسل موثوق من الإجراءات.
Orkas تطبيق وكلاء لسطح المكتب يعمل على جهاز المستخدم نفسه، ويقيم إطار تشغيله بالكامل لدى العميل. يستعرض هذا المقال كيفية بناء هذه الطبقة: كيف تُقسّم إلى طبقات، وشكل حلقة التشغيل، وكيف تُجرّد الأدوات والنماذج، وكيف تُدار الذاكرة والجلسات. نُقّحت تفاصيل الشيفرة وعُمّمت، لكن البنية الهندسية حقيقية.
الطبقات
إذا بسطت بنية منتج قائم على الوكلاء، فستجد تقريبًا هذه الطبقات مرتبة من الأسفل إلى الأعلى:
┌─────────────────────────────────────────┐
│ ميزات المنتج (المحادثة / المهارات / الموصّلات / المزامنة) │
├─────────────────────────────────────────┤
│ إطار تشغيل الوكيل (حلقة التشغيل / الأدوات / الجلسة) │
├─────────────────────────────────────────┤
│ تجريد المزوّدين (توحيد عدة مزوّدي نماذج لغوية كبيرة) │
├─────────────────────────────────────────┤
│ البنية التحتية (الأنواع / الأخطاء / التسجيل / الإعدادات) │
└─────────────────────────────────────────┘هناك خيار ذو تبعات جوهرية مضمّن هنا: كل استدلال النماذج يجري لدى العميل. تطبيق سطح المكتب ليس عميلًا خفيفًا؛ فهو يحتوي إطار التشغيل نفسه ويستدعي النموذج مباشرة. لا يتولى الخادم إلا الحسابات والمزامنة بين الأجهزة والفوترة؛ ولا يشغّل الوكيل إطلاقًا. شكّل هذا القرار كل ما تلاه تقريبًا: تُحفظ الجلسات على القرص المحلي، وتعمل الأدوات مباشرة في دليل عمل المستخدم، ولا تغادر البيانات الحساسة الجهاز أبدًا.
ينقسم إطار التشغيل نفسه إلى بضعة أجزاء: حلقة التشغيل (المشغّل)، والجلسة، والأدوات، وطبقة المزوّد، والذاكرة. لنتناولها واحدًا تلو الآخر.
حلقة التشغيل: مولّد متدفق
المشغّل هو قلب إطار التشغيل. وفي جملة واحدة، مهمته هي: التحدث إلى النموذج مرارًا حتى يقول النموذج «انتهيت».
نُفّذ بوصفه مولّدًا غير متزامن، وهذا الاختيار مهم. تشغيل واحد للوكيل يتجاوز بكثير «أرسل طلبًا وانتظر نتيجة». تحدث أمور كثيرة بينهما: يصدر النموذج رموزًا، ويريد استدعاء أداة، وتنتهي الأداة، ويطول السياق بما يكفي لتفعيل الضغط، وتفشل الشبكة فنعيد المحاولة. باستخدام دوال رد النداء أو كائنات Promise العادية، يصعب إظهار هذه الحالات الوسيطة بوضوح للجهة المستدعية. أما باستخدام مولّد، فتتحول كلها إلى تدفق من الأحداث التي تُمرّر عبر yield:
type AgentRunEvent =
| { type: "text_delta"; text: string } // model emitting tokens
| { type: "tool_start"; name: string; input: unknown } // a tool starts executing
| { type: "tool_end"; name: string; result: string } // a tool finished
| { type: "compaction"; tokensBefore: number; tokensAfter: number } // context compacted
| { type: "retry"; attempt: number; reason: string } // error, retrying
| { type: "done"; result: AgentRunResult } // terminalتشترك الواجهة في تدفق الأحداث هذا وتعرض مخرجات النموذج وتنفيذ الأدوات في الوقت الفعلي. أما نقطة الدخول غير المتدفقة فليست داخليًا سوى «استهلك التدفق حتى النهاية، وخذ الحدث النهائي done»؛ وتشترك نقطتا الدخول في تنفيذ واحد، فلا يوجد مسار شيفرة ثانٍ قد يخرج عن التزامن.
ما يحدث داخل الدور
عند تفصيل الخطوات، يبدو الدور الواحد تقريبًا هكذا:
- أضف رسالة المستخدم، التي قد تحتوي صورًا، إلى سجل الجلسة.
- اجمع موجّه النظام، مع إدراج الأدوات المتاحة حاليًا وفهرس المهارات وما إلى ذلك.
- حلّل السلسلة النصية للنموذج وحدّد منها مزوّدًا بعينه ومعرّف نموذج.
- حوّل جميع الأدوات إلى تعريفات يفهمها النموذج، وأرسلها مع السجل.
- استهلك تدفق استجابة النموذج،
yieldلتمرير النص رمزًا تلو الآخر مع جمع أي استدعاءات أدوات يجريها النموذج. - عندما ينتهي التدفق، افحص سبب توقف النموذج:
- إذا كان
tool_use، فالنموذج يريد استدعاء أداة؛ شغّل الأدوات، ثم عد إلى الخطوة 5 واسأل النموذج مجددًا. - وإلا فقد انتهى الدور؛ اجمع النتيجة،
yield done، ثم عُد.
هناك شرط ثابت واحد يجب الحفاظ عليه هنا: يجب أن يتبع كل استدعاء أداة يجريه النموذج مباشرة في السجل ناتج أداة مطابق له. تفرض واجهة API للنموذج هذا الاقتران بصرامة؛ وإذا خالفته، فسيفشل الطلب التالي بخطأ أو يعلق ببساطة. سنعود إلى هذا عندما نتحدث عن التعافي الذاتي للجلسات.
كيف يُعاد توجيه استدعاء الأداة
لا ينفّذ النموذج الأدوات بنفسه؛ بل يقول فقط «أود استدعاء read_file بهذه الوسائط». ما إن يلتقط المشغّل تلك النية:
for (const call of toolUseBlocks) {
yield { type: "tool_start", name: call.name, input: call.input };
const tool = this.tools.get(call.name);
const ctx = { workingDir, signal, state: { sandboxEnv } };
const result = await tool.execute(call.input, ctx);
// append the result to the session as a tool-result message
session.addToolResult(call.id, result);
yield { type: "tool_end", name: call.name, result: result.content };
}تُشغَّل الأدوات بالتتابع، وتُكتب النتائج في السجل بالترتيب الذي أعلنها به النموذج، ثم يُستدعى النموذج مجددًا وقد أصبحت تلك النتائج بين يديه. وبعد الاطلاع عليها، قد يستدعي أداة أخرى أو يقدّم إجابته النهائية مباشرة. وهذه الحلقة «اسأل ← استدعِ ← أجب ← اسأل مجددًا» هي ما يتيح للوكيل إنجاز المهام متعددة الخطوات.
ثمة تفصيل يستحق ذكرًا مستقلًا: تعيد بعض الأدوات صورًا، مثل لقطات الشاشة والصور المولَّدة. لكن نماذج كثيرة لا تقبل الصور في قناة نتائج الأدوات. يعالج Orkas ذلك بفصل الصورة في رسالة مستخدم مستقلة توضع بعد نتيجة الأداة؛ فيقرأ النموذج أولًا «أعادت الأداة هذا النص»، ثم يرى الصورة المقابلة في الدور التالي مباشرة. إنها تسوية بسيطة تتجاوز اختلاف القدرات بين المزوّدين.
ما العمل عندما يوشك السياق على تجاوز سعته
العائق الذي تصطدم به المهام الطويلة غالبًا هو نافذة السياق. لا ينتظر Orkas حتى تمتلئ، بل يحدد عتبة عند 60%: بعد كل جولة من الأدوات، يقدّر مقدار ما تشغله الرموز الحالية من النافذة، وبمجرد تجاوز 60% يبدأ ضغط السياق استباقيًا.
تطلب عملية الضغط من النموذج تلخيص المحادثة السابقة، ثم تستبدل الرسائل القديمة بذلك الملخص، مع الاحتفاظ بالجزء الأحدث فقط. يبدو الأمر بسيطًا، لكن فيه فخًا: بعد الاستبدال، يجب ألا يبدأ الجزء المحتفَظ به بـ«نتيجة أداة يتيمة»؛ فلا يجوز وجود «نتيجة بلا استدعاء مطابق»، وإلا انتُهكت قاعدة الاقتران مرة أخرى. لذا يضمن منطق الضغط أن يقع القطع عند حد سليم.
ثمة خيار أكثر إثارة للاهتمام يستحق التوضيح هنا: لماذا اتبعنا النهج الإجمالي «لخّص الكتلة كلها عند 60%»، بدلًا من نهج أدق، مثل تقييم كل رسالة وحذفها بحسب أهميتها، أو الاستخراج المنظَّم من مخرجات الأدوات، أو الاحتفاظ بشجرة ذاكرة متعددة الطبقات؟ تبدو هذه الأساليب رائعة في الأوراق البحثية، لكننا تعمدنا عدم سلوك هذا الطريق لثلاثة أسباب.
أولًا، التخزين المؤقت. تعتمد الاستفادة من ذاكرة التخزين المؤقت لموجّه النموذج على البادئة: ما دامت بادئة السجل لم تتغير، تُسترجع تلك المساحة من الذاكرة المؤقتة، مما يوفر التكلفة وزمن الاستجابة معًا. أما الضغط الدقيق فيعيد كتابة وسط السجل باستمرار، فيؤدي إلى تحطيم البادئة المخزَّنة مؤقتًا مرارًا؛ إذ يفرض كل تعديل إعادة معالجة مساحة كبيرة من المدخلات. وتحافظ استراتيجية «اتركه كما هو، ثم اضغطه مرة واحدة عند العتبة» على ثبات البادئة في الغالبية العظمى من الأدوار، ولا تبطلها إلا عملية الضغط الواحدة تلك. وهذا أنسب بكثير للتخزين المؤقت.
ثانيًا، التعقيد. قاعدة «يجب إقران كل استدعاء أداة بنتيجته» التي نكرر التأكيد عليها: كلما زادت دقة اقتطاع السجل، زاد احتمال انتهاكها في حالة خفية. أما الملخص الإجمالي فلا يحتاج إلا إلى حماية نقطة قطع سليمة واحدة، فتقل مواضع الخطأ المحتملة بنحو مرتبة عشرية. وكل فئة من الحالات الحدّية نتخلص منها تعني فئة أقل من حوادث بيئة الإنتاج.
ثالثًا، الاستفادة من عوائد تحسّن النماذج. اتسعت نوافذ السياق باستمرار خلال العامين الماضيين، وأصبحت النماذج تتعامل مع السياقات الطويلة بكفاءة متزايدة. إن بذل جهد كبير اليوم في خوارزمية ضغط معقدة يعني عمليًا محاربة مشكلة آخذة في التقلص؛ فقد تنتهي من ضبطها بالتزامن مع مضاعفة الجيل التالي لنافذة السياق، فيصبح تعقيدك عبئًا خالصًا. وعلى العكس، فإن إسناد التلخيص إلى النموذج نفسه يتحسن تلقائيًا مع تحسن النموذج: كلما ازدادت قدرته على انتقاء المهم، ارتفعت جودة الملخص، من دون أن نغير سطرًا واحدًا. والتعقيد الذي يستطيع النموذج تحمّله عنك لا ينبغي أن تتحمّله بنفسك.
يخفي تقدير عدد الرموز مشكلة يسهل تفويتها: اللغة الصينية. إذا قدّرت النص الصيني وفق تصورات مستمدة من الإنجليزية، أي نحو رمز واحد لكل بضعة محارف، فستقلل العدد كثيرًا. يمنح Orkas محارف الصينية واليابانية والكورية وزنًا منفصلًا في تقديره؛ وإلا لكانت قراءة العتبة في محادثة صينية بالكامل غير صحيحة، ولما بدأ الضغط حين ينبغي له ذلك.
الأخطاء وإعادة المحاولة
عند التشغيل على جهاز المستخدم والاعتماد على واجهة API لنموذج خارجي، تصبح الأخطاء أمرًا معتادًا لا استثناءً. يصنّف المشغّل الأخطاء إلى فئات قليلة، ويعامل كلًا منها بطريقة مختلفة:
- قابلة لإعادة المحاولة: حدود معدل الطلبات، وانتهاء المهلة، وانقطاع الاتصالات، وأخطاء 5xx. تُستخدم فترات انتظار تتزايد أُسّيًا مع تفاوت عشوائي، بحد أقصى قدره 30 ثانية؛ وإذا كان الخطأ متعلقًا بمعدل الطلبات وأرسل الخادم
retry-after، فيُلتزم به. - غير قابلة لإعادة المحاولة: مثل إخفاقات المصادقة؛ فلن يفيد أي عدد من المحاولات، لذا يُعلن الخطأ فورًا.
- خاصة: تجاوز سعة السياق. يُجرَّب الضغط أولًا، ثم تُعاد المحاولة مرة واحدة، ولا يُعلن الخطأ إلا إذا فشلت هذه المحاولة أيضًا.
ثمة فئة أخرى: «الأداة نفسها فشلت». وهذا لا يُفشل الدور كله؛ فإخفاق الأداة معلومة بحد ذاته للنموذج، الذي يستطيع عند رؤية «فشل هذا الأمر» تجربة نهج مختلف تمامًا. يميّز إطار التشغيل بين أخطاء الأدوات العابرة هذه والأعطال الفعلية: فلا يقطع سير العمل ولا يضيّعها، بل تظهر في الإحصاءات اللاحقة. (وتغذي هذه البيانات لاحقًا آلية التطور الذاتي، وهي موضوع المقال التالي.)
تُفحَص إشارة الإلغاء الخارجية (AbortSignal) عند كل نقطة أساسية. يضغط المستخدم «إيقاف»، فيتوقف الدور الحالي فورًا، ولا تبدأ أي محاولات جديدة.
تجريد الأدوات: بسيط بما يكفي للتوسّع
واجهة الأداة بسيطة عمدًا:
interface AgentTool {
readonly name: string;
readonly description: string; // shown to the model
readonly inputSchema: Record<string, unknown>; // JSON Schema to constrain inputs
execute(input: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult>;
}الأداة ليست سوى «اسم + وصف للنموذج + مخطط للمدخلات + دالة تنفيذ». وتطبّق الأدوات المدمجة، مثل قراءة الملفات وكتابتها وتشغيل أوامر الصدفة والبحث في الويب وجلب محتواه، هذه الواجهة جميعًا. وتضيف طبقة سطح المكتب مجموعة من الأدوات الملائمة للاستخدام المحلي، مثل البحث في قاعدة المعرفة وتوليد الصور واستدعاء الموصلات الخارجية، لكن الواجهة تبقى نفسها.
فائدة الواجهة البسيطة أن مصدر الأداة لا يهم المشغّل: سواء كانت مدمجة أو عرّفها المستخدم أو حُمّلت من مهارة، فهي جميعًا من النوع نفسه، وتُسجَّل في بنية واحدة من نوع Map<string, AgentTool> وتُحوَّل إلى تعريفات قابلة للقراءة من النموذج في كل دور.
تمر الأدوات ذات الآثار الجانبية، مثل أوامر الصدفة، عبر منفّذ معزول: مهل زمنية، وحدود لطول المخرجات، وقائمة أوامر محظورة، ومتغيرات بيئة تُمرَّر على نحو منفصل بدلًا من تعديل البيئة العامة للعملية؛ لأن تعديلها قد يتسرب إلى حشد من العمليات الفرعية، ويمكنه بسهولة تعطيل بدء التشغيل في بنية متعددة العمليات مثل Electron.
طبقة المزوّد: توحيد نماذج كثيرة خلف واجهة واحدة
تتباين تفضيلات المستخدمين للنماذج كثيرًا، ولا يمكن لمنتج أن يرتبط ارتباطًا حصريًا بمزوّد واحد. تحت إطار التشغيل، يضع Orkas تجريدًا للمزوّد يوحّد نماذج المزوّدين المختلفين خلف واجهة واحدة:
interface LLMProvider {
readonly id: string;
complete(params: CompletionParams): Promise<CompletionResult>;
stream(params: CompletionParams): AsyncIterable<StreamEvent>;
validateAuth(): Promise<boolean>;
}لا يتعامل المشغّل في الطبقة الأعلى إلا مع هذه الواجهة؛ ولا يعرف أي مزوّد يقف خلفها. ويتولى سجل توجيه الطلبات بحسب سلسلة اسم النموذج: فإذا استُخدمت صيغة صريحة من الشكل provider/model ، تُقسَّم مباشرة؛ أما اسم النموذج المجرد فيُنسب إلى مزوّده بحسب بادئته. وتُدار المصادقة هنا أيضًا، سواء بمفتاح API أو رمز OAuth، ويُجدَّد رمز OAuth تلقائيًا عند انتهاء صلاحيته.
عند توحيد نماذج كثيرة، لا تكمن المشكلة الحقيقية في إكمال النص، بل في المواضع التي تختلف فيها دلالات المزوّدين. وإليك مثالين واجهناهما.
الأول هو الحفاظ على كتل التفكير عبر المزوّدين. تنتج نماذج الاستدلال مقطعًا من محتوى «التفكير»؛ يشفّره بعض المزوّدين ويشترطون إعادته إليهم حرفيًا، بينما يمثله آخرون بمجموعة مختلفة من الحقول. فإذا انتقل المستخدم من المزوّد A إلى المزوّد B في منتصف المحادثة، فلن يعود توقيع مقطع التفكير في السجل مطابقًا. والحل هو وسم كل رسالة في السجل بـ«النموذج الذي أنتجها»، كي تقرر طبقة التحويل ما إذا كانت ستحتفظ بها حرفيًا: النموذج نفسه، احتفظ بها؛ نموذج مختلف، حوّلها إلى تمثيل أبسط وفق القواعد.
والثاني هو ذاكرة التخزين المؤقت للموجّه. تتكرر البادئة بدرجة كبيرة عبر أدوار الجلسة الواحدة، ويوفر تخزينها مؤقتًا قدرًا ملموسًا من التكلفة وزمن الاستجابة. يمرّر التنفيذ معرّف الجلسة بوصفه مفتاح التخزين المؤقت إلى المزوّدين الذين يدعمونه، مع مراعاة حدود طول المفتاح لدى كل مزوّد، كاقتطاعه أو حساب تجزئته إذا كان طويلًا جدًا.
كل هذا عمل شاق وروتيني، لكن هذه الطبقة منه تحديدًا هي التي تتيح للمشغّل في الأعلى التصرف كما لو أنه «لا يوجد إلا نوع واحد من النماذج».
الذاكرة: آليتان، لكل منهما مهمتها
«الذاكرة» في Orkas هي في الواقع آليتان متوازيتان تحلان مشكلتين مختلفتين تمامًا. الأولى قاعدة معرفة قائمة على الاسترجاع، للمواد الكثيفة التي «تبحث عنها عند الحاجة». والثانية ذاكرة عابرة للجلسات، للمجموعة الصغيرة من الحقائق الأساسية التي «ينبغي أن تبقى حاضرة دائمًا». تدمج منتجات كثيرة هاتين الآليتين، لكن الفصل بينهما يجعل الأمور أوضح بكثير.
قاعدة المعرفة: استرجاع هجين
تستهدف الآلية الأولى المحتوى الكبير الذي لا يكون ذا صلة إلا أحيانًا، مثل مستندات المستخدم وملاحظاته السابقة والمعرفة المتخصصة. وهي قاعدة معرفة محلية تدعم الاسترجاع المتجهي، بتنفيذين خلفيين: نسخة خفيفة تعمل بالكامل في الذاكرة للاختبارات والاستخدام المؤقت، ونسخة محفوظة في قاعدة بيانات محلية للإنتاج، مع فهرسة النص الكامل والمتجهات.
تدخل البيانات عبر هذا المسار:
المستندات ← تقسيم عند حدود الأسطر (مع تداخل) ← فهرسة مزدوجة
├─ فهرس النص الكامل (كلمات مفتاحية، بلا تكلفة تضمين)
└─ فهرس متجهي (إذا أُعدّ نموذج تضمين)تُقسَّم المقاطع عند حدود الأسطر مع قدر قليل من التداخل بينها، لتجنب شطر معنى مكتمل من منتصفه. ويكون الاسترجاع هجينًا: جولة متجهية للتقارب الدلالي وجولة كلمات مفتاحية للمطابقات الحرفية، ثم تُدمج مجموعتا النتائج باستخدام RRF، أي دمج مقلوب الرتب:
score = Σ 1 / (k + rank_i)كلما ارتفعت رتبة النتيجة في إحدى الجولتين، زادت مساهمتها؛ وبجمع المساهمات من الجولتين، تراعي الصلة الدلالية ولا تفقد المطابقات الحرفية الدقيقة. ويمكن ضبط أوزان المتجهات والكلمات المفتاحية، مع تفضيل الدلالة افتراضيًا. وبعد الدمج، تُزال النتائج المكررة بحسب «(المستند، سطر البداية)»، مع الاحتفاظ بأفضل نتيجة فقط لكل موضع، ثم تُستبعد النتائج دون عتبة معينة، وتُعاد أفضل K نتيجة.
لماذا لا نعتمد على المتجهات وحدها؟ لأن الاسترجاع المتجهي كثيرًا ما يتعثر مع أسماء الأعلام ورموز الشيفرة والسلاسل الحرفية الدقيقة؛ وهي استعلامات قد لا تتميز دلاليًا، لكن تطابقها الحرفي بالغ الأهمية. وفي المقابل، لا تستطيع الكلمات المفتاحية وحدها التقاط «المعنى نفسه بصياغة مختلفة». لذا فإن تشغيل الاثنين معًا موازنة عملية جدًا بين جودة الاسترجاع والتكلفة.
الذاكرة العابرة للجلسات: إبقاء المستخدم حاضرًا
تحل قاعدة المعرفة مشكلة «مواد أكثر مما يمكن الاحتفاظ به». لكن ثمة فئة أخرى من المعلومات، ضئيلة الحجم ومع ذلك يجب أن تبقى حاضرة دائمًا: من هو هذا المستخدم، وما تفضيلاته، وما الذي اتُّفق عليه في المرة السابقة. ينبغي ألا تعتمد هذه المعلومات على الاسترجاع كي «يحالفها الحظ فتُستدعى»، بل يجب أن تكون موجودة في كل دور.
لهذا، ينشئ Orkas طبقة مستقلة من الذاكرة العابرة للجلسات، تنقسم بحسب المحتوى إلى جزأين:
- ملف المستخدم: حقائق ثابتة عن الشخص — دوره وتفضيلاته وأسلوب تواصله وحزمته التقنية.
- ملاحظات الحقائق: حقائق دائمة عن العمل — القرارات والمحطات الرئيسية وأعراف المشروع.
كلاهما صغير، ولكل منهما حد صارم لا يتجاوز بضعة آلاف من المحارف، مما يفرض الاحتفاظ بما يفيد فعلًا على المدى الطويل فقط. ولا يمران عبر الاسترجاع؛ بل يُثبَّتان مباشرة في موجّه النظام عند بداية كل دور، أي إن الوكيل «يعرف» هذه الأمور ببساطة، من دون أن يضطر إلى تذكّر البحث عنها. وهذا نقيض موقف قاعدة المعرفة تمامًا: قاعدة المعرفة «تُجلب عند الحاجة فقط ثم تزول»، والذاكرة العابرة للجلسات «حاضرة دائمًا ومرئية دائمًا».
تتم الكتابة عبر أداة ذاكرة مخصصة يستدعيها النموذج عندما يقدّر أثناء المحادثة أن «هذا يستحق التذكر طويلًا»، وتدعم الإضافة واستبدال جزء من النص والحذف. ويوضح وصف الأداة بدقة ما ينبغي حفظه وما لا ينبغي: تأتي تصحيحات المستخدم وتفضيلاته في المقام الأول؛ وتُحفظ القرارات والأعراف الدائمة؛ أما الحالة المؤقتة للمهمة الحالية، ومعلومات تصحيح الأخطاء التي تُستخدم مرة واحدة، وكل ما يسهل اكتشافه مجددًا، فلا يُحفظ. فالذاكرة مخصصة لـ«الحقائق الدائمة عن المستخدم والمشروع»، لا لـ«النقطة التي وصلت إليها هذه المرة».
ثمة تفصيل يسهل إغفاله لكنه مهم جدًا: يُجرى فحص أمني قبل كل عملية كتابة. يدخل هذا المحتوى إلى موجّه النظام حرفيًا ويبقى عبر الجلسات طويلًا، مما يجعله عمليًا سطحًا دائمًا لحقن التعليمات. لذلك تُفحَص كل ذاكرة قبل كتابتها على القرص بحثًا عن أنماط مريبة: صيغ حقن الموجّهات التقليدية، مثل «تجاهل جميع التعليمات السابقة» وما شابهها، وأوامر تحاول تسريب المفاتيح، ومحارف Unicode غير المرئية المخفية في النص. وتُرفض أي مطابقة مباشرة. ومع إزالة التكرار والاقتطاع عند تجاوز الحد، تبقى طبقة الذاكرة مفيدة من دون أن تتحول إلى مصدر خطر.
تغطي الآليتان معًا الطرفين: «ضخم لكن يُحتاج إليه أحيانًا» و«صغير لكن دائم الحضور»؛ تتولى قاعدة المعرفة الأول، والذاكرة العابرة للجلسات الثاني. وإذا أضفت إلى ذلك فهم الوكيل لـ ذاته (موضوع المقال التالي)، فإن وكيل Orkas يبدأ عمله حاملًا ثلاثة أنواع من الذاكرة في آن واحد: عن المواد، وعن المستخدم، وعن نفسه.
الجلسات: مصممة لاحتمال التعطل وللتعافي
تدير الجلسة سجل الرسائل. والنسخة الأساسية مجرد مصفوفة رسائل في الذاكرة تدعم اقتطاع السجل وضغطه. لكن أي شيء يعمل على جهاز المستخدم يجب أن يفترض إمكانية إنهائه في أي لحظة: يغلق المستخدم التطبيق، أو يُعاد تشغيل النظام، أو تنهي مهلة المراقبة العملية. لذلك تُستخدم في الإنتاج جلسة دائمة تُكتب في ملف JSONL محلي، برسالة واحدة في كل سطر.
هناك استراتيجيتان للكتابة: تُضاف الرسالة الجديدة بإلحاق ذري؛ أما ما يعيد كتابة الملف كله، مثل الضغط أو المسح، فيستخدم «كتابة ملف مؤقت + إعادة تسمية ذرية». وهكذا، حتى لو انقطعت الكهرباء أثناء الكتابة، لا تترك وراءك نصف سجل تالف.
الجزء الأكثر إثارة للاهتمام هو إصلاح استدعاءات الأدوات اليتيمة. لنعد إلى قاعدة الاقتران: يستدعي النموذج أداة، وينفذها إطار التشغيل، ثم تُكتب النتيجة في السجل؛ وأي انقطاع في واحدة من هذه الخطوات الثلاث يترك عنصرًا يتيمًا على القرص، أي «استدعاءً بلا نتيجة». وإذا حمّلت الجلسة في المرة التالية وأرسلتها كما هي إلى النموذج، فإما أن ترفضها واجهة API أو تتعطل عن الاستجابة.
يعمل منطق الإصلاح كلما حُمّلت جلسة من القرص، وتكراره لا يغيّر النتيجة:
- افحص جميع رسائل المساعد واجمع معرّفات استدعاءات الأدوات التي أصدرتها.
- ابحث في الرسائل اللاحقة عن نتائج الأدوات المطابقة.
- لكل استدعاء يفتقد نتيجة مطابقة، أنشئ نتيجة موسومة بأنها «انقطعت».
- وفي أثناء ذلك، طابق ترتيب النتائج مع ترتيب إعلان الاستدعاءات، واحذف أي نتائج يتيمة لا يقابلها استدعاء.
بعد هذه الجولة، تُضمن للجلسة حالة تستوفي شرط الاقتران لدى واجهة API وتكون آمنة للإرسال. تبدو الآلية عادية، لكنها شبكة الأمان التي تضمن «ألا تتوقف محادثة المستخدم نهائيًا بسبب تعطل واحد».
قرارات قليلة اتضحت أهميتها لاحقًا
عند جمع هذه العناصر كلها، تبدو بعض القرارات ذات قيمة خاصة بأثر رجعي.
المولّدات بوصفها الواجهة الأساسية. يشترك البث وعدم البث في تنفيذ واحد، وتظهر الحالة الوسيطة طبيعيًا، ويمكن لواجهة المستخدم عرض ما تشاء من التفاصيل. وقد جنبنا هذا فئة كاملة من أخطاء عدم الاتساق التي كان سيخلقها نهج «نفّذ عدم البث أولًا ثم أضف البث لاحقًا».
اضغط عند 60%، لا عند الامتلاء. يترك ذلك مساحة لعملية الضغط نفسها، التي تتطلب أيضًا استدعاءً للنموذج، ويتجنب الارتباك في اللحظة الأخيرة.
تسري قاعدة الاقتران في كل شيء. من نقطة قطع الضغط إلى الكتابة على القرص والإصلاح عند التحميل، يلتزم كل موضع يتعامل مع الجلسة بالقاعدة نفسها. ومع وجود قاعدة واحدة، لا يضطر أي موضع إلى ابتكار منطق ترقيع خاص به.
حصر العمل الشاق الروتيني في طبقة المزوّد. تستوعب هذه الطبقة وحدها جميع تعقيدات الاختلاف بين المزوّدين، من كتل التفكير ومفاتيح التخزين المؤقت إلى تفاوت القدرات، مقابل مشغّل واضح في الأعلى. وإذا أُضيف مزوّد نماذج جديد يومًا ما، فلن يمتد التغيير خارجها إلا قليلًا.
ختامًا
لا يحتوي إطار تشغيل Orkas على خوارزمية مبهرة. تكمن قيمته في أخذ هدف «تشغيل وكيل بموثوقية في بيئة حقيقية» وتقسيمه إلى وحدات ذات حدود واضحة، تتولى كل منها جانبًا: المشغّل يتولى الحلقة وإعادة المحاولة، والأدوات تتولى القدرات، وطبقة المزوّد تتولى توحيد النماذج، والذاكرة تتولى الاسترجاع، والجلسة تتولى الحفظ الدائم والإصلاح. لا شيء منها معقد بمفرده؛ لكنها مجتمعة تدعم شيئًا يستخدمه الناس كل يوم.
إن كان ثمة ما يستحق الاحتفاظ به: اجعل حلقة التشغيل مولّدًا متدفقًا، فتغدو معالجة الحالة الوسيطة أسهل بكثير؛ وبمجرد وضع قاعدة أساسية، مثل «يجب إقران استدعاءات الأدوات بنتائجها»، التزم بها باستمرار عند الضغط والكتابة على القرص والتحميل، ولا تجعل أي موضع استثناءً؛ واجمع العمل الروتيني المتعلق باختلاف المزوّدين في طبقة واحدة وأبعده عن منطق العمل؛ والأوضح من ذلك كله: افترض أن عمليتك ستُنهي في أسوأ لحظة ممكنة، واكتب آلية التعافي من تلك اللحظة مسبقًا.
يتناول المقال التالي جانبًا أكثر إثارة للاهتمام في Orkas: كيف يتعلم هذا الوكيل من استخدامه، ويستخلص من الخبرة مهارات قابلة لإعادة الاستخدام، ويجعل نفسه أكثر نفعًا تدريجيًا.