Pierwsze kroki z LiteRT-LM na Androidzie

Interfejs Kotlin API LiteRT-LM na AndroidaJVM (Linux, MacOS, Windows) z funkcjami takimi jak akceleracja GPU i NPU, wielomodowośćkorzystanie z narzędzi.

Wprowadzenie

Oto przykładowa aplikacja do obsługi czatu w terminalu utworzona za pomocą interfejsu 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) }
      }
    }
  }
}

Demonstracja przykładowego kodu w Kotlinie

Aby wypróbować powyższy przykład, sklonuj repozytorium i uruchom je za pomocą pliku example/Main.kt:

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

Dostępne .litertlm modele znajdziesz w społeczności HuggingFace LiteRT. Powyższa animacja została utworzona przy użyciu modelu Gemma3-1B-IT.

Przykładową aplikację na Androida znajdziesz w Galerii Google AI Edge.

Pierwsze kroki z Gradle

Model LiteRT-LM jest opracowywany za pomocą narzędzia Bazel, ale udostępniamy pakiety Maven dla użytkowników Gradle/Maven.

1. Dodaj zależność 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")
}

Dostępne wersje znajdziesz w Google Maven w przypadku litertlm-androidlitertlm-jvm.

latest.release można użyć do pobrania najnowszej wersji.

2. Inicjowanie silnika

Engine to punkt wejścia do interfejsu API. Zainicjuj go ścieżką modelu i konfiguracją. Pamiętaj, aby zamknąć silnik, aby zwolnić zasoby.

Uwaga: wczytanie modelu za pomocą metody engine.initialize() może zająć sporo czasu (np. do 10 sekund). Zdecydowanie zalecamy wywoływanie tej funkcji w wątku w tle lub w korutynie, aby uniknąć blokowania wątku interfejsu.

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()

Aby korzystać z backendu GPU na Androidzie, aplikacja musi wyraźnie zażądać zależnych bibliotek natywnych, dodając do elementu AndroidManifest.xml w tagu <application> ten kod:

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

Aby używać backendu NPU, może być konieczne określenie katalogu zawierającego biblioteki NPU. Jeśli na Androidzie biblioteki są dołączone do aplikacji, ustaw wartość context.applicationInfo.nativeLibraryDir. Więcej informacji o bibliotekach natywnych NPU znajdziesz w artykule LiteRT-LM NPU.

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

3. Tworzenie rozmowy

Po zainicjowaniu silnika utwórz instancję Conversation. Możesz podać ConversationConfig, aby dostosować jego działanie.

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 implementuje AutoCloseable, więc możesz użyć bloku use do automatycznego zarządzania zasobami w przypadku jednorazowych lub krótkotrwałych rozmów:

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

4. Wysyłanie wiadomości

Wiadomości możesz wysyłać na 3 sposoby:

  • sendMessage(contents): Message: wywołanie synchroniczne, które blokuje działanie programu do momentu, aż model zwróci pełną odpowiedź. Jest to prostsze w przypadku podstawowych interakcji typu żądanie/odpowiedź.
  • sendMessageAsync(contents, callback): wywołanie asynchroniczne do przesyłania strumieniowego odpowiedzi. Jest to lepsze rozwiązanie w przypadku długotrwałych żądań lub gdy chcesz wyświetlać odpowiedź w trakcie jej generowania.
  • sendMessageAsync(contents): Flow<Message>: wywołanie asynchroniczne, które zwraca Kotlin Flow do przesyłania strumieniowego odpowiedzi. Jest to zalecane podejście dla użytkowników Coroutines.

Przykład synchroniczny:

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

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

Przykład asynchroniczny z wywołaniem zwrotnym:

Użyj sendMessageAsync, aby wysłać wiadomość do modelu i otrzymać odpowiedź za pomocą wywołania zwrotnego.

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)

Przykład asynchroniczny z przepływem:

Użyj sendMessageAsync (bez argumentu wywołania zwrotnego), aby wysłać wiadomość do modelu i otrzymywać odpowiedzi za pomocą 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. Wiele rodzajów danych

Obiekty Message mogą zawierać różne typy Content, w tym Text, ImageBytes, ImageFile, AudioBytes i 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. Definiowanie i używanie narzędzi

