بث مباشر 18.11 · الدفعة 318 نوفمبر، 19:00 بتوقيت موسكو — ورشة عملية مجانية لوكلاء الذكاء الاصطناعي · الدفعة 3 تبدأ 23.11ورشة 18 نوفمبر ←الدفعة 3 (23.11)
إصدارتحليل · على الرادارHabr AI→ المصدر

مطوّر يبني عميل Ollama بـ KMP: خطآن في التسلسل والبث

عرض مطوّر كيفية ربط تطبيق جوال مبني بـ Kotlin Multiplatform بخادم Ollama محلي: واجهة مشتركة بـ Compose، وKoin للتبعيات، وKtor للشبكة. يُفتح الخادم أمام الهواتف في الشبكة بمتغير واحد هو OLLAMA_HOST=0.0.0.0. وذهب معظم الوقت في خطأين خفيّين: اختفاء الحقل stream، ووصول الرد كاملاً دفعة واحدة بدلاً من تدفق. ويُحَل كلاهما بسطرين إذا عرفتَ أين تبحث.

معالج بواسطة الذكاء الاصطناعي من Habr AI؛ بتحرير Hamidun News
مطوّر يبني عميل Ollama بـ KMP: خطآن في التسلسل والبث
المصدر: Habr AI. كولاج: Hamidun News.
استمع للمقال
بطاقة الحقائق والأرقام
المعيارالتفاصيل
المصدر الأصليHabr AI ↗
رئيس التحرير
حقيقة رئيسية #1الإعداد encodeDefaults = true يعيد stream=false إلى طلب JSON
حقيقة رئيسية #2للتدفق تحتاج preparePost().execute وليس post() العادية
لاستخدام HTTPusesCleartextTraffic في أندرويد، وNSAllowsArbitraryLoads في iOS

أصبح تشغيل نموذج محلي على حاسوب محمول أمراً سهلاً اليوم. أما الأصعب فهو بناء عميل جوال جيد له يعمل على أندرويد وآيفون معاً دون أن يضطرك إلى كتابة الواجهة مرتين. سلك كاتب مقال على Habr هذا الطريق بلغة Kotlin Multiplatform ووصف بصدق المواضع التي تعثّر فيها. وهذا أنفع لمن ينوي فعل الأمر نفسه من أي نظرية.

الخادم: ثلاثة أوامر ومتغير واحد

يُثبَّت Ollama عبر brew أو عبر سكربت التثبيت على Ubuntu. افتراضياً يستمع الخادم إلى localhost فقط، لذا لا يستطيع المحاكي ولا الهاتف الحقيقي في الشبكة نفسها الوصول إليه. ولفتح الوصول يُشغَّل الخادم بالمتغير OLLAMA_HOST=0.0.0.0:11434. بعد ذلك يُنزَّل النموذج عبر ollama pull، وعلى أجهزة ماك بشرائح Apple يستخدم الكاتب إصدارات MLX، مثل gemma4:12b-mlx. ويُظهر الأمر ollama ps أن النموذج موجود بالكامل في ذاكرة وحدة الرسوميات (في المخرجات: PROCESSOR 100% GPU).

يمكن فحص الخادم بأمر curl عادي على /api/generate. وإذا كان الطلب يحتوي على stream: false فسيصل الرد في JSON واحد.

العميل: Compose وKoin وKtor

مشروع KMP بواجهة مشتركة: يتولى Koin التبعيات وViewModel، وKtor الشبكة، وNavigation3 التنقل. اختار الكاتب composable KoinApplication بدلاً من startKoin العام. تُنشأ الحاوية داخل الدالة App()، ولذلك لا يتعارض @Preview في Android Studio مع حالة عامة، ولا حاجة لتعديل شيفرة Swift التي ولّدها المحرر. وثمن هذا القرار أن جزءاً من الواجهة البرمجية غير متاح، ومنه by inject() وby viewModel() وandroidContext(). أما كيفية تقسيم تطبيق كبير إلى وحدات بهذا الأسلوب فلم يتوصل الكاتب إليها بعد.

ولا غنى عن Xcode وmacOS للبناء على iOS. فـ KMP يوفّر عليك كتابة الواجهة بـ Swift، لكنه لا يعفيك من أدوات Apple.

فخّان كلّفا ساعات

يتعلق الفخ الأول بالمُسلسِل. فمكتبة Kotlinx.Serialization لا تكتب افتراضياً في JSON الحقول ذات القيم الافتراضية. فإذا كانت لديك stream = false، يختفي الحقل ببساطة من الطلب، وعندما يغيب يفعّل Ollama وضع البث. والنتيجة أنك تنتظر ردّاً واحداً فتتلقى سيلاً من أسطر JSON. ويُعالَج ذلك بالإعداد Json { encodeDefaults = true }. وقد ساعد Network Inspector في Android Studio على اكتشاف السبب.

يتعلق الفخ الثاني بالبث نفسه. كان الرد يصل كاملاً في النهاية، وظن الكاتب في البداية أن السبب تخزين مؤقت في OkHttp، بل وزّع الشيفرة على المنصات عبر expect/actual. لكن السبب كان مختلفاً: بدلاً من post() يجب استخدام preparePost() وقراءة الجسم عبر execute وbodyAsChannel سطراً بسطر. وكل سطر هو جزء JSON مستقل يحوي الحقول message وdone وerror. وفي العميل يتحول كل ذلك إلى Flow من الأحداث: Token وThinking وDone وError. وإلغاء الكوروتين يغلق الاتصال ويوقف التوليد. أما رموز التفكير (Thinking) فتصل منفصلة إن كان النموذج يرسلها.

