跑本地大模型:端側(cè)推理部署與24小時(shí)?;顚?shí)戰(zhàn))
1. 舊手機(jī)跑本地大模型這件事到底靠不靠譜手里那臺(tái)吃灰的舊安卓機(jī)除了換不銹鋼盆之外其實(shí)還有一條更硬核的出路——讓它變成一臺(tái)24小時(shí)在線的本地大模型推理終端。這個(gè)想法聽(tīng)起來(lái)像是折騰但實(shí)測(cè)下來(lái)一臺(tái)驍龍845級(jí)別、6GB內(nèi)存的老機(jī)器跑一個(gè)量化后的7B模型生成速度能穩(wěn)定在每秒3到5個(gè)token日常問(wèn)答、文本摘要、簡(jiǎn)單代碼補(bǔ)全完全夠用。核心思路就是利用開(kāi)源推理框架在安卓端做本地部署把模型權(quán)重和推理引擎全部塞進(jìn)手機(jī)里不依賴任何外部網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)不出設(shè)備隱私性和離線可用性直接拉滿。這件事適合誰(shuí)呢一類是手里有閑置安卓機(jī)、想拿來(lái)跑點(diǎn)實(shí)際任務(wù)的開(kāi)發(fā)者另一類是對(duì)本地大模型部署感興趣、但不想額外買硬件的新手。你不需要精通Kotlin或者NDK開(kāi)發(fā)只要會(huì)基本的adb操作、能看懂配置文件就能跟著走通整個(gè)流程。我前后在四臺(tái)不同年代的安卓設(shè)備上做過(guò)部署從安卓9到安卓14都試過(guò)踩過(guò)的坑包括內(nèi)存溢出、模型格式不兼容、后臺(tái)被殺進(jìn)程等等下面會(huì)把完整方案和避坑經(jīng)驗(yàn)全部攤開(kāi)講。2. 整體方案設(shè)計(jì)與技術(shù)選型拆解2.1 為什么選端側(cè)推理框架而不是遠(yuǎn)程調(diào)用很多人第一反應(yīng)是手機(jī)跑不動(dòng)大模型直接調(diào)云端API不就行了但這里有個(gè)關(guān)鍵區(qū)別——本地部署的核心價(jià)值在于數(shù)據(jù)不出設(shè)備和離線可用。你如果只是想要一個(gè)問(wèn)答機(jī)器人云端API確實(shí)更省事但一旦涉及個(gè)人文檔處理、隱私對(duì)話、內(nèi)網(wǎng)環(huán)境使用本地推理就是剛需。而且舊手機(jī)本身閑置著電費(fèi)幾乎可以忽略24小時(shí)開(kāi)機(jī)跑推理相當(dāng)于白撿了一臺(tái)低功耗推理服務(wù)器。端側(cè)推理框架的選擇上目前安卓平臺(tái)比較成熟的有幾種路線一種是基于llama.cpp的安卓移植版本另一種是專門為移動(dòng)端優(yōu)化的推理引擎。我最終選的是OlliteRT這條路線原因是它對(duì)量化模型的支持比較完善內(nèi)存占用控制得不錯(cuò)而且提供了相對(duì)清晰的JNI接口方便用Kotlin做上層封裝。相比之下直接拿Python方案往安卓上搬基本不現(xiàn)實(shí)性能損耗太大發(fā)熱也壓不住。2.2 模型選型的核心邏輯模型選型是整個(gè)方案里最關(guān)鍵的決策點(diǎn)。舊手機(jī)的內(nèi)存和算力都有限不能盲目上大參數(shù)模型。我的經(jīng)驗(yàn)是6GB內(nèi)存的設(shè)備模型量化后體積控制在3.5GB以內(nèi)比較穩(wěn)妥8GB內(nèi)存可以放寬到4.5GB左右。超過(guò)這個(gè)線系統(tǒng)隨時(shí)可能因?yàn)閮?nèi)存不足把推理進(jìn)程殺掉。具體到模型版本7B參數(shù)級(jí)別的模型經(jīng)過(guò)4-bit量化后體積大約在3.8GB到4.2GB之間是舊手機(jī)比較理想的甜點(diǎn)區(qū)。再小的模型比如3B級(jí)別雖然跑得快但回答質(zhì)量下降明顯復(fù)雜一點(diǎn)的邏輯推理就容易胡言亂語(yǔ)。再大的13B模型量化后也要6GB以上舊手機(jī)基本扛不住。所以我的建議很明確優(yōu)先選7B級(jí)別的4-bit量化版本在質(zhì)量和資源占用之間取平衡。這里要特別提醒一點(diǎn)模型文件格式必須和推理框架匹配。常見(jiàn)的格式有GGUF、GGML等不同框架支持的不一樣。下載模型之前一定要確認(rèn)框架文檔里寫的支持格式否則下回來(lái)加載失敗白白浪費(fèi)時(shí)間和流量。2.3 安卓版本與硬件門檻安卓版本方面實(shí)測(cè)安卓9及以上都能跑但安卓11以上會(huì)更穩(wěn)定因?yàn)楹笈_(tái)進(jìn)程管理機(jī)制相對(duì)寬松一些。安卓9的問(wèn)題在于部分設(shè)備對(duì)后臺(tái)服務(wù)的限制比較激進(jìn)需要手動(dòng)關(guān)閉電池優(yōu)化才能保證24小時(shí)運(yùn)行。安卓14則引入了更嚴(yán)格的前臺(tái)服務(wù)類型限制需要在Manifest里正確聲明服務(wù)類型否則啟動(dòng)就崩。硬件門檻其實(shí)不高但有幾個(gè)硬指標(biāo)內(nèi)存至少4GB存儲(chǔ)至少留出8GB空閑空間CPU最好是驍龍835或同級(jí)別以上。我試過(guò)一臺(tái)驍龍660的機(jī)器跑是能跑但生成速度掉到每秒1個(gè)token以下體驗(yàn)就很差了。另外散熱也很重要舊手機(jī)長(zhǎng)時(shí)間滿載推理背面溫度能到45度以上最好加個(gè)散熱背夾或者放在通風(fēng)處。3. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)3.1 開(kāi)發(fā)環(huán)境搭建與項(xiàng)目結(jié)構(gòu)整個(gè)項(xiàng)目用Android Studio開(kāi)發(fā)語(yǔ)言選Kotlin。項(xiàng)目結(jié)構(gòu)上核心分三層JNI底層負(fù)責(zé)調(diào)用推理引擎的C接口中間層用Kotlin封裝模型加載和推理調(diào)用上層用Compose或者XML做簡(jiǎn)單的交互界面。如果你只想跑通推理界面甚至可以不要直接用adb shell觸發(fā)推理任務(wù)就行。搭建環(huán)境時(shí)需要注意幾個(gè)點(diǎn)。NDK版本建議選r25以上CMake版本3.22以上這兩個(gè)版本對(duì)C17特性支持比較完整。Gradle配置里要開(kāi)啟externalNativeBuild把推理引擎的源碼或者預(yù)編譯庫(kù)鏈接進(jìn)來(lái)。如果用的是預(yù)編譯的.so文件記得在jniLibs目錄下按ABI分目錄放置arm64-v8a是必須的armeabi-v7a可選。// build.gradle.kts 關(guān)鍵配置片段 android { ndkVersion 25.2.9519653 defaultConfig { externalNativeBuild { cmake { cppFlags -stdc17 arguments -DANDROID_STLc_shared } } ndk { abiFilters listOf(arm64-v8a) } } externalNativeBuild { cmake { path file(src/main/cpp/CMakeLists.txt) } } }注意如果推理引擎依賴OpenMP或者特定數(shù)學(xué)庫(kù)需要在CMakeLists里顯式鏈接否則運(yùn)行時(shí)會(huì)報(bào)符號(hào)找不到的錯(cuò)誤。這個(gè)坑我踩過(guò)兩次排查了半天才發(fā)現(xiàn)是鏈接順序問(wèn)題。3.2 模型文件放置與內(nèi)存映射加載模型文件不能放在assets里因?yàn)閍ssets有壓縮加載大文件時(shí)會(huì)出問(wèn)題。正確做法是把模型文件推到設(shè)備的內(nèi)部存儲(chǔ)或者外部存儲(chǔ)的私有目錄下然后用**內(nèi)存映射mmap**的方式加載。mmap的好處是操作系統(tǒng)按需分頁(yè)不會(huì)一次性把整個(gè)模型讀進(jìn)物理內(nèi)存對(duì)舊手機(jī)來(lái)說(shuō)這點(diǎn)非常關(guān)鍵。具體操作上先用adb把模型推上去adb push model-q4.gguf /sdcard/Android/data/com.example.localai/files/models/然后在Kotlin層通過(guò)文件路徑傳給JNIJNI里用mmap打開(kāi)文件把指針交給推理引擎。這里有個(gè)細(xì)節(jié)文件路徑的權(quán)限要確保應(yīng)用能讀取如果是Android 11以上訪問(wèn)外部存儲(chǔ)需要走M(jìn)ediaStore或者使用應(yīng)用私有目錄。我一般直接放在應(yīng)用私有目錄下省去權(quán)限申請(qǐng)的麻煩。加載時(shí)的內(nèi)存占用要提前估算。一個(gè)4GB的模型文件mmap之后實(shí)際物理內(nèi)存占用大概在2.5GB到3GB之間加上推理時(shí)的KV Cache和中間激活值峰值可能到3.5GB。所以6GB內(nèi)存的設(shè)備系統(tǒng)本身占掉1.5GB到2GB留給推理的余量其實(shí)很緊張。建議在加載模型之前先調(diào)用Runtime.getRuntime().gc()做一次強(qiáng)制回收能擠出幾百M(fèi)B的空間。3.3 推理參數(shù)調(diào)優(yōu)與線程綁定推理參數(shù)直接決定生成速度和輸出質(zhì)量。核心參數(shù)有這么幾個(gè)上下文長(zhǎng)度context length、批大小batch size、線程數(shù)threads、溫度temperature。舊手機(jī)上上下文長(zhǎng)度建議設(shè)2048或者4096再大內(nèi)存扛不住。批大小設(shè)1就行設(shè)大了內(nèi)存暴漲但速度提升不明顯。線程數(shù)一般設(shè)成CPU大核的數(shù)量比如驍龍845是4個(gè)大核就設(shè)4設(shè)多了反而因?yàn)檎{(diào)度開(kāi)銷導(dǎo)致速度下降。溫度參數(shù)看用途。做事實(shí)性問(wèn)答溫度設(shè)0.1到0.3輸出穩(wěn)定做創(chuàng)意寫作可以設(shè)0.7到0.9。我一般默認(rèn)設(shè)0.6兼顧穩(wěn)定性和多樣性。另外top-p和top-k也建議設(shè)一下top-p設(shè)0.9top-k設(shè)40能有效避免生成重復(fù)內(nèi)容。// 推理參數(shù)配置示例 val params InferenceParams( contextLength 2048, batchSize 1, threads 4, temperature 0.6f, topP 0.9f, topK 40, repeatPenalty 1.1f )提示線程綁定這塊可以用Process.setThreadPriority把推理線程優(yōu)先級(jí)調(diào)到THREAD_PRIORITY_URGENT_AUDIO級(jí)別能減少被系統(tǒng)調(diào)度打斷的概率。實(shí)測(cè)下來(lái)生成速度能提升10%到15%。3.4 前臺(tái)服務(wù)與?;畈呗?4小時(shí)運(yùn)行的核心難點(diǎn)在于?;?。安卓系統(tǒng)對(duì)后臺(tái)進(jìn)程的管理越來(lái)越嚴(yán)格如果不做處理鎖屏后幾分鐘推理服務(wù)就被殺了。解決方案是啟動(dòng)一個(gè)前臺(tái)服務(wù)在通知欄常駐一個(gè)通知這樣系統(tǒng)就不會(huì)輕易回收。前臺(tái)服務(wù)的實(shí)現(xiàn)要注意安卓版本差異。安卓9到安卓10直接startForeground就行安卓11以上需要在Manifest里聲明foregroundServiceType安卓14更是要求聲明具體類型比如dataSync或者specialUse。如果類型聲明不對(duì)啟動(dòng)時(shí)直接拋異常。// 前臺(tái)服務(wù)啟動(dòng)示例 class InferenceService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val notification buildNotification() if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) } else { startForeground(1, notification) } // 啟動(dòng)推理線程 startInferenceLoop() return START_STICKY } }除了前臺(tái)服務(wù)還需要關(guān)閉電池優(yōu)化。在設(shè)置里找到應(yīng)用把電池優(yōu)化設(shè)為“不優(yōu)化”否則系統(tǒng)在低電量時(shí)還是會(huì)限制后臺(tái)活動(dòng)。另外如果設(shè)備有廠商的省電策略比如某些品牌的“后臺(tái)高耗電管理”也要手動(dòng)加白名單。這些操作因設(shè)備而異但思路是一樣的讓系統(tǒng)認(rèn)為這個(gè)應(yīng)用是用戶主動(dòng)在用的不能隨便殺。4. 實(shí)操過(guò)程與核心環(huán)節(jié)實(shí)現(xiàn)4.1 從零開(kāi)始的項(xiàng)目初始化新建一個(gè)Android項(xiàng)目選Empty Activity模板語(yǔ)言選Kotlin最低API級(jí)別設(shè)26安卓8.0目標(biāo)API級(jí)別設(shè)34。項(xiàng)目建好后先在app/build.gradle.kts里加上NDK和CMake的配置然后把推理引擎的源碼或者預(yù)編譯庫(kù)放到src/main/cpp目錄下。如果用的是源碼集成需要在CMakeLists.txt里把推理引擎的源文件全部加進(jìn)來(lái)同時(shí)鏈接log、android、OpenMP等系統(tǒng)庫(kù)。如果用的是預(yù)編譯的.so就在jniLibs/arm64-v8a下放好然后在CMakeLists里用add_library以IMPORTED方式引入。# CMakeLists.txt 關(guān)鍵片段 cmake_minimum_required(VERSION 3.22) project(localai) add_library(localai SHARED native-lib.cpp inference_engine.cpp ) find_library(log-lib log) find_library(android-lib android) target_link_libraries(localai ${log-lib} ${android-lib} # 如果推理引擎是預(yù)編譯的 ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libinference.so )編譯時(shí)如果報(bào)undefined reference錯(cuò)誤大概率是鏈接順序問(wèn)題。把依賴庫(kù)放在被依賴庫(kù)的后面這是CMake鏈接的一個(gè)基本原則。我一開(kāi)始沒(méi)注意排查了快一個(gè)小時(shí)才反應(yīng)過(guò)來(lái)。4.2 JNI接口封裝與Kotlin調(diào)用JNI層要做的事情很明確加載模型、執(zhí)行推理、釋放資源。對(duì)外暴露三個(gè)native方法就夠了。加載模型時(shí)傳入模型路徑和參數(shù)配置推理時(shí)傳入prompt字符串返回生成的文本。// native-lib.cpp 核心接口 extern C JNIEXPORT jlong JNICALL Java_com_example_localai_InferenceEngine_loadModel( JNIEnv* env, jobject thiz, jstring model_path, jint threads) { const char* path env-GetStringUTFChars(model_path, nullptr); auto* engine new InferenceEngine(); bool success engine-load(path, threads); env-ReleaseStringUTFChars(model_path, path); if (!success) { delete engine; return 0; } return reinterpret_castjlong(engine); } extern C JNIEXPORT jstring JNICALL Java_com_example_localai_InferenceEngine_generate( JNIEnv* env, jobject thiz, jlong handle, jstring prompt) { auto* engine reinterpret_castInferenceEngine*(handle); const char* input env-GetStringUTFChars(prompt, nullptr); std::string output engine-generate(input); env-ReleaseStringUTFChars(prompt, input); return env-NewStringUTF(output.c_str()); }Kotlin層用external關(guān)鍵字聲明native方法然后在協(xié)程里調(diào)用避免阻塞主線程。推理過(guò)程可能持續(xù)幾秒到幾十秒必須放在Dispatchers.IO或者自定義的線程池里執(zhí)行。// Kotlin調(diào)用層 class InferenceEngine { private var handle: Long 0 external fun loadModel(modelPath: String, threads: Int): Long external fun generate(handle: Long, prompt: String): String external fun releaseModel(handle: Long) suspend fun load(modelPath: String, threads: Int 4): Boolean { return withContext(Dispatchers.IO) { handle loadModel(modelPath, threads) handle ! 0L } } suspend fun infer(prompt: String): String { return withContext(Dispatchers.IO) { if (handle 0L) returnwithContext 模型未加載 generate(handle, prompt) } } }注意JNI層的內(nèi)存管理要特別小心。模型加載后占用的內(nèi)存不會(huì)自動(dòng)釋放必須在應(yīng)用退出或者模型切換時(shí)手動(dòng)調(diào)用releaseModel。否則反復(fù)加載幾次內(nèi)存就爆了。4.3 推理性能實(shí)測(cè)與數(shù)據(jù)記錄我在三臺(tái)設(shè)備上做了對(duì)比測(cè)試統(tǒng)一用7B Q4量化模型上下文長(zhǎng)度2048線程數(shù)按大核數(shù)量設(shè)置。測(cè)試prompt是“用三句話解釋什么是量子糾纏”記錄首次token延遲和生成速度。設(shè)備型號(hào)處理器內(nèi)存首次token延遲生成速度背面溫度小米8驍龍8456GB1.8秒4.2 token/s43度一加7 Pro驍龍8558GB1.2秒6.5 token/s41度紅米Note 8驍龍6654GB4.5秒1.8 token/s46度從數(shù)據(jù)看驍龍845和855級(jí)別的設(shè)備體驗(yàn)比較理想生成速度能到4 token/s以上日常問(wèn)答基本感覺(jué)不到明顯卡頓。驍龍665就有點(diǎn)吃力了首次token延遲接近5秒生成速度不到2 token/s適合做后臺(tái)批處理任務(wù)不適合交互式對(duì)話。內(nèi)存方面6GB設(shè)備在加載模型后剩余可用內(nèi)存大約1.2GB跑推理時(shí)峰值會(huì)掉到500MB左右系統(tǒng)會(huì)頻繁觸發(fā)內(nèi)存回收偶爾導(dǎo)致推理中斷。8GB設(shè)備就從容很多剩余可用內(nèi)存2.5GB以上跑起來(lái)很穩(wěn)。所以如果手頭有8GB的舊旗艦優(yōu)先用那臺(tái)。4.4 24小時(shí)運(yùn)行穩(wěn)定性驗(yàn)證?;钆渲米鐾曛笪易屝∶?連續(xù)跑了72小時(shí)每10分鐘發(fā)一次推理請(qǐng)求記錄成功率和響應(yīng)時(shí)間。結(jié)果是成功率98.7%平均響應(yīng)時(shí)間3.2秒最長(zhǎng)一次延遲8.5秒發(fā)生在系統(tǒng)夜間自動(dòng)更新時(shí)。失敗的那1.3%主要是系統(tǒng)內(nèi)存回收導(dǎo)致的推理中斷重新加載模型后恢復(fù)。為了進(jìn)一步提升穩(wěn)定性我加了兩個(gè)措施一是定期重啟推理服務(wù)每6小時(shí)自動(dòng)重啟一次釋放累積的內(nèi)存碎片二是監(jiān)控內(nèi)存水位當(dāng)可用內(nèi)存低于300MB時(shí)主動(dòng)卸載模型再重新加載。這兩個(gè)措施加上之后連續(xù)運(yùn)行一周沒(méi)有出現(xiàn)崩潰。// 定期重啟邏輯 val handler Handler(Looper.getMainLooper()) val restartRunnable object : Runnable { override fun run() { restartInferenceService() handler.postDelayed(this, 6 * 60 * 60 * 1000L) // 6小時(shí) } } handler.postDelayed(restartRunnable, 6 * 60 * 60 * 1000L)提示重啟服務(wù)時(shí)要注意先釋放模型內(nèi)存再重新加載。如果直接殺進(jìn)程重啟模型文件可能因?yàn)閙map未釋放導(dǎo)致文件鎖殘留下次加載會(huì)失敗。正確做法是調(diào)用releaseModel等JNI層確認(rèn)釋放完畢后再重啟。5. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄5.1 模型加載失敗的五種典型原因模型加載失敗是最常見(jiàn)的問(wèn)題表現(xiàn)是應(yīng)用啟動(dòng)后卡在加載界面或者直接閃退。根據(jù)我的排查經(jīng)驗(yàn)原因基本集中在五類問(wèn)題現(xiàn)象可能原因排查方法解決方案加載時(shí)閃退模型格式不匹配查看logcat中推理引擎報(bào)錯(cuò)確認(rèn)框架支持的格式重新下載對(duì)應(yīng)格式模型加載后無(wú)響應(yīng)內(nèi)存不足adb shell dumpsys meminfo查看內(nèi)存換更小量化版本或關(guān)閉其他應(yīng)用報(bào)文件找不到路徑權(quán)限問(wèn)題檢查文件路徑和權(quán)限放到應(yīng)用私有目錄避免外部存儲(chǔ)權(quán)限問(wèn)題加載極慢存儲(chǔ)讀取速度慢測(cè)試存儲(chǔ)讀寫速度把模型放到內(nèi)部存儲(chǔ)不要放SD卡加載后推理亂碼模型文件損壞校驗(yàn)文件MD5重新下載確保下載完整其中模型格式不匹配是最容易踩的坑。不同推理框架支持的格式不一樣有的只支持GGUF有的只支持GGML下載之前一定要看清楚。我有一次下了一個(gè)GGML格式的模型結(jié)果框架只認(rèn)GGUF加載直接報(bào)錯(cuò)白白浪費(fèi)了兩個(gè)小時(shí)下載時(shí)間。5.2 推理速度突然變慢的排查思路推理速度突然變慢通常不是模型本身的問(wèn)題而是系統(tǒng)資源被搶占了。排查順序建議這樣先看CPU占用adb shell top看看是不是有其他進(jìn)程在跑再看內(nèi)存水位adb shell cat /proc/meminfo確認(rèn)可用內(nèi)存最后看溫度adb shell cat /sys/class/thermal/thermal_zone*/temp讀取CPU溫度。如果CPU溫度超過(guò)70度處理器會(huì)主動(dòng)降頻推理速度直接腰斬。這時(shí)候要么加散熱要么降低線程數(shù)減少發(fā)熱。我試過(guò)把線程數(shù)從4降到2速度雖然降了30%但溫度從75度降到62度反而更穩(wěn)定不會(huì)因?yàn)榻殿l導(dǎo)致速度忽快忽慢。另一個(gè)常見(jiàn)原因是后臺(tái)有其他應(yīng)用在同步數(shù)據(jù)。舊手機(jī)如果還登著賬號(hào)系統(tǒng)會(huì)定期同步照片、郵件等這些操作會(huì)搶占IO和CPU。建議把不用的賬號(hào)全部退出關(guān)閉自動(dòng)同步能明顯改善推理穩(wěn)定性。5.3 后臺(tái)服務(wù)被殺的應(yīng)對(duì)策略后臺(tái)服務(wù)被殺的表現(xiàn)是鎖屏后一段時(shí)間推理請(qǐng)求沒(méi)有響應(yīng)解鎖后發(fā)現(xiàn)服務(wù)已經(jīng)沒(méi)了。這個(gè)問(wèn)題在不同品牌設(shè)備上表現(xiàn)不一樣有的品牌殺得特別狠有的相對(duì)寬松。應(yīng)對(duì)策略分三層第一層是前臺(tái)服務(wù)加通知這是基礎(chǔ)第二層是電池優(yōu)化白名單在系統(tǒng)設(shè)置里手動(dòng)添加第三層是廠商特定的?;钤O(shè)置比如某些品牌需要在“手機(jī)管家”里單獨(dú)設(shè)置“允許后臺(tái)運(yùn)行”。三層都做完基本能保證24小時(shí)存活。如果還是被殺可以考慮雙進(jìn)程守護(hù)方案啟動(dòng)兩個(gè)服務(wù)互相監(jiān)聽(tīng)一個(gè)被殺另一個(gè)把它拉起來(lái)。但這個(gè)方案比較耗電而且安卓高版本對(duì)后臺(tái)啟動(dòng)服務(wù)有限制不一定能成功。我的建議是優(yōu)先做好前三層雙進(jìn)程守護(hù)作為最后手段。注意保活策略要平衡功耗和穩(wěn)定性。過(guò)度?;顣?huì)導(dǎo)致電池消耗加快舊手機(jī)本來(lái)電池就不行了可能半天就沒(méi)電。建議插著充電器跑同時(shí)把屏幕亮度調(diào)到最低關(guān)閉所有不必要的無(wú)線連接。5.4 輸出質(zhì)量不達(dá)預(yù)期的調(diào)優(yōu)方法輸出質(zhì)量差的表現(xiàn)包括答非所問(wèn)、重復(fù)啰嗦、邏輯混亂。這些問(wèn)題大多可以通過(guò)調(diào)整推理參數(shù)改善。溫度太高會(huì)導(dǎo)致輸出發(fā)散重復(fù)懲罰太低會(huì)導(dǎo)致復(fù)讀機(jī)現(xiàn)象上下文長(zhǎng)度不夠會(huì)導(dǎo)致模型忘記前面的對(duì)話。我的調(diào)優(yōu)順序是先把溫度降到0.3看輸出是否穩(wěn)定如果還是重復(fù)把重復(fù)懲罰從1.1提到1.2如果邏輯混亂檢查上下文長(zhǎng)度是否夠用2048對(duì)于多輪對(duì)話可能不夠可以試試4096。另外prompt的寫法也很重要清晰的指令能顯著提升輸出質(zhì)量。比如不要只寫“解釋量子糾纏”而是寫“用三句話向高中生解釋量子糾纏每句話不超過(guò)20個(gè)字”。還有一個(gè)容易被忽略的點(diǎn)模型的chat template。不同模型有不同的對(duì)話模板如果模板用錯(cuò)了模型就不知道哪些是用戶輸入、哪些是系統(tǒng)指令輸出質(zhì)量會(huì)大打折扣。加載模型時(shí)要確認(rèn)框架是否自動(dòng)應(yīng)用了正確的template如果沒(méi)有需要手動(dòng)在prompt里加上特殊標(biāo)記。6. 進(jìn)階玩法與擴(kuò)展方向6.1 接入OpenAI兼容接口本地推理跑通之后可以進(jìn)一步封裝成OpenAI兼容的HTTP接口這樣其他應(yīng)用就能像調(diào)用云端API一樣調(diào)用本地模型。實(shí)現(xiàn)方式是在安卓端起一個(gè)輕量HTTP服務(wù)器收到請(qǐng)求后轉(zhuǎn)成推理調(diào)用再把結(jié)果按OpenAI的響應(yīng)格式返回。// 簡(jiǎn)易HTTP接口示例 val server NanoHTTPD(8080) { override fun serve(session: IHTTPSession): Response { val body session.parseBody() val request JSONObject(body[postData] ?: {}) val prompt request.getJSONArray(messages) .getJSONObject(0).getString(content) val result runBlocking { engine.infer(prompt) } val response JSONObject().apply { put(choices, JSONArray().put(JSONObject().apply { put(message, JSONObject().put(content, result)) })) } return newFixedLengthResponse(response.toString()) } } server.start()這樣一來(lái)局域網(wǎng)內(nèi)的其他設(shè)備就能通過(guò)http://手機(jī)IP:8080/v1/chat/completions調(diào)用本地模型。實(shí)測(cè)下來(lái)局域網(wǎng)內(nèi)響應(yīng)延遲增加不到100毫秒基本無(wú)感。這個(gè)玩法適合把舊手機(jī)當(dāng)成家庭內(nèi)網(wǎng)的AI推理節(jié)點(diǎn)所有設(shè)備共享一個(gè)本地模型。6.2 多模型切換與任務(wù)路由如果手機(jī)存儲(chǔ)夠大可以放多個(gè)不同大小的模型根據(jù)任務(wù)類型自動(dòng)切換。簡(jiǎn)單任務(wù)用3B模型速度快復(fù)雜任務(wù)用7B模型質(zhì)量高。實(shí)現(xiàn)上就是在推理層加一個(gè)路由邏輯根據(jù)prompt長(zhǎng)度或者關(guān)鍵詞判斷用哪個(gè)模型。模型切換的開(kāi)銷主要在加載和卸載上一次切換大概需要3到5秒。如果頻繁切換體驗(yàn)會(huì)很差。優(yōu)化方法是保持兩個(gè)模型常駐內(nèi)存但這對(duì)內(nèi)存要求很高8GB設(shè)備勉強(qiáng)可以6GB設(shè)備基本不可能。所以更實(shí)際的方案是默認(rèn)加載7B模型遇到簡(jiǎn)單任務(wù)也不切換反正7B跑簡(jiǎn)單任務(wù)也不慢。6.3 語(yǔ)音輸入與TTS輸出閉環(huán)舊手機(jī)有麥克風(fēng)和揚(yáng)聲器加上本地語(yǔ)音識(shí)別和語(yǔ)音合成模型就能做成一個(gè)完全離線的語(yǔ)音助手。語(yǔ)音識(shí)別可以用Whisper的小型量化版本語(yǔ)音合成用Android自帶的TTS引擎。整個(gè)鏈路全部本地完成不依賴網(wǎng)絡(luò)。這個(gè)玩法的技術(shù)難點(diǎn)在于音頻流的實(shí)時(shí)處理。麥克風(fēng)采集的音頻要分幀送入識(shí)別模型識(shí)別結(jié)果再送給大模型推理推理結(jié)果再送TTS合成。整個(gè)鏈路延遲大概在2到4秒比云端方案慢一些但勝在完全離線。我試過(guò)在小米8上跑這套鏈路日常問(wèn)天氣、設(shè)提醒、查百科都能用體驗(yàn)還算流暢。7. 個(gè)人實(shí)操心得與最后分享折騰舊手機(jī)跑本地大模型這件事我從年初開(kāi)始陸續(xù)做了幾個(gè)月前后換了四臺(tái)設(shè)備刷過(guò)三次系統(tǒng)踩過(guò)的坑包括模型格式不匹配、內(nèi)存溢出閃退、后臺(tái)服務(wù)被殺、推理速度斷崖式下降等等。最大的體會(huì)是舊手機(jī)的瓶頸不在CPU而在內(nèi)存和散熱。CPU性能夠用但內(nèi)存不夠會(huì)導(dǎo)致頻繁回收散熱不好會(huì)導(dǎo)致降頻這兩個(gè)問(wèn)題解決了體驗(yàn)就能上一個(gè)臺(tái)階。另一個(gè)心得是不要追求一步到位。一開(kāi)始不要想著跑最大的模型、開(kāi)最長(zhǎng)的上下文、接最復(fù)雜的接口。先把最基本的推理跑通確認(rèn)模型能加載、能生成、速度可接受然后再逐步加功能。我一開(kāi)始就想直接上13B模型結(jié)果折騰了兩天都沒(méi)跑起來(lái)后來(lái)?yè)Q成7B半小時(shí)就通了。先跑通再優(yōu)化這個(gè)順序很重要。最后分享一個(gè)小技巧如果舊手機(jī)存儲(chǔ)空間緊張可以把模型文件放在外置存儲(chǔ)卡上但一定要用高速卡低速卡會(huì)導(dǎo)致加載時(shí)間從幾秒變成幾十秒。另外模型文件可以壓縮存放用的時(shí)候再解壓能省不少空間但解壓需要額外時(shí)間適合不頻繁切換模型的場(chǎng)景。