الانتقال إلى المحتوى الرئيسي

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

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

كيف يُبنى السياق

قبل كل استدعاء لنموذج اللغة، يُجمّع المكون التنفيذي (prefrontal) الطلب الكامل:
موجه النظام (~13 ألف رمز)
  <identity>      — soul.md + user.md
  <device>        — نظام التشغيل، العتاد، اللغة، المنطقة الزمنية
  <variables>     — متغيرات التكوين المعرّفة من المستخدم
  <prefrontal>    — agents.core.md + agents.md
  <tools>         — جميع تعريفات أدوات القدرات المحمّلة
  <skills>        — تعريفات المهارات (مثل التخطيط)
  <memory>        — الحلقات والمعرفة ذات الصلة (مسجّلة بواسطة RAS)
  <runtime>       — عداد التكرار، عدد الأدوات، تعليمات التجميع

+ رسائل المحادثة (تنمو مع كل تكرار)
  [رسالة المستخدم]
  [رد المساعد + استدعاءات الأدوات]
  [نتيجة أداة 1] [نتيجة أداة 2] ...
  [رد المساعد + استدعاءات الأدوات]
  [نتيجة أداة 3] [نتيجة أداة 4] ...
  ...
يبقى موجه النظام مستقرًا نسبيًا عبر التكرارات (~13 ألف رمز). ما ينمو هو رسائل المحادثة — كل نتيجة أداة يمكن أن تُضيف آلاف الرموز، ودور واحد يقرأ 41 إيميلًا يمكن أن يُراكم أكثر من 500 ألف رمز من نتائج الأدوات وحدها.

المشكلة

فكّر في موجز إيميل نموذجي: يبحث النموذج عن الرسائل غير المقروءة، ثم يقرأ كل واحدة على حدة. بعد قراءة 30+ إيميلًا:
التكرار 1:    42 ألف رمز  (النظام + رسالة المستخدم + استدعاء الحسابات)
التكرار 2:    47 ألف رمز  (+ نتائج بحث الإيميل لحسابين)
التكرار 3:   541 ألف رمز  (+ 15 محتوى إيميل كامل)
التكرار 4:  ~1 مليون رمز  (+ 15 إيميل إضافي — سيتجاوز الحد!)
بدون الضغط، سيتجاوز التكرار 4 ميزانية الإدخال البالغة 967 ألف رمز ويفشل بخطأ 400. والأسوأ، بدون تذكير بالمتابعة، قد يعتبر النموذج السياق المضغوط “مكتملاً” ويتخطى العمل المتبقي — يقرأ 30 من 43 إيميلًا ويُنتج ملخصًا نهائيًا، متخطيًا الـ13 الأخيرة بصمت.

الدفاع ذو الثلاث طبقات

يستخدم الضاغط ثلاث طبقات لتحديد ما إذا كان الضغط مطلوبًا. كل طبقة تلتقط الحالات التي قد تفوتها الطبقة السابقة.

الطبقة 1: عتبة استباقية عند 75%

يُحوَّل عدد أحرف كل رسالة إلى تقدير لعدد الرموز باستخدام نسبة محافظة:
const CHARS_PER_TOKEN = 1.5
const COMPACTION_THRESHOLD = 0.75  // التحفيز عند 75% من الميزانية
const COMPACTION_TARGET = 0.50     // الضغط إلى 50%
نسبة الأحرف-للرموز عدوانية عمدًا. المزودون وأنواع المحتوى المختلفة يُرمّزون بكثافات مختلفة جدًا:
نوع المحتوىأحرف/رمز نموذجية
نص إنجليزي~4
JSON/HTML~1.5-2.5
DeepSeek على JSON~1.2-1.8
استخدام 1.5 يعني أن التقدير سيُبالغ أحيانًا، مما يُحفّز الضغط مبكرًا. تكلفة المبالغة (جولة ضغط إضافية) أقل بكثير من التقليل (خطأ 400 يُعطّل الدور). يعمل الضغط عندما يتجاوز الحمل 75% من ميزانية الإدخال ويضغط إلى 50%، تاركًا مساحة كافية للنموذج لمواصلة العمل قبل جولة الضغط التالية.

الطبقة 2: المعايرة عبر العدد الفعلي للرموز

