Skip to main content

مشكلة نافذة السياق

وولفيش عديم الحالة — كل استدعاء لنموذج اللغة يُعيد تشغيل سجل المحادثة. وهذا يولّد ضغطين متمايزين:
  1. داخل الدور: نتائج الأدوات (محتوى الإيميلات، صفحات الويب، مخرجات الطرفية، لقطات الشاشة) تتراكم ويمكن أن تدفع دورًا واحدًا إلى تجاوز ميزانية إدخال النموذج.
  2. عبر الأدوار: إعادة تشغيل محادثة طويلة العمر تنمو مع كل رسالة. وإذا تُركت وشأنها، تكلّف المحادثة القديمة أكثر مع كل إرسال، وفي النهاية يستحيل إعادة تشغيلها أصلًا.
كان التصميم القديم يجيب عن كلا الضغطين بأداة فظة واحدة: الضغط داخل الدور. فما إن تتجاوز المحادثة العتبة، حتى تُعيد كل رسالة لاحقة دفع ثمن تمريرة ضغط في الذاكرة — بما فيها استدعاء تلخيص LLM جديد — لأن التعديلات لم تكن تُخزَّن أبدًا. الإصدار v1.0.203 يستبدل بذلك دورة حياة:

البداية الرشيقة

الضغط عَرَضٌ لسياقٍ لم تكن تحتاجه. أكبر مكسب في v1.0.203 هو ما لا يدخل الطلب من الأساس. موجّه النظام لمحادثة جديدة يبلغ ~5,000 رمز (كان ~44,000)، ومع مخططات الأدوات الأساسية يبلغ الطلب الأول بأكمله ~9.4 ألف رمز — انخفاضًا من ~94 ألفًا، أي تقليص بمقدار 10 أضعاف. ما زال محذوفًا من تجميع الموجّه:
  • حقن <memory> للملفات الكاملة وإغراق حلقات اليومين — استُبدل بهما بديل تغطية مدمج <memory_map> وأدوات استرجاع عند الطلب (memory_search وmemory_get)
  • كتالوج الأدوات النثري البالغ ~16 ألف رمز — استُبدل به فهرس <capabilities> بسطر واحد لكل قدرة بالإضافة إلى الاكتشاف عبر tool_search/tool_activate
  • حقن نصوص المهارات المحفَّز بالكلمات المفتاحية
بادئة الموجّه ثابتة على مستوى البايت (خريطة الذاكرة تُخزَّن مؤقتًا لكل يوم تقويمي)، وهذا ما يجعل التخزين المؤقت لموجّهات المزوّد يعمل: ~99% نسبة تطابق مقيسة عبر 50 محادثة. وبالقياس الحي، انخفضت أتمتة تعمل كل ساعة من 96,442 رمز إدخال (0.046 دولار/تشغيل) إلى ~5.1 ألف رمز موجّه (0.019 دولار/تشغيل). ما ينمو من ذلك الحد الأدنى الرشيق هو المحادثة نفسها — وهي ما تديره بقية هذه الصفحة.

ملخص البادئة المتجدد

بعد كل دور مُخزَّن، يفحص مُلخِّصٌ يعمل بأسلوب أطلِق-وانسَ (conversation-summarizer.ts) المحادثةَ. عندما تتجاوز المنطقة غير الملخَّصة من النص نحو 120,000 حرف، يطوي كل ما قبل حدّ الدور في ملخص واحد مُخزَّن، يُحفظ داخل ملف المحادثة نفسه بصيغة {summary, summarizedThroughMessage, summarizedThroughMessageId} — يثبّت المعرّفُ الحدَّ إلى أول رسالة غير مغطاة بعينها، فلا يستطيع كاتب متزامن يدمج رسائل قبله أن يزحزح ما يزعم الملخص أنه يغطيه. آلية العمل:
  • استدعاء LLM واحد لكل طيّة. يرسل الطيّ مقتطفات مسقوفة من المنطقة الجاري طيّها إلى thalamus.summarize() ويخزّن النتيجة. أي استدعاء تلخيص واحد لكل ~120 ألف حرف من نمو المحادثة — إجمالًا، إلى الأبد. النظام القديم كان يدفع استدعاء تلخيص مع كل رسالة في المحادثات الكبيرة.
  • الالتقاط عند حدود الأدوار. تُبقي علامة الطيّ آخر 8 رسائل حرفيًا، ثم تتراجع إلى أقرب رسالة مستخدم — فالمزودون يرفضون إعادة تشغيل لا تبدأ بدور مستخدم.
  • تكراري. يتضمن موجّه كل طيّة الملخصَ السابق ويوجّه النموذج إلى دمجه، فتحمل المحادثة دائمًا ملخصًا واحدًا بالضبط مهما بلغ عدد الطيّات.
  • لا يعطّل دورًا أبدًا. يعمل الملخِّص بعد حفظ الدور، ويسمح بتشغيل واحد قيد التنفيذ لكل محادثة، ويُعيد تحميل المحادثة قبل الكتابة — فإذا وصل دور جديد أثناء استدعاء LLM، يُكتب الملخص على أحدث نسخة ولا يطمس الرسائل أبدًا.

