很多开发者在做安卓端 AI 聊天应用时,第一反应是接入云端大模型 API。但在实际项目中,你会遇到几个绕不开的问题:云端接口的按量计费、网络传输带来的延迟、用户聊天数据的隐私合规压力,以及断网环境下功能直接不可用。本文要讨论的是另一个路线——在安卓端本地部署 AI 模型,让聊天 App 完全脱离云端也能跑通。
这篇文章会先厘清本地 AI 聊天 App 到底解决了什么问题,然后重点讲解端侧推理的核心概念、模型选型与量化、安卓工程集成、完整代码示例、运行验证、常见问题排查,以及生产环境下的最佳实践。如果你正在考虑给 App 加 AI 能力,或者好奇本地模型在手机上的真实表现,这篇内容可以帮你少走不少弯路。
1. 这篇文章真正要解决的问题
很多人会把“安卓端本地 AI 聊天 App”理解成“把一个大模型塞进手机里”。这个理解是对的,但只看表面容易踩坑。真正的问题不是模型能不能跑,而是如何在有限的算力、有限的内存、有限功耗约束下,让模型跑得稳、跑得快、回答有用。
先从几个真实场景说起。
场景一:你的 App 做的是知识库问答、离线助手、会议纪要这种工具型功能,用户明确不希望把文档内容上传到云端。这时候把数据送到 API 是行不通的,必须本地推理。
场景二:你的产品面向海外市场或特定行业客户,网络环境差,用户可能在电梯、地库、飞机上用功能。云端 API 一旦断网就歇菜,本地模型反而成了核心竞争力。
场景三:你是个独立开发者,不想每个月承受大模型 API 的浮动账单,更不想因为用户量上来之后成本失控。本地推理虽然前期投入模型调优的精力,但边际成本接近零。
从实际工程角度看,8GB 内存的手机和 128GB 内存的服务器是完全不同的运行环境。本地模型必须经过量化压缩,推理引擎必须针对移动端优化,App 的架构也必须考虑模型加载、流式输出、内存回收这些移动端特有的问题。这篇文章要解决的,就是这条链路上的完整技术方案:模型选型、离线转换、安卓工程集成、代码实现、性能验证和发布经验。
不管你是安卓开发工程师、独立开发者,还是正在做 AI 产品原型的创业者,这篇文章都会给你一套可以直接落地的实践路径。需要先说明的是,本文介绍的是通用技术思路,具体版本号请以你实际使用的依赖为准。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心概念:本地推理、模型量化与端侧推理引擎
在动手写代码之前,先把几个关键概念讲清楚。很多问题之所以出现,就是因为这些概念没有建立起来。
2.1 云侧推理与端侧推理的本质差异
云侧推理的逻辑是:手机发送请求到服务器,服务器加载大模型进行计算,然后把结果返回给手机。整个过程依赖网络、依赖服务端算力,也依赖服务商的 API 策略。
端侧推理的逻辑完全不同:模型文件放在手机本地,推理引擎直接在 CPU、GPU 或 NPU 上执行模型计算。没有网络请求,没有服务端计算,所有数据都不离开设备。
这两个路线没有绝对优劣,关键看场景。云端能用更大的模型,回答质量更高,但代价是隐私、成本和延迟。端侧模型通常较小,回答质量有一定上限,但换来的是隐私、离线能力、零边际成本和低延迟。这里有一个很关键的技术事实:端侧模型推理的延迟,在多次运行预热之后,往往比一次完整的云端请求还要低,因为省掉了网络传输时间。
2.2 为什么模型必须量化
大模型动辄几十亿参数,原始权重文件可能达到几十 GB。手机装不下,也跑不动。量化的本质,是把模型的浮点权重转换成低比特整型,比如从 FP32 转成 INT8 或 INT4。这样模型文件体积可以缩小到原来的四分之一甚至八分之一,推理时内存占用也大幅下降。
量化会带来质量损失,但好的量化方法能把损失压到人类难以感知的程度。实际项目中,一个 7B 模型经过 4-bit 量化后,体积大约降到 4GB 左右,配合移动端推理引擎,在较新的手机上可以达到每秒几个 token 的生成速度。这个速度虽然比不上云端的几百 token 每秒,但用作聊天对话,体验已经基本可用。
2.3 端侧推理引擎有哪些选择
目前主流的端侧推理引擎有几种路线。
一种是 llama.cpp 系列,专门为大语言模型设计,支持多种量化格式,C++ 实现,天然适合通过 JNI 集成到安卓工程。它底层用 mmap 映射权重文件,内存利用率高,是当前移动端 LLM 推理的热门选择。
另一种是 ONNX Runtime,微软开源的跨平台推理引擎,支持多种模型格式。ONNX 路线更通用,不仅支持 LLM,也能跑视觉模型、语音模型。但要用 ONNX 跑 LLM,往往需要先把模型转成 ONNX 格式,并对算子做适配。
还有 TensorFlow Lite、MediaPipe LLM Inference 等方案。MediaPipe 的 LLM Inference 在安卓端有一定生态,但对模型格式有限制,需要按要求转换。
实际项目里怎么选?如果目标是跑通一个本地聊天机器人,llama.cpp 路线的资料最密集、坑最少。如果你的 App 里还有图像分类、语音识别等需求,希望统一推理引擎,那么 ONNX Runtime 更合适。没有绝对正确答案,关键是把需求列清楚再选。
3. 环境准备与前置条件
开始写代码前,先把开发环境列一下。这里省略具体版本的纠结,建议使用当前稳定版本即可。
3.1 硬件与系统要求
开发机建议 16GB 内存以上,因为模型转换工具本身要加载大模型权重,内存不够容易 OOM。安卓测试机建议 6GB 内存以上,且优先选择芯片支持较好机型。4GB 内存以下也能跑较小的模型,但体验会受限。
安卓系统版本建议 9.0 及以上。本地推理虽然不强制要求高系统版本,但较新的系统对内存分配和 JIT 优化更好。
3.2 开发工具
- Android Studio,建议使用较新版本,本文示例代码基于 Kotlin 编写。
- JDK 17 或更高版本,具体以当前稳定版为准。
- Android SDK Platform 对应你的 targetSdkVersion。
- CMake、ninja 等构建工具,如果使用 llama.cpp 路线,还需要 NDK 和 CMake 配置。
- Python 3.9 或以上版本,用于模型下载、量化和格式转换。
3.3 依赖管理
项目需要支持 JNI 调用本地代码。在 app/build.gradle.kts 里需要配置 externalNativeBuild。如果采用 llama.cpp 路线,可以选择两种接入方式:一种是直接下载预编译的 .so 文件,另一种是把 llama.cpp 源码作为 CMake 子项目编译。后者更灵活,但构建时间更长。预编译方案上手更快,适合快速验证。
4. 技术选型与整体架构设计
在做本地 AI 聊天 App 之前,架构设计比写代码更重要。架构设计的核心问题包括:模型放哪里、推理引擎怎么接、聊天数据怎么管理、UI 和推理线程怎么分工。
4.1 模型文件管理
模型文件通常放在两个位置之一。放在 assets 目录,打包进 APK,优点是首次安装即自带模型;缺点是 APK 体积巨大,应用商店可能限制包体积,而且用户下载成本高。另一个方案是首次运行时从应用内置下载服务下载模型到私有目录,优点是 APK 体积小,模型可以独立更新;缺点是需要处理断点续传和下载失败。
从工程实践看,更推荐第二种方案。理由有三个:应用包体积直接影响转化率;模型文件动辄几个 GB,不适合打进 APK;模型更新时不需要重新发版,可以通过配置中心下发新模型版本。
4.2 推理引擎选型建议
如果你准备把模型和引擎都塞进 App,建议优先考虑 llama.cpp 路线。原因并不复杂:llama.cpp 对移动端做了专门优化,支持内存映射、支持 4-bit 量化,资料多,踩坑少。
如果模型不只是 LLM,还包含语音识别、视觉编码器,那么 ONNX Runtime 更合适。特别是当你已经有了 ONNX 模型资产时,不需要引入第二套推理基础设施。
4.3 App 端整体架构
推荐使用 MVVM 架构:
- UI 层负责聊天界面的渲染、消息列表的展示,使用 RecyclerView。
- ViewModel 层持有对话状态,维护消息列表,调用推理服务。
- 推理服务层封装模型加载、推理调用、流式输出回调。
- 底层是 JNI 层,桥接 Java/Kotlin 代码与 C++ 推理引擎。
跟普通的网络请求型聊天 App 不同,本地推理 App 的 ViewModel 不需要关心网络层,但需要更关心生命周期管理。推理是 CPU 密集型操作,必须放到后台线程执行,不能阻塞 UI 线程。同时,模型加载非常耗时,需要在启动时异步初始化,不要让用户卡在启动页。
这里有一个容易犯的错误:把推理会话放在 Activity 里直接管理。旋转屏幕、切换后台都会导致 Activity 重建,如果推理线程还在运行,可能崩溃。正确做法是把推理会话的生命周期放到 Application 级别,或者在 ViewModel 中持有并妥善处理。
5. 模型准备与离线转换流程
确定了技术路线,接下来进入实操。模型准备是本地 AI App 里最需要耐心的一步,直接决定最终效果。
5.1 下载基础模型
以 7B 规模的对话模型为例,你需要先下载模型权重。常见渠道是 Hugging Face 或 ModelScope,如果网络环境访问困难,ModelScope 更稳妥。下载时选择 GGUF 格式的量化版本,比如 Q4_K_M,这是大小和质量的折中选择。
这里要特别提醒:不要用未量化的 FP16 原始模型直接塞进 App。一个 7B FP16 模型体积约 14GB,手机内存不够,推理速度也无法接受。
5.2 用 llama.cpp 进行量化转换
如果你下载的不是 GGUF 格式,需要先做格式转换。这里以 Python 环境操作为例。
bash复制# 克隆 llama.cpp 仓库
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
# 安装 Python 依赖
pip install -r requirements.txt
# 将 Hugging Face 格式权重转换为 FP16 GGUF
python convert_hf_to_gguf.py <你的模型目录> --outfile model-f16.gguf
# 将 FP16 GGUF 量化为 Q4_K_M
./llama-quantize model-f16.gguf model-q4_k_m.gguf Q4_K_M
执行完成后,你会得到一个量化后的 GGUF 文件,体积约 4GB。这个文件就是最终要集成到安卓工程中的模型文件。
5.3 验证量化后的模型
在集成到安卓工程之前,先在电脑上用 llama.cpp 的命令行工具验证模型可以正常运行。
bash复制./llama-cli -m model-q4_k_m.gguf -p "你好,请介绍一下你自己" -n 256
如果这条命令能输出合理回答,说明模型文件本身没问题。否则,先排查模型下载是否完整、量化参数是否正确、Prompt 格式是否匹配。这一步可以帮你节省大量安卓端排查时间。
5.4 模型文件存储方案
在正式环境里,模型文件下载流程可以封装成一个类。关键点包括:
- 模型文件的下载 URL 不应硬编码在代码里,而应该由远端配置下发。
- 校验下载文件的 SHA256,避免模型文件损坏。
- 存储路径放在
Context.getFilesDir()下的子目录,不要放在外部存储,避免权限问题。
6. 安卓工程集成与推理层封装
模型准备完毕,终于进入安卓开发环节。这一步的核心任务是把推理引擎集成到工程,封装出业务友好的接口。
6.1 创建安卓工程
在 Android Studio 中新建一个空 Activity 项目,语言选择 Kotlin,包名根据自己的产品定义。项目创建完毕后,需要配置 Gradle 脚本。
如果你的方案是使用预编译的 llama.cpp 安卓库,可以直接把 .so 文件放入 app/src/main/jniLibs/ 对应架构目录。常见架构目录有 arm64-v8a 和 x86_64,模拟器调试需要 x86_64,真机主要用 arm64-v8a。
6.2 封装推理会话
创建一个 LocalInferenceEngine 类,负责加载模型、执行推理和释放资源。这里展示一个极简的 Kotlin 封装示例。
kotlin复制// 文件路径:app/src/main/java/com/example/localchat/LocalInferenceEngine.kt
package com.example.localchat
import java.io.File
class LocalInferenceEngine(private val modelPath: String) {
// 静态库加载
init {
System.loadLibrary("llama")
}
private var isLoaded = false
fun loadModel(): Boolean {
// 检查模型文件存在
val modelFile = File(modelPath)
if (!modelFile.exists() || modelFile.length() == 0L) {
return false
}
// 调用 JNI 方法加载模型
isLoaded = nativeLoadModel(modelPath)
return isLoaded
}
fun chatStreaming(prompt: String, onToken: (String) -> Unit) {
if (!isLoaded) return
nativeChatStreaming(prompt) { token ->
onToken(token)
}
}
fun release() {
if (isLoaded) {
nativeRelease()
isLoaded = false
}
}
private external fun nativeLoadModel(path: String): Boolean
private external fun nativeChatStreaming(prompt: String, callback: (String) -> Unit)
private external fun nativeRelease()
}
这个封装把 JNI 细节都藏起来,业务层只需要关心 loadModel、chatStreaming、release 三个接口。真实项目中,你还需要处理错误码、日志、回调线程切换等问题,但思路是一致的。
6.3 JNI 层的 C++ 实现
JNI 层是 Kotlin 和 C++ 引擎之间的桥梁。核心任务是把 Kotlin 传入的字符串转成 C++ 字符串,调用推理引擎,然后把生成的 token 逐段传回 Kotlin 回调。
由于具体接口依赖你选择的 llama.cpp 封装库,这里不展开完整 C++ 代码。关键点有三个:
- 调用引擎时,
context指针设计成全局变量或持有在 Java 层,不能每次推理都重新加载模型。 - Kotlin 侧的回调方法要执行在子线程,不能直接在 JNI 调用线程里执行更新 UI 的操作。
- 注意字符串编码问题,UTF-8 是必须的,GBK 会导致乱码。
6.4 流式输出设计
本地模型的推理速度比较慢,一次性等待完整回答会让用户觉得卡死。因此,必须实现流式输出。流式输出的本质是:推理引擎每生成一个 token,就通过 JNI 回调传给 Kotlin 层,Kotlin 层再通过 LiveData 或 StateFlow 更新 UI。
具体实现时,需要在 nativeChatStreaming 的回调里,把 token 追加到当前回答的缓冲区,并更新 ViewModel 中的状态。每次更新不要直接触发 RecyclerView 的全局刷新,而是仅更新当前正在生成的 item,避免界面卡顿。
7. 聊天界面与业务代码实现
推理引擎封装完成后,接下来是聊天业务层和 UI 层的实现。
7.1 消息数据模型
定义消息实体:
kotlin复制// 文件路径:app/src/main/java/com/example/localchat/ChatMessage.kt
package com.example.localchat
data class ChatMessage(
val id: Long,
val content: String,
val isUser: Boolean,
val timestamp: Long = System.currentTimeMillis()
)
这个实体用于 RecyclerView 展示和状态管理。如果你的对话需要支持 Markdown 渲染,可以在 UI 解析阶段处理,数据模型不关心。
7.2 ViewModel 设计
kotlin复制// 文件路径:app/src/main/java/com/example/localchat/ChatViewModel.kt
package com.example.localchat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class ChatViewModel(
private val inferenceEngine: LocalInferenceEngine
) : ViewModel() {
private val _messages = MutableStateFlow<List<ChatMessage>>(emptyList())
val messages: StateFlow<List<ChatMessage>> = _messages
private val _isGenerating = MutableStateFlow(false)
val isGenerating: StateFlow<Boolean> = _isGenerating
private var generationJob: Job? = null
fun sendMessage(text: String) {
if (_isGenerating.value) return
val userMessage = ChatMessage(
id = System.currentTimeMillis(),
content = text,
isUser = true
)
_messages.value = _messages.value + userMessage
val robotMessage = ChatMessage(
id = System.currentTimeMillis() + 1,
content = "",
isUser = false
)
_messages.value = _messages.value + robotMessage
_isGenerating.value = true
generationJob = viewModelScope.launch(Dispatchers.IO) {
val buffer = StringBuilder()
inferenceEngine.chatStreaming(text) { token ->
buffer.append(token)
// 把局部变量捕获到 coroutine 中,更新 state
updateRobotMessage(robotMessage.id, buffer.toString())
}
_isGenerating.value = false
}
}
fun stopGeneration() {
generationJob?.cancel()
_isGenerating.value = false
}
private fun updateRobotMessage(id: Long, newContent: String) {
val current = _messages.value
_messages.value = current.map { msg ->
if (msg.id == id) msg.copy(content = newContent) else msg
}
}
override fun onCleared() {
super.onCleared()
inferenceEngine.release()
}
}
这个 ViewModel 有几个要点值得说明:
- 用户发送消息后,立即往消息列表插入一个空的机器人消息,然后用流式回调不断更新它。这是聊天类 App 的标准做法。
- 使用
Dispatchers.IO执行推理,避免卡 UI。 - 提供
stopGeneration方法,用户取消回答时调用。这对本地推理特别重要,因为生成一条长回答耗时很长,用户必须有中断能力。
7.3 主界面
kotlin复制// 文件路径:app/src/main/java/com/example/localchat/MainActivity.kt
package com.example.localchat
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.localchat.databinding.ActivityMainBinding
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val chatViewModel: ChatViewModel by viewModels {
val engine = LocalInferenceEngine(filesDir.resolve("models/model-q4_k_m.gguf").absolutePath)
// 实际项目中应该从 Application 获取单例引擎,这里简化
ChatViewModelFactory(engine)
}
private lateinit var chatAdapter: ChatAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
chatAdapter = ChatAdapter(emptyList())
binding.recyclerMessages.layoutManager = LinearLayoutManager(this)
binding.recyclerMessages.adapter = chatAdapter
binding.btnSend.setOnClickListener {
val text = binding.etInput.text.toString().trim()
if (text.isNotEmpty()) {
chatViewModel.sendMessage(text)
binding.etInput.setText("")
}
}
lifecycleScope.launch {
chatViewModel.messages.collect { messages ->
chatAdapter.updateData(messages)
binding.recyclerMessages.scrollToPosition(messages.size - 1)
}
}
lifecycleScope.launch {
chatViewModel.isGenerating.collect { generating ->
binding.btnSend.isEnabled = !generating
}
}
// 启动时异步加载模型
Thread {
val loaded = chatViewModel.loadModel()
runOnUiThread {
if (loaded) {
Toast.makeText(this, "模型加载成功", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "模型加载失败", Toast.LENGTH_SHORT).show()
}
}
}.start()
}
}
以上是核心界面逻辑。实际代码还需要处理输入法弹起、RecyclerView 动画优化、以及加载过程中的进度态展示。
7.4 适配器与布局
ChatAdapter 使用 RecyclerView.Adapter 实现,需要区分用户消息和机器人消息两种布局。这里的关键点有两个:
- 机器人消息的 TextView 使用
setText更新时,要保持 RecyclerView 的 item 位置稳定,不要在流式更新时触发全量重排。 - 长消息的复制需求要用
TextView加textIsSelectable = true支持。
8. 运行结果与效果验证
代码写完了,接下来是验证阶段。这个阶段的目的不是证明代码能正常运行就结束,而是要确认模型质量、运行性能、内存表现三个维度是否达标。
8.1 运行命令
使用 Android Studio 直接跑真机。连接手机后,点击 Run。运行成功后 App 启动,页面提示模型加载。加载通常会持续几秒到十几秒,取决于模型大小和手机性能。
8.2 对话测试用例
建议准备一份测试用例清单:
- 单轮问答:测试模型基本对话能力。
- 多轮对话:测试上下文保持能力。
- 长文本生成:测试流式输出的稳定性和内存表现。
- 空输入和超长输入:测试边界情况。
- 后台切换:在生成过程中切到桌面再回来,确认不会崩溃。
记录每个用例的耗时、内存变化和回答质量。这里有一个经验判断:模型加载时间如果持续超过 20 秒,用户大概率会流失。可以考虑在应用启动时预热加载,或在主界面给出明确的加载进度条。
8.3 性能监控
性能验证有几个关键指标。
首 token 延迟:从用户点击发送到第一个 token 返回的时间。这个数字直接影响用户体验。理想状态下应该在 1 到 3 秒以内。
生成速度:每秒生成的 token 数,通常用 t/s 表示。7B 量化模型在手机上的表现大约在 3 到 10 t/s 之间,不同芯片差异很大。
内存峰值:生成过程中 App 占用的最大内存。安卓系统对单进程内存有阈值,太高会被系统杀死。你可以通过 Android Studio 的 Profiler 查看。
8.4 判断成功的标准
判断这套技术方案是否成功,不是看模型能否回答“你好”,而是看用户能不能顺畅地完成一次多轮对话。我的建议是,把测试目标定为:在目标手机上,连续进行 10 轮对话,不发生崩溃和 OOM,生成速度不低于 3 t/s,首 token 延迟不超过 5 秒。
如果达不到,优先检查模型是不是选得太大。在硬件有限的情况下,模型越小越好,交互体感远比单次回答质量重要。
9. 常见问题与排查思路
本地 AI 聊天 App 的开发过程中,出现频率最高的问题集中在几个方面。下面列成表格,方便快速定位。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 模型加载失败 | 模型文件损坏或路径不正确 | 检查模型文件是否存在、大小是否合理、SHA256 是否匹配 | 重新下载模型文件,把路径打日志确认 |
| 首 token 延迟过长 | 模型量级过大或 prompt 过长 | 使用 Profiler 查看耗时分布 | 换更小模型或减少历史上下文长度 |
| 生成过程中 App 崩溃 | 内存不足或 JNI 层未处理异常 | 查看 logcat 中的 native crash 日志 | 缩小模型量化位数;关闭其他高内存功能;在子线程中执行推理 |
| 回答乱码 | 字符串编码不一致 | 检查 JNI 层是否按 UTF-8 处理 | 统一使用 UTF-8 编码 |
| 流式输出不生效 | 回调线程阻塞或 UI 未刷新 | 查看回调是否在 UI 线程执行 | 用 runOnUiThread 或 coroutine 主线程调度 |
| 切换后台被杀 | 进程级内存占用过高 | 查看系统内存统计 | 减少缓存的历史消息;推理完成后再保存对话 |
| 模型回答与云端差距大 | 模型量化和规模限制 | 与云端模型对比同一提问 | 调整产品预期,本地模型适合任务型对话而非通用创作 |
还有一个常见问题值得单独强调:如果 System.loadLibrary 抛异常,优先确认 .so 文件是否放对目录,CPU 架构是否匹配。很多人在 x86 模拟器上调试 arm64 的库,必然失败。
10. 最佳实践与工程建议
到了这个阶段,你已经能跑通一个本地 AI 聊天 App。接下来是从“能跑”到“好用”的工程建议。
10.1 模型更新机制
本地模型不能发版即固化。建议在 App 内置一个配置中心,通过远端配置下发模型版本号和下载地址。当新模型发布时,App 在 WiFi 环境下自动下载新模型并校验完整性。老模型建议保留下一个版本,防止新模型有问题时无法回滚。
10.2 对话上下文的长度管理
大模型对话需要拼接历史消息。但手机内存有限,历史消息不能无限增长。建议在拼接 prompt 时只保留最近 N 轮对话,超出部分进行摘要或丢弃。典型的配置是保留最近 10 轮。
10.3 功耗与发热控制
本地推理是 CPU/GPU 密集型任务,长时间运行会导致发热和降频。建议增加一个温度检测逻辑,当设备温度过高时减少生成长度或暂停生成。同时,推理任务的优先级不要设成最高,避免和系统核心进程抢资源。
10.4 数据存储安全
虽然是本地模型,但聊天记录仍然涉及用户隐私。数据库推荐使用 SQLite 加密方案,比如 SQLCipher。模型文件本身也建议只放在 App 私有目录,不要导出到公共存储。
10.5 并发与线程安全
推理引擎必须设计成串行调用。多线程同时调用同一个推理会话会导致崩溃或回答错乱。可以在推理服务层加锁,保证同一时间只有一个生成任务。
10.6 合规与用户告知
你的 App 在应用商店上架时,需要明确告知用户使用了本地 AI 处理数据。授权弹窗、隐私政策、个人信息保护声明都要写好。另外,所有功能必须在安全、合法、合规的框架下设计,不要做擦边功能,不要试图绕过平台的审核机制。
11. 总结
本地 AI 聊天 App 的核心不在于“我在手机里跑了个模型”这件事本身,而在于通过模型量化、推理引擎选型、线程管理和资源控制,把原本需要服务器算力支撑的能力压缩到了用户口袋里。
从技术链路看,你需要依次解决模型选型与量化、JNI 集成、会话封装、流式展示、内存优化、更新机制和合规问题。这条路线的典型收益是隐私可控、离线可用和成本趋近于零,代价是模型能力上限低于云端大模型、调试门槛更高。
如果你准备自己动手,建议按这样的顺序推进:先在一台电脑上用 llama.cpp 跑通一个小模型,别急着碰安卓;确认模型效果和 fits 之后,再集成 JNI,做一个最简单文本框输入输出;最后再完善界面、流式输出和更新机制。本地模型不要贪大,一台中端手机上 3B 到 7B 的量化模型已经足够顺畅运行。最重要的是,先从最小闭环开始,把问题一个个拆解,你会发现自己做的这个安卓端本地 AI 聊天 App,远比想象中更有价值。
