mcpField
L
litert-lm

litert-lm

github.com/android开发10.0k116 次安装2 天前更新
代码仓库

关于 litert-lm

为 Android 开发者提供 LiteRT-LM Kotlin API 的实现指导,用于在设备 CPU 或 GPU 上运行文本 LLM。内容覆盖模型准备、引擎初始化、流式对话及生命周期管理,不涉及图像、音频、工具调用或 NPU。

主要功能

  1. 01配置 LiteRT-LM Android 依赖
  2. 02从设备文件路径加载模型
  3. 03在后台线程初始化引擎
  4. 04通过单线程执行器管理对话
  5. 05流式接收回复并管理状态
  6. 06处理模型与对话关闭生命周期

适用场景

  1. 01在 Android 应用中构建本地文本聊天
  2. 02实现 CPU 或 GPU 推理
  3. 03将模型下载或交付到设备后加载
  4. 04处理对话流式回复与取消

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/android/skills --skill litert-lm

安装后在 Claude Code 中输入/litert-lm确认已加载。

附带文件references/android/google/play/on-device-ai.mdreferences/get-a-model.md

详细介绍

此技能提供分步指导,帮助你通过 LiteRT-LM Kotlin API (com.google.ai.edge.litertlm)在 CPU 或 GPU 上运行开放文本大模型,构建 Android 应用。模型是一个 .litertlm 文件。不涉及图像、音频、工具调用和 NPU 后端。对于使用 .tflite 文件的视觉和音频模型,请推荐 litert-runtime 技能。

当此技能使用的 LiteRT-LM 类或方法无法编译时,你必须先阅读 LiteRT-LM Android 指南 和 LiteRT-LM README,再修改代码。

前提条件

  • Kotlin Android 项目(Android Studio 的 Empty Activity 模板即可)。litertlm-android 0.18.0 AAR 声明了 minSdk 24。
  • 在 app 级 build.gradle.kts 文件中添加来自 Google Maven 的依赖项:implementation("com.google.ai.edge.litertlm:litertlm-android:0.18.0")。Google Maven 上列有后续版本。
  • 使用 GPU 后端时,在 AndroidManifest.xml 的 <application> 内添加两条 uses-native-library 条目。如果应用会下载模型,还要添加 INTERNET 权限。
  • 模型文件存放在设备文件系统中(几百 MB 到几 GB),绝不能放在 assets/ 中或打包进 APK。

关键要求

  • 先添加依赖:在编写导入 com.google.ai.edge.litertlm 的代码之前,如果项目尚未添加依赖,先按照步骤 1 添加 litertlm-android。使用 ./gradlew assembleDebug 验证。
  • 在后台初始化:Engine(config).initialize() 会加载权重,并阻塞数秒。必须在后台线程调用,绝不能在主线程调用。
  • 唯一所有者:由一个单线程执行器独占 Engine 和 Conversation。创建、流式处理、取消和关闭都在该执行器上运行。
  • 只关闭一次:Engine 和 Conversation 均实现了 AutoCloseable。对任一对象再次调用 close 都会抛出 IllegalStateException。对初始化失败的引擎调用 close 也会如此。只有在 initialize 返回后才保存引擎。
  • 模型放在 APK 外部:向 EngineConfig 传入绝对路径。绝不能将 .litertlm 文件打包到 assets/ 中。

详细步骤

步骤 1:配置 Gradle 和清单文件

编辑构建文件之前,先在项目中查找 libs.versions.toml、app/build.gradle.kts 和 AndroidManifest.xml。在 app/build.gradle.kts 中配置 minSdk 并添加 LiteRT-LM 依赖项:


android {
    defaultConfig {
        minSdk = 24
    }
}

dependencies {
    implementation("com.google.ai.edge.litertlm:litertlm-android:0.18.0")
}

