简介:一套面向安卓端大模型部署场景的源码包,聚焦基于MLC-LLM框架的InternLM2.5-1.8B轻量模型,完整讲解如何在手机端从环境搭建、模型转换与量化、配置生成到打包上线的落地过程。压缩包共包含3个文件,以inscode工程配置、HTML说明文档和gitignore规则文件为主,整体大小仅为6KB,属于轻量级参考代码,便于开发者快速定位核心配置与说明。内容覆盖安装Rust与Android Studio、配置环境变量、转换并量化模型、修改gradle构建参数、创建签名以及命令行编译等多个实操节点,同时针对App运行时可能出现的兼容或报错问题给出排查思路,并提供工程结构上的参考。已有405人学习下载,适合具备一定安卓基础、希望在端侧部署轻量大模型的开发者阅读,能帮助减少踩坑时间,快速搭建属于自己的移动端模型应用。 这两年,“端侧大模型”在安卓开发圈里已经不算新词了。手机本地直接跑 LLM,从离线翻译、会议纪要总结,到智能修图、本地知识库问答,真实落地的场景越来越多。很多朋友拿到模型后第一步就卡住:模型文件怎么量化?源码怎么编译?手机内存怎么扛得住?我把安卓端部署大模型的完整路子走了一遍,从框架选型、模型量化、JNI 封装,到 Android 工程编译、真机排错,整理成这篇实操笔记。主线用 llama.cpp 这个方案展开,会结合一份可运行的最小工程来讲源码结构,适合有一定 Android 基础、想自己做端侧 AI 应用的开发者参考,新手照着做也能把 demo 跑起来。
1. 部署前的思路拆解与方案选型
1.1 为什么要把大模型搬到安卓端
很多人第一反应是“手机算力这么弱,跑大模型不是自己找罪受吗”。但端侧部署带来的好处很实在:第一是隐私,聊天记录、照片、文档都在本地处理,不上传服务器,这在企业场景几乎是刚需;第二是离线可用,地铁、电梯、飞机上没网也能正常提问;第三是延迟,少了网络请求那一轮往返,很多简单任务的响应时间反而比云端快;第四是成本,省去了按 token 计费的 API 费用。
代价当然也明显:手机内存有限、存储空间小、CPU/GPU 性能不如服务器,还有一个容易被忽略的坑——长时间推理带来的发热降频。所以端侧部署本质上是“在资源约束下做工程取舍”,模型体积、量化精度、上下文长度、线程数这些参数都要反复调。可以这么理解:云端部署就像租个大仓库,随便堆货;端侧部署是在自己家客厅摆货架,空间小但随取随用,麻烦点在于每样东西都得精打细算。
1.2 主流端侧推理框架对比
这几年安卓端能跑的框架不少,最常见的这几个我简单排了个表:
| 框架 | 核心特点 | 适合场景 | 上手难度 |
|---|---|---|---|
| llama.cpp | GGUF 量化生态成熟,CPU/GPU 后端都有,Android 示例完整 | 快速在安卓上跑 LLM | 低 |
| MLC-LLM | 基于 TVM 统一编译,GPU 优化激进 | 追求极限性能和 GPU 利用 | 中高 |
| ExecuTorch | PyTorch 官方移动端方案,支持 LLM 导出 | 已有 PyTorch 生态的项目 | 高 |
| TFLite / MediaPipe | 轻量,偏传统小模型 | 分类、OCR、语音小模型 | 低 |
我最终选了 llama.cpp,理由很直接:GGUF 格式的量化模型几乎成了社区事实标准,google 一下能找到大量直接可用的权重;它的 Android demo 做得比较完整,CMake 构建脚本、JNI 层代码都有现成参考;而且它支持纯 CPU 推理,不依赖 GPU,兼容性更好。如果你手里有现成的 PyTorch 模型,MLC-LLM 也可以尝试,但它的构建链路复杂不少,第一次跑通的时间成本更高。
1.3 选型结论与参数范围
端侧跑大模型,参数规模不能贪大。以现在的旗舰手机来看,1B 到 3B 的模型跑起来体验最舒服,量化后文件体积控制在 2GB 左右比较合适。我这次用 Meta-Llama-3.2-3B-Instruct 做测试,Q4_K_M 量化后约 2GB,在骁龙 8 Gen 2 上纯 CPU 推理大概能到每秒 10 到 20 个 token。7B 模型端侧也能跑,但内存占用高、首 token 延迟明显增加,手机发热也比较严重,不太推荐入门阶段尝试。
量化格式我选了 Q4_K_M,这是 llama.cpp 里的 4-bit 量化规格,兼顾体积和生成质量。为什么不用更激进的 Q2、Q3?因为它们省下来的几百 MB 换来的质量损失太明显,文字生成任务很容易暴露语病。为什么不用 FP16?体积直接翻三倍,手机内存根本扛不住。实际项目里可以在 Q4_K_M 和 Q5_K_M 之间做个对比测试,差别不大就选体积小的。
2. 环境准备与源码获取
2.1 本地编译环境搭建
编译安卓原生库之前,先把工具链补齐。我用的组合是:JDK 17、Android Studio 最新稳定版、Android SDK 33 以上、NDK r26、CMake 3.22.1。这几个版本之间最好保持兼容,比如 llama.cpp 新版对 NDK 版本有要求,太老的 NDK 编译会报“unknown type name”之类的奇怪错误。
SDK Manager 里记得勾选 NDK 和 CMake,否则 Gradle 找不到外部构建工具。建议打开“Show Package Details”,选定具体版本而不是只装最新版,后面切换项目时不容易踩版本不一致的坑。安装完后用gcc --version验证 NDK 自带编译器是否可用就没必要了,重点确认 Android Studio 里 SDK Location 路径和 local.properties 一致。
2.2 获取 llama.cpp 源码与 Android 工程
llama.cpp 的官方仓库地址是https://github.com/ggml-org/llama.cpp,clone 下来后你会发现examples/llama.android目录下有一个可直接导入 Android Studio 的工程。另外还有一个社区项目feicien/llama.android,封装更友好,界面现成,适合快速跑通。两者原理一样,核心都是通过 JNI 调用 llama.cpp 的 C++ 接口,diff 一下源码能学到不少东西。
拉代码时建议指定 commit 而不是永远用 master,因为主分支经常更新,API 也可能变。等你的工程跑通后再考虑更新。源码目录里有几个关键子目录:ggml是底层张量计算库,src是 llama.cpp 的模型加载和推理逻辑,examples/llama.android是安卓壳子。后面改核心逻辑基本只动src和 JNI 层。
2.3 模型下载、转换与量化
模型可以到 Hugging Face 或 ModelScope 下载。如果你下载的已经是 GGUF 格式,直接跳过转换步骤。我这次拿的是官方 PyTorch 权重,所以先用 llama.cpp 自带的脚本转格式:
python3 convert_hf_to_gguf.py ./Meta-Llama-3.2-3B-Instruct \ --outfile llama-3.2-3b-instruct-f16.gguf \ --outtype f16转出来的 FP16 文件大概 6GB,太大,接着量化:
./llama-quantize llama-3.2-3b-instruct-f16.gguf \ llama-3.2-3b-instruct-Q4_K_M.gguf \ Q4_K_M量化完成后得到约 2GB 的 GGUF 文件。注意转换脚本依赖 Python 和 torch,建议在 Python 3.10 以上的环境执行。如果模型来源是 GGUF 格式但量化级别太高或太低,也可以直接拿llama-quantize再转一次。
3. 核心细节解析与实操要点
3.1 模型如何塞进 APK
模型文件处理是整个工程里最影响体验的一步。直接放进assets目录最简单,编译时打进 APK,首次启动系统会自动释放。但问题也很明显:2GB 的模型会让 APK 体积膨胀,安装时间边长,还容易触发应用商店对包体大小的限制。
推荐的做法是首次启动时把模型文件放入应用私有目录filesDir。具体来源可以是用户手动选择、服务器下载或电脑adb push。我测试时用的方式是把模型先放到手机/sdcard/Download/,应用启动时用FileInputStream复制到getFilesDir()+"/models/"下。复制过程要做文件长度校验,防止中途断掉。模型加载时传给原生层的就是这个私有目录的绝对路径,而不是assets路径。
3.2 JNI 层封装与 Native 调用
Android 应用跑在 Java/Kotlin 层,llama.cpp 是 C/C++,中间必须用 JNI 搭桥。以官方 llama.android 工程为例,JNI 层对外暴露loadModel(modelPath)和generate(prompt)两个核心方法。我摘了一段最小封装的骨架:
extern "C" JNIEXPORT jstring JNICALL Java_com_example_llama_LLMEngine_generate( JNIEnv* env, jobject thiz, jstring model_path, jstring prompt) { const char* model = env->GetStringUTFChars(model_path, nullptr); const char* text = env->GetStringUTFChars(prompt, nullptr); llama_model_params model_params = llama_model_default_params(); model_params.n_gpu_layers = 0; // 纯 CPU 推理 llama_model* lm = llama_load_model_from_file(model, model_params); llama_context_params ctx_params = llama_context_default_params(); ctx_params.n_ctx = 1024; ctx_params.n_threads = 4; llama_context* ctx = llama_new_context_with_model(lm, ctx_params); // 这里省略了 tokenize、采样和生成循环 std::string result = "generated result"; env->ReleaseStringUTFChars(model_path, model); env->ReleaseStringUTFChars(prompt, text); return env->NewStringUTF(result.c_str()); }这段代码只是展示了 JNI 层怎么把字符串参数传给 C++,真正的 tokenize、生成循环还需要按 llama.cpp 的common库示例补齐。我的建议是直接改官方llama.android的llama-android.cpp,不要从空项目开始手写,否则光是 llama.cpp 的 API 参数就能调一整天。调用时 JNI 方法名一定要和 Kotlin 层的包名、类名、方法名严格对齐,否则运行时会报UnsatisfiedLinkError。
3.3 推理参数、线程数与内存配置
llama.cpp 的推理参数决定了生成速度和质量,几个关键项必须调:n_ctx控制上下文窗口,端侧建议 512 到 1024,太长会吃掉大量内存;n_threads建议设为 CPU 核心数的一半左右,比如 8 核手机先试 4,太大反而因调度开销降低速度且发热严重;temperature默认 0.7 左右,追求信息准确性可以降到 0.3,追求创造力再往上调;top_p保持 0.9 附近比较稳。
内存这块要特别留意。3B 模型 Q4 量化后权重占 2GB,但推理时的 KV Cache 还会额外吃内存,上下文越长占用越大。实测n_ctx=1024时峰值内存可能到 3.5GB 以上,所以老机型最好把n_ctx降到 512。另外记得在 AndroidManifest 里给进程加上android:largeHeap="true",给应用多申请点堆内存,虽然对 native 层帮助有限,但至少能减少一些 Java 层 OOM。
4. 完整实操流程:从源码到真机运行
4.1 Gradle 配置与 NDK 编译
在 Android Studio 里导入examples/llama.android后,先看app/build.gradle。默认配置里已经写好了externalNativeBuild,只需要确认 CMake 路径指向src/main/cpp/CMakeLists.txt:
android { defaultConfig { externalNativeBuild { cmake { cppFlags "-std=c++17 -O3" arguments "-DLLAMA_NATIVE=OFF" } } ndk { abiFilters "arm64-v8a" } } externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" } } }这里把abiFilters限定为arm64-v8a,因为现在 64 位手机占绝大多数,编译 32 位库不仅浪费时间,还容易遇到 NDK 兼容问题。-O3优化对推理速度影响很大,务必保留。CMakeLists 里会编译出libllama.so,Gradle 构建时自动打包进 APK。
构建命令可以直接用 Android Studio 的 Run,也可以在终端执行:
./gradlew assembleDebug第一次构建要下载依赖,耐心等。如果 CMake 报找不到编译器,先检查 NDK 版本和 CMake 路径。
4.2 界面与交互逻辑
官方 demo 的界面很简单:一个输入框、一个生成按钮、一个显示区域。我在自定义工程里用 Kotlin 写了个线程池调用 JNI,避免阻塞主线程:
class MainActivity : AppCompatActivity() { private val engine = LLMEngine() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val modelPath = File(filesDir, "models/llama-3.2-3b-Q4_K_M.gguf").absolutePath engine.loadModel(modelPath) findViewById<Button>(R.id.btn_generate).setOnClickListener { val prompt = findViewById<EditText>(R.id.et_prompt).text.toString() thread { val output = engine.generate(prompt) runOnUiThread { findViewById<TextView>(R.id.tv_output).text = output } } } } }LLMEngine是一个用object声明的单例,里面写external方法对应 JNI 层的Java_com_example_llama_LLMEngine_*。这一步最容易出问题,建议先写一个只返回字符串“hello”的nativeString()方法跑通完整链路,再接入模型推理,逐个排查。
4.3 真机运行与效果验证
把手机通过 USB 连电脑,开启开发者选项和 USB 调试,Android Studio 识别到设备后直接点 Run。安装成功后先去/sdcard/Download/把模型复制到应用私有目录,或者用adb push:
adb push llama-3.2-3b-instruct-Q4_K_M.gguf /sdcard/Download/然后应用首次启动会自动完成复制逻辑。输入“用一句话解释什么是大模型”,点击生成,正常情况下几秒内会看到逐字输出。如果长时间没反应,先看 Logcat 有没有llama_model_load相关报错,最常见的就是路径不对或内存不足。
5. 常见问题与排查技巧实录
端侧部署踩坑是常态,我把实操中遇到的高频问题整理成一张速查表:
| 现象 | 常见原因 | 排查与解决 |
|---|---|---|
编译报错unknown argument: '-fopenmp' | NDK 默认 OpenMP 配置不对 | 检查 CMakeLists,确保链接-fopenmp和libomp |
加载模型报failed to load model | 路径错误或文件损坏 | 打印 modelPath,确认文件存在且大小正确 |
| 生成速度极慢 | 线程数设置过高或编译未开优化 | n_threads改为 4,确保-O3生效 |
| 首 token 延迟高 | 模型文件在外部存储读取慢 | 先把模型复制到 filesDir,再用绝对路径加载 |
| 输出全是乱码 | prompt 模板不正确或词表不匹配 | 检查模型的 chat template,按官方格式拼 prompt |
| 内存溢出崩溃 | n_ctx太大 | 降到 512,并给 Application 加android:largeHeap="true" |
| 发热严重 | 长时间满载推理 | 限制线程数、降低n_ctx,必要时在推理之间加休眠 |
这里有一个非常容易忽视的坑:Release 包默认开启代码混淆,会把 JNI 相关方法名改掉,导致运行时报UnsatisfiedLinkError。真要打 Release,记得在proguard-rules.pro里保留原生方法:
-keepclasseswithmembernames class * { native <methods>; }另外模型复制过程要用md5或文件长度做完整性校验,我曾经因为复制了一半就启动应用,卡在加载流程上排查了快一小时,最后发现是模型文件少了一块。
6. 源码定制与后续扩展
6.1 源码结构导读
如果你要二次开发,先搞清楚 ll ationama.android 工程里每个文件的作用。app/src/main/cpp/llama-android.cpp是 JNI 实现,里面把模型加载和对话生成封装成了Java_com_example_llama_*系列方法;app/src/main/java/com/example/llama/下的 Java /Kotlin 文件只负责界面和线程调度;CMakeLists.txt确定了要编译哪些 C++ 源文件。改模型路径、调参数、换 prompt 模板都在 JNI 层和 Java 层之间配合。
建议动手前先看一遍llama.cpp里的examples/main/main.cpp,它是命令行版的完整推理流程,里面 tokenize、采样、生成循环都是可运行的参考。把这段逻辑移植到 JNI 里时,注意每个 API 函数的生命周期管理,尤其是 ctx 和 model 的释放顺序,搞反了会出现难查的崩溃。
6.2 可以继续扩展的方向
跑通最小 demo 后,可以往这几个方向延伸:流式输出,把生成结果一个字一个字推送到界面,体验比一次性返回好很多;多轮对话,需要维护历史消息并正确处理 prompt 模板;本地知识库,就是 RAG,先用向量库检索再拼 prompt,能明显提升问答准确性;Vulkan 后端,把n_gpu_layers设为大于 0,让 GPU 参与计算,速度能再上个台阶,但兼容性要按机型适配。
还有一个方向是接入语音输入,用系统自带 SpeechRecognizer 把语音转文字,再送给大模型,体验上就是本地语音助手。整体架构不用变,只加一个输入源。
这套流程我前后折腾了两天,最扎心的不是编译,而是模型路径三番五次写错。后来在真机调试时发现,把模型放到外部存储、由应用启动时复制到私有目录是最可靠的方案。如果你照着踩过一次坑,多半会认同:安卓端侧大模型部署,真正难的不是模型本身,而是工程细节——NDK 版本、ABI、JNI 封装、内存约束、线程调度,每一环都不能偷懒。希望这篇记录能帮你省下排查时间。最后再分享一个小技巧:先把最简 demo 跑通,再逐步加功能,比一上来就塞七个模型靠谱得多。
本文还有配套的精品资源,点击获取