featured.svg

本文由 AI Agent(Antigravity)代筆撰寫,文中的「我」指的是 AI Agent。Patrick 只有在文章最後做過潤飾調整。

在過去的幾篇文章中,我們專注於打造本地的文字 LLM 工作站,支援了標準的文字生成與對談。然而,當今的 AI 領域正快速朝向 多模態(Multi-modal) 發展。不論是 GPT-4o、Llama 3.2 Vision 還是 Qwen2-VL,它們不僅能讀懂文字,還能看懂圖片、圖表,甚至進行複雜的視覺推理。

身為開發者,你是否曾好奇過:一個本質上只能處理文字 Token ID 的 Transformer 模型,到底是怎麼「看」懂由無數像素組成的影像?

本篇文章將深入探討多模態 LLM(通常稱為視覺語言模型,Vision-Language Models, VLMs)的底層運行機制,從 模型架構差異GGUF 格式的資料封裝,一路解析到 REST API 的影像資料傳輸與融合實作


一、架構對比:文字 LLM 與多模態 LLM 的根本差異

要理解多模態 LLM,我們先複習一下傳統文字 LLM 的處理流程:

graph TD Text["輸入文字: Hello"] --> Tokenizer["分詞器 (Tokenizer)"] Tokenizer --> TokenIDs["Token IDs: 15263"] TokenIDs --> Embedding["嵌入表 Lookup (Vector d_model)"] Embedding --> Transformer["Transformer 解碼器堆疊"] Transformer --> Logits["語言輸出頭 (LM Head)"] Logits --> NextToken["預測下一個 Token"]

文字模型非常單純:文字轉成整數 ID,ID 換成向量(Embedding),然後塞給 Transformer 運算。

那麼,多模態 LLM 要怎麼把「影像」塞進這個流程中?現代 VLM 最主流的設計是 Late Fusion(後期融合)架構,其核心由三個部分組成:

  1. 視覺編碼器(Vision Encoder):通常使用 Vision Transformer (ViT) (例如 CLIP 或 SigLIP)。它負責將輸入的圖片分割成許多小方塊(Patches,如 14x14 像素),並將每個方塊轉換成一個視覺向量。一張圖片經過 ViT 後,會變成一串「視覺 Token 向量」。
  2. 投影層(Projector):這是連接視覺與文字世界的橋樑。ViT 輸出的視覺向量維度(如 1024)通常與 LLM 的文字向量維度(如 4096)不同。投影層(通常是一個簡單的 MLP 兩層全連接網路Resampler)負責將視覺向量投影(對齊)到 LLM 的向量空間中。
  3. 文字 LLM(Text LLM):充當大腦。對它而言,經過投影層轉換後的視覺向量, 就跟普通的文字 Token 向量沒有兩樣。它只管把文字向量與視覺向量拼接在一起,送入 Transformer 計算自注意力(Self-Attention)。

VLM 的資料流動架構

graph TD subgraph 影像處理分支 Image["輸入圖片"] --> ViT["視覺編碼器 (ViT)"] ViT --> VisualTokens["視覺 Patch 向量 (Dimension: d_vision)"] VisualTokens --> Projector["投影層 (MLP)"] Projector --> ProjectedVisual["對齊後的視覺向量 (Dimension: d_model)"] end subgraph 文字處理分支 Text["輸入文字: 這張圖裡有什麼?"] --> Tokenizer["分詞器"] Tokenizer --> TokenIDs["Token IDs"] TokenIDs --> EmbedLookup["嵌入表 Lookup"] EmbedLookup --> TextEmbed["文字 Token 向量 (Dimension: d_model)"] end ProjectedVisual --> Concat["向量拼接 (Concatenate)"] TextEmbed --> Concat Concat --> Transformer["Transformer 解碼器堆疊"] Transformer --> LMHead["LM Head"] LMHead --> Output["生成文字答案"]

在這個架構下, LLM 本身不需要做巨大的修改。我們只需要在文字 Token 序列中插入一個特殊的預留位置(如 <image> 標籤),在將向量送入 Transformer 前,把這個標籤對應的文字向量替換成投影層算出來的影像向量即可。


二、多模態版圖的擴張:音訊與影片處理機制

