كنتُ أريد تحقيق هدفٍ معظمه تحدّيات هندسيّة: تشغيل نموذج لغوي بوزن يقارب 2 جيجابايت داخل متصفّح المستخدم مباشرةً؛ بحيث يقرأ الأسئلة باللغة العربية، ويسترجع الآيات والتفاسير ذات الصلة دون أن يرسل ببتًا واحدًا إلى أي خادم خارجي.
الفكرة في جوهرها ليست جديدة؛ فهي تتردد دائمًا في النقاشات التقنية حول "ماذا لو عمل الذكاء الاصطناعي محليًّا بالكامل؟". لكن الفجوة الحقيقية تكمن في تحويل هذه الفكرة النظرية إلى تجربة سلسة وتفاعلية تُنفَّذ على عتاد المستخدم النهائي.
هذه المقالة ليست منشورًا ترويجيًا، بل يوميات هندسية وتقنية. أستعرض فيها القرارات المعمارية التي اتخذتها، المشكلات التي واجهتني، الحلول التي اخترتها، والبدائل التي استبعدتها وأسباب استبعادها.
البداية: لماذا المتصفّح تحديدًا؟
السبب الأساسي لاختيار المتصفّح هو أنّه أوسع منصة انتشار ممكنة. رابط واحد فقط يتيح لأي مستخدم تشغيل التطبيق على أي جهاز بلا تثبيت، ولا تحديثات يدوية، ولا موافقات من متاجر التطبيقات.
مقابل هذه المرونة، يفرض المتصفّح قيودًا صارمة على مستوى الأمان والتطوير:
- سياسة الأصل المتقاطع (
same-origin policy)
- تفعيل الذاكرة المشتركة (
SharedArrayBuffer)
- قيود حصص التخزين المحلي (
Storage Quotas)
- دعم واجهات العتاد المتقدمة (
WebGPU)
جميع هذه القيود يمكن التعامل معها معماريةً، لكنها تتطلب تصميمًا دقيقًا.
البنية البرمجية الكاملة (The Tech Stack)
يوضح الجدول التالي المكونات الأساسية للبنية البرمجية للتطبيق، مع توضيح دور كل أداة والبدائل التي تم استبعادها:
| الأداة | الدور في المشروع | البديل المرفوض وسبب الرفض |
@litert-lm/core | بيئة تشغيل لنموذج Gemma 4 فوق WebGPU | llama.cpp/WASM (بطيء على المعالج المركزي دون GPU) |
| WebGPU | تسريع تسليم وتنفيذ عمليات النموذج | WASM تقليدي يقتصر على CPU |
quran-search-engine | محرك محلي للنص العثماني والمعالجة الصرفية | استدعاء APIs خارجية (ينقض الخصوصية) |
| Cross-Origin Storage | تخزين الأوزان معنونة بـ SHA ومشاركتها بين المواقع | الاعتماد على Cache API فقط |
| Cache API | طبقة تخزين احتياطية للمتصفح | — |
1. LiteRT-LM من Google — اختيار بيئة التشغيل
عند بناء نموذج محلي، تتصدر الخصوصية قائمة الأولويات. إرسال أسئلة المستخدم الخاصة بالقرآن وتدبره إلى خوادم خارجية يعارض الفلسفة الأساسية للمشروع.
عند تقييم خيارات التشغيل المحلي، برزت ثلاثة مسارات:
llama.cpp/WASM دون GPU: يعمل بشكل مستقر، لكن زمن توليد الرموز (Tokens) يعتمد كليًا على المعالج المركزي. في الأجهزة المتوسطة، قد يستغرق توليد إجابة قصيرة لآية واحدة زهاء 30 ثانية، وهو زمن غير عملي للتفاعل.
ONNX Runtime Web: ممتاز للنماذج الصغيرة (أقل من 500 ميجابايت)، لكنه يواجه صعوبة في كفاءة الأداء مع النماذج الكبيرة، كما يفتقر إلى الدعم المباشر لفك الترميز المقيّد (enableConstrainedDecoding).
LiteRT-LM من Google: يعتمد على WebGPU مباشرة، مما يتيح تشغيل أوزان Gemma 4 بإنتاجية تصل إلى عشرات الرموز في الثانية. والنقطة الأهم: يوفر ميزة فك الترميز المقيّد (enableConstrainedDecoding) كخاصية أساسية من الدرجة الأولى.
تكمن أهمية التقييد في منع النموذج من توليد نصوص حرة غير منتظمة، وإجباره على إخراج بيانات ببيان مفهرس (JSON Schema) صالح مباشرةً للتحليل. هذا يتفادى الحاجة لكتابة تعابير نمطية (Regex) معقدة أو معالجة الأخطاء غير المتوقعة في المخرجات.
// services/chat.ts — العقدة المركزية لتهيئة المحادثة مع تقييد المخرجات
const conversation = await engine.createConversation({
preface: { messages: [{ role: 'system', content }] },
enableConstrainedDecoding: true,
preface: { tools: [{ name: 'respond', parameters: JSON.parse(schema) }] },
});
بهذا الإعداد، يُجبر النموذج على الالتزام بـ Schema محددة صراحةً، مما يضمن أن مراحل الاسترجاع والعرض التالية تتلقى بيانات مهيكلة وموثوقة.
2. WebGPU — الاستفادة من معالجة الرسوميات
تعتمد بيئة LiteRT-LM على واجهة WebGPU المدمجة في المتصفحات الحديثة. المعالجة عبر المعالج المركزي (CPU) حتى باستخدام WASM المحسّن تعجز عن تشغيل أوزان بمليارات المعلمات بالسرعة المطلوبة للتفاعل اللحظي. باستخدام WebGPU، تُنقل العمليات المترية الثقيلة إلى كارت الرسوميات (GPU)، مما يخفض زمن توليد الرمز إلى بضع عشرات من المللي ثانية.
يقوم التطبيق بفحص دعم WebGPU فور تشغيله في أول 200 مللي ثانية؛ وفي حال عدم توفره، يتم إعلام المستخدم بوضوح عبر الواجهة بـ WebGPU: unsupported.
من النقاط المعمارية المهمة عند التعامل مع WebGPU:
- لا دعم لـ
AbortController المباشر: الدالة Engine.create غير قابلة للإلغاء المباشر في منتصف الطريق أثناء حجز ذاكرة الـ VRAM وتجميع الشيدرات.
- إدارة التزامن وسباق الحالات: إذا بدأ تحميل نموذج ثم غير المستخدم رأيه أو اختار نموذجًا آخر، تستمر عملية التهيئة الأولى في الخلفية حتى تنتهي.
- الحل المعماري: استخدام عداد الأجيال (Generation Counter)؛ حيث نتحقق بعد اكتمال التهيئة من مطابقة طلب المستخدم الحالي. إذا تغير العداد وكان الطلب قديمًا، يتم التخلص من الكائن المسترجع بـ
engine.dispose() فورًا لتحرير ذاكرة الـ GPU وتجنب تداخل الحالات (State Race Conditions).
3. Gemma 4 E2B و E4B — موازنة الحجم والأداء
تم اعتماد نموذجين من عائلة Gemma 4 المخصصة لـ LiteRT-LM:
| المعرف | النموذج | الحجم التقريبي | الاستخدام |
e2b | gemma-4-E2B-it-litert-lm | 1.87 جيجابايت | الخيار الافتراضي لسرعة الاستجابة |
e4b | gemma-4-E4B-it-litert-lm | 2.77 جيجابايت | خيار إضافي لدقة أعلى |
تم تسجيل أوزان النموذجين في التطبيق مع بصمات SHA-256 المستخرجة من مؤشر LFS في HuggingFace لضمان السلامة.
تجنب استخدام النماذج الأكبر (مثل 7B) يعود لعدة أسباب هندسية:
- استهلاك الذاكرة العشوائية: حساب بصمة SHA-256 للملفات الكبيرة يتطلب وضع الملف في الذاكرة، مما قد يسبب استهلاكًا مرتفعًا عند التحميل.
- قيود التخزين المحلي: تتنافس أوزان النموذج مع قيود التخزين المتاحة للمتصفح (
Cache API Quotas).
- زمن الإقلاع: يستغرق نموذج E2B نحو 15 ثانية للإقلاع في المرة الأولى، بينما يتضاعف الوقت مع النماذج الأكبر.
- كفاية المهمة: النصوص القرآنية والتفاسير المرفقة لا تتطلب نماذج ضخمة للاستدلال؛ حيث يغطي نموذج E2B الغالبية العظمى من استفسارات البحث والتفسير بكفاءة عالية.
4. Cross-Origin Storage — مشاركة الأوزان عبر المواقع
تعد واجهة navigator.crossOriginStorage خيارًا معوّلًا عليه لمشاركة الملفات الضخمة المعنونة بـ SHA-256 بين المواقع المختلفة دون الحاجة لتكرار تنزيلها.
المعنى العملي لهذه الميزة:
- تحميل نموذج Gemma 4 مرة واحدة يتيح استخدامه في أي موقع آخر يطلب نفس البصمة (
SHA-256).
- تخفيف العبء عن ذاكرة المتصفح المحلية المخصصة لموقع واحد.
- اعتبار الملف ملكًا لبيئة المتصفح ككل وليس لموقع مفرد.
نظرًا لأن الواجهة لم تصبح مدمجة افتراضيًا في جميع المتصفحات في 2026، يعتمد التطبيق عليها عند توفرها (عبر إضافات Firefox و Chrome المخصصة)، ويتراجع تلقائيًا لاستخدام Cache API عند عدم توفرها.
قاعدة أساسية: تجنب إعادة قراءة الملفات من COS
عند التعامل مع الملفات المسترجعة من Cross-Origin Storage (COS):
قراءة الملف عبر file.arrayBuffer() لحساب SHA-256 تؤدي إلى استهلاك مجرى البيانات (Data Stream) مرة واحدة. تمرير نفس الكائن لاحقًا إلى Engine.create يجعل المحرك يقرأ ملفًا فارغًا مما يسبب توقف الإجراء وتعليق المحرك.
الحل المعماري:
تعتبر الملفات المسترجعة من COS موثوقة لأن المتصفح أو الإضافة يتحققان من البصمة عند التخزين عبر writable.close(). لذلك يقتصر حساب SHA-256 المحلي على الملفات المحملة عبر Cache API أو التنزيلات الجديدة، وتتخطى ملفات COS هذه المرحلة.
5. النزاهة باستخدام WebCrypto SHA-256
للتحقق من سلامة الأوزان والتأكد من عدم تلفها أثناء التنزيل أو التخزين، يُستخدم المحرك المدمج window.crypto.subtle.digest('SHA-256', buffer):
- قبل التخزين: تجري تجزئة كاملة للملف المحمل ومقارنته بالمفتاح المعتمد.
- عند التحميل: يُعاد التحقق من الملفات المخزنة في
Cache API أثناء إقلاع التطبيق (داخل useAppBootstrap).
- عند حدوث عدم تطابق: يتم رفض الملف وحذفه فورًا لتجنب تمرير أوزان غير صالحة إلى محرك التشغيل.
6. Vite وترويسات أمان COOP/COEP
يتطلب تشغيل WASM متعدد الخيوط (Multi-threaded WASM) تفعيل الذاكرة المشتركة SharedArrayBuffer. ولأسباب أمنية في المتصفحات، لا تتفعل هذه الميزة إلا عند تطبيق سياسة عزل الأصل المتقاطع (Cross-Origin Isolation).
تم ضبط الترويسات التالية في ملف التكوين:
// vite.config.ts
export default defineConfig({
server: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Resource-Policy': 'cross-origin',
},
},
});
في بيئة الإنتاج، يتم ضبط هذه الترويسات على مستوى منصة الاستضافة (مثل Cloudflare Pages عبر _headers, أو Netlify, أو Vercel عبر vercel.json, أو Firebase Hosting).
ملاحظة تشغيلية: GitHub Pages لا يستطيع استضافة هذا المشروع بشكل مباشر لعدم وجود آلية لتعديل ترويسات الاستجابة هناك، والحل الوحيد هو وضع موكِّل (Proxy) مثل Cloudflare أمامه.
يمكن التأكد من تفعيل العزل عبر تشغيل الأوامر التالية في نافذة المتصفح:
crossOriginIsolated // يُرجع true
typeof SharedArrayBuffer === 'function' // يُرجع true
9. معالجة البحث القرآني محليًا (quran-search-engine)
تُستخدم حزمة quran-search-engine لتوفير النص القرآني العثماني مجردًا ومعالجًا صرفيًا داخل المتصفح.
تم بناء طبقة مغلفة (Wrapper) في services/quran-search.ts فوق الحزمة تقدم الميزات التالية:
- التحميل الكسول (Lazy Loading): تحميل ملفات النص والصرف (700 كيلوبايت) بالتوازي عند إجراء أول عملية بحث فقط.
- استرجاع متعدد المراحل:
- المطابقة التامة للنص.
- البحث بالحروف الأصلية (Lemma) والجذور.
- البحث الضبابي (Fuzzy Search عبر Arabic Fuse) مع تخزين LRU بـ 50 مدخلًا.
- تجريد السوابق واللواحق (مثل البحث عن "ذو النون" لاسترجاع المواضع التي وردت فيها "ذا النون").
سبب تجنب خوادم MCP للبحث
بدلًا من ترك النموذج يستدعي أدوات بحث خارجية عبر بروتوكولات مثل MCP، يعتمد التطبيق على آلية حتمية:
- يُخرج النموذج الكلمات أو الألقاب ذات الصلة (مثال: الألقاب المرتبطة بالأنبياء).
- يتولى العميل محليًا البحث في النص القرآني المعتمد بناءً على تلك الكلمات.
هذا الفصل يضمن حتمية النتائج، ويمنع هلوسة النصوص القرآنية، ويحافظ على استقلالية التطبيق الكاملة عن أي خوادم خارجية.
10. ضبط المخرجات: Constrained Decoding
تعتمد جودة إجابات التطبيق على توجيه مزدوج لضمان المضمون والشكل:
1. توجيه المضمون ( في data/prompts.ts)
تُحدد تعليمات النظام قواعد الاستدلال والتفسير: تمنع استخراج الألفاظ كمرادفات لنفسها (مثل إرجاع "ذو النون" كلقب عند البحث عن "يونس")، وتمنع استخدام حقل الجذر اللغوي، وتلزم النموذج بالاعتماد على التفاسير المعتمدة دون إدخال ألفاظ حديثة.
2. توجيه الشكل (Constrained Decoding)
تُمرر هيكلية JSON Schema صريحة لأداة respond لتحديد شكل المخرجات المطلوبة:
{
"type": "object",
"properties": {
"context": { "type": "string" },
"related_words": {
"type": "array",
"items": {
"type": "object",
"properties": {
"term": { "type": "string" },
"note": { "type": "string" }
},
"required": ["term", "note"],
"additionalProperties": false
}
},
"note": { "type": "string" }
},
"required": ["context", "related_words"],
"additionalProperties": false
}
معالجة الاستثناءات عبر extract-json.ts
في حال تعذر تطبيق التقييد الصارم لأي سبب تقني في الـ Runtime، توجد طبقة فحص احتياطية تستخرج نصوص JSON من المخرجات وتحللها (fenced JSON, balanced brackets) مع التحقق من مطابقتها للأنواع المطلوبة عبر Type-guard المسمى QuranicReply.
الخيارات المعمارية المرفوضة — ولِمَ؟
- النماذج السحابية (OpenAI / Anthropic): استُبعدت تمامًا لأنها تنقض مبدأ الخصوصية الأساسي للمشروع.
- تضمين خوادم خارجية للبحث (MCP): استُبعدت لتجنب إدخال أي طبقة تشغيلية خارج المتصفح أو الاعتماد على خوادم.
- استضافة GitHub Pages المباشرة: استُبعدت لعدم إمكانية ضبط ترويسات
COOP/COEP الضرورية لـ SharedArrayBuffer.
الخلاصة
يقدم مشروع QuranLM نموذجًا لتطبيقات الذكاء الاصطناعي التي تعمل محليًا بالكامل داخل المتصفح، حيث يجمع بين:
- معالجة لغوية واستدلالية باستخدام أوزان محملة على GPU الجهاز.
- محرك بحث محلي وسريع للنص القرآني استجاباته أقل من 50ms.
- ضبط صارم للمخرجات باستخدام JSON Schema والمِحراب.
- حماية كاملة لخصوصية المستخدم دون نقل أي بيانات خارج جهازه.
النمط الهندسي المستخدم هنا يمكن تطبيقه على مجالات متعددة تتطلب خصوصية عالية واستجابة سريعة (مثل التطبيقات الطبية، القانونية، أو الملاحظات الشخصية).
الموارد والمراجع