MiniCPM-V 多模态模型 Android APP 集成指南

本教程主要讲述的是如何将 MiniCPM-V 系列多模态模型(图文/视频理解)集成到你自己的 Android App,原始项目地址:https://github.com/OpenBMB/MiniCPM-V-Apps


1. 前置条件

为了避免出现其他兼容性问题建议与原始项目MiniCPM-V-demo-Android中的版本保持一致

项目 要求
Android Studio 2024.1+(支持 AGP 8.9)
AGP 8.9.1
Gradle 8.11.1
NDK 27.0.12077973
CMake 3.22.1
Kotlin 2.0.21
minSdk 24
目标 ABI arm64-v8a(仅支持 64 位 ARM)
磁盘空间 ≥ 10GB(llama.cpp 编译 + 模型文件)
Java 11

2. 文件清单

必须复制

Kotlin层

  • LlamaEngine.kt:引擎单例,封装全部 JNI 调用
  • ModelInfo.kt:模型元数据(下载源、文件名、MD5校验等)
  • ModelDownloadService.kt:模型下载服务
  • CpuFeatures.kt:CPU 特性检测(选优化的 .so)

JNI层

  • CMakeLists.txt:CMake 配置
  • llama_jni.cpp:JNI 桥接
  • logging.h:日志

可选文件

  • MarkdownEscape.kt:V4.6 输出的 \n 转真换行(显示优化)
  • VideoFrameExtractor.kt:视频理解功能,只有MiniCPM-V 4.6模型支持

3. 步骤一:搭建 Native 层

3.1 添加 llama.cpp 子模块

在项目根目录:

cd /path/to/your/project
git submodule add -b Support-iOS-Demo https://github.com/tc-mb/llama.cpp.git llama.cpp
cd llama.cpp

⚠️ 重要:必须使用 Support-iOS-Demo 分支,它包含 MiniCPM-V 的 clip.cpp 兼容代码,主线分支的 mmproj 转换格式不兼容。

3.2 复制 C++ 文件

llama_jni.cpplogging.hCMakeLists.txt 复制到你项目的 app/src/main/cpp/ 下。

3.3 修改 CMakeLists.txt 中的 llama.cpp 路径

打开 CMakeLists.txt,修改第 38-39 行的 LLAMA_SRC 路径:

# 默认在项目4级目录上找 llama.cpp
if(NOT DEFINED LLAMA_SRC)
    set(LLAMA_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../../../llama.cpp)
endif()

# 如果你的llama.cpp 在项目根目录下,改成:
if(NOT DEFINED LLAMA_SRC)
    set(LLAMA_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../../llama.cpp)
endif()

路径关系:app/src/main/cpp/ → 上 4 级到项目根 → llama.cpp/

3.4 修改 JNI 函数名

llama_jni.cpp 中的 JNI 函数名基于包名 com.example.minicpm_v_demo,格式为:

Java_包名_类名_方法名(. → _,_ → _1)

当前函数名示例:

Java_com_example_minicpm_1v_1demo_LlamaEngine_load

如果你的包名是 com.yourapp.ai,需要把所有 JNI 函数前缀改为:

Java_com_yourapp_ai_LlamaEngine_load

完整替换列表(在 llama_jni.cpp 中搜索替换):

原始前缀 新前缀
Java_com_example_minicpm_1v_1demo_LlamaEngine_ Java_com_yourapp_ai_LlamaEngine_

4. 步骤二:配置 Gradle 构建

4.1 gradle/libs.versions.toml

[versions]
agp = "8.9.0"
kotlin = "2.0.21"
coreKtx = "1.10.1"
appcompat = "1.6.1"
material = "1.10.0"
constraintlayout = "2.1.4"
lifecycleRuntimeKtx = "2.6.2"
activityKtx = "1.8.0"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

4.2 gradle/wrapper/gradle-wrapper.properties

distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip

4.3 app/build.gradle.kts

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
}

android {
    namespace = "com.xxx.xxx" //改为自己的项目包路径
    compileSdk = 36
    ndkVersion = "27.0.12077973"

    defaultConfig {
        applicationId = "com.xxx.xxx"
        minSdk = 24
        targetSdk = 36
        versionCode = 1
        versionName = "1.0"
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        ndk {
            abiFilters.add("arm64-v8a")   // 只构建 ARM64
        }

        //cmake相关的配置
        externalNativeBuild {
            cmake {
                arguments += "-DCMAKE_BUILD_TYPE=Release"
                arguments += "-DBUILD_SHARED_LIBS=ON"
                arguments += "-DLLAMA_BUILD_COMMON=ON"
                arguments += "-DLLAMA_OPENSSL=OFF"
                arguments += "-DGGML_NATIVE=OFF"
                arguments += "-DGGML_LLAMAFILE=ON"
                arguments += "-DGGML_CPU_ARM_ARCH=armv8.2-a+dotprod+fp16"
            }
        }

    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }

    kotlinOptions {
        jvmTarget = "11"
    }

    externalNativeBuild {
        cmake {
            path = file("src/main/cpp/CMakeLists.txt")
            version = "3.22.1"
        }
    }

    buildFeatures {
        compose = true
    }
}

dependencies {

    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.activity.compose)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.ui.graphics)
    implementation(libs.androidx.ui.tooling.preview)
    implementation(libs.androidx.material3)
    implementation(libs.androidx.material.icons.extended)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    androidTestImplementation(platform(libs.androidx.compose.bom))
    androidTestImplementation(libs.androidx.ui.test.junit4)
    debugImplementation(libs.androidx.ui.tooling)
    debugImplementation(libs.androidx.ui.test.manifest)
    
}

