لتوليد واجهات الشاشة انطلاقًا من نظام التصميم، يجب بناء بوابة التحقق قبل النموذج
هذا المقال سجل لما تعلّمه فريق المنصة لدينا أثناء بناء نموذج يُولّد تلقائيًا واجهات لوحة تحكم لسطح المكتب باستخدام نظام تصميم THAKI (@thaki/tds). الجمهور المستهدف هو مهندسو الواجهة الأمامية وتعلّم الآلة الذين يحاولون استخلاص واجهات مستخدم من نموذج لغوي كبير. ولنبدأ بالخلاصة مباشرة: جودة توليد الشاشات لم يحددها حجم النموذج، بل البوابة التي تُترجم المخرجات فعليًا إلى كود وتتحقق منها.
بدلًا من تعليم القواعد النحوية عبر التدريب، فرضنا عبر القواعد ألا يخرج سوى مخرجات صالحة.
لماذا لم نُولّد الكود مباشرة
توليد كود JSX طويل دفعة واحدة من لقطة شاشة أو من متطلبات نصية نمط فشل معروف منذ زمن: تُحذف عناصر، وتتشوّه التسلسلات الهرمية، وينبسط التخطيط. نقلنا إلى واجهة المستخدم مبدأً مفاده الحفاظ على التمثيل العام وتركيز التعلّم القوي على الجزء الخاص بالمجال فقط. جعلنا النموذج لا يُولّد البكسلات ولا الكود مباشرة، بل يُنتج تمثيلين وسيطين فقط. أولًا، تمثيل UX-IR المستقل عن نظام التصميم يعبّر عن “ماذا تفعل هذه الشاشة”، ثم تمثيل TDS-IR يعبّر عن “بأي مكوّنات TDS تُترجم إلى واقع”. الكود الفعلي لا يملكه النموذج بل مترجم حتمي (deterministic compiler). وبما أن TDS يُنفّذ أصلًا الحشو والطباعة والتفاعل، لا يحتاج النموذج إلى إعادة تعلّم الأنماط البصرية، بل يكفي أن يتعلّم أي مكوّن يستخدم وبأي بنية.
أول ما بنيناه لم يكن النموذج، بل السجل والبوابة
حلّلنا مستودع tds_ssot واستخرجنا آليًا خصائص (props) وفتحات (slots) وقيم التعداد (enum) لواحد وستين مكوّنًا، إضافة إلى القواعد التي تحدد أي مكوّن يمكن أن يدخل في أي فتحة، وجمعنا كل ذلك في Component Registry. تدخل هنا قواعد مثل ضرورة أن يكون variant الخاص بـ Button واحدًا من ثمانية أنواع محددة، أو أن المكوّنات المسموح بها في فتحة control الخاصة بـ FormField محدودة سلفًا. من الاكتشافات اللافتة أن @thaki/tds ليس نظامًا للجوال، بل نظام لوحة تحكم لسطح المكتب يتمحور حول Table وDrawer وModal، وهذا هو سبب كون مسار العرض التوضيحي /desktop. لذلك ضبطنا أيضًا خرائط تحقيق أدوار تجربة المستخدم عبر مكوّنات TDS بما يتوافق مع أنماط لوحة التحكم.
جعلنا البوابة مملوكة من قبل الكود. فهي تتحقق حتميًا مما إذا كان المكوّن موجودًا فعلًا، وما إذا كانت الخاصية قيمة معرَّفة، وما إذا كانت الفتحة متوافقة، وما إذا كان العنصر القابل للنقر مرتبطًا بفعل، وما إذا كان زر primary غير مكرر. أن يُقرّ النموذج ذاتيًا بأنه “أنجز عملًا جيدًا” ليس تحققًا. التحقق هو أن يُصرَّف الكود بنجاح وأن يجتاز المخطط (schema).
هل تكفي 100 عينة من البيانات؟
كانت طلبات المستخدمين مؤلفة من 100 عينة فقط. ولا بد من الاعتراف بصراحة هنا: إجراء ضبط دقيق كامل لنموذج بحجم 27 مليار معامل باستخدام 100 عينة فقط يعني حتمًا فرط التخصيص (overfitting) وانهيار الأنماط (mode collapse). لذلك لم نستخدم المئة عينة كمجموعة تدريب لنموذج 27B، بل كبيانات تُثبت صحة خط الأنابيب. ولّدنا أنماط شاشات لوحة التحكم الفعلية عبر نحو احتمالي (probabilistic grammar)، واشتققنا أمثلة التدريب فقط من الشاشات التي اجتازت البوابة. من كل شاشة واحدة استخرجنا مهامًا متعددة: من النص إلى UX-IR، ومن النص إلى TDS-IR، ومن تعليمات التحرير إلى JSON Patch، والاسترجاع من IR خاطئ، إضافة إلى أزواج تفضيل تقرن نسخة جيدة بأخرى تالفة. من 20 شاشة بذرية من أصل 22 اجتازت البوابة، حصلنا على 100 مثال SFT و20 زوج تفضيل. التزمنا بمبدأ أن اجتياز التحقق القابل للتنفيذ أهم من كمية البيانات.
الرقم الأهم
أثمن نتيجة حصلنا عليها في هذا المشروع لم تكن منحنى التدريب، بل مقارنة القدرة التمييزية بين المقاييس. حذفنا مكوّنًا واحدًا من IR الصحيح وأفسدنا قيمة تعداد إحدى الخصائص، ثم قسنا مدى قدرة كل مقياس على رصد هذا التلف.
| المقياس | عادي | تالف | التغيّر |
|---|---|---|---|
| component F1 (التشابه) | 1.00 | 0.81 | لم ينخفض تقريبًا |
| valid-prop rate | 1.00 | 0.30 | انهيار |
| schema-valid rate | 0.93 | 0.23 | انهيار |
| compile-ok | 1.00 | 1.00 | لا استجابة |
كان المقياس القائم على التشابه متساهلًا، بينما لم يستجب نجاح التصريف (compile) إطلاقًا. بما أن المُصرِّف يُخرج حتى الخصائص الخاطئة كما هي دون اعتراض، فإن نجاح التصريف لا يعني جودة عالية. في المقابل، استجاب التحقق من المخطط وصلاحية الخصائص بدقة للتلف، فانخفضا إلى أقل من النصف. بعبارة أخرى، إذا اعتمدنا على التشابه أو نجاح التصريف وحدهما لضبط جودة توليد الشاشات، فإننا لن نستطيع تصفية المخرجات التي تبدو صحيحة تقريبًا لكنها في الواقع خاطئة. ما يصنع الجودة هو بوابة تُغلق الحلقة عبر التصريف والمخطط والفتحات والتفاعل.
جرّبنا التدريب، وفشلنا بصراحة
إذا كان ما سبق قصة الهيكل التنفيذي (harness)، فما يلي هو التدريب الفعلي. شغّلنا محرك البيانات وولّدنا 5924 مثال تدريب من 1185 شاشة اجتازت البوابة، وألحقنا LoRA بنموذج Qwen3.6-27B ودرّبناه على عنقود H200 الداخلي. تقارب التدريب نفسه بشكل جيد؛ انخفضت الخسارة إلى 0.07 ووصلت دقة الرموز (token accuracy) إلى 96%. ثم قدّمنا المحوّل (adapter) المدرَّب فعليًا للخدمة وقارنّا ما قبل التدريب بما بعده.
بصراحة، كانت النتيجة فشلًا. لم يُغيّر LoRA التشخيصي المكوَّن من 300 خطوة نتائج التوليد التلقائي إلا بالكاد. كان ملف الأخطاء متطابقًا فعليًا بين النموذج قبل التدريب وبعده. كلاهما ألحق خصائص الإدخال مباشرة بـ FormField، وألحق بـ Card عنوانًا (title) غير موجود فيه أصلًا. البنية الفعلية لنظام التصميم تجعل FormField يُغلّف الإدخال داخل فتحة متداخلة، لكن كلا النموذجين كان يتخيل بنية أكثر تسطيحًا.
عند تعمّق السبب وجدنا ثلاثة عوامل. أولًا، كان التدريب قصيرًا وتشبّعت دقة الرموز مبكرًا فضعفت إشارة التدرّج (gradient). والأهم من ذلك كان في البيانات: دالة realize التي ولّدت البيانات الاصطناعية لم تُظهر البنية المتداخلة الفعلية، فلم يكن بإمكان النموذج تعلّم عقد بيانات غير موجودة أصلًا في البيانات. وأخيرًا، كانت عادات نموذج 27B السابقة قوية.
هنا يبرز الدرس الثاني. المقاييس البديلة الثلاثة جميعها فاتتها جودة التوليد الحقيقية. لا نجاح التصريف، ولا دقة الرموز بأسلوب teacher-forcing، ولا حتى التغيّر في دقة الرموز بعد إلحاق LoRA، كان أي منها متسقًا مع معدل اجتياز البوابة. فقط بوابة التنفيذ المستندة إلى سجل كامل كشفت الفجوة الحقيقية المشتركة بين النموذجين. وفي هذه الأثناء اكتشفنا أن سجلنا نفسه كان ناقصًا وكان يُسقط مخرجات سليمة، فاستخرجنا خصائص 30 مكوّنًا بشكل شامل وصحّحناها.
فأصلحنا البيانات، وأعدنا التدريب، ووجدنا الجواب أخيرًا
أعدنا كتابة دالة realize لتُنتج البنية المتداخلة الفعلية. الآن يُغلّف FormField الإدخال داخل فتحة، ويستخدم كل مكوّن خصائصه الحقيقية. أنتجنا 2771 شاشة تجتاز البوابة بشكل كامل، واشتققنا منها 13855 مثال تدريب. وهذه المرة شغّلنا التدريب لفترة أطول بكثير، إذ نفّذنا 1500 تحديث أمثلة (optimization update).
ومع ذلك، ظل النموذج المدرَّب غير قادر على اجتياز البوابة. كانت النتائج قبل التدريب وبعده متطابقة تقريبًا مرة أخرى. عند هذه النقطة اتضح الأمر: عادة النموذج ذي 27 مليار معامل نحو البنى المسطّحة في هذه المسألة لا يمكن كسرها بحجم تدريب يمكننا تحمّله.
لذلك غيّرنا الاتجاه. بدلًا من إقناع النموذج، قررنا فرض التوليد نفسه. ولّدنا آليًا مخطط JSON من السجل، مخططًا لا يسمح إلا بأسماء المكوّنات الموجودة فعلًا وبالخصائص التي يملكها كل مكوّن فعليًا حصرًا، ويفرض البنية المتداخلة عبر التكرار (recursion). فرضنا هذا المخطط كقيد على فك التشفير (decoding constraint). كانت النتيجة واضحة: على نفس عيّنة الاختبار المحجوبة (held-out) المكوّنة من 40 حالة، ودون أي تدريب، ارتفع معدل اجتياز البوابة من 0% إلى 39 حالة، أي 97.5% (فاصل ثقة 95% بين 86.8% و99.9%).
مقارنة رباعية: ارتفع معدل اجتياز البوابة إلى 39/40 بالتقييد وحده دون أي تدريب. قيم مقاسة (n=40).
هذه المقارنة الثلاثية هي خلاصة هذا المشروع. كل من النموذج الأساسي دون أي معالجة والنموذج المضبوط دقيقًا سجّلا 0/40، بينما اجتاز النموذج الأساسي المقيَّد في فك التشفير وحده 39/40. عندما نطلب مخرجات مُهيكلة من نموذج ذي عادات قوية، فإن تقييد فك التشفير بقواعد كبوابة التنفيذ كان في هذا السياق أرخص وأكثر يقينًا من محاولة محو تلك العادات عبر الضبط الدقيق.
من هنا يجب أن نكون صادقين تمامًا. أولًا، النسبة 97.5% وليست 100%. الحالة الوحيدة التي لم تجتز البوابة أنتجت JSON مقطوعًا في منتصفه رغم فرض القيد. أي أن تقييد المخطط لا يضمن حتى إنتاج JSON قابلًا للتحليل بشكل كامل. ثانيًا، هذا الانتصار انتصار للصلاحية لا للجودة. تشابه المكوّنات (F1) مقارنة بالشاشة الصحيحة انخفض بالفعل قليلًا عند فرض القيد (من 0.317 إلى 0.292). كون المخرجات صالحة وكونها متوافقة مع النية المطلوبة أمران مختلفان، وهذه الفجوة هي الواجب الحقيقي للمرحلة التالية. ثالثًا، لا ينبغي أن نُسيء فهم سبب ارتفاع معدل الاجتياز. ظننا في البداية أن هذه النسبة العالية سببها أن المخطط يغطي معظم قواعد البوابة، لكن عندما قارنّا القاعدتين بحالات عدائية (adversarial cases) اتضح العكس. البوابة كانت أشد صرامة من المخطط. المخطط لا يمنع سوى أكثر خطأين شيوعًا (مكوّن غير موجود، خاصية غير موجودة)، بينما يسمح بمرور مخالفات مثل غياب schemaVersion أو وضع مكوّن خاطئ في فتحة. أما الذي يرصد تلك المخالفات فهو البوابة. إذن سبب فائدة التقييد هو أن المخطط أزال أشيع أنواع الفشل، وأن المخرجات المتبقية استوفت بالمصادفة القواعد الأشد صرامة للبوابة أيضًا. أما الخطر الحقيقي لكون النتيجة صالحة by-construction فهو أن المخطط والبوابة كلاهما مُشتق من السجل نفسه، وهذا لا يمكن استبعاده إلا عبر مُتحقق خارج السجل (تصيير React وفحص أنواع TypeScript).
الشاشة الصالحة لم تكن شاشة جيدة
لو توقفنا هنا لكانت الخلاصة “التقييد هو الحل”، لكن ذلك كان نصف الحقيقة فقط. حفرنا أعمق. استخرجنا عقد المتطلبات من الشاشات الصحيحة المحجوبة، وقسنا مدى استعادة كل طريقة للمحتوى الفعلي للشاشة. كانت النتيجة موجعة. التقييد سجّل أعلى صلاحية، لكنه سجّل أدنى استعادة لمعلومات المرجع ومكوّناته. اشترينا الصلاحية دون تدريب، لكن استعادة المحتوى كانت أدنى حتى من النموذج الأساسي دون أي معالجة. الأمر نفسه صحّ على طريقة repair التي تُصلح المخرجات الأساسية حتميًا دون GPU، وعلى طريقة وضع خطة حرة أولًا ثم فرض القيد عليها. كلتا الطريقتين قايضتا المحتوى بالصلاحية. كون المخرجات صالحة وكونها مطابقة للمتطلبات محوران مختلفان.
فقسنا أولًا أين يجب أن نستثمر
كان الدافع الغريزي التالي هو تكبير النموذج أو توليد عدة نسخ ثم الاختيار من بينها. لكننا سألنا أولًا: إذا ولّدنا ثماني مرات، هل توجد نسخة جيدة من بينها أصلًا؟ ولّدنا ثماني مخرجات مقيّدة لكل مطالبة (prompt) وقارنّا عيّنة واحدة بأفضل مرشّح، فتبيّن أن أفضل مرشّح يتفوّق بوضوح على تشابه العيّنة الواحدة: 0.29 مقابل 0.45. كانت الشاشات الجيدة تُولَّد بالفعل. ما كنا عاجزين عنه هو اختيار الجيد من بينها. لم تكن العقبة في التوليد بل في الاختيار.
المُنتقي لم يكن حكمًا، بل كان مبنيًا على المتطلبات
لذلك بنينا مُنتقيًا لا يعرف الإجابة الصحيحة سلفًا، وقسنا كم من ذلك السقف يستطيع فعلًا التقاطه.
من بين المُنتقين الذين لا يعرفون الإجابة الصحيحة، استعاد المُسنِد (predicate) القائم على المتطلبات 27% من هامش التحسين، بينما كان حَكَم النموذج اللغوي الكبير أسوأ من عدم الاختيار إطلاقًا. قيم مقاسة (n=40).
اختيار المرشّح الأغنى بنيويًا لم يكن مفيدًا تقريبًا. بل إن اختيار المرشّح الذي اجتاز البوابة أدى إلى نتيجة أسوأ. رأينا هنا مرة أخرى أن الاختيار بناءً على الصلاحية يُضعف المحتوى. سؤال النموذج الأساسي مباشرة عن الأفضل من بين المرشحين كان أسوأ من عدم الاختيار إطلاقًا. الشيء الوحيد الذي نجح كان استخراج ما يجب أن تحتويه الشاشة من المطالبة أولًا، ثم اختيار المرشّح الذي يلبّي ذلك المتطلب بأفضل شكل. هذا التقط ربع هامش تحسين الاختيار. لم يكن الحَكَم هو المُنتقي، بل كانت المتطلبات هي المُنتقي.
في ختام المشروع، تبقى أربعة دروس. أولًا، أول مكان يستحق الاستثمار ليس نموذجًا أكبر، بل الهيكل الذي يُنفّذ المخرجات فعليًا ويتحقق منها. ثانيًا، يجب أن يكون ذلك الهيكل دقيقًا حتى يظهر الفشل والنجاح بدقة. ثالثًا، عندما تتصادم العادات القوية مع الحاجة إلى مخرجات مُهيكلة، كان فرض القيد أرخص من دفع التدريب أكثر، على الأقل في هذه المسألة تحديدًا. لا نعمّم هذا على كل الحالات. رابعًا، الصلاحية والإخلاص للمحتوى محوران مختلفان، والعقبة المتبقية ليست في التوليد بل في الاختيار، وذلك المُنتقي يجب أن يكون قائمًا على المتطلبات لا على حَكَم. الخطوة التالية التي يجب إثباتها هي رفع مستوى المُنتقي والدقة الدلالية معًا عبر عقود متطلبات مكتوبة بشرية.
المراجع
نترك هنا المصادر الأصلية للتقنيات الخارجية التي استندت إليها هذه التجربة.
- الأساس النظري لفك التشفير المقيَّد (الطريقة التي انتصرت في النهاية): Willard, Louf, Efficient Guided Generation for Large Language Models. هي طريقة تُقيِّد فك التشفير عبر مخطط أو نحو (grammar) بحيث لا تخرج سوى مخرجات صالحة.
- المكتبة مفتوحة المصدر التي نفّذت هذه الطريقة فعليًا: Outlines (dottxt-ai/outlines).
- الضبط الدقيق الفعّال من حيث المعاملات الذي جرّبناه وفشل: Hu وآخرون، LoRA: Low-Rank Adaptation of Large Language Models.
- جذر best-of-N (توليد ثماني نسخ واختيار الأفضل من بينها) والاختيار القائم على المُتحقق: Cobbe وآخرون، Training Verifiers to Solve Math Word Problems.