كيف تبدو إعادة التشغيل

كلا مساري إعادة البناء — إعادة تشغيل قناة العملية الرئيسية وسجل الدردشة في المُعرِّض — يُعيدان تشغيل تمهيد الملخص + الذيل الحرفي بدلًا من النص الكامل. التمهيد مؤشر استرجاع دائم:
يليه إقرار اصطناعي من المساعد، ثم آخر 8 رسائل أو أكثر حرفيًا.

الأصول لا تُمَس أبدًا

الملخص عدسة وقت قراءة، وليس تحريرًا. الرسائل الأصلية لا تُعدَّل ولا تُحذَف أبدًا: تعرض صفحة السجل (History) النص الكامل دون مساس، ويغطي فهرس cortex كل رسالة مطويّة، ويسترجع conversation_read أي دور مطويّ — بما في ذلك أدوار المحادثة الحالية التي لُخِّصت في منتصف الجلسة.

نمط سنّ المنشار

تتبع تكلفة إعادة التشغيل الآن نمط سنّ المنشار بدلًا من منحدر متصاعد:
يضيف كل دور رسائل إلى الذيل الحرفي؛ وعندما تتجاوز المنطقة غير الملخَّصة ~120 ألف حرف، تُقلّصها طيّة واحدة إلى ملخص ≤6 آلاف حرف بالإضافة إلى آخر 8 رسائل. المحادثة اللانهائية تتقارب إلى الملخص + الذيل ولا يمكنها أبدًا أن تتجاوز نافذة السياق — فالحجم القابل لإعادة التشغيل محدود بالعتبة، لا بعمر المحادثة. (ما زالت القنوات تدوّر المحادثات الخاملة بعد 3 ساعات، دون تغيير — لكن التدوير أصبح الآن نظافة، لا آلية نجاة.)

بدائل نتائج الأدوات القديمة

حتى داخل المنطقة الحرفية، نتائج الأدوات القديمة وزن ميت: إغراق صفحة بحجم 40 كيلوبايت من قبل عشرة تبادلات نادرًا ما يُحتاج إليه مجددًا، لكنه كان يركب كل طلب لاحق. عند إعادة التشغيل، نتائج الأدوات التي تكون في آنٍ واحد:
  • ≥ 2,000 حرف، و
  • أقدم من آخر تبادلَي مستخدم
تُستبدل ببديل استرجاع يصف نفسه:
هذا استبدال نصي حتمي — بلا استدعاء LLM — يُطبَّق في كلا مساري إعادة البناء. تبقى البايتات الكاملة مُخزَّنة في ملف المحادثة ومفهرسة في cortex؛ ويسمّي البديل الاستدعاء الدقيق الذي يسترجعها. تتوقف تكلفة إعادة التشغيل عن النمو مع كل إغراق صفحة قديم، دون أن يضيع أي شيء فعليًا.

مرفقات يقودها النموذج