بعد كل استجابة من نموذج اللغة، تُرجع الـ API العدد الفعلي لـ inputTokens المستخدمة. في التكرار التالي، تُمرّر هذه القيمة للضاغط كـ lastKnownInputTokens. يستخدم الضاغط الأعلى من التقدير الحرفي والعدد الفعلي:
const currentTokens = Math.max(charEstimate, lastKnownInputTokens)
هذا يلتقط الحالات التي يُقلّل فيها التقدير الحرفي — مثلاً عندما يُنتج مُرمّز DeepSeek رموزًا أكثر مما يتوقع معدل 1.5 حرف/رمز.

الطبقة 3: إعادة المحاولة عند خطأ فيضان 400

إذا قلّلت كلتا الطبقتين من التقدير وأرجع نموذج اللغة خطأ فيضان سياق 400، يلتقطه الوكيل ويفرض الضغط بـ force: true. هذا يتخطى فحص العتبة ويضغط أكبر الرسائل بلا شروط، ثم يُعيد استدعاء النموذج.
الطبقة 1: التقدير يقول تحت 75%              → لا ضغط
الطبقة 2: الرموز الفعلية تقول تحت 75%       → لا ضغط
استدعاء النموذج → خطأ فيضان 400
الطبقة 3: فرض الضغط + إعادة المحاولة        → ضغط + إعادة محاولة
هذه شبكة الأمان. عمليًا، تلتقط الطبقتان 1 و2 معظم الحالات. الطبقة 3 تعمل فقط عندما تُرجّح نتيجة أداة كبيرة الكفة بين التقدير والحد الفعلي.

اختيار الأهداف

عندما يعمل الضغط، تُختار الأهداف بجشع حسب الأولوية حتى تُغطي الوفورات المتوقعة الفائض بين العدد الحالي وهدف الـ50%: المرحلة 1 — نتائج الأدوات (الأكبر أولًا): تُتخطى الأخطاء وأحدث 3 نتائج. تُضغط الأكبر أولًا لأنها تعطي أكبر وفورات لكل هدف. المرحلة 2 — رسائل المساعد الأقدم (الأقدم أولًا): تُتخطى أحدث 3. ردود النموذج تحتوي على تحليلات قد يُشار إليها. المرحلة 3 — رسائل المستخدم الأقدم (الأقدم أولًا): تُتخطى messages[0] (مطالبة المهمة الأصلية) وأحدث 3. الملاذ الأخير. المحمي دائمًا:
  • messages[0] — رسالة المستخدم الأولى (مطالبة المهمة الأصلية) لا تُضغط أبدًا
  • آخر 3 رسائل لكل دور (أداة، مساعد، مستخدم)
  • نتائج الأدوات الخاطئة (isError: true)
  • الرسائل تحت 500 حرف
  • الملخصات المضغوطة سابقًا غير محمية — مما يسمح بالضغط التكراري عبر تمريرات متعددة

الثوابت

الثابتالقيمةالغرض
CHARS_PER_TOKEN1.5نسبة أحرف-لكل-رمز محافظة
COMPACTION_THRESHOLD0.75تحفيز الضغط عندما يتجاوز الحمل هذه النسبة
COMPACTION_TARGET0.50الضغط إلى هذه النسبة — يترك مساحة للعمل
PROTECT_RECENT3أحدث رسائل لكل دور محمية من الضغط
MIN_COMPACTION_SIZE500الحد الأدنى للأحرف ليستحق الضغط
COMPACTION_RATIO0.25الاحتفاظ المتوقع بعد الاقتطاع (~25% من الأصل)
HEAD_RATIO / HEAD_CAP0.15 / 6,000مقتطع المقدمة: 15% من الأصل، بحد أقصى 6 آلاف حرف
TAIL_RATIO / TAIL_CAP0.08 / 3,000مقتطع الخاتمة: 8% من الأصل، بحد أقصى 3 آلاف حرف

تدفق الضغط

يعمل الضغط في خمس خطوات — كلها مباشرة على مصفوفة messages:

الخطوة 1: إزالة الصور

تُزال جميع الصور من نتائج الأدوات عبر مصفوفة الرسائل بالكامل. تُضاف علامة نصية إذا لم تكن موجودة:
[Screenshot analyzed — image omitted from context]
هذا يعمل أولًا لأن الصور (لقطات شاشة مُرمّزة بـ base64) هي أكبر مستهلك للمساحة ولا تحتاج للبقاء بعد التكرار الذي حُلّلت فيه.

الخطوة 2: حفظ الأصول

يُحفظ المحتوى الأصلي الكامل لكل هدف في الذاكرة قبل أي تعديل. تُستخدم هذه الأصول لبناء موجه التلخيص — يرى نموذج اللغة المحتوى الكامل حتى لو كانت مصفوفة الرسائل تحتوي بالفعل على نسخ مقتطعة.

