بدء استخدام LiteRT-LM على Android

واجهة برمجة تطبيقات Kotlin الخاصة بحزمة LiteRT-LM لنظام التشغيل Android وJVM (Linux وmacOS وWindows) مع ميزات مثل تسريع وحدة معالجة الرسومات ووحدة المعالجة العصبية والوسائط المتعددة واستخدام الأدوات

مقدمة

في ما يلي نموذج لتطبيق محادثة على الجهاز الطرفي تم إنشاؤه باستخدام واجهة برمجة تطبيقات Kotlin:

import com.google.ai.edge.litertlm.*

suspend fun main() {
  Engine.setNativeMinLogSeverity(LogSeverity.ERROR) // Hide log for TUI app

  val engineConfig = EngineConfig(modelPath = "/path/to/model.litertlm")
  Engine(engineConfig).use { engine ->
    engine.initialize()

    engine.createConversation().use { conversation ->
      while (true) {
        print("\n>>> ")
        conversation.sendMessageAsync(readln()).collect { print(it) }
      }
    }
  }
}

عرض توضيحي لرمز Kotlin النموذجي

لتجربة المثال أعلاه، استنسِخ المستودع وشغِّله باستخدام example/Main.kt:

bazel run -c opt //kotlin/java/com/google/ai/edge/litertlm/example:main -- <abs_model_path>

تتوفّر نماذج .litertlm على منتدى HuggingFace LiteRT. كانت الصورة المتحركة أعلاه تستخدم نموذج Gemma3-1B-IT.

للحصول على عيّنة من Android، يمكنك تجربة تطبيق معرض Google AI Edge (متاح على Google Play).

بدء استخدام Gradle

على الرغم من أنّ LiteRT-LM تم تطويرها باستخدام Bazel، إلا أنّنا نوفّر حِزم Maven لمستخدمي Gradle أو Maven.

1. إضافة اعتمادية Gradle

dependencies {
    // For Android
    implementation("com.google.ai.edge.litertlm:litertlm-android:latest.release")

    // For JVM (Linux, macOS, Windows)
    implementation("com.google.ai.edge.litertlm:litertlm-jvm:latest.release")
}

يمكنك العثور على الإصدارات المتاحة على Google Maven في litertlm-android و litertlm-jvm.

يمكن استخدام latest.release للحصول على أحدث إصدار.

2. تهيئة المحرّك

Engine هي نقطة الدخول إلى واجهة برمجة التطبيقات. يجب تهيئته باستخدام مسار النموذج وإعداداته. تذكَّر إغلاق المحرّك لتحرير الموارد.

ملاحظة: قد تستغرق طريقة engine.initialize() وقتًا طويلاً (مثلاً، ما يصل إلى 10 ثوانٍ) لتحميل النموذج. يُنصح بشدة باستدعاء هذا الإجراء في سلسلة محادثات أو روتين فرعي في الخلفية لتجنُّب حظر سلسلة واجهة المستخدم.

import com.google.ai.edge.litertlm.Backend
import com.google.ai.edge.litertlm.Engine
import com.google.ai.edge.litertlm.EngineConfig

val engineConfig = EngineConfig(
    modelPath = "/path/to/your/model.litertlm", // Replace with your model path
    backend = Backend.GPU(), // Or Backend.NPU(nativeLibraryDir = "...")
    // Optional: Pick a writable dir. This can improve 2nd load time.
    // cacheDir = "/tmp/" or context.cacheDir.path (for Android)
)

val engine = Engine(engineConfig)
engine.initialize()
// ... Use the engine to create a conversation ...

// Close the engine when done
engine.close()

على نظام التشغيل Android، لاستخدام الخلفية المستندة إلى وحدة معالجة الرسومات، يجب أن يطلب التطبيق المكتبات الأصلية ذات الصلة بشكل صريح عن طريق إضافة ما يلي إلى AndroidManifest.xml داخل العلامة <application>:

  <application>
    <uses-native-library android:name="libvndksupport.so" android:required="false"/>
    <uses-native-library android:name="libOpenCL.so" android:required="false"/>
  </application>

