Memoria-智能影记创新实训博客(四):Qwen3.5-0.8B 模型的端侧部署与跑通

博客主题:技术基础
时间跨度:2026.04.23 - 2026.04.26(第8周)
进度总结:上一篇博客中提到了故事生成功能提供了两种故事生成方式,一种是直接调deepseek根据标签和时空数据来生成,一种是本地vlm打caption,再交由deepseek生成。本博客实现了本地vlm的端侧部署与成功调用。

1. 目标

把项目里的 Qwen3.5-0.8B 本地模型真正部署到手机侧,并让它在 App 内稳定跑起来。当前项目采用一条更适合工程落地的路径:把 llama.cpp 的可执行文件、Qwen GGUF 模型和 mmproj 一起准备好,打进 APK 或外部部署目录,由 Android 侧通过 MethodChannel 拉起常驻 llama-server,再让 Flutter 通过 127.0.0.1 的 OpenAI 兼容接口发起请求。这样做的目标有三个:第一,保证图片可以在手机本地完成理解,不把原图直接上传云端;第二,把模型加载和推理留在 Android 原生层,避免 Dart 层直接承受本地模型运行成本;第三,为后面的本地 caption、本地故事、多图理解等功能提供统一底座。

2. 部署前提

项目真正依赖的是三类文件:llama-serverllama-mtmd-cli 两个可执行文件,libggml / libllama / libmtmd 等共享库,以及 Qwen3.5-0.8B-Q4_K_M.gguf + mmproj-F16.gguf 这组模型文件。代码里对模型和投影文件的命名是写死匹配的,尤其会优先寻找 checkpoints/qwen/Qwen3.5-0.8B-Q4_K_M.ggufcheckpoints/qwen/mmproj-F16.gguf,所以文件名和目录结构不能随意改。

3. 目录组织与打包方式

项目里对本地模型资源的打包路径已经约定好了,Gradle 会在构建阶段自动把本地资源同步到 APK 资产目录中。核心逻辑在 android/app/build.gradle.kts

from("../../third_party/llama.cpp/install-android-baseline/bin") {
    include("llama-server", "llama-mtmd-cli")
    into("local_llm/install-android-baseline/bin")
}

from("../../third_party/llama.cpp/install-android-baseline/lib") {
    include("libggml-base.so", "libggml-cpu.so", "libggml.so", "libllama.so", "libmtmd.so")
    into("local_llm/install-android-baseline/lib")
}

from("../../checkpoints/qwen") {
    include("Qwen3.5-0.8B-Q4_K_M.gguf", "mmproj-F16.gguf")
    into("local_llm/checkpoints/qwen")
}

这段配置说明了两件事:第一,项目默认把 third_party/llama.cpp/install-android-baseline 视为本地运行时来源,把 checkpoints/qwen 视为模型来源;第二,真正打进 APK 之后,这些文件都会统一落到 assets/local_llm/... 下面。

4. 安装与运行

Android 侧在 OnDeviceInternvlBridge.kt 里做了两层转移:先把打包进 APK 的资源解压到 noBackupFilesDir/local_llm,再把真正需要执行的 llama-serverllama-mtmd-cli 和共享库复制到 noBackupFilesDir/internvl_runtime。这样做的原因很实际:资产目录只适合读取,不适合直接作为可执行运行时目录;而私有目录既能持久保存,又可以给二进制文件设置执行权限。

代码里对应的关键逻辑是:

private fun ensureBundledAssetsInstalledIfNeeded() {
    copyAssetTree("$packagedAssetRoot/install-android-baseline/bin", File(bundledInstallRoot, "bin"))
    copyAssetTree("$packagedAssetRoot/install-android-baseline/lib", bundledLibDir)
    copyAssetTree("$packagedAssetRoot/checkpoints/qwen", bundledCheckpointsRoot)
}

以及:

private fun ensureRuntimeServerStaged() {
    copyFileIfChanged(sourceServer, appRuntimeServerFile)
    sourceLibFiles.forEach { sourceLib ->
        val targetLib = File(appRuntimeLibDir, sourceLib.name)
        copyFileIfChanged(sourceLib, targetLib)
    }
}

所以,真正的运行路径不是 APK 内部,而是:

  • 安装目录:noBackupFilesDir/local_llm
  • 运行目录:noBackupFilesDir/internvl_runtime

5. 拉起本地服务

Flutter 侧并不直接操作模型文件,而是通过 MethodChannel("memoria/on_device_internvl") 调 Android 原生桥。桥接入口在 MainActivity.kt,真正处理逻辑在 OnDeviceInternvlBridge.kt。Dart 侧最关键的几个方法是:

  • getServerDeploymentStatus():检查本地依赖是否齐全
  • getServerStatus():查看当前服务有没有运行、能不能连通
  • ensureServerStarted():真正启动或复用本地常驻服务
  • stopServer():停止服务

启动时,Android 侧会拼出一条标准 llama-server 命令:

val command = mutableListOf(
    linkerPath,
    appRuntimeServerFile.absolutePath,
    "-m", modelFile.path,
    "--mmproj", mmprojFile.path,
    "--host", serverHost,
    "--port", serverPort.toString(),
    "--threads", threads.toString(),
    "--ctx-size", contextSize.toString(),
    "--alias", serverModelAlias,
    "--no-webui",
    "--no-mmproj-offload",
)

这里几个参数很重要:

  • 模型路径和 mmproj 路径都来自部署状态检查结果
  • 服务固定监听 127.0.0.1:8080
  • 模型别名固定为 local-qwen3.5-0.8b-vl
  • 线程数和上下文大小由设备画像动态推荐

也就是说,这条部署方案本质上是“App 内自启动一个本地 OpenAI 兼容服务”,而不是“每次调用都临时起一个进程”。

6. 验证部署成功

在这个项目里,“文件存在”不等于“部署成功”。代码对成功的判断分了三层。

第一层是依赖完整。getServerDeploymentStatus() 会检查:

  • llama-server 是否存在
  • lib 目录是否存在
  • Qwen GGUF 是否存在
  • mmproj 是否存在
  • linker64 是否存在

只要这些项目里有缺失,就会返回 missingItems,同时 isRunnable = false

第二层是端口可达。即使服务已经拉起,如果 127.0.0.1:8080 还连不上,也不能说明服务真的可用。

第三层是推理预热成功。项目不会只检查端口,而是会主动向 /v1/chat/completions 发一个带 1x1 测试图的 warmup 请求。只有端口可达并且 warmup 成功,ready 才会变成 true。也就是说,当前代码里真正意义上的“部署成功”是:

  • isRunnable = true
  • running = true
  • reachable = true
  • ready = true

这比“看到进程 PID”更严格,也更贴近真实可用状态。

7. 总结

当前项目里,Qwen3.5-0.8B 的本地部署已经形成了一条完整链路:先在仓库中准备 llama.cpp 运行时和 Qwen 模型文件,再由 Gradle 在构建阶段把这些资源打进 APK;应用首次运行时把资产解压到私有目录,再把服务可执行文件和共享库复制到运行目录;随后通过 MethodChannel 拉起常驻 llama-server,用 127.0.0.1/v1/chat/completions 承接推理请求,并通过测试页验证部署、启动、预热和推理结果是否全部正常。它的价值不只是“本地能跑一个模型”,而是为后面的本地 caption、多图故事、本地 VLM 辅助生成等功能提供了稳定、统一、可复用的本地推理底座。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