كانت المرفقات القاتل الصامت مرتين: كتل المحتوى الكاملة (صور base64، نص PDF مستخرَج) كانت تركب مع الرسائل الحديثة ويُعاد دفع ثمنها في كل طلب — وملف ضخم حقًّا كان قادرًا على خنق الدور بأكمله، إذ يُستخرج من PDF بثلاثة آلاف صفحة ميجابايتات من النص لا تتّسع لها أي نافذة. اعتبارًا من الإصدار 1.0.224 زال مسار الحقن كليًّا:
  • لا يُحقَن محتوى أي مرفق تلقائيًّا أبدًا. كل ملف ترفقه — أيًّا كان حجمه أو نوعه — يصير بطاقة تعريف موجزة: اسمه، ومساره المطلق، وحقائق حقيقية عنه (عدد صفحات الـPDF يُسبَر بتؤدة من فهرس الملف نفسه، فينال حتى المسح الضوئي الهائل بطاقة دقيقة دون تحليل كامل).
  • يقرأ وولفيش عند الطلب. تسمّي البطاقة الأداة المناسبة للنوع: pdf_info / pdf_search / pdf_read تمشي في وثائق الألف صفحة نطاقَ صفحات فنطاق، وfile_read يبثّ نطاقات أسطر من الملفات النصية العملاقة، وأدوات الجداول والمستندات تتولى صيغها، ونماذج الرؤية تسحب بكسلات الصورة الفعلية عبر image_view حين تحتاج أن تنظر فقط. وسقطت حدود الحجم الاعتباطية في الإضافات (100 ميجابايت للمستندات والجداول، 500 ميجابايت للصوت) مع إعادة التصميم — درجة تفصيل البطاقة تتغير مع حجم الملف، أما توافر المحتوى فلا.
  • لا شيء مجهول بصمت أبدًا. عقد التشغيل يمنع الإجابة عن ملف لم يُقرأ أو يُبحَث فيه خلال المحادثة — ولأن القراءات العميقة في الملفات الكبيرة تأخذ وقتًا حقيقيًّا، يرويها وولفيش (“أول بحث في هذا الكتاب ذي الثلاثة آلاف صفحة — قد يستغرق دقيقة”) بدل أن يصمت.
السياسة نفسها تشغّل ملفات المشاريع: قائمة ملفات المشروع تُحقَن أسماءً وحقائق، والمحتوى يُسحَب دائمًا عبر الأدوات.

شبكة الأمان داخل الدور

يدير الملخص المتجدد النمو عبر الأدوار. لكن دورًا وحشيًا واحدًا — قراءة 40 إيميلًا، أو كشط عشرات الصفحات — ما زال بإمكانه تفجير الميزانية داخل الدور، ولهذا بالضبط يبقى الضاغط داخل الدور. لقد أصبح الآن آلية أزمات، لا تشغيلًا روتينيًا.

المُحفِّز: 75% من الميزانية، معايَر بحسب المزوّد

قبل كل استدعاء لنموذج اللغة، يقدّر الضاغط الحمولة بنسبة محافظة قدرها 1.5 حرف/رمز ويأخذ الأعلى من ذلك التقدير وقيمة inputTokens الفعلية التي أبلغ عنها المزوّد للاستدعاء السابق. عندما يتجاوز العدد الفعلي 75% من ميزانية الإدخال (نافذة السياق ناقص احتياطي المخرجات)، يعمل الضغط ويضغط نزولًا إلى 50%، تاركًا مساحة لمواصلة العمل.

ما تفعله التمريرة

  1. إزالة الصور من نتائج الأدوات عبر الدور كله (لقطات الشاشة المرمَّزة بـ base64 لا تحتاج أبدًا للبقاء بعد التكرار الذي حلّلها).
  2. اقتطاع الأهداف تناسبيًا — أكبر نتائج الأدوات أولًا، ثم رسائل المساعد الأقدم، ثم رسائل المستخدم الأقدم؛ مقتطفات مقدمة + خاتمة مع تسمية واضحة [TRUNCATED — …]. فوري، بلا استدعاء LLM. تُحمى دائمًا messages[0] (موجّه المهمة الأصلي)، وآخر 3 رسائل لكل دور، ونتائج الأخطاء.
  3. استدعاء تلخيص LLM واحد على الأصول المحفوظة، يُنتج حالة منظمة — TASK / PROGRESS / REMAINING / DATA / DECISIONS — مع معرّفات وأعداد دقيقة لأعمال الدفعات.
  4. حقن الملخص مع تذكير بالمتابعة يوجّه النموذج إلى الاستئناف من REMAINING وعدم إعادة العمل أو الإنهاء المبكر. بدون هذا التذكير، تعامل النماذج باستمرار السياقَ المُقصَّر على أنه “مكتمل” وتُسقط بصمت أعمال الدفعات المتبقية.

