[控场AI]
· 12 分钟阅读· 6,070 字

MediaPipe LLM API 已停更:迁移到 LiteRT-LM 并加上 ABI 检查

MediaPipe LLM API 已停更:迁移到 LiteRT-LM 并加上 ABI 检查

从 MediaPipe 迁移到 LiteRT-LM 的端侧大模型工程实践指南。

文章指出 MediaPipe LLM Inference API 已进入仅维护模式,新项目应直接采用 Google 基于 LiteRT 的 LiteRT-LM 运行时。作者围绕 Engine 与 Conversation 的生命周期、Flow 流式输出、模型量化选型给出了可复用的代码骨架,并重点强调四类来自真实崩溃与踩坑的工程细节:litertlm-android 仅支持 arm64-v8a 与 x86_64,加载前必须检查 Build.SUPPORTED_ABIS 并捕获 Throwable;flow { } 内必须用 flowOn 而非 withContext;GPU 优先、CPU 兜底的多层回退;以及按 token 预算裁剪历史与在 onTrimMemory 中释放 Engine。文末给出迁移检查清单。

端侧运行大语言模型是隐私保护最彻底的方案,但也是最苛刻的:模型文件大、运行时占用大量内存、持续消耗电量。作为回报,它能在最近的 64 位手机上以飞行模式工作,并且模型完全由你选择。过去两年,Android 开发者做这件事的标准方式是 MediaPipe 的 LLM Inference API。现在情况变了。

LiteRT-LM 取代 MediaPipe LLM Inference API

Google 的 MediaPipe 文档已经挂出通知:LLM Inference API(Android、iOS 和 Web)进入仅维护模式,新功能和优化将集中在 LiteRT-LM 上,项目应当迁移。这意味着新代码不应再基于 MediaPipe LLM Inference API 构建,而应直接从 LiteRT-LM 起步。

LiteRT-LM 是 Google 基于 LiteRT 的语言模型运行时。它的 Kotlin API 相当精简:Engine 负责加载 .litertlm 模型,Conversation 持有对话状态,sendMessageAsync 以 Kotlin Flow 的形式流式返回回复。截至撰写本文时(2026 年 10 月),Google Maven 上的当前版本是 0.18.0,尚未到 1.0。依赖声明很简单:

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

版本号低于 1.0 意味着 API 仍可能变动,接入时建议锁定版本并关注更新日志。

LiteRT 是 Google 于 2024 年推出的统一设备端 AI 运行时,取代了原先分散的 TensorFlow Lite 和 TensorFlow Lite GPU Delegate 等组件,目标是为移动与嵌入式设备提供一套跨框架(含 PyTorch、JAX、TensorFlow)的推理执行引擎。LiteRT-LM 则是构建在 LiteRT 之上、专为大语言模型场景设计的上层运行时,负责模型加载、KV 缓存管理、采样解码与流式输出编排。与 MediaPipe LLM Inference API 相比,LiteRT-LM 更贴近底层推理栈,更新节奏更快,也因此 API 尚未稳定。

.litertlm 是 LiteRT-LM 自定义的容器格式,把权重、分词器、模型元数据和对话模板打包在同一文件中,使运行时无需额外配置文件即可完成初始化。这与 MediaPipe 使用的 .task 包思路相近,但内部结构针对 LiteRT 的算子与量化方案重新设计。

模型获取与选型策略

LiteRT-LM 读取 .litertlm 格式的文件。Google 指向 Hugging Face 上的 LiteRT Community 组织获取现成模型,其中包括 Gemma 系列构建。

对于模型选择,一个来自实际 Android 应用开发的规则是:优先选择最小的 CPU 友好量化(int4 或 q8),而不是 f32,这样移动数据下载量保持在合理范围,模型也真正能装进 RAM。以下是两个典型条目:

目录条目文件大致大小
Gemma 3 270M IT (q8)gemma3-270m-it-q8.litertlm304 MB
Gemma 3 1B IT (int4)gemma3-1b-it-int4.litertlm584 MB

下文假设模型文件已位于应用私有存储中,并且你已获得其路径。

Engine 与 Conversation 的生命周期

文档明确指出,engine.initialize() 可能耗时很长(给出的例子是“最多 10 秒”),因此必须在后台线程或协程上运行。Engine 应加载一次并保留,每个聊天会话创建新的 Conversation。一个封装示例:

class LocalLlm(private val context: Context, private val modelPath: String) {
    private var engine: Engine? = null
    private val lock = Mutex()

    suspend fun engine(): Engine = lock.withLock {
        engine ?: withContext(Dispatchers.Default) {
            Engine(EngineConfig(
                modelPath = modelPath,
                backend = Backend.CPU(),
                cacheDir = context.cacheDir.path, // 加速后续加载
            )).also { it.initialize() }
        }.also { engine = it }
    }

    fun release() { engine?.close(); engine = null }
}

Conversation 接受可选的系统指令、历史消息和采样设置。系统指令用于设定助手人设,initialMessages 把已有对话带进新会话,SamplerConfig 控制 topK、topP 和 temperature。

用 Flow 流式接收回复

不带回调的 sendMessageAsync(text) 返回 Flow<Message>,这是文档推荐给协程用户的方式。实践中,通常把它藏在一个 TextGenerator 接口后面,让 UI 不关心后端是本地模型还是远程 API。

一个关键细节:在 flow { } 构建器内部应使用 flowOn,而不是 withContext。原因在运行时层面:flow { } 构建器不能从与收集器不同的调度器发射值,把主体包在 withContext 中会抛出“Flow invariant is violated”。这个坑在真实项目中已经写进代码注释,属于必须记住的规则。