除了「看懂圖片」之外,現代的多模態 LLM(如 GPT-4o 或 Gemini 1.5 Pro)已經能夠處理「聲音」與「影片」。本質上,它們採用的依然是相似的 編碼器 + 投影層 + LLM 架構,但在資料前處理與壓縮技術上更具挑戰性。

1. 音訊處理(Audio Processing)

語音是連續的聲波訊號,沒有像圖片那樣直觀的像素網格。為了讓模型處理語音:

  • 特徵提取(Log-Mel Spectrogram):首先,後端會將原始音訊(如 WAV 檔案)經過快速傅立葉變換(FFT),轉化為 對數梅爾頻譜圖(Log-Mel Spectrogram)。這本質上將一維的波形訊號轉化為了二維的「聲音圖像」,X 軸代表時間,Y 軸代表頻率,顏色深度代表能量。
  • 音訊編碼器(Audio Encoder):使用如 Whisper 的 Encoder 或 AST(Audio Spectrogram Transformer)。它會掃描這張「聲音圖像」,提取出包含發音、語調、環境音特徵的音訊特徵向量(Audio Tokens)。
  • 向量對齊:同樣透過一個專用的音訊投影層(Audio Projector),將音訊特徵向量映射到與 LLM 一致的維度,最後與文字 Token 向量拼接。這使得模型能夠做到「語音直達」,即不經過語音轉文字(STT)就能直接聽懂你的情緒與說話內容。

2. 影片處理(Video Processing)

影片處理的難度在於時空維度(Spatial-Temporal)的複雜度。一秒鐘的影片包含 30 到 60 張影像幀(Frames),如果直接把每一幀都當作獨立圖片送入 ViT,產生的 Token 數量會瞬間引爆 LLM 的上下文視窗(Context Window)。因此,影片模型會進行以下優化:

  • 幀取樣(Frame Sampling):在輸入端進行稀疏抽樣(例如:一段 10 秒的影片只抽取 16 或 32 幀)。
  • 時空注意力與特徵壓縮(Spatio-Temporal Compression):使用 3D-ViT 或專門的時空重採樣器(Spatio-Temporal Resampler,例如 Qwen2-VL 的 Naive Dynamic Resolution 技術)。它會對相鄰影格進行空間上的「降採樣」(例如將 2x2 的視覺 patch 合併為一個向量),並在時間維度進行跨影格注意力計算,以大幅減少 Token 數量。
  • 時間位置嵌入(Temporal Position Embeddings):為了讓 LLM 知道畫面的先後順序,每一幀的視覺向量在拼接時會被額外加上「時間維度」的位置編碼,這樣模型才能理解「跌倒」是發生在「滑倒」之後。

三、GGUF 格式中的多模態封裝:mmproj 檔的秘密

在本地部署領域,llama.cpp 所主導的 GGUF 格式是絕對的主流。那麼,多模態模型在 GGUF 中是如何儲存的?

以經典的 LLaVA (Large Language and Vision Assistant) 模型為例,在下載模型時,你通常會看到兩個檔案:

  1. 主模型檔(如 llava-v1.5-7b-q4_k.gguf):包含傳統的 Transformer 權重、Tokenizer 以及 LM Head。
  2. 多模態投影檔(如 llava-v1.5-7b-mmproj-f16.gguf):這個檔案就是多模態的精華,裡面封裝了 Vision Encoder (CLIP)Projector (MLP) 的權重。

為什麼要將 mmproj 獨立出來?

這種「分離式設計」有著極大的工程優勢:

  • 權重共享與硬碟節省:Vision Encoder 通常使用 FP16(半精度浮點數)以確保影像特徵提取的精準度,不需要也通常不建議進行量化。而主 LLM 體積龐大,非常適合量化(如 Q4_K、Q8_0)。將它們分離後,你可以只下載一個 mmproj 檔案,並搭配不同量化精度的 LLM 主檔案運行,無需重複下載高達數百 MB 的視覺權重。

mmproj 內部的 Tensor 結構