آخر خطّي دفاع

  • إعادة المحاولة المفروضة عند فيضان 400. إذا أخطأ التقدير والمعايرة معًا وأرجع المزوّد خطأ فيضان سياق 400، يفرض الوكيل الضغط (متجاوزًا العتبة) ويُعيد الاستدعاء — مرة واحدة بالضبط.
  • حارس امتلاء السياق. التوقف بسبب max_tokens يعني عادةً أن الرد قُطع، فيطلب الوكيل من النموذج المتابعة. لكن عندما يملأ الإدخال نافذةَ السياق أصلًا (ضمن 512 رمزًا منها)، لا توجد مساحة للتوليد — وستدور حلقة المتابعة إلى الأبد مُصدرةً نحو رمز واحد لكل استدعاء. يكتشف الوكيل تلك الحالة ويُنهي الدور بما لديه.

في الذاكرة الحية فقط

يعدّل الضغط داخل الدور مصفوفة رسائل الدور في الذاكرة فقط. يحتفظ ملف المحادثة على القرص بالمحتوى الكامل — فالطيّ على جانب التخزين مهمة الملخِّص المتجدد، والاثنان لا يتصارعان أبدًا: الضاغط يقلّص دورًا حيًا، والملخِّص يطوي السجل المحفوظ.

المقايضة الصادقة

البادئة المطويّة مفقودة من السياق لكنها قابلة للاسترجاع بالكامل:
  • في الطلب، لا توجد الأدوار القديمة إلا كملخص ≤6 آلاف حرف وبدائل مختصرة. لا يراها النموذج حرفيًا — وهذا هو المقصود.
  • على القرص وفي الفهرس، يبقى كل شيء بايتًا-ببايت. يحمل تمهيد الملخص وكل بديل مؤشرًا صريحًا إلى conversation_read / memory_search، ويوجّه مذهبُ الاسترجاع النموذجَ إلى اللجوء إليهما فور ورود إشارة محددة (“ذلك الملف”، “خطة الرحلة”).
  • بالنسبة للمستخدم، لا شيء يتغير: تعرض صفحة السجل (History) النص الكامل دون مساس.
يقايض وولفيش السجلَّ الحاضر دائمًا بطلبات رخيصة مع استرجاع مقصود — وهي المقايضة نفسها التي يعقدها للذاكرة والأدوات.

إصلاحات مساندة

تغييرات أصغر تركب دورة الحياة نفسها:
  • مقاطع الوكلاء لا تُعاد مرتين. في وضع سير العمل، لا تُعاد المقاطع الموسومة بالوكلاء إلى دور القائد التالي مرة ثانية أبدًا.
  • تُدمج دلتات التدفق عند الحفظ. كان الرد المتدفق يُخزَّن كآلاف المقاطع النصية المكوّنة من كلمة واحدة لكل رسالة؛ وهي الآن تنهار إلى مقطع واحد عند الحفظ، مما يقلّص الملف ووزن إعادة تشغيله معًا.

مراقبتها

  • بطاقات الضغط في واجهة الدردشة: بطاقة compaction_started نابضة أثناء تشغيل تمريرة أزمة، تحل محلها بطاقة compaction بالأهداف والرموز الموفَّرة والمدة وتفاصيل كل هدف. إذا كنت نادرًا ما ترى واحدة، فدورة الحياة تعمل.
  • أحداث Corpus: يصل compaction.started وcompaction.applied إلى سجل corpus اليومي في brain/corpus/YYYY-MM-DD.log.md.
  • لقطات الموجّهات في brain/prefrontal/.debug/ تُظهر عدد رموز كل طلب مقابل الميزانية (بحد أقصى 50 لقطة مع تدوير).
  • الملخص نفسه مرئي في ملف المحادثة (summary / summarizedThroughMessage / summarizedThroughMessageId) — واستدعاء conversation_read على أي محادثة قديمة يُثبت أن شيئًا لم يُفقد.