5. 步骤三:复制 Kotlin 核心文件

5.1 必须文件

把以上提到的必须复制文件,复制到你的包名目录下,并修改 package 声明。

5.2 LlamaEngine.kt 的 JNI 声明

如果你的包名改变了,需要同步修改 LlamaEngine.kt 中的 external 函数声明。Kotlin 的 external 函数名必须和 C++ 端一致,不改 Kotlin 声明,改 C++ 端的函数名来匹配。


6. 步骤四:配置 AndroidManifest

<manifest ...>
    <!-- 网络权限(下载模型必须) -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <!-- 以下权限仅在集成 ModelDownloadService 时需要 -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

    <application
        android:networkSecurityConfig="@xml/network_security_config"
        ...>
        
        <!-- 如果使用 ModelDownloadService -->
        <service
            android:name=".ModelDownloadService"
            android:exported="false"
            android:foregroundServiceType="dataSync" />
    </application>
    
</manifest>

network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>

<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

7. 步骤五:模型文件管理

7.1 模型存储结构

/data/data/com.yourapp.ai/files/models/
├── minicpm-v-4/                        # ModelInfo.id
│   ├── ggml-model-Q4_K_M.gguf         # LLM 主模型
│   └── mmproj-model-f16.gguf           # 视觉投影模型
└── minicpm-v-4_6-instruct/            # 另一个模型
    ├── MiniCPM-V-4_6-Q4_K_M.gguf
    └── mmproj-model-merger-f16.gguf

7.2 下载源

模型 大小 下载策略
MiniCPM-V-4 (Q4_K_M) ~4.1B HuggingFace → ModelScope 回退
MiniCPM-V-4.6 (Q4_K_M) ~1.2B 华为云 OBS 直链

7.3 下载方式

方式 A:App 内下载(推荐)

集成 ModelDownloadService.kt,用户点击下载按钮即可,支持断点续传和 MD5 校验。

//启动下载
ModelDownloadService.start(context)

//下载状态
lifecycleScope.launch {
    ModelDownloadController.status.collect { status ->
        when (status) {
            is ModelDownloadController.Status.Running -> showProgress(status.message)
            is ModelDownloadController.Status.Completed -> onDownloadComplete()
            is ModelDownloadController.Status.Failed -> showError(status.message)
            else -> {}
        }
    }
}

方式 B:adb 侧载

#推送模型文件到设备 测试时可以这样
adb push ggml-model-Q4_K_M.gguf /data/data/com.yourapp.ai/files/models/minicpm-v-4/
adb push mmproj-model-f16.gguf /data/data/com.yourapp.ai/files/models/minicpm-v-4/

8. 步骤六:在 App 中使用

// 初始化引擎 条件加载模型 这里建议放到MainActivity的onCreate中 在启动的时候自动加载模型
val engine = LlamaEngine.getInstance(this)

if (LlamaEngine.modelsExist(this)) {
    lifecycleScope.launch {
        engine.state.collect { state ->
                              if (state is LlamaState.Initialized) {
                                  Log.i("ModelLoad", "模型文件已就绪,开始加载...")
                                  try {

                                      engine.loadModel(
                                          LlamaEngine.modelPath(this@MainActivity),
                                          LlamaEngine.mmprojPath(this@MainActivity)
                                      )

                                      Log.i("ModelLoad", "模型加载成功!")

                                  } catch (e: Exception) {
                                      Log.e("ModelLoad", "模型加载失败", e)
                                  }
                                  return@collect  // 只触发一次
                              }
                             }
    }
}

9. API 表

LlamaEngine 公开 API

方法 简要说明
getInstance(context) 获取单例
loadModel(path, mmprojPath?) 加载模型+视觉模型
setSystemPrompt(prompt) 设置系统提示词
sendUserPrompt(msg, predictLen) 发送用户消息
prefillImage(imageData) 预填充图片
prefillVideoFrames(frames, onProgress) 预填充视频帧
clearContext() 清空上下文
unloadModel() 卸载模型
cancelGeneration() 取消当前生成
destroy() 销毁引擎
setImageMaxSliceNums(n) 设置图片切片数
state 引擎状态流
isVisionSupported 是否支持图片
isVideoUnderstandingSupported 是否支持视频

LlamaEngine 静态 API

方法 简要说明
getSelectedModel(context) 获取当前选择的模型
setSelectedModel(context, id) 选择模型
modelPath(context) GGUF 文件路径
mmprojPath(context) mmproj 文件路径
modelsExist(context) 模型文件是否都在
modelDir(context) 模型根目录
modelDirFor(context, model) 某模型目录
getImageMaxSliceNums(context) 图片切片数
setImageMaxSliceNumsPref(context, n) 持久化切片数
downloadModels(context, onProgress) 下载模型文件
migrateLegacyLayoutIfNeeded(context) 迁移旧文件布局

9.1 自定义推理参数

llama_jni.cpp 中修改

constexpr int   N_THREADS            = 4;    // 推理 线程数影响推理速度 亲测 这里可以稍微调大
constexpr int   DEFAULT_CONTEXT_SIZE = 4096; // KV cache 大小
constexpr int   V46_CONTEXT_SIZE     = 8192; // V4.6 的 KV cache
constexpr int   BATCH_SIZE           = 2048; // 批处理大小 这里也可以稍微调大一些 
constexpr float DEFAULT_SAMPLER_TEMP = 0.7f; // 温度
Logo

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

更多推荐