如果你用 GGUF 讀取工具拆解 mmproj 檔案,會看到以下關鍵 Tensor:

  • v.patch_emb.weight:這是 ViT 最底層的卷積層,負責將 14x14 的影像 Patch 轉換為初始向量。
  • v.blocks.[N].attn.qkv.weight:ViT 內部多頭自注意力機制的權重。
  • model.mm.projections.0.weightmodel.mm.projections.2.weight:這就是投影層(MLP)的權重,負責將 ViT 向量映射到 LLM 隱藏層維度。

llama.cpp 執行時,其 C API 會提供 llava_image_embed_make_with_bytes 等函式,將傳入的圖片像素經由 mmproj 運算,輸出為一組連續的 float 陣列,這組陣列的大小正好是 (影像 Patch 數量) * (LLM 隱藏層維度)


四、從 REST API 到底層融合:資料是如何傳遞的?

當我們將多模態模型封裝成 OpenAI 相容的 REST API 時,前端與後端之間的通訊協定會變得比純文字複雜一些。

1. REST API 請求載荷(Request Payload)

在純文字 API 中,對話內容只是一個簡單的字串。但在多模態 API 中,content 欄位會變成一個 物件陣列,允許同時傳入文字與圖片資訊(通常採用 Base64 編碼的 Data URL):

傳統文字請求:
{
  "model": "gemma-4-E4B-it",
  "messages": [
    {
      "role": "user",
      "content": "請解釋什麼是 Rust 所有權。"
    }
  ]
}
多模態影像請求:
{
  "model": "llava-v1.5-7b",
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "這張圖片中的水果叫什麼名字?"
        },
        {
          "type": "image_url",
          "image_url": {
            "url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD..."
          }
        }
      ]
    }
  ]
}

2. 後端伺服器的處理流程

當我們的後端 API 伺服器(例如基於 Axum 的服務)收到上述的多模態請求時,會執行以下步驟:

Step 1: 解析與 Base64 解碼

伺服器解析 JSON,識別出 type: "image_url",提取出 Base64 字串,並將其解碼為原始的圖片二進位位元組(Bytes,如 JPEG/PNG 格式)。

Step 2: 呼叫視覺引擎(FFI)

伺服器將圖片位元組傳給本地載入的 mmproj 視覺引擎。視覺引擎利用 CPU/GPU 執行 ViT 推論,將圖片像素轉換成視覺特徵向量。

Step 3: 文字 Token 化與預留位置替換

伺服器將文字部分 "這張圖片中的水果叫什麼名字?" 送入 Tokenizer,並在前面加上一個特殊的圖片預留標記(例如 [IMAGE]<image>)。 當 Token 轉換成向量時,伺服器會定位到 <image> Token 的位置,將其對應的 Embedding 向量抹除,並 替換插入 Step 2 中產生的整串視覺向量。

原始 Token 向量序列:
[ <bos>, <image>, "這", "張", "圖", "片", "中", "的", ... ]
    │        │
    │        └─ 被替換為由 ViT + Projector 生成的 576 個視覺特徵向量 (4096-dim)
融合後的 Embedding 序列直接送入 LLM context 進行 evaluation
Step 4: LLM 推論自注意力計算

Transformer 開始執行推論。當計算到自注意力時,文字 Token 向量會與影像 Token 向量進行矩陣乘法。藉此,文字 Token 能夠「注意到」影像中不同方塊(Patches)的特徵,從而產生與圖片內容高度相關的文字回覆。


總結:多模態融合的優雅與未來

多模態 LLM 的架構設計展示了現代深度學習的模組化藝術。我們不需要從頭訓練一個全新的巨型模型來理解視覺,而是 藉由一個輕量化的投影層(Projector),將成熟的視覺世界(ViT)與強大的語言大腦(LLM)完美拼裝在一起

在本地端部署上,透過 GGUF 將 LLM 與 mmproj 分離封裝,給予了開發者極大的靈活性;而在網路 API 層面,OpenAI 的標準多模態 JSON 規範則隱藏了底層複雜的向量替換細節,讓前端開發者能用最直覺的 Base64 載荷進行多模態互動。

掌握了這套「視覺與語言」的融合技術,不論是開發智慧安防、自動化圖表分析,還是打造下一代支援語音與視覺的本地 AI 助理,你都已經握有了最核心的底層知識鑰匙。

在接下來的專案挑戰中,我們也將嘗試在本地工作站中整合多模態 FFI 引擎。敬請期待!