如果项目使用 Hilt,请先检查 Dagger 版本。litertlm-android 0.18.0 携带 Kotlin 2.4 元数据,Hilt 2.59.2 无法读取。此时构建会在 hiltJavaCompileDebug 中失败,并显示 Provided Metadata instance has version 2.4.0, while maximum supported version is 2.3.0。选择一种修复方案:

  • 方案 A:将 Hilt 版本(com.google.dagger)升级到 2.60.1 或更高版本。编辑 gradle/libs.versions.toml 之前,请先征求用户同意。
  • 方案 B:保留 Hilt 版本,并在 app/build.gradle.kts 中的注解处理器路径添加更高版本的元数据读取器:

dependencies {
    ksp("org.jetbrains.kotlin:kotlin-metadata-jvm:2.4.21")
}

使用 GPU 后端时,在 AndroidManifest.xml 的 <application> 内添加这两行。应用会下载模型时,在 <application> 外添加 INTERNET 权限:


<uses-permission android:name="android.permission.INTERNET"/>

<application>
    <uses-native-library
        android:name="libvndksupport.so"
        android:required="false"/>
    <uses-native-library
        android:name="libOpenCL.so"
        android:required="false"/>
</application>
步骤 2:从 litert-community 选择模型

从 Hugging Face 上的 litert-community 选择一个 .litertlm 文件。可以先试用 litert-community/gemma-4-E2B-it-litert-lm(文件 gemma-4-E2B-it.litertlm, 大小为 2.6 GB)。如需 0.6 GB 的模型,可以先试用 litert-community/Qwen3-0.6B(文件 Qwen3-0.6B.litertlm)。这两个文件都可以在 CPU 和 GPU 上运行。SoC 后缀(如 _Google_Tensor_G5 或 .mediatek.mt6993)表示这是 NPU 构建版本,本技能不涉及这类版本。模型卡会列出经过测试的后端和文件大小。

在选择这两个模型之外的其他模型文件、编写应用内下载代码,以及模型没有对应的 .litertlm 文件时,必须先阅读 获取模型。

步骤 3:将模型文件放到设备上

选择一种方式:

  • 方式 A(使用 adb 本地测试):只有在以下三项条件全部满足时,才运行这些命令:adb devices 列出了设备、应用的调试构建版本已安装在该设备上,并且用户提供了本地 .litertlm 路径。如果任一条件不满足,就不要运行这些命令。将它们作为实现完成后的步骤告知用户。将 <path> 替换为用户的本地路径,将 <package> 替换为应用 ID:

    1. adb push <path> /data/local/tmp/model.litertlm
    2. adb shell run-as <package> cp /data/local/tmp/model.litertlm files/
  • 方式 B(应用内下载):在应用中编写下载代码。使用应用的 HTTP 客户端将文件写入 filesDir,或使用 DownloadManager 将文件写入 getExternalFilesDir(null)。DownloadManager 无法写入内部存储。显示下载进度,并在加载前根据模型卡检查文件大小。绝不要在 Agent 自己的会话中下载模型文件。

  • 方式 C(Play for On-Device AI):编写代码,通过 AI Delivery 库(com.google.android.play:ai-delivery)获取快速跟进或按需 AI 包的路径: AiPackManagerFactory.getInstance(context).getPackLocation(name)?.assetsPath()。 在 AI 包完成下载之前,此调用会返回 null,因此在这种情况下要显示下载状态。创建 AI 包模块并将应用上传到 Play Console,是实现完成后由用户执行的步骤。

无论使用哪种方式,都要将文件的绝对路径传给引擎。

步骤 4:在主线程之外初始化引擎

使用 EngineConfig(modelPath = ..., backend = ..., cacheDir = ...) 创建引擎,并在后台线程中调用 initialize:

  • Backend.CPU() 是 EngineConfig 的默认值,不需要设备提供任何支持。
  • Backend.GPU() 需要步骤 1 中的两行清单配置。缺少这些配置时,引擎仍会初始化,但首次回复会以错误 Can not find OpenCL library on this device 结束。
  • cacheDir 可以加快第二次加载。
步骤 5:实现对话 ViewModel

在 AndroidViewModel 内使用单线程执行器来驱动引擎和对话,流式输出回复,并向 UI 暴露不可变状态:


import android.app.Application import androidx.lifecycle.AndroidViewModel import com.google.ai.edge.litertlm.Backend import com.google.ai.edge.litertlm.Contents import com.google.ai.edge.litertlm.Conversation import com.google.ai.edge.litertlm.ConversationConfig import com.google.ai.edge.litertlm.Engine import com.google.ai.edge.litertlm.EngineConfig import java.util.concurrent.Executors import kotlin.coroutines.cancellation.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock
data class ChatState(
    val ready: Boolean = false,
    val busy: Boolean = false,
    val reply: String = "",
    val error: String? = null
)

class ChatViewModel(app: Application) : AndroidViewModel(app) {
    private val executor = Executors.newSingleThreadExecutor()
    private val scope = CoroutineScope(
        SupervisorJob() + executor.asCoroutineDispatcher()
    )
    private val mutex = Mutex()
    private var engine: Engine? = null
    private var conversation: Conversation? = null
    private val _state = MutableStateFlow(ChatState())
    val state: StateFlow<ChatState> = _state.asStateFlow()

    fun load(modelPath: String, backend: Backend = Backend.CPU()) {
        scope.launch {
            mutex.withLock {
                if (engine?.engineConfig?.modelPath == modelPath) return@launch
                release()
                try {
                    val config = EngineConfig(
                        modelPath = modelPath,
                        backend = backend,
                        cacheDir = getApplication<Application>().cacheDir.path
                    )
                    engine = Engine(config).also { it.initialize() }
                    conversation = checkNotNull(engine).createConversation(
                        ConversationConfig(
                            systemInstruction =
                                Contents.of("你是一位乐于助人的助手。")
                        )
                    )
                    _state.value = ChatState(ready = true)
                } catch (e: Exception) {
                    release()
                    _state.value = ChatState(error = e.message)
                }
            }
        }
    }

    fun send(text: String) {
        scope.launch {
            if (!_state.value.ready || _state.value.busy) return@launch
            _state.update { it.copy(busy = true, reply = "", error = null) }
            try {
                val replies = checkNotNull(conversation).sendMessageAsync(text)
                replies.collect { message ->
                    _state.update { it.copy(reply = it.reply + message) }
                }
            } catch (e: CancellationException) {
                throw e
            } catch (e: Exception) {
                _state.update { it.copy(error = e.message) }
            } finally {
                _state.update { it.copy(busy = false) }
            }
        }
    }
    private suspend fun release() {
        _state.update { it.copy(ready = false) }
        while (_state.value.busy) {
            conversation?.cancelProcess()
            delay(100)
        }
        conversation?.close()
        conversation = null
        engine?.close()
        engine = null
        _state.value = ChatState()
    }

    override fun onCleared() {
        scope.launch {
            mutex.withLock { release() }
            executor.shutdown()
        }.invokeOnCompletion { scope.cancel() }
    }
}

sendMessageAsync(text) 返回一个由消息块组成的 Flow<Message>,toString 会返回消息块的文本。sendMessage(text) 会阻塞并返回完整回复。 对话会保留历史记录,因此下一次 send 会接着当前聊天继续。 在流完成前,busy 会让按钮保持禁用状态。

要设置采样参数,请在 ConversationConfig 中传入 SamplerConfig(topK = 10, topP = 0.95, temperature = 0.8)。这三个参数都必需,且 topP 和 temperature 的类型为 Double。

如果 load 使用的模型路径已加载,无论它请求哪个后端,都不会执行任何操作。 旋转屏幕后再次调用时,会保留已加载的模型,即使应用已回退到另一个后端也是如此。 只有当状态为 ready 且不为 busy 时,send 才会运行;按钮也使用同样的判断条件。 当 release 正在停止回复时点击按钮,不会有任何操作。在 load 后排队的点击, 若当时没有正在流式传输的回复,就会发送到 load 所加载的模型。

步骤 6:连接 Compose 屏幕

viewModel 和 collectAsStateWithLifecycle 来自 androidx.lifecycle:lifecycle-viewmodel-compose 和 androidx.lifecycle:lifecycle-runtime-compose(2.10.0):


import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.ai.edge.litertlm.Backend

