本地多模態 VLM 實戰:用一部機自動整理相簿、讀單據做 OCR
唔使上傳雲端、唔使 API 費——用 llama.cpp 跑 Qwen3.8-VL,自己部機睇得明圖片,實測手機相簿分類同單據 OCR
重點整理
- 痛點:愈來愈多「睇圖」工作(整理相簿、讀收據、抽錶數、睇圖搵錯)但雲端 API 又貴又涉及私隱——本地 VLM 係最實際嘅出路
- 原理:VLM=視覺編碼器(ViT)+投影層+語言模型,llama.cpp 用 mmproj 檔把圖片餵入模型,一個 GGUF + 一個 mmproj 就搞掂
- 實戰:由揀模型(2B/7B 點揀)、行 llama-server 開 OpenAI 相容 API、到寫 Python 批次處理成個相簿目錄嘅完整程式碼
- 香港場景:護照/車牌/錶數唔想上雲、公司單據要本地 OCR(避免機密外洩)、旅行相自動分類落相簿——全部一部 16GB 機搞掂
點解 2026 年要留意本地 VLM?
過去「睇圖」係雲端專利——GPT-4V、Claude 視覺、Gemini 都要傳圖上去。但 2026 年開源 VLM 已經追到實用級:Qwen3.8-VL、InternVL3、Llama 4 Vision 等模型嘅 7B 版本,喺 8–16GB VRAM 就跑到,準確度足夠做單據 OCR 同相簿分類。
本地 VLM 嘅三個殺手級場景(香港尤其啱):
- 私隱敏感:身份證、護照、車牌、銀行月結單——呢啲圖永遠唔應該上傳第三方 API
- 成本:一日要 OCR 幾百張單,雲端 API 一個月幾百蚊;本地跑一次過裝好就零成本
- 離線/批量:成個相簿目錄一次過處理,唔使逐張等 API 回應
VLM 內部結構:三個部分
唔使驚,其實好簡單:
[圖片] → 視覺編碼器 (ViT) → 投影層 (projector) → [文字 token] + [視覺 token] → LLM → 文字輸出
- 視覺編碼器 (ViT):把圖片切成 patch,編碼成一堆向量(同 CLIP 差唔多)
- 投影層 (projector / mmproj):把視覺向量「翻譯」成 LLM 讀得明嘅 token
- 語言模型:同純文字 LLM 一樣,只係 context 入面多咗視覺 token
關鍵係 llama.cpp 會分開兩個檔:主模型 GGUF(LLM 部分)+ mmproj GGUF(視覺部分)。兩個都要 download。
揀模型:2B 定 7B?
# 2B 級(快、食資源少、啱 OCR 同簡單描述)
huggingface-cli download ggml-org/Qwen3.8-VL-2B-Instruct-GGUF --include "*Q4_K_M*" --include "*mmproj*" --local-dir ./models
# 7B 級(準、啱複雜推理/圖表理解,要 ~6GB VRAM)
huggingface-cli download ggml-org/Qwen3.8-VL-7B-Instruct-GGUF --include "*Q4_K_M*" --include "*mmproj*" --local-dir ./models
揀法:
| 需求 | 建議 | VRAM |
|---|---|---|
| 單據 OCR、相片一句描述 | Qwen3.8-VL-2B Q4 | 2–3GB |
| 圖表理解、多圖比較、推理 | Qwen3.8-VL-7B Q4 | 6–8GB |
| 中文手寫、複雜版面 | InternVL3-8B | 8–10GB |
行 llama-server(OpenAI 相容 API)
llama.cpp 嘅 llama-server 由 2025 年中開始支援 mmproj(multimodal),用法同純文字一樣,加 --mmproj 就得:
llama-server -m ./models/Qwen3.8-VL-7B-Instruct-Q4_K_M.gguf --mmproj ./models/mmproj-Qwen3.8-VL-7B-f16.gguf -ngl 99 -c 8192 --port 8080
⚠️ 注意:-ngl 99 係把盡量多層放上 GPU。如果 VRAM 唔夠,VLM 會 fallback 落 CPU 但慢到唔想用——所以 7B 建議最少 8GB VRAM。
開好之後,用 OpenAI 相容格式傳圖(base64):
import base64, requests
def ask_image(path, question, model="qwen3.8-vl"):
b64 = base64.b64encode(open(path, "rb").read()).decode()
r = requests.post("http://localhost:8080/v1/chat/completions", json={
"model": model,
"messages": [{"role": "user", "content": [
{"type": "text", "text": question},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}},
]}],
"temperature": 0.1,
})
return r.json()["choices"][0]["message"]["content"]
print(ask_image("receipt.jpg", "呢張單據嘅商戶名、總金額同日期係咩?用 JSON 答。"))
實戰一:單據 OCR 抽結構化資料
本地 VLM 最大優勢係直接出 JSON,唔使自己寫 regex 抽文字:
import json, base64, requests, os
PROMPT = """你係單據 OCR 助手。睇圖後只輸出 JSON,唔要其他文字:
{"merchant": "商戶名", "date": "YYYY-MM-DD", "total": 數字, "currency": "HKD/JPY/CNY", "items": ["項目1","項目2"]}
如果睇唔到某欄位,用 null。"""
def ocr_receipt(path):
b64 = base64.b64encode(open(path, "rb").read()).decode()
r = requests.post("http://localhost:8080/v1/chat/completions", json={
"messages": [{"role": "user", "content": [
{"type": "text", "text": PROMPT},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}},
]}],
"temperature": 0.0,
"response_format": {"type": "json_object"},
}, timeout=120)
txt = r.json()["choices"][0]["message"]["content"]
try:
return json.loads(txt)
except json.JSONDecodeError:
return {"error": "parse_failed", "raw": txt}
out = []
for f in sorted(os.listdir("receipts")):
if f.lower().endswith((".jpg", ".jpeg", ".png")):
rec = ocr_receipt(os.path.join("receipts", f))
rec["file"] = f
out.append(rec)
print(f, "->", rec.get("merchant"), rec.get("total"))
with open("receipts.json", "w", encoding="utf-8") as fp:
json.dump(out, fp, ensure_ascii=False, indent=2)
實戰二:手機相簿自動分類
香港人影相多,相簿一萬張唔知邊張打邊張。本地 VLM 可以逐張描述再分類:
CATEGORIES = ["收據/單據", "食物", "風景", "人物合照", "寵物", "截圖/文字", "其他"]
CLASSIFY_PROMPT = "睇圖,由以下類別揀一個最貼切嘅,只輸出類別名,唔要解釋:" + " / ".join(CATEGORIES)
def classify(path):
b64 = base64.b64encode(open(path, "rb").read()).decode()
r = requests.post("http://localhost:8080/v1/chat/completions", json={
"messages": [{"role": "user", "content": [
{"type": "text", "text": CLASSIFY_PROMPT},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}},
]}],
"temperature": 0.0,
}, timeout=120)
return r.json()["choices"][0]["message"]["content"].strip()
效能參考(RTX 4060 8GB,Qwen3.8-VL-7B Q4,一張 1080p 圖):
- 首次載入模型:約 8–15 秒
- 每張圖推論:約 2–4 秒
- 一千張相簿:約 45–70 分鐘(可以放住背景跑)
踩過嘅坑
- mmproj 版本一定要夾主模型:Qwen3.8-VL 嘅 mmproj 唔可以配 InternVL3 主模型,會出亂碼或者 crash
- 圖片太大反而唔準:llama.cpp 會 resize,但 4000px 相片 resize 後細節流失;OCR 前最好自己裁到相關區域(約 1000–1500px 邊長最平衡)
- 繁體中文輸出:prompt 用繁體,但模型有時會出簡體。要喺 prompt 明確寫「用繁體中文(香港)回答」
- JSON 唔一定乾淨:即使用
response_format,細模型仍可能包json標記。上面程式碼嘅 try/except 係必須嘅 - 批量要加 timeout:VLM 單張可能 2–4 秒,但偶爾卡住。一定要設 timeout 同 retry,否則成個 batch 死喺一張圖
幾時應該用雲端?
本地 VLM 唔係萬能。以下情況老實講雲端更好:
- 需要極高準確度(例如法律文件、醫療報告)——GPT-4V / Gemini 仍然贏
- 需要理解極複雜版面(多欄學術論文、手寫日文)
- 偶爾用一次——唔值得為咗省幾蚊而搞本地部署
判斷法則:如果啲圖唔可以上傳(私隱),或者量好大(成本),本地 VLM 幾乎必勝。如果兩樣都唔係,就用雲端慳返自己時間。