Narzędzia można zdefiniować na 2 sposoby:

  1. Z funkcjami Kotlin (zalecane w większości przypadków)
  2. Ze specyfikacją Open API (pełna kontrola nad specyfikacją narzędzia i jego działaniem)

Definiowanie narzędzi za pomocą funkcji Kotlin

Możesz zdefiniować niestandardowe funkcje Kotlin jako narzędzia, które model może wywoływać w celu wykonywania działań lub pobierania informacji.

Utwórz klasę implementującą interfejs ToolSet i dodaj do metod adnotacje @Tool oraz do parametrów adnotacje @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()
    }
}

W tle interfejs API sprawdza te adnotacje i sygnaturę funkcji, aby wygenerować schemat w stylu OpenAPI. Ten schemat opisuje funkcje narzędzia, parametry (w tym ich typy i opisy z @ToolParam) oraz typ zwracany do modelu językowego.

Typy parametrów

Typy parametrów oznaczonych adnotacją @ToolParam mogą być String, Int, Boolean, Float, Double lub List tych typów (np. List<String>). Używaj typów dopuszczających wartość null (np. String?), aby wskazać parametry dopuszczające wartość null. Ustaw wartość domyślną, aby wskazać, że parametr jest opcjonalny, i podaj ją w opisie w @ToolParam.

Typ zwracanej wartości

Typ zwracany funkcji narzędzia może być dowolnym typem w Kotlinie. Zanim wynik zostanie odesłany do modelu, zostanie przekonwertowany na element JSON.

  • Typy List są konwertowane na tablice JSON.
  • Typy Map są konwertowane na obiekty JSON.
  • Typy proste (String, Number, Boolean) są konwertowane na odpowiednie typy proste JSON.
  • Inne typy są konwertowane na ciągi znaków za pomocą metody toString().

W przypadku danych strukturalnych zalecamy zwracanie wartości Map lub klasy danych, która zostanie przekształcona w obiekt JSON.

Definiowanie narzędzi za pomocą specyfikacji OpenAPI

Narzędzie możesz też zdefiniować, implementując klasę OpenApiTool i podając opis narzędzia jako ciąg znaków JSON zgodny ze specyfikacją Open API. Ta metoda jest przydatna, jeśli masz już schemat OpenAPI dla narzędzia lub potrzebujesz precyzyjnej kontroli nad jego definicją.

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}"""
    }
}

Rejestrowanie narzędzi

Dodaj instancje narzędzi do 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)

Model decyduje, kiedy wywołać narzędzie na podstawie rozmowy. Wyniki działania narzędzia są automatycznie przesyłane z powrotem do modelu, aby wygenerować ostateczną odpowiedź.

Ręczne wywoływanie narzędzi

Domyślnie wywołania narzędzi wygenerowane przez model są automatycznie wykonywane przez LiteRT-LM, a wyniki wykonania narzędzia są automatycznie odsyłane do modelu w celu wygenerowania następnej odpowiedzi.

Jeśli chcesz ręcznie uruchamiać narzędzia i przesyłać wyniki z powrotem do modelu, możesz ustawić automaticToolCallingConversationConfig na false.

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

Jeśli wyłączysz automatyczne wywoływanie narzędzi, musisz ręcznie wykonywać narzędzia i wysyłać wyniki z powrotem do modelu w kodzie aplikacji. Metoda execute obiektu OpenApiTool nie będzie wywoływana automatycznie, gdy automaticToolCalling będzie ustawiony na 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."
}

Przykład

Aby wypróbować korzystanie z narzędzi, sklonuj repozytorium i uruchom je za pomocą pliku example/ToolMain.kt:

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

Obsługa błędów

Metody interfejsu API mogą zgłaszać błędy LiteRtLmJniException z warstwy natywnej lub standardowe wyjątki Kotlin, takie jak IllegalStateException, w przypadku problemów z cyklem życia. Zawsze umieszczaj wywołania interfejsu API w blokach try-catch. Funkcja zwrotna onErrorMessageCallback będzie też zgłaszać błędy podczas operacji asynchronicznych.