本文由 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 的流程快速複習一遍:
文字模型其實單純得很:文字轉成整數 ID,ID 換成向量(Embedding),然後丟給 Transformer 去算。
那問題來了——多模態 LLM 要怎麼把「影像」硬塞進這條流水線?現代 VLM 最主流的設計是 Late Fusion(後期融合)架構,核心就三個部分:
- 視覺編碼器(Vision Encoder):通常使用 Vision Transformer (ViT) (例如 CLIP 或 SigLIP)。它負責將輸入的圖片分割成許多小方塊(Patches,如 14x14 像素),並將每個方塊轉換成一個視覺向量。一張圖片經過 ViT 後,會變成一串「視覺 Token 向量」。
- 投影層(Projector):這是連接視覺與文字世界的橋樑。ViT 輸出的視覺向量維度(如 1024)通常與 LLM 的文字向量維度(如 4096)不同。投影層(通常是一個簡單的 MLP 兩層全連接網路 或 Resampler)負責將視覺向量投影(對齊)到 LLM 的向量空間中。
- 文字 LLM(Text LLM):負責當大腦。妙就妙在這裡——對它來說,經過投影層轉換後的視覺向量, 跟普通的文字 Token 向量根本沒兩樣。它就傻傻地把文字向量跟視覺向量拼在一起,送進 Transformer 算自注意力(Self-Attention)而已。
VLM 的資料流動架構
在這個架構底下, 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) 模型來說吧,下載的時候你通常會看到兩個檔案:
- 主模型檔(如
llava-v1.5-7b-q4_k.gguf):包含傳統的 Transformer 權重、Tokenizer 以及 LM Head。 - 多模態投影檔(如
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.weight與model.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 進行 evaluationStep 4: LLM 推論自注意力計算
Transformer 開始執行推論。當計算到自注意力時,文字 Token 向量會與影像 Token 向量進行矩陣乘法。藉此,文字 Token 能夠「注意到」影像中不同方塊(Patches)的特徵,從而產生與圖片內容高度相關的文字回覆。
總結:多模態融合的優雅與未來
我想多模態 LLM 的架構設計,最迷人的地方就在於那種「模組化的美感」。我們根本不必砍掉重練、從頭訓練一個會看圖的超巨型模型,而是 靠一個輕量的投影層(Projector),就把成熟的視覺世界(ViT)跟強大的語言大腦(LLM)漂亮地拼裝在一起。說穿了,這招其實是「站在巨人肩膀上」的工程智慧囉。
在本地部署這塊,GGUF 把 LLM 跟 mmproj 拆開來存,給了開發者很大的彈性;到了網路 API 那一層,OpenAI 那套標準多模態 JSON 規範又把底層那些繁瑣的向量替換細節通通藏起來,讓前端開發者用最直覺的 Base64 載荷就能玩多模態。一個藏一個露,分工得相當漂亮。
把這套「視覺與語言」的融合心法搞懂之後,不論你想做智慧安防、自動化圖表分析,還是打造下一代會聽會看的本地 AI 助理,最核心的那把底層鑰匙,你都已經握在手裡了。
下一篇的專案挑戰,我們就要捲起袖子,試著在本地工作站裡把多模態 FFI 引擎整合進來,敬請期待囉!