前 9 篇里,我们的图像理解、语音转写、视频分析几乎全部跑在闭源 API 上——快是快,但票据里有客户名称、截图里有内网拓扑、病历里有身份证号,这些图每出一次域,安全评审材料就厚一页。这篇是《多模态开发实战》第 10 篇,把视觉能力搬回自己的机器:开源 VLM 现在到了什么水平、显存怎么算账、vLLM 怎么起服务、图像 token 怎么压,最后落一个本地票据 OCR Agent。推理工具链与已完结的《本地大模型部署实战》16 篇一脉相承,llama.cpp、vLLM、Ollama 那一套对 VLM 原样适用,本篇只讲 VLM 特有的部分。文中版本与参数均为 2026 年 10 月官方文档口径。
1、开源 VLM 格局
2025 年下半年开源 VLM 完成了一轮集体换代,眼下值得认真选型的是三家,先上对照表。
| 系列 | 开源档位 | 许可证 | 中文与定位 |
|---|---|---|---|
| Qwen3-VL | 2B/4B/8B/32B dense + 235B-A22B | Apache 2.0 | 中文 OCR 最强,档位最全 |
| InternVL3.5 | 1B~38B dense + 三个 MoE 档 | MIT | 评测榜常客,尺寸最密 |
| Llama 4 | Scout/Maverick,均 17B 激活 | Llama 社区协议 | 上下文最长,中文偏弱 |
Qwen3-VL 官方仓库的节奏很清晰:旗舰 235B-A22B(总参数 2350 亿、激活 220 亿)2025 年 9 月 23 日开源,10 月 14 日补上 4B/8B,10 月 22 日再放 2B/32B,每个尺寸都有指令版与思维链版。2B 小到能塞进手机(约 4 GB 内存、40 token/秒),32B 量化后正好压进 24 GB 单卡,加上 256K 上下文与 32 语种 OCR,从端到云一条线通吃。InternVL3.5 2025 年 8 月 26 日发布,dense 从 1B 铺到 38B,MoE 有 20B-A4B、30B-A3B、241B-A28B 三档,旗舰在通用多模态、推理、文本与 Agent 基准上都稳居开源第一梯队,MIT 协议对商用最友好;小尺寸档做端侧视觉助手、大尺寸档做评测刷榜,两头都有人用。
Llama 视觉系目前停在 Llama 4:2025 年 4 月发布的 Scout 与 Maverick 是 Meta 首批原生多模态模型——从预训练起就图文联合建模,而非后期外挂视觉模块。Scout 以 iRoPE 注意力做到 1000 万 token 上下文、单张 H100 可跑;Maverick 总参数 400B、激活同为 17B,上下文 100 万。短板也明确:中文与票据类 OCR 比 Qwen3-VL 弱一档,社区协议对月活超限的巨头有约束。选型结论:中文场景默认 Qwen3-VL;要极限尺寸密度或 MIT 协议选 InternVL3.5;已在 Llama 生态里才考虑 Llama 4。
2、VLM 的显存账
同样标称 8B,VLM 比纯文本模型多吃显存,这笔账由三部分构成。
第一笔是权重。VLM = 文本主干 + 视觉编码器 + 连接投影层,Qwen3-VL-8B 的视觉部分约多出 2~3 GB,BF16 权重从纯文本 8B 的约 16 GB 涨到 18~19 GB。第二笔是 KV cache,它按"文本 + 视觉 token 总数"计费,而视觉 token 由分辨率决定:视觉 token 与文本 token 进同一份 KV cache,单价完全相同,差别全在量。Qwen3-VL 把图切成 14×14 的 patch、两两合并后,每 784 像素(28×28)对应 1 个 token。一张 1920×1080 截图约 2691 个视觉 token——相当于 8 页 A4 文字,一张未压缩的手机原图能到 1.5 万。第三笔是视觉塔的中间激活,随分辨率线性增长,推理引擎按上限预留,是最容易爆的"账外"项。
| 显存项 | 纯文本 8B | Qwen3-VL-8B |
|---|---|---|
| 权重(BF16) | ≈16 GB | ≈18~19 GB |
| 典型单条请求 | 数百 token | 数千~1.6 万 token |
| 24 GB 单卡结论 | 宽裕 | 紧张,量化或压图后从容 |
结论落在最后一行:24 GB 单卡跑 8B 档,要么换 int4 量化权重(AWQ 约 7 GB),要么把单图 token 上限压下来——第 5 节专门讲怎么压。往上的档位同一笔账等比放大:32B 的 BF16 权重约 65 GB,48 GB 显存的双卡或者量化后单卡才装得下;235B-A22B 官方直接给 FP8 细粒度量化版,vLLM 与 SGLang 都能跑,但入门配置就是多卡集群,属于"云上租"而不是"家里放"的级别。
3、vLLM 部署 Qwen3-VL
VLM 的首选推理引擎仍是 vLLM:模型支持面最广,Qwen3-VL 发布当天就在支持列表里。vllm serve 的公共参数(--max-model-len、--gpu-memory-utilization 等)与《本地大模型部署实战》第 6 篇完全一致,这里只看 VLM 特有的两个。一个可以照抄的起点如下。
vllm serve Qwen/Qwen3-VL-8B-Instruct \
--host 0.0.0.0 --port 8000 \
--served-model-name qwen3-vl-8b \
--max-model-len 32768 \
--limit-mm-per-prompt '{"image": 3}' \
--mm-processor-kwargs '{"max_pixels": 1003520}'
--limit-mm-per-prompt 限制单条 prompt 里的多模态条目数,默认 1 张图——想一次传多图必须显式调高。--mm-processor-kwargs 把处理器的 max_pixels 压到 1003520(约 100 万像素),单图视觉 token 上限从默认的 16384 降到 1280。
注意:这两个参数是一对。每张图按
max_pixels上限预留 token 预算,把{"image": 12}配上默认 16384 的上限,意味着每条序列预留近 20 万 token 的多模态预算,任何单卡都扛不住。先压max_pixels再调条目数,才是正确顺序。
首跑有两件事要等:从 HuggingFace 拉权重(8B 约 17 GB,国内网络建议先设 HF_ENDPOINT 镜像或用 huggingface-cli download 落盘后传本地路径),以及 CUDA graph 预热。日志里打出 KV cache 的 token 容量与 Maximum concurrency 两行才算就绪——这两个数字就是此配置下这张卡的真实容量,VLM 场景要特别盯第一个数:max_pixels 没压时,日志里的容量可能小得反直觉。
服务起来后,调用侧就是标准 OpenAI 兼容格式,图像走 base64 Data URL,跟第 2 篇闭源 API 的写法几乎逐字相同。
import base64
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
def ask_image(path: str, question: str) -> str:
b64 = base64.b64encode(open(path, "rb").read()).decode()
resp = client.chat.completions.create(
model="qwen3-vl-8b",
messages=[{"role": "user", "content": [
{"type": "image_url", "image_url": {
"url": f"data:image/jpeg;base64,{b64}"}},
{"type": "text", "text": question},
]}],
max_tokens=512,
)
return resp.choices[0].message.content
这正是本地部署的隐性红利:消息结构不用改,换个 base_url 和 model,闭源代码就跑在开源模型上。完整参数说明见 vLLM 多模态输入文档。
4、SGLang 与 Ollama 快速路径
SGLang 值得单独点名:Qwen3-VL 同样在它的官方支持列表里,多图、高并发的 VLM 负载下吞吐常被社区评测排第一梯队,背后是《本地大模型部署实战》第 7 篇拆过的 RadixAttention——多轮 Agent 里逐字不变的 system prompt 前缀自动复用 KV cache。启动也是一行 python -m sglang.launch_server --model-path Qwen/Qwen3-VL-8B-Instruct --host 0.0.0.0,细节不展开,选型时拿真实负载对比两家即可,见 SGLang 仓库。
只是本地玩票、验一张图,门槛可以低到 Ollama 一条命令,模型页见 Ollama 的 qwen3-vl。
ollama pull qwen3-vl:4b # int4 量化,约 3 GB
ollama run qwen3-vl:4b # 交互里直接把图片文件拖进终端
想更贴近底层就用 llama.cpp:VLM 的 GGUF 是两件套,主模型之外还要一个 mmproj 视觉投影文件,llama-server 同样起 OpenAI 兼容接口。
llama-server \
-m Qwen3-VL-8B-Instruct-Q4_K_S.gguf \
--mmproj mmproj-F16.gguf \
-c 8192 --host 127.0.0.1 --port 8000
4B 档的 OCR 与日常截图问答已经够用;2B 档适合塞进桌面应用做离线兜底。这是《本地大模型部署实战》第 3、4 篇的路线,VLM 只是多了一个 --mmproj 参数。
5、分辨率与 token 控制
压 token 的入口是 Qwen-VL 处理器的 smart_resize:先把图缩放到 28 的倍数,再夹进 min_pixels 与 max_pixels 区间。不同像素量对应的视觉 token 账单如下。
| 输入像素 | 视觉 token | 说明 |
|---|---|---|
| 1280×720 | ≈1196 | 网页截图常态 |
| 1920×1080 | ≈2691 | 高清截图 |
| 100 万(max_pixels 封顶) | 1280 | OCR 推荐值 |
| 4032×3024 手机原图 | ≈15552 | 未超默认上限,账单刺客 |
自己控制预算,在加载处理器时传参即可。
from transformers import AutoProcessor
processor = AutoProcessor.from_pretrained(
"Qwen/Qwen3-VL-8B-Instruct",
min_pixels=56 * 56, # 下限:4 个视觉 token,防小图信息塌缩
max_pixels=28 * 28 * 1280, # 上限:单图最多 1280 token
)
不走 transformers、纯 API 调用的场景,客户端预缩放是等价且更省的做法:上传前用 PIL 的 thumbnail 把长边压到 1300 像素上下,既压住了 token,也把 base64 请求体从十几 MB 降到几百 KB。服务端则用第 3 节的 --mm-processor-kwargs 做全局兜底,防止个别请求绕过客户端逻辑。经验值:票据与文档 OCR 给 768~1280 token 足够;图表问答、密集小字场景再放到 2688;盲目放原图,成本与显存都是双输。还有一招是裁剪优先于放大:印章、金额栏这类小字看不清时,与其抬高整图的 token 上限,不如把关键区域裁出来单独再送一次识别,token 花在刀刃上。
6、本地票据 OCR 实战
把前面的零件拼成一条完整流水线:vLLM 起服务 → 结构化提取 → SQLite 入库。票据场景的特点是版式固定、字段有限、允许人工复核,正是 8B 开源 VLM 的主场。先起服务,AWQ 量化权重让 24 GB 单卡从容。
vllm serve Qwen/Qwen3-VL-8B-Instruct-AWQ \
--served-model-name qwen3-vl-8b \
--max-model-len 8192 --max-num-seqs 16 \
--limit-mm-per-prompt '{"image": 1}' \
--mm-processor-kwargs '{"max_pixels": 1003520}'
OCR 单图单问,上下文 8192、每图 1280 token、16 路并发,一张 24 GB 卡吃满。提取函数复用第 3 节的 ask_image,重点是提示词约束与 JSON 容错解析。
import json, re
FIELDS = "type(发票/收据/行程单),date,merchant,total,tax,items"
def extract_receipt(path: str) -> dict:
prompt = (
"你是票据录入员。提取图中票据信息,只输出 JSON,"
f"字段:{FIELDS}。items 为数组,元素含 name 与 amount。"
"看不清的字段填 null,绝不编造。"
)
raw = ask_image(path, prompt)
m = re.search(r"\{.*\}", raw, re.S) # 容错:剥掉可能包裹的 json 代码围栏
return json.loads(m.group())
两个工程细节:模型偶尔会包一层 Markdown 代码围栏或加前后寒暄,正则取首个大括号块最稳;"看不清填 null"这句提示能把幻觉金额压下去大半。入库与批量跑目录的收尾如下。
import sqlite3, datetime, json
from pathlib import Path
db = sqlite3.connect("receipts.db")
db.execute("""CREATE TABLE IF NOT EXISTS receipts(
id INTEGER PRIMARY KEY, type TEXT, date TEXT, merchant TEXT,
total REAL, tax REAL, items TEXT, created_at TEXT)""")
def save(r: dict) -> None:
db.execute("INSERT INTO receipts(type,date,merchant,total,tax,"
"items,created_at) VALUES(?,?,?,?,?,?,?)",
(r.get("type"), r.get("date"), r.get("merchant"),
r.get("total"), r.get("tax"),
json.dumps(r.get("items"), ensure_ascii=False),
datetime.datetime.now().isoformat()))
db.commit()
for p in Path("receipts").glob("*.jpg"):
try:
rec = extract_receipt(str(p))
save(rec)
print(p.name, "->", rec.get("total"))
except Exception as e:
print(p.name, "FAILED:", e) # 失败留档,转人工复核
跑完的验收清单:total 解析后校验数值区间,越界记异常;同一文件按内容哈希去重防重复入库;失败样本单独归档,正是下一节微调的现成训练集。整套服务数据不出机器,安全评审材料一页都不用加。
7、自建与闭源的决策线
什么时候值得把 VLM 搬回自己的机器?三条硬理由。
- 数据不出域:票据、病历、内网截图,合规成本高于一切硬件成本
- 批量成本交叉点:百万张图的视觉 token 账单,很快追平一张 24 GB 卡的月成本
- 微调入口:闭源 API 无从微调,开源权重才有后训练这条路
反向也成立:要顶级图表推理与复杂版面理解,闭源旗舰仍领先一档;没有 GPU 运维能力、或流量峰谷剧烈,API 的弹性依然省心。经验判断线画在"日均千张图"与"数据敏感"两个维度上,命中任意一条就值得认真评估自建,两条都中就别犹豫了。而自建的终点不止于推理:OCR 场景攒下来的失败样本,正好是后训练系列的入场券——用 LoRA 在自家票据版式上微调一轮,8B 档常能追平闭源旗舰在该版式上的抽取准确率,那是《后训练实战》系列要展开的主题。
把这条决策线画成图:
前三问是三条硬理由,命中任何一条就值得认真评估;最后一问是反向闸门——硬理由与运维能力两头都过,自建才真正成立。
8、总结
开源 VLM 在 2025 年下半年完成换代:Qwen3-VL 以 2B 到 235B-A22B 的完整档位与最强中文 OCR 成为默认选项,InternVL3.5 用密度与 MIT 协议卡位,Llama 4 靠原生多模态与超长上下文守住生态。部署侧的增量知识其实只有四件事:显存账要算视觉塔与图像 token、vLLM 用 limit_mm_per_prompt 配 mm-processor-kwargs 控预算、max_pixels 是成本旋钮、消息格式与闭源 API 逐字兼容。
练习两道:
- 把第 2 篇的发票提取脚本改指到本地:只替换
base_url与model两个字段,跑同一批发票,对比两边的字段一致率与单张成本 - 把
max_pixels分别设为 640、1280、2688 token 三档,各跑 20 张票据,记录total准确率与耗时,画出自己的成本-精度曲线
对于有数据合规压力或日均千图以上批量的团队,建议先用一张 24 GB 卡把 Qwen3-VL-8B 的 AWQ 版跑起来,拿真实票据验证一周——它大概率比你预期的更能打。