الخطوة 3: الاقتطاع التناسبي

يُقتطع كل هدف مباشرة باستخدام حجم مقدمة وخاتمة تناسبي:
المقدمة: min(طول_المحتوى × 0.15, 6000) حرف
الخاتمة: min(طول_المحتوى × 0.08, 3000) حرف
تُدرج تسمية واضحة بين المقدمة والخاتمة:
[TRUNCATED — 45,000 chars original, 36,000 chars omitted,
showing first 6,000 + last 3,000 chars]
لرسائل المساعد، يُمسح reasoningContent (التفكير اُستهلك عند إنشاء الرد — لا حاجة له في السجل). يُحتفظ بـ toolUses ليرى النموذج الأدوات التي استدعاها. هذه الخطوة فورية — بدون استدعاءات LLM. المحتوى المقتطع يبقى في مصفوفة الرسائل بشكل دائم.

الخطوة 4: التلخيص بمرة واحدة

استدعاء LLM واحد يعالج جميع الأصول المحفوظة في ملخص محادثة منظم. موجه التلخيص يوجّه النموذج لإنتاج خمسة أقسام:
TASK: ما طلبه المستخدم أصلاً
PROGRESS: قائمة مرقمة بالخطوات المكتملة مع النتائج
REMAINING: ما يجب القيام به (عناصر محددة، أعداد، معرّفات)
DATA: قيم رئيسية — أسماء، إيميلات، تواريخ، معرّفات، أرقام، روابط، أخطاء
DECISIONS: أي قرارات أو تأكيدات تمت
يستخدم الاستدعاء thalamus.summarize()، الذي يمر عبر نفس سلسلة المزودين كبث الوكيل الرئيسي. إذا تجاوز المحتوى نافذة سياق نموذج التلخيص، يُقسّم إلى أجزاء، كل جزء يُلخّص على حدة، وتُدمج النتائج. يُعاد الاستدعاء حتى 5 مرات مع تراجع تصاعدي: 1ث → 2ث → 4ث → 8ث → 16ث.

الخطوة 5: حقن الملخص + تذكير المتابعة

تُدفع رسالة مستخدم على مصفوفة الرسائل تحتوي الملخص وتعليمات صريحة بالمتابعة: عند نجاح التلخيص:
[Compaction Summary]

TASK: قراءة جميع الإيميلات غير المقروءة وإنتاج موجز.
PROGRESS:
1. بحث في حسابين — وُجدت 43 غير مقروءة
2. قراءة الإيميلات 1-30 (دفعات من 15)
REMAINING:
1. 13 إيميل لم تُقرأ بعد: [المعرّفات]
2. الموجز النهائي لم يُنتج
DATA:
- طلب Keeta #KT-9284 تم التوصيل 2:43 صباحًا
- موعد Apple DPLA: 9 يوليو 2026
DECISIONS:
- المستخدم أكد تضمين جميع الحسابات

[Status: تم ضغط السياق. الرسائل أعلاه تحتوي نسخًا مقتطعة.
هذا الملخص يلتقط حالة المحادثة الكاملة. تابع من حيث توقفت.
لا تُنتج مخرجات نهائية حتى اكتمال جميع الخطوات.]
عند فشل التلخيص (استنفاد المحاولات):
[إشعار ضغط: تم ضغط السياق باقتطاع الرسائل القديمة. لم يتمكن
من إنشاء ملخص محادثة. راجع المحتوى المقتطع بعناية لإعادة بناء
ما تم إنجازه وما يتبقى. تابع من حيث توقفت. لا تُنتج مخرجات
نهائية حتى اكتمال جميع الخطوات.]
تذكير المتابعة هذا حاسم — بدونه، تعتبر النماذج باستمرار السياق المضغوط (الأقصر) “مكتملاً” وتُنتج مخرجات نهائية، متخطيةً عمل الدفعات المتبقي بصمت.

ملخصات ملفات المهام

