Pierwsze kroki z LiteRT-LM na Androidzie

Kotlinowy interfejs API LiteRT-LM na Androida i JVM (Linux, macOS, Windows) z funkcjami takimi jak akceleracja GPU i NPU, obsługa wielu modalności i korzystanie z narzędzi.

Wprowadzenie

Oto przykładowa aplikacja do czatu w terminalu utworzona za pomocą Kotlinowego interfejsu 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, skopiuj repozytorium i uruchom je za pomocą example/Main.kt:

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

Dostępne modele .litertlm znajdziesz w społeczności HuggingFace LiteRT. W powyższej animacji użyto modelu Gemma3-1B-IT.

Przykładową aplikację na Androida znajdziesz w Galerii Google AI Edge (dostępnej w Google Play).

Pierwsze kroki z Gradle

LiteRT-LM jest opracowywany za pomocą Bazela, ale udostępniamy pakiety Maven dla użytkowników Gradle lub Maven.

Dodawanie zależności 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 litertlm-android i litertlm-jvm.

Aby uzyskać najnowszą wersję, możesz użyć latest.release.

Inicjowanie silnika

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

Uwaga: wczytywanie modelu za pomocą metody engine.initialize() może zająć sporo czasu (np. do 10 sekund). Zdecydowanie zalecamy wywoływanie tej metody w wątku w tle lub w procedurze współprogramowej, aby uniknąć blokowania wątku UI.

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 w Androidzie korzystać z backendu GPU, aplikacja musi wyraźnie poprosić o zależne biblioteki natywne, dodając ten kod do pliku AndroidManifest.xml w tagu <application>:

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

Aby korzystać z backendu NPU, być może trzeba będzie określić katalog zawierający biblioteki NPU. Jeśli w 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)
)

Tworzenie rozmowy

Po zainicjowaniu silnika utwórz instancję Conversation. Aby dostosować jej działanie, możesz podać 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 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
}

Wysyłanie wiadomości

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

  • sendMessage(contents): Message: wywołanie synchroniczne, które blokuje działanie aplikacji, dopóki model nie zwróci pełnej odpowiedzi. Jest to prostsze w przypadku podstawowych interakcji z żądaniami i odpowiedziami.
  • sendMessageAsync(contents, callback): wywołanie asynchroniczne do przesyłania strumieniowego odpowiedzi. Jest to lepsze 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 Kotlinowy przepływ do przesyłania strumieniowego odpowiedzi. Jest to zalecane rozwiązanie dla użytkowników procedur współprogramowych.

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 otrzymywać odpowiedzi 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ą Kotlinowego przepływu.

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

🔴 Nowość: przewidywanie wielu tokenów (MTP)

Przewidywanie wielu tokenów (MTP) to optymalizacja wydajności, która znacznie przyspiesza dekodowanie. MTP jest powszechnie zalecane w przypadku wszystkich zadań w backendach GPU.

Aby używać MTP, przed zainicjowaniem silnika włącz spekulatywne dekodowanie za pomocą ExperimentalFlags.

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

// Enable MTP via speculative decoding
@OptIn(ExperimentalApi::class)
ExperimentalFlags.enableSpeculativeDecoding = true

val engineConfig = EngineConfig(
    modelPath = "/path/to/your/model.litertlm",
    backend = Backend.GPU(),
)

val engine = Engine(engineConfig)
engine.initialize()

// The same steps to create Conversation and send messages as below...

Obsługa wielu modalności

Obiekty Message mogą zawierać różne typy Content, w tym Text, ImageBytes, ImageFile, AudioBytes i 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."),
))

Definiowanie i używanie narzędzi

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

  1. Za pomocą funkcji Kotlin (zalecane w większości przypadków).
  2. Za pomocą specyfikacji OpenAPI (pełna kontrola nad specyfikacją i wykonaniem narzędzia).

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ą ToolSet i oznacz metody za pomocą @Tool i parametry za pomocą @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()
    }
}

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

Typy parametrów

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

Typ zwracanej wartości

Zwracany typ funkcji narzędzia może być dowolnym typem Kotlin. 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 pierwotne (String, Number, Boolean) są konwertowane na odpowiednie typy pierwotne JSON.
  • Inne typy są konwertowane na ciągi znaków za pomocą metody toString().

W przypadku danych strukturalnych zalecamy zwracanie Map lub klasy danych, która zostanie przekonwertowana na 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ą OpenAPI. Ta metoda jest przydatna, jeśli masz już schemat OpenAPI dla narzędzia lub jeśli potrzebujesz szczegółowej kontroli nad definicją narzędzia.

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

Rejestrowanie narzędzi

Dołącz 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 zdecyduje, kiedy wywołać narzędzie, na podstawie rozmowy. Wyniki wykonania narzędzia są automatycznie odsyłane do modelu w celu wygenerowania ostatecznej odpowiedzi.

Ręczne wywoływanie narzędzi

Domyślnie wywołania narzędzi generowane 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 wykonywać narzędzia i odsyłać wyniki do modelu, możesz ustawić automaticToolCalling w ConversationConfig 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 odsyłać wyniki do modelu w kodzie aplikacji. Gdy automaticToolCalling jest ustawione na false, metoda execute klasy OpenApiTool nie będzie wywoływana automatycznie.

// 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, skopiuj repozytorium i uruchom je za pomocą 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ć wyjątki LiteRtLmJniException w przypadku błędów 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. Wywołanie zwrotne onError w MessageCallback będzie też zgłaszać błędy podczas operacji asynchronicznych.