Flow 的上下文保持(context preservation)机制要求:flow { } 构建器内部的发射必须发生在收集器所在的协程上下文中。withContext 会把代码块切换到另一个调度器执行,此时 emit 就发生在不同上下文,运行时会检测到并抛出 Flow invariant is violated。flowOn 则只改变上游(即 flow { } 主体)的执行上下文,向下游收集器保证上下文一致,因此是官方推荐写法。

在 LLM 场景中,推理是计算密集型阻塞操作,必须切到 Dispatchers.Default 或专用线程池,这正是需要 flowOn 的典型原因。若使用 callbackFlow 封装回调式 API,则应用 trySend 并在 awaitClose 中清理资源。

ABI 检查不是可选项

这是来自真实崩溃报告的部分。litertlm-android 的 AAR 只附带 arm64-v8a 和 x86_64 的原生库,没有 32 位 ARM 构建。不支持这些 ABI 的手机在原生库加载的瞬间就会抛出 UnsatisfiedLinkError。

修复分为两步。第一步,在加载前检查 Build.SUPPORTED_ABIS:

override suspend fun availability(): Availability =
    if (Build.SUPPORTED_ABIS.any { it == "arm64-v8a" || it == "x86_64" })
        Availability.Ready
    else Availability.Unavailable("This phone's processor is not supported")

第二步,在创建 Engine 时捕获 Throwable 而非 Exception。UnsatisfiedLinkError 属于 Error 而不是 Exception,旧的 catch 块会漏掉它。这个细节决定了应用是优雅降级还是直接崩溃。

ABI(Application Binary Interface)指应用与底层 CPU 架构之间的二进制契约,决定原生 .so 库能否在特定设备上加载。Android 常见的 ABI 包括 32 位的 armeabi-v7a、x86 和 64 位的 arm64-v8a、x86_64。由于大模型推理需要 64 位地址空间来容纳数 GB 的模型权重与 KV 缓存,LiteRT-LM 直接放弃了 32 位构建。

UnsatisfiedLinkError 属于 java.lang.Error 的子类,与 Exception 是平行分支而非继承关系,因此 catch (e: Exception) 无法捕获它。同理,NoClassDefFoundError、OutOfMemoryError 也属于此类,涉及原生库或资源分配的代码在捕获时应使用 Throwable,再按类型区分处理。

GPU 优先,CPU 兜底

GPU 后端对语言模型通常更快,但并非每台手机、每个模型都能用。LiteRT-LM 的 Android 指南要求在 manifest 中声明与 OpenCL 相关的原生库,并标记为非必需:

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

然后先尝试 GPU,失败后回退到 CPU。一个更稳健的做法是构建后端尝试列表,并在每个后端内部再尝试不同的历史长度:完整压缩历史、一半、一轮对话、无历史。只有所有层级都失败时,才向用户显示错误。这种多层回退把端侧模型的不确定性封装在内部,用户看到的是可用或不可用,而不是各种底层异常。

<uses-native-library> 是 Android 12(API 31)引入的 manifest 标签,用于声明应用依赖的厂商原生库。当 android:required="false" 时,即使设备没有该库,应用仍可安装,只是运行时加载会失败——这正是 GPU 后端需要的语义:同一 APK 要能装到没有 OpenCL 驱动的设备上。

libvndksupport.so 是 Android 的厂商动态库加载桥接层,GPU delegate 通过它访问厂商提供的 OpenCL 实现;libOpenCL.so 则是 OpenCL 运行时本身。两者都标记为非必需,可避免在不支持 GPU 推理的设备上被应用商店过滤掉。

把对话塞进上下文窗口

手机上的语言模型上下文窗口很小,而对话每发一条消息都在增长。历史装不下时,生成就会失败。一种在每次请求前按 token 预算裁剪历史的策略是:

  1. 用字符数除以四加每条消息少量开销来粗略估算 token。
  2. 允许历史使用模型 token 上限的 55%,其余留给新消息和回复。
  3. 保留能装下的最新若干轮对话,若有空间则把最早的一轮加回来,再加入最多几轮与新消息关键词重叠的较旧对话。

聊天记录本身仍存在 Room 中,只有发送给模型的那份副本会被裁剪。这样既保住了用户可见的完整历史,又避免了上下文溢出。

释放模型内存

加载后的模型占用大量原生内存。聊天结束时关闭 Conversation(上面的 use 块会处理),当用户离开应用的 AI 部分或系统发出内存压力信号时关闭 Engine:

class AssistantApp : Application() {
    lateinit var localLlm: LocalLlm

    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            localLlm.release()
        }
    }
}

在 onTrimMemory 中响应后台内存压力,是端侧模型应用避免被系统杀死的必要手段。

迁移检查清单

  • 新代码使用 LiteRT-LM,而不是 MediaPipe LLM Inference API。
  • engine.initialize() 在主线程之外执行,且只执行一次。
  • 在 flow { } 内使用 flowOn,绝不用 withContext。
  • 加载前检查 Build.SUPPORTED_ABIS 是否包含 arm64-v8a 或 x86_64。
  • 在 Engine 创建处捕获 Throwable。
  • GPU 优先、CPU 兜底,manifest 条目标记为非必需。
  • 内存压力时释放 Engine。

从 MediaPipe LLM Inference API 迁移到 LiteRT-LM 不只是换一个依赖坐标。ABI 检查、Flow 调度规则、上下文裁剪和内存释放,这些细节共同决定端侧 AI 功能是稳定可用还是频繁崩溃。LiteRT-LM 仍在 0.18.0,API 可能继续演进,但上述工程实践在 1.0 之前都不会过时。

#LiteRT-LM#MediaPipe LLM Inference API#Android 端侧 AI#Kotlin Flow#ABI 检查#端侧大语言模型#Gemma 模型
分享:

相关推荐