@Composable
fun ChatScreen(modelPath: String, viewModel: ChatViewModel = viewModel()) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    var input by remember { mutableStateOf("") }
    LaunchedEffect(modelPath) { viewModel.load(modelPath, Backend.GPU()) }
    Column {
        Text(
            state.reply,
            Modifier.weight(1f).verticalScroll(rememberScrollState())
        )
        state.error?.let { Text(it) }
        TextField(value = input, onValueChange = { input = it })
        Button(
            onClick = { viewModel.send(input); input = "" },
            enabled = state.ready && !state.busy
        ) {
            Text("Send")
        }
    }
}
步骤 7:管理引擎生命周期
  • 只要屏幕仍然存在,就将引擎保留在 ViewModel 中。多个屏幕共用引擎时,将其保存在应用级容器中。
  • ViewModel 使用自己的线程和作用域。viewModelScope 会在运行 onCleared 之前取消,因此在其中启动的关闭操作不会执行。关闭完成后,onCleared 会取消 scope。
  • release 会关闭 ready,并持续调用 cancelProcess,直到流式回复的流结束。最迟在回复自行结束时,流就会结束。随后,release 会分别关闭会话和引擎,各一次。
  • onCleared 和用于替换模型的 load 都会在 Mutex 下调用 release,一次只执行一个,因为回复停止期间 release 会挂起。
  • 要开始新的聊天,请关闭当前会话,并使用 createConversation 创建新会话。保持引擎已加载。

用户需要完成的实施后步骤

代码编译后,告知用户适用的步骤:

  • 如果步骤 3 中的路径 A 未执行,请使用路径 A 中的两个 adb 命令将模型复制到设备。
  • 如果应用使用步骤 3 中的路径 C,请创建用于存放 .litertlm 文件的 AI 包模块,并将应用上传到 Play Console。
  • 如果模型没有对应的 .litertlm 文件,请在工作站上转换模型。此技能不会转换模型。
  • 如果用户需要完整的参考应用,请引导他们查看 Google AI Edge Gallery。

故障排除

症状原因修复方法
首次回复较慢。首次使用时运行了 initialize。在应用启动时加载,并设置 cacheDir。
首次 GPU 回复以 Can not find OpenCL library on this device 结束。清单中缺少 uses-native-library 条目。在 <application> 内添加步骤 1 中的两个条目。
close 抛出 IllegalStateException。引擎或会话被关闭了两次,或者 initialize 抛出了异常。仅在 initialize 返回后保存引擎,并在 close 后将各字段设为 null。
构建失败,提示 [Hilt] Provided Metadata instance has version 2.4.0。Hilt 2.59.2 无法读取 litertlm-android 0.18.0 中的 Kotlin 2.4 元数据。按照步骤 1 中的路径 A 或路径 B 操作。

检查清单

  • [ ] minSdk 是否设为 24 或更高,并且是否已在 app/build.gradle.kts 中添加 com.google.ai.edge.litertlm:litertlm-android:0.18.0?
  • [ ] 对于 GPU 后端,AndroidManifest.xml 是否在 <application> 内使用 android:required="false" 声明了 libvndksupport.so 和 libOpenCL.so?
  • [ ] .litertlm 文件是否从设备文件系统中的绝对路径加载,而不是从 assets/ 加载?
  • [ ] initialize、sendMessageAsync、cancelProcess 和 close 是否都在同一个单线程后台执行器上运行?
  • [ ] 会话是否在引擎之前关闭,并且各自只关闭一次?
  • [ ] 项目能否通过 ./gradlew assembleDebug 编译?

反模式

  • 绝不要在 Android 主线程上调用 Engine.initialize。
  • 绝不要将 .litertlm 文件放在 assets/ 中或打包进 APK。
  • 绝不要对 initialize 抛出异常的引擎调用 close。
  • 绝不要在回复仍在流式输出时关闭引擎;应先持续调用 cancelProcess,直到流结束。
  • 绝不要在 onCleared 中通过 viewModelScope 关闭 Engine 或 Conversation;该作用域此时已经取消。

评论

支持 Markdown