把“星火”塞进手机:在 Windows 上构建本地大模型 App 的 11 天环境恶战
摘要:想用 PocketPal AI 在安卓上离线跑通讯飞「星火 X2.5-4B」量化模型,结果 90% 的时间不是花在 AI 上,而是花在和 Windows 构建环境搏斗上。这篇记录从接手一个失败的交接文档,到把整条 RN + llama.cpp 链路几乎跑通的全过程,以及一份可以直接抄作业的避坑清单。
一、缘起:我想在手机里养一个本地大模型
最近在折腾端侧大模型推理,目标很明确:
把 Spark-X2.5-4B-Q4_K_M.gguf(讯飞星火 X2.5-4B 的 4-bit 量化版)塞进安卓手机,用 PocketPal AI 这个开源 RN 应用离线跑起来,不依赖任何云端。
PocketPal 本身是微软出品、基于 React Native 的本地模型客户端,底层用 llama.rn 绑定 llama.cpp。理论上只要把最新的 llama.cpp 里已经合入的 spark2_5 架构支持接上,再打出 APK 就行。
听起来像是「git clone → 改两行 → 构建」的下午茶项目。
事实证明,我太天真了。
二、接手时的一地鸡毛
前一位同学(用 VS Code 跑了一阵)留了一份交接文档,结论是:
「
spark2_5代码补丁已经补齐,但 Android 打包链路在当前 Windows 环境里没跑通,建议从.env+ Gradle 任务 + 输出路径 + 变体过滤继续排查。」
乍看很合理。但真正动手后我发现,这份文档漏掉了 4 个会直接让构建起不来的硬阻塞点。换句话说,光按它的建议走,连第一步都迈不出去:
| # | 被漏掉的阻塞 | 现象 | 修法 |
|---|---|---|---|
| 1 | SDK 没装齐 | 缺 android-36 platform、build-tools 36.0.0、cmake |
sdkmanager 一把装齐 |
| 2 | NDK 版本冲突 | CXX1104:local.properties 写死 ndk.dir=27.3,但某模块要求 27.0 |
删掉 ndk.dir,让 AGP 按各模块 ndkVersion 自适应 |
| 3 | 缺 .env / Firebase 配置 |
Missing .env file;缺 google-services.json 直接 FAIL |
复制 .env.example;注释掉 google-services 插件和 firebase 依赖 |
| 4 | spark2_5 必须源码编译 |
llama.rn 默认用预编译 .so 且没下载,补丁白打 |
加 -PrnllamaBuildFromSource=true -PrnllamaVariants=rnllama |
第 4 点是最关键的认知,后面单独讲。
三、核心认知:为什么补丁打了却没用
llama.rn 默认走预编译好的 .so 库(RNLLAMA_BUILD_FROM_SOURCE=OFF),而且仓库里压根没带这个预编译库。即便你给它把 spark2_5.cpp 的补丁打进了 node_modules/llama.rn/cpp/models/,只要它不重新从源码编译,那补丁就是个摆设。
正确姿势是强制源码编译:
1 | gradle.bat assembleProdDebug ` |
好消息是 rnllama 的 CMake 用 file(GLOB models/*.cpp) 自动扫模型实现目录,spark2-5.cpp 会被自动编进去;再配合 -PrnllamaVariants=rnllama 只编 generic 变体,避免把一堆用不上的架构都拉来编译。
这一条值得所有人记住:改了 llama.cpp 的模型源码,就必须从源码编,预编译
.so不会自己更新。
四、真正的 Boss:Windows 构建环境
代码层修完,本以为胜利在望。然后 Windows 教我做人。
技术栈:RN 0.82.1 / AGP 8.12.0 / Gradle 8.13 / Kotlin 2.1.20 / compileSdk 36 / NDK 27。下面按踩坑时间线记录。
1. Defender 实时扫描锁死 Gradle 原生库
Gradle 8.13 一启动就报:
1 | Could not initialize native services. |
dll 本身能加载,普通文件读写删都正常,唯独 Gradle 用 RandomAccessFile 给 .lock 加锁时被微软 Defender 实时防护在创建瞬间排他锁住**。
解法:把这三个目录加进 Defender 排除项(或临时关掉实时保护):
C:\Users\\.gradle(含各种缓存目录)- 独立的 Android 构建目录(如
E:\android-build) - 项目目录本身(这点最容易被漏,C++ 编译产物
android/app/.cxx就在项目里,不排它一样卡死)
2. 260 字符路径上限 + 过时的 ninja
C++ 编译一开,ninja 报 Filename longer than 260 characters。
注册表开了 LongPathsEnabled=1 并重启后,ninja 1.10.2(SDK 自带)还是翻车——它太老,不认长路径。
解法:把 SDK 里的 cmake/3.22.1/bin/ninja.exe 备份后,替换成 ninja 1.12.1。
3. clang 死锁假死
clang++ 进程 CPU 占用 0%,看起来在编译,实际死等。根因还是 Defender 在扫 .cxx 产物 + CMake 高并行度下的文件锁竞争。
解法:降并行度 CMAKE_BUILD_PARALLEL_LEVEL=4,并把项目目录彻底排除出实时扫描。
4. 残留进程 / 陈旧 daemon 锁
最阴的是:中途强杀后台构建,会留下一堆陈旧锁文件(.o.d、registry.bin.lock、native-platform.dll.lock)和还活着、占着文件句柄的 java/clang/ninja 进程。下次构建一上来就被这些僵尸锁挡住,报错和「第一次」一模一样,极其迷惑。
解法:跑之前先
1 | # 杀残留进程 |
而且同一时刻只跑一条构建,别让多个后台任务抢同一批锁文件。
五、来之不易的进展
排除项加全、长路径和 ninja 升好、并行度降下来之后,链路是这样跑通的:
- ✅ 配置所有 autolinked 原生库
- ✅ 下载 3.4GB 依赖
- ✅ Java / Kotlin 全量编译
- ✅ JS Bundle(Metro)打包
- ✅ reanimated / vision-camera / worklets 等 C++ 原生模块陆续编译通过
- ⏳ 仅差
llama.cppnative 收尾 + 最终 APK 打包
也就是说,代码和构建配置层面已经全部打通,离出包只差最后一段原生编译。只是这段最慢、也最容易因为前面说的「残留锁」被打断——我最后几次就是卡在 Gradle 又被一个陈旧 native-platform.dll.lock 拦下来,APK 暂时还没产出。
六、如果你也要在 Windows 上搞 RN + 本地 LLM,照这个清单来
- 先确认 SDK 组件齐全:
sdkmanager装platforms;android-36、build-tools;36.0.0、cmake。 - NDK 别写死:
local.properties里删ndk.dir,让 AGP 按模块ndkVersion自适应,避免CXX1104。 - 缺
.env/ Firebase 就绕:复制.env.example;没有google-services.json就注释掉google-services插件和 firebase 依赖。 - 改了 llama.cpp 模型源码就必须源码编:
-PrnllamaBuildFromSource=true -PrnllamaVariants=rnllama。 - ABI 收敛到
arm64-v8a:手机基本都是这个,能省掉一大半编译量。 - Defender 排除三项:
.gradle缓存、Android 构建目录、项目目录本身。 - 开
LongPathsEnabled,并把 ninja 升到 ≥ 1.11(SDK 自带 1.10.2 不够)。 CMAKE_BUILD_PARALLEL_LEVEL=4,别让 clang 把文件锁打满。- 跑前清残留:杀
java/clang/ninja进程 + 删.cxx+ 清 Gradle daemon;同一时刻只跑一条构建。
七、写在最后
回头看,这场「下午茶项目」前后耗了十来天,真正和 AI / 模型相关的改动其实就一个补丁文件;剩下 90% 的精力,都耗在 Windows 构建环境的各种隐性锁、路径限制和杀软冲突上。
端侧大模型的门槛,从来不只是模型本身,还有这条把模型送进设备的、脆弱又磨人的工具链。
APK 我还在收尾——等最后那段原生编译跑完,下一篇可以聊聊真机上的首 token 延迟和续航实测。
(本文已对文件路径中的用户名、主机名等敏感信息做脱敏处理。)