لاستخدام الخلفية البرمجية NPU، قد تحتاج إلى تحديد الدليل الذي يحتوي على مكتبات NPU. على نظام التشغيل Android، إذا كانت المكتبات مجمّعة مع تطبيقك، اضبطها على context.applicationInfo.nativeLibraryDir. لمزيد من التفاصيل حول المكتبات الأصلية لوحدة المعالجة العصبية، يُرجى الاطّلاع على LiteRT-LM NPU.

val engineConfig = EngineConfig(
    modelPath = modelPath,
    backend = Backend.NPU(nativeLibraryDir = context.applicationInfo.nativeLibraryDir)
)

3- إنشاء محادثة

بعد إعداد المحرّك، أنشئ مثيلاً من Conversation. يمكنك تقديم ConversationConfig لتخصيص سلوكه.

import com.google.ai.edge.litertlm.ConversationConfig
import com.google.ai.edge.litertlm.Message
import com.google.ai.edge.litertlm.SamplerConfig

// Optional: Configure the system instruction, initial messages, sampling
// parameters, etc.
val conversationConfig = ConversationConfig(
    systemInstruction = Contents.of("You are a helpful assistant."),
    initialMessages = listOf(
        Message.user("What is the capital city of the United States?"),
        Message.model("Washington, D.C."),
    ),
    samplerConfig = SamplerConfig(topK = 10, topP = 0.95, temperature = 0.8),
)

val conversation = engine.createConversation(conversationConfig)
// Or with default config:
// val conversation = engine.createConversation()

// ... Use the conversation ...

// Close the conversation when done
conversation.close()

تنفّذ Conversation AutoCloseable، لذا يمكنك استخدام الحظر use لإدارة الموارد تلقائيًا للمحادثات التي تتم لمرة واحدة أو التي لا تدوم طويلاً:

engine.createConversation(conversationConfig).use { conversation ->
    // Interact with the conversation
}

4. إرسال الرسائل

تتوفّر ثلاث طرق لإرسال الرسائل:

  • sendMessage(contents): Message: مكالمة متزامنة يتم حظرها إلى أن يعرض النموذج ردًا كاملاً. هذا الإعداد أبسط للتفاعلات الأساسية بين الطلبات والردود.
  • sendMessageAsync(contents, callback): طلب غير متزامن لبث الردود. يكون هذا الخيار أفضل للطلبات التي تستغرق وقتًا طويلاً أو عندما تريد عرض الرد أثناء إنشائه.
  • sendMessageAsync(contents): Flow<Message>: استدعاء غير متزامن يعرض مسار Kotlin لبث الردود. هذه هي الطريقة التي ننصح بها لمستخدمي Coroutine.

مثال على التزامن:

import com.google.ai.edge.litertlm.Content
import com.google.ai.edge.litertlm.Message

print(conversation.sendMessage("What is the capital of France?"))

مثال غير متزامن مع ردّ الاتصال:

استخدِم sendMessageAsync لإرسال رسالة إلى النموذج وتلقّي الردود من خلال دالة ردّ الاتصال.

import com.google.ai.edge.litertlm.Content
import com.google.ai.edge.litertlm.Message
import com.google.ai.edge.litertlm.MessageCallback
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

val callback = object : MessageCallback {
    override fun onMessage(message: Message) {
        print(message)
    }

    override fun onDone() {
        // Streaming completed
    }

    override fun onError(throwable: Throwable) {
        // Error during streaming
    }
}

conversation.sendMessageAsync("What is the capital of France?", callback)

مثال غير متزامن باستخدام Flow:

استخدِم sendMessageAsync (بدون وسيطة معاودة الاتصال) لإرسال رسالة إلى النموذج وتلقّي الردود من خلال Kotlin Flow.

import com.google.ai.edge.litertlm.Content
import com.google.ai.edge.litertlm.Message
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch

// Within a coroutine scope
conversation.sendMessageAsync("What is the capital of France?")
    .catch { ... } // Error during streaming
    .collect { print(it.toString()) }

5- تعدُّد الوسائط

يمكن أن تحتوي عناصر Message على أنواع مختلفة من Content، بما في ذلك Text وImageBytes وImageFile وAudioBytes وAudioFile.

// Initialize the `visionBackend`, `audioBackend`, or both
val engineConfig = EngineConfig(
    modelPath = "/path/to/your/model.litertlm", // Replace with your model path
    backend = Backend.CPU(), // Or Backend.GPU() or Backend.NPU(...)
    visionBackend = Backend.GPU(), // Or Backend.NPU(...)
    audioBackend = Backend.CPU(), // Or Backend.NPU(...)
)