عندما يُنفّذ الوكيل مهمة متعددة الخطوات، تُكتب كل نتيجة أداة في ملف المهمة بـ brain/motor/tasks/TASK-{id}.md. للحفاظ على هذه الملفات صغيرة بما يكفي لمكون RAS لفهرستها، تُكتب المخرجات كمعاينات من 2,000 حرف:
### Step 3: google_gmail_search
- **Output:** (9,450 chars) {"account":"younes@wolffi.sh","count":41,... [7,450 chars omitted]
المخرج الكامل يُكتب في سجل تفصيلي منفصل بـ TASK-{id}-detail.log. هذا الملف لا يُفهرس — موجود فقط للتشخيص.

تعليمات التجميع في وقت التشغيل

يتضمن قسم <runtime> المُلحق بكل موجه نظام تعليمة استشارية للتعامل مع مجموعات الأدوات الكبيرة:
<runtime>
  Tool iteration this turn: 3
  Tools called this turn: 15
  IMPORTANT: When a task requires calling a tool for each item in a set
  (e.g. reading N emails, fetching N pages), you MUST call the tool for
  EVERY item before producing final output. Batch 10-15 calls per response
  for efficiency, then continue with the remaining items in your next
  response. Metadata from search/list results is NOT a substitute for
  calling the per-item tool — if the task says "read all," call read for
  ALL, not just a subset.
</runtime>
هذه تعليمة ناعمة — النموذج يتبعها طوعيًا. تخدم غرضين:
  1. منع التوقف المبكر: بدونها، تميل النماذج لقراءة مجموعة فرعية واستخدام بيانات البحث الوصفية للباقي.
  2. تشجيع التجميع: باقتراح 10-15 استدعاء لكل استجابة، يُنتج النموذج دفعات يمكن لفحص الضغط التعامل معها.

حلقة الوكيل

يعمل الضغط في نقطة واحدة في حلقة الوكيل — قبل كل استدعاء لنموذج اللغة:
┌─────────────────────────────────────────────────────┐
│ compactOverflow(messages, lastKnownTokens)          │  ← قبل استدعاء النموذج
│   الطبقة 1: تقدير الأحرف > 75% من الميزانية؟       │
│   الطبقة 2: الرموز الفعلية > 75% من الميزانية؟     │
│   → إزالة الصور                                     │
│   → اقتطاع الأهداف تناسبياً (فوري)                  │
│   → استدعاء LLM واحد: تلخيص الأصول                  │
│   → حقن الملخص + تذكير المتابعة                     │
│   → onStarted يُصدر الأحداث والبطاقة                │
├─────────────────────────────────────────────────────┤
│ thalamus.stream(messages)                           │  ← استدعاء النموذج
│   → إذا خطأ فيضان 400: الطبقة 3 فرض + إعادة         │
├─────────────────────────────────────────────────────┤
│ تنفيذ استدعاءات الأدوات بالتتابع:                    │
│   أداة 1 → دفع النتيجة                               │
│   أداة 2 → دفع النتيجة                               │
│   ...                                               │
│   أداة N → دفع النتيجة                               │
│   → التكرار التالي (العودة للأعلى)                    │
└─────────────────────────────────────────────────────┘

ميزانيات نافذة السياق

ميزانية الإدخال هي نافذة سياق النموذج ناقص احتياطي رموز المخرجات:
النموذجنافذة السياقاحتياطي المخرجاتميزانية الإدخال
DeepSeek V4 Pro1,000,00032,768967,232
Claude Opus 4.6+1,000,0000 (منفصل)1,000,000
Claude Sonnet 4.61,000,0000 (منفصل)1,000,000
Grok 4.31,000,00065,536934,464
GPT-5400,00065,536334,464
GPT-4.11,000,00032,768967,232
Kimi K2262,14465,536196,608
Qwen 3.7 Max1,000,00065,536934,464

الأداء الفعلي

هكذا يبدو الضغط في مهمة حقيقية لقراءة 41 إيميلًا مع DeepSeek V4 Pro تحت النظام الجديد:
المرحلةالإيميلات المقروءةالسياق بعدهاالوقت
الاكتشاف47 ألف (5%)30 ثانية
الدفعة 115541 ألف (56%)22 ثانية
الضغط #1~170 ألف (اقتطاع + تلخيص)~25 ثانية
الدفعة 215627 ألف (65%)33 ثانية
الضغط #2~170 ألف (اقتطاع + تلخيص)~25 ثانية
الدفعة 311673 ألف (70%)44 ثانية
الإجمالي41/41~3 دقائق
ملاحظات رئيسية:
  • الضغط يعمل عند 75% — أبكر من النظام القديم (100%)، يمنع أي خطر فيضان
  • الضغط أصبح سريعاً — ~25 ثانية لكل جولة (استدعاء LLM واحد) مقابل 3-9 دقائق سابقًا
  • الضغط لم يعد يُهيمن على الوقت — النظام القديم أمضى 83% من وقت التشغيل في الضغط؛ النظام الجديد يُمضي ~15%
  • تذكير المتابعة يضمن قراءة جميع الـ41 إيميلًا — سابقًا كان النموذج قد يتوقف عند 30

النظام السابق مقابل الجديد

المقياسالسابقالجديد
عتبة التحفيز100% من الميزانية75% من الميزانية
الطريقةN استدعاء تلخيص LLM متوازياقتطاع فوري + استدعاء تلخيص واحد
الوقت لكل جولة3-9 دقائق~25 ثانية
المتابعةبدون تذكير (النموذج قد يتخطى العمل المتبقي)ملخص منظم + تذكير صريح
الحمايةآخر 3 نتائج أدوات، آخر 2 مساعد/مستخدمآخر 3 لكل دور + رسالة المستخدم الأولى
التكراريلا (الرسائل المضغوطة محمية)نعم (الملخصات القديمة قابلة لإعادة الضغط)

بطاقات الضغط

يعرض الضغط بطاقتين في واجهة الدردشة: أثناء الضغط — تظهر بطاقة compaction_started بشارة زرقاء نابضة تعرض عدد الرسائل الجاري ضغطها وعدد الأهداف المحددة. تختفي هذه البطاقة عند اكتمال الضغط. بعد الضغط — تحل بطاقة compaction محلها، وتعرض:
  • عدد الأهداف: كم رسالة تم ضغطها
  • الرموز الموفّرة: قياس قبل/بعد لتقدير الرموز
  • المدة: الوقت الفعلي لجولة الضغط
  • تفاصيل كل هدف (قابلة للتوسيع): اسم الأداة، الحجم الأصلي، الحجم بعد الضغط، نسبة التقليل، وطريقة الضغط

أحداث Corpus

يُصدر الضغط حدثين على ناقل أحداث corpus:
// يُطلق عند بدء الضغط (بعد تأكيد اختيار الأهداف أن العمل مطلوب)
corpus.emit('compaction.started', {
  messagesCount: 52,      // إجمالي الرسائل في السياق
  force: false             // true فقط عند إعادة محاولة خطأ 400
})

// يُطلق عند اكتمال الضغط
corpus.emit('compaction.applied', {
  tokensSaved: 467089,    // تقدير الرموز المستردة
  targetsCount: 8         // عدد الرسائل المضغوطة
})
يُرسل كلا الحدثين إلى الجدول الزمني في المُعرِّض عبر TURN_RELAYED_EVENTS ويُسجَّلان في سجل corpus اليومي بـ brain/corpus/YYYY-MM-DD.log.md.

التشخيص

لفحص سلوك الضغط:
  1. سجلات corpus (brain/corpus/YYYY-MM-DD.log.md): ابحث عن أحداث compaction.started و compaction.applied. إذا لم تظهر، لم يعمل الضغط — السياق كان ضمن 75% من الميزانية.
  2. لقطات التشخيص التنفيذية (brain/prefrontal/.debug/): كل تكرار يكتب لقطة تُظهر tokenCount و tokenBudget. قارن مع inputTokens من llm.response.
  3. بطاقات الضغط في الواجهة: وسّع التفاصيل لترى أي رسائل تم ضغطها وبكم وبأي طريقة.
  4. سجلات وحدة التحكم: يُسجّل الضاغط قراره في كل تكرار:
    [compactor] charEstimate=839203 lastKnown=541090 effective=839203
                budget=967232 threshold=725424 (75%)
                messages=52 sysChars=26000
                tools=47 force=false needsCompaction=false
    
  5. سجلات التفاصيل (brain/motor/tasks/TASK-{id}-detail.log): تحتوي المخرجات الكاملة. قارن مع معاينات الـ 2,000 حرف في ملف المهمة.
يعمل الضغط عند 75% من الميزانية بالتصميم — هذا يعطي النظام مساحة للاقتطاع والتلخيص قبل أن يصبح ضغط السياق حرجاً. إذا رأيت أحداث ضغط عندما يكون السياق عند 50-60% فقط، فهذه طبقة المعايرة (الطبقة 2) تستخدم بيانات الرموز الفعلية لاكتشاف أن التقدير الحرفي متفائل أكثر من اللازم. يضغط النظام بعدها إلى 50% من الميزانية، تاركًا مساحة كافية للنموذج لمواصلة العمل.

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

جميع التعديلات محلية لمصفوفة messages أثناء التشغيل داخل حلقة الوكيل. ملف المحادثة على القرص يحتفظ بالمحتوى الكامل غير المضغوط. عند إعادة تحميل المحادثة لدور جديد، يكون المحتوى الكامل متاحًا — ويعمل الضغط مجددًا من الصفر.