
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.
Early bird pricing starts at $14.50/mo — locked in for life. Plans for builders and agencies.
Keep reading

On-Device AI Model Size Guide: 1B vs 3B vs 7B for Mobile
How to choose the right model size for your mobile app. Capability breakdown, device requirements, quality benchmarks, and the fine-tuning factor that changes the math.

Quantization for Mobile: Q4, Q5, and Q8 Across Real Devices
A practical guide to GGUF quantization levels for mobile deployment. How Q4, Q5, and Q8 affect model size, speed, quality, and memory usage on iPhones and Android devices.

Llama 3.2 for Mobile Apps: Fine-Tuning and On-Device Deployment
A complete guide to using Meta's Llama 3.2 1B and 3B models in mobile apps. Fine-tuning with LoRA, exporting to GGUF, and deploying on iOS and Android via llama.cpp.