// Sends a message with multi-modality.
// See the Content class for other variants.
conversation.sendMessage(Contents.of(
    Content.ImageFile("/path/to/image"),
    Content.AudioBytes(audioBytes), // ByteArray of the audio
    Content.Text("Describe this image and audio."),
))

6. تحديد الأدوات واستخدامها

هناك طريقتان لتحديد الأدوات:

  1. باستخدام دوال Kotlin (يُنصح بها في معظم الحالات)
  2. باستخدام مواصفات Open API (التحكّم الكامل في مواصفات الأداة وتنفيذها)

تحديد الأدوات باستخدام دوال Kotlin

يمكنك تحديد دوال Kotlin مخصّصة كأدوات يمكن للنموذج استدعاؤها لتنفيذ إجراءات أو جلب معلومات.

أنشئ فئة تنفّذ ToolSet وأضِف تعليقات توضيحية إلى الطرق باستخدام @Tool وإلى المَعلمات باستخدام @ToolParam.

import com.google.ai.edge.litertlm.Tool
import com.google.ai.edge.litertlm.ToolParam

class SampleToolSet: ToolSet {
    @Tool(description = "Get the current weather for a city")
    fun getCurrentWeather(
        @ToolParam(description = "The city name, e.g., San Francisco") city: String,
        @ToolParam(description = "Optional country code, e.g., US") country: String? = null,
        @ToolParam(description = "Temperature unit (celsius or fahrenheit). Default: celsius") unit: String = "celsius"
    ): Map<String, Any> {
        // In a real application, you would call a weather API here
        return mapOf("temperature" to 25, "unit" to  unit, "condition" to "Sunny")
    }

    @Tool(description = "Get the sum of a list of numbers.")
    fun sum(
        @ToolParam(description = "The numbers, could be floating point.") numbers: List<Double>,
    ): Double {
        return numbers.sum()
    }
}

وراء الكواليس، تفحص واجهة برمجة التطبيقات هذه التعليقات التوضيحية وتوقيع الدالة لإنشاء مخطط على نمط OpenAPI. يصف هذا المخطط وظائف الأداة ومعلَماتها (بما في ذلك أنواعها وأوصافها من @ToolParam) ونوع القيمة التي تم إرجاعها إلى النموذج اللغوي.

أنواع المَعلمات

يمكن أن تكون أنواع المَعلمات التي تمّت إضافة التعليقات التوضيحية إليها باستخدام @ToolParam هي String أو Int أو Boolean أو Float أو Double أو List من هذه الأنواع (مثل List<String>). استخدِم الأنواع التي تقبل القيم الفارغة (مثل String?) للإشارة إلى المَعلمات التي تقبل القيم الفارغة. اضبط قيمة تلقائية للإشارة إلى أنّ المَعلمة اختيارية، واذكر القيمة التلقائية في الوصف في @ToolParam.

نوع القيمة التي يتم عرضها

يمكن أن يكون نوع القيمة التي تم إرجاعها لدالة الأداة أي نوع من أنواع Kotlin. سيتم تحويل النتيجة إلى عنصر JSON قبل إرسالها مرة أخرى إلى النموذج.

  • يتم تحويل أنواع List إلى مصفوفات JSON.
  • يتم تحويل أنواع Map إلى عناصر JSON.
  • يتم تحويل الأنواع الأساسية (String وNumber وBoolean) إلى نوع JSON الأساسي المقابل.
  • يتم تحويل الأنواع الأخرى إلى سلاسل باستخدام الطريقة toString().

بالنسبة إلى البيانات المنظَّمة، ننصح بعرض Map أو فئة بيانات سيتم تحويلها إلى عنصر JSON.

تحديد الأدوات باستخدام مواصفات OpenAPI

بدلاً من ذلك، يمكنك تحديد أداة من خلال تنفيذ فئة OpenApiTool وتقديم وصف الأداة كسلسلة JSON متوافقة مع مواصفات Open API. تكون هذه الطريقة مفيدة إذا كان لديك مخطط OpenAPI لأداتك أو إذا كنت بحاجة إلى تحكّم دقيق في تعريف الأداة.

