
GGUF 模型分发:应用商店打包 vs 安装后下载
两种将 GGUF 模型送达用户设备的方式。与应用打包以获得简单性,或安装后下载以获得灵活性。两种方案的架构、大小限制和最佳实践。
您的模型已经微调并导出为 GGUF。现在需要将它送达用户的设备。有两种基本方式:与应用二进制文件打包,或安装后下载。
每种方式在应用大小、用户体验、更新灵活性和平台限制方面都有不同的取舍。
选项 1:与应用打包
将 GGUF 文件包含在应用安装包中。用户下载应用时同时下载模型。
iOS 打包
直接包含: 将 GGUF 文件添加到 Xcode 项目中。它随 IPA 一起分发。通过 Bundle.main 访问:
let modelPath = Bundle.main.path(forResource: "model", ofType: "gguf")!
On Demand Resources(ODR): 将模型标记为按需资源。iOS 在首次需要时下载,而非安装时。初始应用下载保持较小。
let request = NSBundleResourceRequest(tags: ["ai-model"])
request.beginAccessingResources { error in
guard error == nil else { return }
let modelPath = Bundle.main.path(forResource: "model", ofType: "gguf")!
loadModel(at: modelPath)
}
ODR 文件由 iOS 管理,在存储空间紧张时可能被清除。您的应用必须处理重新下载。
Android 打包
APK assets: 对于 150MB 以内的模型,将 GGUF 放在 assets/ 中。首次启动时复制到内部存储供 llama.cpp 访问。
Play Asset Delivery: 对于更大的模型,使用 Google Play 的资产分发系统:
// 安装时分发(随应用下载)
// build.gradle.kts
assetPacks += ":model_pack"
Play Asset Delivery 支持三种模式:
- Install-time: 随应用下载。最简单但增加初始下载大小。
- Fast-follow: 安装后立即在后台下载。
- On-demand: 应用请求时才下载。
大小限制
| 平台 | 限制 | 说明 |
|---|---|---|
| iOS IPA | 4GB | 包含所有资源 |
| iOS OTA 下载 | 200MB | 蜂窝网络下载限制(用户可覆盖) |
| Android APK | 150MB | 不 使用 Play Asset Delivery 的情况 |
| Android AAB | 150MB 基础 + 2GB 资产 | 使用 Play Asset Delivery |
| Play Asset Delivery 包 | 每包 512MB | 允许多个包 |
1B GGUF Q4 模型(约 600MB)在 iOS 的 4GB 限制内,但超过了 200MB 蜂窝 OTA 阈值。在 Android 上需要 Play Asset Delivery。
3B GGUF Q4 模型(约 1.7GB)在两个平台的上限内,但会是一个较大的下载。
打包的优缺点
优点:
- 首次启动时模型立即可用(无需等待下载)
- 不需要 CDN 基础设施
- 首次使用不需要网络连接
- 更简单的架构(无需下载/验证/恢复逻辑)
缺点:
- 显著增加应用下载大小
- 模型更新需要通过商店进行完整的应用更 新
- 每次模型更改都需要应用商店审核
- 用户可能不愿下载 600MB-1.7GB 的应用
- 在 iOS 上,200MB 蜂窝限制意味着用户可能需要 WiFi 来下载
选项 2:安装后下载
应用安装时不包含模型。首次启动时(或用户访问 AI 功能时),应用从您的 CDN 下载模型。
下载流程
[应用已安装] -> [用户打开 AI 功能] -> [未找到模型]
-> [显示下载提示:"下载 AI 模型(1.7GB)?"]
-> [用户点击下载] -> [进度条]
-> [下载完成] -> [验证哈希] -> [模型就绪]
iOS 实现
class ModelDownloader: ObservableObject {
@Published var progress: Double = 0
@Published var isDownloading = false
@Published var isReady = false
private let modelURL = URL(string: "https://cdn.example.com/model.gguf")!
private var modelPath: URL {
FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("model.gguf")
}
func checkModelAvailable() -> Bool {
FileManager.default.fileExists(atPath: modelPath.path)
}
func downloadModel() async throws {
isDownloading = true
let (tempURL, response) = try await URLSession.shared.download(
from: modelURL,
delegate: ProgressDelegate { progress in
Task { @MainActor in self.progress = progress }
}
)
try FileManager.default.moveItem(at: tempURL, to: modelPath)
// 验证完整性
guard verifyHash(modelPath, expected: expectedSHA256) else {
try FileManager.default.removeItem(at: modelPath)
throw ModelError.corruptedDownload
}
isDownloading = false
isReady = true
}
}
Android 实现
class ModelDownloader(private val context: Context) {
private val modelFile = File(context.filesDir, "model.gguf")
fun isModelAvailable(): Boolean = modelFile.exists()
suspend fun downloadModel(
onProgress: (Float) -> Unit
) = withContext(Dispatchers.IO) {
val client = OkHttpClient()
val request = Request.Builder().url(MODEL_CDN_URL).build()
val response = client.newCall(request).execute()
val body = response.body ?: throw IOException("Empty response")
val totalBytes = body.contentLength()
var downloadedBytes = 0L
modelFile.outputStream().use { output ->
body.byteStream().use { input ->
val buffer = ByteArray(8192)
var read: Int
while (input.read(buffer).also { read = it } != -1) {
output.write(buffer, 0, read)
downloadedBytes += read
onProgress(downloadedBytes.toFloat() / totalBytes)
}
}
}
// 验证完整性
val hash = modelFile.sha256()
if (hash != EXPECTED_SHA256) {
modelFile.delete()
throw IOException("Corrupted download")
}
}
}
CDN 设置
将 GGUF 文件托管在 CDN 上以实现快速、可靠的分发:
- AWS CloudFront + S3: 标准方案。约 $0.085/GB 传输费用。
- Cloudflare R2: 无出站费用。仅约 $0.015/GB 存储费用。
- Firebase Hosting: 适合小项目。10GB 免费,之后 $0.15/GB。
以每月 10,000 次下载 1.7GB 模型为例的成本:
- CloudFront:约 $1,445/月
- Cloudflare R2:约 $0.26/月(仅存储费,无出站费)
- Firebase:约 $2,550/月
Cloudflare R2 的零出站费定价使其在模型分发方面成本大幅降低。
断点续传支持
大文件下载会被中断。支持断点续传:
// iOS:恢复中断的下载
let resumeData = try? Data(contentsOf: resumeDataURL)
if let resumeData = resumeData {
downloadTask = session.downloadTask(withResumeData: resumeData)
} else {
downloadTask = session.downloadTask(with: modelURL)
}
安装后下载的优缺点
优点:
- 初始应用下载小(安装快,不因商店大小而犹豫)
- 模型更新无需应用商店审核(将新模型推送到 CDN)
- 可提供多种模型大小(1B 给所有人,3B 作为升级选项)
- 用户只在使用 AI 功能时才下载
缺点:
- 首次使用有延迟(下载需要 1-5 分钟)
- 首次使用需要网络
- CDN 基础设施和成本
- 更复杂的代码(下载、验证、断点续传、存储管理)
建议方案
| 场景 | 方案 |
|---|---|
| 1B 模型,AI 是应用核心 | 打包(600MB 可接受) |
| 3B 模型,AI 是应用核心 | Fast-follow / On-demand 分发 |
| AI 是可选功能 | 安装后下载 |
| 模型频繁更新(每月) | 安装后下载 |
| 模型稳定(每季度更新) | 打包或 fast-follow |
| 目标市场网络慢 | 打包 |
对于大多数应用:安装后下载并提供清晰的下载提示。 这样可以保持初始应用下载小,让您独立更新模型,并且只为实际使用 AI 功能的用户下载。
完整性验证
始终在下载后验证模型文件。损坏的 GGUF 文件会在推理过程中导致崩溃:
func verifyHash(_ fileURL: URL, expected: String) -> Bool {
guard let data = try? Data(contentsOf: fileURL) else { return false }
let hash = SHA256.hash(data: data)
let hashString = hash.compactMap { String(format: "%02x", $0) }.joined()
return hashString == expected
}
存储管理
GGUF 模型文件很大。尊重用户的存储空间:
- 下载前显示模型大小
- 允许删除并重新下载模型
- 在 iOS 上,将模型排除在 iCloud 备份之外(可以重新下载)
- 优雅处理低存储空间场景
模型本身才是关键。通过 Ertas 等平台生成的高质量微调 GGUF,无论通过打包还是下载分发,都能提供在本地即时运行、零使用成本的领域特定 AI。
Ship AI that runs on your users' devices.
Free plan with 30 credits/mo, no card required. Paid plans from $25/mo USD.


