关于 litert-lm
为 Android 开发者提供 LiteRT-LM Kotlin API 的实现指导,用于在设备 CPU 或 GPU 上运行文本 LLM。内容覆盖模型准备、引擎初始化、流式对话及生命周期管理,不涉及图像、音频、工具调用或 NPU。
主要功能
- 01配置 LiteRT-LM Android 依赖
- 02从设备文件路径加载模型
- 03在后台线程初始化引擎
- 04通过单线程执行器管理对话
- 05流式接收回复并管理状态
- 06处理模型与对话关闭生命周期
适用场景
- 01在 Android 应用中构建本地文本聊天
- 02实现 CPU 或 GPU 推理
- 03将模型下载或交付到设备后加载
- 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-android0.18.0 AAR 声明了minSdk24。 - 在 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:adb push <path> /data/local/tmp/model.litertlmadb 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;该作用域此时已经取消。