把“星火”塞进手机:在 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 版本冲突 CXX1104local.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 默认走预编译好的 .soRNLLAMA_BUILD_FROM_SOURCE=OFF),而且仓库里压根没带这个预编译库。即便你给它把 spark2_5.cpp 的补丁打进了 node_modules/llama.rn/cpp/models/,只要它不重新从源码编译,那补丁就是个摆设。

正确姿势是强制源码编译:

1
2
3
gradle.bat assembleProdDebug `
-PrnllamaBuildFromSource=true `
-PrnllamaVariants=rnllama

好消息是 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
2
3
Could not initialize native services.
Failed to load native library 'native-platform.dll' ...
java.io.FileNotFoundException: ...native-platform.dll.lock (拒绝访问。)

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.dregistry.bin.locknative-platform.dll.lock)和还活着、占着文件句柄的 java/clang/ninja 进程。下次构建一上来就被这些僵尸锁挡住,报错和「第一次」一模一样,极其迷惑。

解法:跑之前先

1
2
3
4
5
6
# 杀残留进程
taskkill /F /IM java.exe /IM clang*.exe /IM ninja.exe
# 清陈旧构建态
Remove-Item -Recurse -Force android/app/.cxx
# 清 Gradle daemon 与 native 锁
Remove-Item -Recurse -Force $env:GRADLE_USER_HOME/daemons

而且同一时刻只跑一条构建,别让多个后台任务抢同一批锁文件。


五、来之不易的进展

排除项加全、长路径和 ninja 升好、并行度降下来之后,链路是这样跑通的:

  • ✅ 配置所有 autolinked 原生库
  • ✅ 下载 3.4GB 依赖
  • ✅ Java / Kotlin 全量编译
  • ✅ JS Bundle(Metro)打包
  • ✅ reanimated / vision-camera / worklets 等 C++ 原生模块陆续编译通过
  • ⏳ 仅差 llama.cpp native 收尾 + 最终 APK 打包

也就是说,代码和构建配置层面已经全部打通,离出包只差最后一段原生编译。只是这段最慢、也最容易因为前面说的「残留锁」被打断——我最后几次就是卡在 Gradle 又被一个陈旧 native-platform.dll.lock 拦下来,APK 暂时还没产出。


六、如果你也要在 Windows 上搞 RN + 本地 LLM,照这个清单来

  1. 先确认 SDK 组件齐全sdkmanagerplatforms;android-36build-tools;36.0.0cmake
  2. NDK 别写死local.properties 里删 ndk.dir,让 AGP 按模块 ndkVersion 自适应,避免 CXX1104
  3. .env / Firebase 就绕:复制 .env.example;没有 google-services.json 就注释掉 google-services 插件和 firebase 依赖。
  4. 改了 llama.cpp 模型源码就必须源码编-PrnllamaBuildFromSource=true -PrnllamaVariants=rnllama
  5. ABI 收敛到 arm64-v8a:手机基本都是这个,能省掉一大半编译量。
  6. Defender 排除三项.gradle 缓存、Android 构建目录、项目目录本身
  7. LongPathsEnabled,并把 ninja 升到 ≥ 1.11(SDK 自带 1.10.2 不够)。
  8. CMAKE_BUILD_PARALLEL_LEVEL=4,别让 clang 把文件锁打满。
  9. 跑前清残留:杀 java/clang/ninja 进程 + 删 .cxx + 清 Gradle daemon;同一时刻只跑一条构建

七、写在最后

回头看,这场「下午茶项目」前后耗了十来天,真正和 AI / 模型相关的改动其实就一个补丁文件;剩下 90% 的精力,都耗在 Windows 构建环境的各种隐性锁、路径限制和杀软冲突上。

端侧大模型的门槛,从来不只是模型本身,还有这条把模型送进设备的、脆弱又磨人的工具链。

APK 我还在收尾——等最后那段原生编译跑完,下一篇可以聊聊真机上的首 token 延迟和续航实测。


(本文已对文件路径中的用户名、主机名等敏感信息做脱敏处理。)