import com.google.ai.edge.litertlm.OpenApiTool

class SampleOpenApiTool : OpenApiTool {

    override fun getToolDescriptionJsonString(): String {
        return """
        {
          "name": "addition",
          "description": "Add all numbers.",
          "parameters": {
            "type": "object",
            "properties": {
              "numbers": {
                "type": "array",
                "items": {
                  "type": "number"
                }
              },
              "description": "The list of numbers to sum."
            },
            "required": [
              "numbers"
            ]
          }
        }
        """.trimIndent() // Tip: trim to save tokens
    }

    override fun execute(paramsJsonString: String): String {
        // Parse paramsJsonString with your choice of parser or deserializer and
        // execute the tool.

        // Return the result as a JSON string
        return """{"result": 1.4142}"""
    }
}

تسجيل الأدوات

أدرِج أمثلة على أدواتك في ConversationConfig.

val conversation = engine.createConversation(
    ConversationConfig(
        tools = listOf(
            tool(SampleToolSet()),
            tool(SampleOpenApiTool()),
        ),
        // ... other configs
    )
)

// Send messages that might trigger the tool
conversation.sendMessageAsync("What's the weather like in London?", callback)

سيحدّد النموذج الوقت المناسب لاستخدام الأداة استنادًا إلى المحادثة. يتم تلقائيًا إعادة إرسال نتائج تنفيذ الأداة إلى النموذج لإنشاء الرد النهائي.

استخدام الأدوات يدويًا

بشكلٍ تلقائي، تنفّذ LiteRT-LM تلقائيًا طلبات استخدام الأدوات التي ينشئها النموذج، ويتم إرسال نتائج تنفيذ الأداة تلقائيًا إلى النموذج لإنشاء الرد التالي.

إذا أردت تنفيذ الأدوات يدويًا وإرسال النتائج إلى النموذج، يمكنك ضبط automaticToolCalling في ConversationConfig على false.

val conversation = engine.createConversation(
    ConversationConfig(
        tools = listOf(
            tool(SampleOpenApiTool()),
        ),
        automaticToolCalling = false,
    )
)

في حال إيقاف ميزة "استدعاء الأدوات تلقائيًا"، عليك تنفيذ الأدوات يدويًا وإرسال النتائج إلى النموذج في رمز تطبيقك. لن يتم استدعاء طريقة execute الخاصة بـ OpenApiTool تلقائيًا عند ضبط automaticToolCalling على false.

// Send a message that triggers a tool call.
val responseMessage = conversation.sendMessage("What's the weather like in London?")

// The model returns a Message with `toolCalls` populated.
if (responseMessage.toolCalls.isNotEmpty()) {
    val toolResponses = mutableListOf<Content.ToolResponse>()
    // There can be multiple tool calls in a single response.
    for (toolCall in responseMessage.toolCalls) {
        println("Model wants to call: ${toolCall.name} with arguments: ${toolCall.arguments}")

        // Execute the tool manually with your own logic. `executeTool` is just an example here.
        val toolResponseJson = executeTool(toolCall.name, toolCall.arguments)

        // Collect tool responses.
        toolResponses.add(Content.ToolResponse(toolCall.name, toolResponseJson))
    }

    // Use Message.tool to create the tool response message.
    val toolResponseMessage = Message.tool(Contents.of(toolResponses))

    // Send the tool response message to the model.
    val finalMessage = conversation.sendMessage(toolResponseMessage)
    println("Final answer: ${finalMessage.text}") // e.g., "The weather in London is 25c."
}

مثال

لتجربة استخدام الأداة، استنسِخ المستودع وشغِّله باستخدام example/ToolMain.kt:

bazel run -c opt //kotlin/java/com/google/ai/edge/litertlm/example:tool -- <abs_model_path>

خطأ أثناء المعالجة

يمكن أن تعرض طرق واجهة برمجة التطبيقات LiteRtLmJniException بسبب أخطاء من الطبقة الأصلية أو استثناءات Kotlin العادية، مثل IllegalStateException، بسبب مشاكل في مراحل النشاط. احرص دائمًا على تضمين طلبات البيانات من واجهة برمجة التطبيقات في كتل try-catch. ستُبلغ الدالة onError في MessageCallback أيضًا عن الأخطاء التي تحدث أثناء العمليات غير المتزامنة.