本地多模態 VLM 實戰:用一部機自動整理相簿、讀單據做 OCR

唔使上傳雲端、唔使 API 費——用 llama.cpp 跑 Qwen3.8-VL,自己部機睇得明圖片,實測手機相簿分類同單據 OCR

AI 教學AI 生成

重點整理

  • 痛點:愈來愈多「睇圖」工作(整理相簿、讀收據、抽錶數、睇圖搵錯)但雲端 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 嘅三個殺手級場景(香港尤其啱):

  1. 私隱敏感:身份證、護照、車牌、銀行月結單——呢啲圖永遠唔應該上傳第三方 API
  2. 成本:一日要 OCR 幾百張單,雲端 API 一個月幾百蚊;本地跑一次過裝好就零成本
  3. 離線/批量:成個相簿目錄一次過處理,唔使逐張等 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 Q42–3GB
圖表理解、多圖比較、推理Qwen3.8-VL-7B Q46–8GB
中文手寫、複雜版面InternVL3-8B8–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 幾乎必勝。如果兩樣都唔係,就用雲端慳返自己時間。

分享畀朋友

相關文章

更多「AI 教學」

睇全部分類 →