איך מתחילים להשתמש ב-LiteRT-LM ב-Android

‫Kotlin API של LiteRT-LM ל-Android ול-JVM (Linux,‏ MacOS,‏ Windows) עם תכונות כמו האצה של GPU ו-NPU,‏ multi-modality ושימוש בכלים.

מבוא

הנה דוגמה לאפליקציית צ'אט לטרמינל שנבנתה באמצעות Kotlin API:

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 Gallery.

תחילת העבודה עם 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 היא נקודת הכניסה ל-API. מאחלים אותו עם נתיב המודל וההגדרה. חשוב לזכור לסגור את המנוע כדי לשחרר משאבים.

הערה: יכול להיות שיעבור זמן רב עד שהמודל ייטען בשיטה engine.initialize() (למשל, עד 10 שניות). מומלץ מאוד להפעיל את הפונקציה הזו ב-thread או ב-coroutine ברקע כדי למנוע חסימה של ה-thread של ממשק המשתמש.

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, כדי להשתמש בעורף ה-GPU, האפליקציה צריכה לבקש באופן מפורש את הספריות המקומיות התלויות על ידי הוספת הקוד הבא ל-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>

כדי להשתמש ב-backend של NPU, יכול להיות שתצטרכו לציין את הספרייה שמכילה את ספריות ה-NPU. ב-Android, אם הספריות מצורפות לאפליקציה, צריך להגדיר את הערך context.applicationInfo.nativeLibraryDir. פרטים נוספים על ספריות מקוריות של NPU זמינים במאמר בנושא 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 implements AutoCloseable, so you can use the use block for automatic resource management for one-shot or short-lived conversations:

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

4. שליחת הודעות

יש שלוש דרכים לשלוח הודעות:

  • sendMessage(contents): Message: קריאה סנכרונית שנחסמת עד שהמודל מחזיר תשובה מלאה. השיטה הזו פשוטה יותר לאינטראקציות בסיסיות של בקשה/תגובה.
  • sendMessageAsync(contents, callback): קריאה אסינכרונית להזרמת תגובות. האפשרות הזו מתאימה יותר לבקשות ארוכות או כשרוצים להציג את התשובה בזמן שהיא נוצרת.
  • sendMessageAsync(contents): Flow<Message>: קריאה אסינכרונית שמחזירה Kotlin Flow לתגובות סטרימינג. זו הגישה המומלצת למשתמשי Coroutine.

דוגמה סינכרונית:

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

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

דוגמה אסינכרונית עם קריאה חוזרת (callback):

משתמשים ב-sendMessageAsync כדי לשלוח הודעה למודל ולקבל תשובות באמצעות קריאה חוזרת (callback).

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` and/or the `audioBackend`
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()
    }
}

מאחורי הקלעים, ה-API בודק את ההערות האלה ואת חתימת הפונקציה כדי ליצור סכימה בסגנון OpenAPI. הסכימה הזו מתארת למודל השפה את הפונקציונליות של הכלי, את הפרמטרים שלו (כולל הסוגים והתיאורים שלהם מתוך @ToolParam) ואת סוג הערך המוחזר.

סוגי פרמטרים

הסוגים של פרמטרים שמסומנים בהערה @ToolParam יכולים להיות String,‏ Int,‏ Boolean,‏ Float,‏ Double או List של הסוגים האלה (לדוגמה, List<String>). משתמשים בסוגים שניתנים לאיפוס (למשל, String?) כדי לציין פרמטרים שניתן להגדיר להם ערך null. מגדירים ערך ברירת מחדל כדי לציין שהפרמטר הוא אופציונלי, ומציינים את ערך ברירת המחדל בתיאור ב-@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/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>

טיפול בשגיאות

שיטות API יכולות להחזיר את השגיאה LiteRtLmJniException עבור שגיאות מהשכבה המקורית או חריגים סטנדרטיים של Kotlin כמו IllegalStateException עבור בעיות במחזור החיים. תמיד כדאי להוסיף קריאות ל-API בבלוקים של try-catch. הקריאה החוזרת (callback) onError ב-MessageCallback תדווח גם על שגיאות במהלך פעולות אסינכרוניות.