وثمة تفصيل آخر: في /api/chat يكون الدور إلزامياً لكل رسالة، بينما لا توجد أدوار في /api/generate، إذ يُمرَّر prompt فقط.

ما يجب مراعاته عملياً

إذا كان الخادم يعمل عبر HTTP فيلزم usesCleartextTraffic="true" في أندرويد، وفي iOS يجب إضافة NSAllowsArbitraryLoads إلى Info.plist. غير أن محاكي آيفون في تجربة الكاتب مرّر الطلبات حتى بدون ذلك. وفي بيئة الإنتاج لا يجوز استخدام HTTP غير مشفّر ولا خادم على 0.0.0.0 بلا مصادقة: فالنص لا يتطرق إلى الحماية بكلمة، ولذلك فهذه بيئة منزلية أو داخلية للتجربة وليست بنية جاهزة.

لا يزال المشروع خاماً: الواجهة بحاجة إلى صقل، ولا يوجد سجل للمحادثات. ويضيف الكاتب أن عميلاً للنماذج السحابية من cloud.ru يدعم الصور والصوت قد كُتب بالفعل. واستغرقت مهمة إعادة تسمية المشروع نحو خمس دقائق من النموذج وحوالي 50 روبلاً، لكن السعر يعتمد كثيراً على النموذج المختار.

الخلاصة للمطوّر: إذا كنت تبني عميلاً جوالاً لـ Ollama، ففعّل encodeDefaults فوراً، واستخدم preparePost للتدفقات من البداية، ولا تبحث عن المشكلة في OkHttp. المصادر متاحة على GitHub (app-z/OllameChatKmp) ويمكن اتخاذها قالباً للانطلاق.

القصة

أسئلة وأجوبة حول الخبر

ما أبرز التفاصيل حول الخادم: ثلاثة أوامر ومتغير واحد؟

يُثبَّت Ollama عبر brew أو عبر سكربت التثبيت على Ubuntu. افتراضياً يستمع الخادم إلى localhost فقط، لذا لا يستطيع المحاكي ولا الهاتف الحقيقي في الشبكة نفسها الوصول إليه. ولفتح الوصول يُشغَّل الخادم بالمتغير OLLAMA_HOST=0.0.0.0:11434. بعد ذلك يُنزَّل النموذج عبر ollama pull، وعلى أجهزة ماك بشرائح Apple يستخدم الكاتب إصدارات MLX، مثل gemma4:12b-mlx. ويُظهر الأمر ollama ps أن النموذج موجود بالكامل في ذاكرة وحدة الرسوميات (في المخرجات: PROCESSOR 100% GPU). يمكن فحص الخادم بأمر curl عادي على /api/generate.

ما أبرز التفاصيل حول العميل: Compose وKoin وKtor؟

مشروع KMP بواجهة مشتركة: يتولى Koin التبعيات وViewModel، وKtor الشبكة، وNavigation3 التنقل. اختار الكاتب composable KoinApplication بدلاً من startKoin العام. تُنشأ الحاوية داخل الدالة App()، ولذلك لا يتعارض @Preview في Android Studio مع حالة عامة، ولا حاجة لتعديل شيفرة Swift التي ولّدها المحرر. وثمن هذا القرار أن جزءاً من الواجهة البرمجية غير متاح، ومنه by inject() وby viewModel() وandroidContext(). أما كيفية تقسيم تطبيق كبير إلى وحدات بهذا الأسلوب فلم يتوصل الكاتب إليها بعد.

ما أبرز التفاصيل حول فخّان كلّفا ساعات؟

يتعلق الفخ الأول بالمُسلسِل. فمكتبة Kotlinx.Serialization لا تكتب افتراضياً في JSON الحقول ذات القيم الافتراضية. فإذا كانت لديك stream = false، يختفي الحقل ببساطة من الطلب، وعندما يغيب يفعّل Ollama وضع البث. والنتيجة أنك تنتظر ردّاً واحداً فتتلقى سيلاً من أسطر JSON. ويُعالَج ذلك بالإعداد Json { encodeDefaults = true }. وقد ساعد Network Inspector في Android Studio على اكتشاف السبب. يتعلق الفخ الثاني بالبث نفسه.

ZK
جمال حميدون — مدير المنتجات في Alpina Digital ومدير منتج AlpinaGPT. يبني منتجات الذكاء الاصطناعي منذ 2019. بدأ Hamidun News كمشروع شخصي ثم أصبح مُجمِّعًا إنتاجيًا بأكثر من 6000 قارئ.
✦ Unified AI Workspace · Nomad

Test Frontier LLMs & Autonomous Agents in One Workspace

Run Claude Opus 5.5, GPT-6, Gemini 3.1, and DeepSeek side-by-side in Nomad AI (nomadnet.ai) — unified model routing, deep research, and multimodal workflows under one subscription.

Try Nomad AI →Launch Nomad Agent
ما رأيك؟
جارٍ تحميل التعليقات…