文档token消耗:图像 vs. 文本

关于 VLM,几乎所有人都有一个直觉,而且它是错误的:处理图像总是比处理文本更昂贵。这似乎有道理——图像"数据更多",对吧?我构建了一个基准测试来精确测量这一点,结果在特定的、可测量的点上颠覆了这一直觉:对于一个 5 页的文档,在 DeepSeek-OCR 方案下将其表示为图像的成本比等效文本少 23.6 倍。这不是近似值——这是我的分词器得出的数字。

这是 2026 年多模态效率研究的驱动力,对于任何构建长上下文代理的人来说都有巨大的实际影响:与其将巨大的文档作为文本塞入上下文窗口,不如将其渲染为图像,让视觉编码器对其进行压缩。DeepSeek-OCR(2025 年 10 月)将其形式化为"光学上下文压缩",并报告在压缩低于 10 倍时 OCR 精度为 97%。这个项目通过测量回答的问题是:什么时候值得这样做,你能获得多少收益?

这是一个更大项目的基础文章。在运行真正的 VLM 并测量精度之前(这将在我在 DGX Sparks 上进行时实现),我需要建立确定性的、可复现的一半:token 成本。精度取决于模型和硬件;token 成本是表示策略的闭合函数,可以在笔记本电脑上精确测量。这是整个项目的诚实基准。

方法论诚实性的说明,我会重复这一点,因为它很重要:在这篇文章中,我测量的是成本(token),而不是精度。精度数字(DeepSeek-OCR 的 97% 等)来自论文,并标记为引用。token 数字是在这里使用真正的分词器测量的,并且是可复现的。

1、为什么 token 数量是决定一切的指标

在基准测试之前,值得确定为什么这个指标如此重要。在 VLM 中,推理成本——时间、内存、金钱——随着进入语言模型的 token 数量而缩放。图像被视觉编码器转换为一系列"视觉 token",这些 token 占据相同的上下文窗口,并且在 LLM 运行时与文本 token 成本相同。一个变成 16,000 个视觉 token 的文档在解码器中的成本与 16,000 个文本 token 相同:相同的 KV 缓存内存压力,相同的二次注意力成本。

所以"我的图像花费多少 token"这个问题不是实现细节——它是核心成本变量。而且它在不同分词方案之间变化剧烈,远超大多数人的想象。同一文档根据你的表示方式可能花费 100 个 token 或 16,000 个 token。这种两个数量级的变化正是基准测试所测量的。

2、测试框架:测量什么和引用什么

基准测试是一个闭合函数。它接受文本,测量其花费多少文本 token(使用真正的 GPT-4 分词器,通过 tiktoken),并计算相同内容在每种视觉分词方案下会花费多少视觉 token。测量部分:

import tiktoken
ENC = tiktoken.get_encoding("cl100k_base")   # real GPT-4/4o tokenizer

def text_tokens(text: str) -> int:
    return len(ENC.encode(text))              # measured, not estimated

视觉方案来自每个模型的公开规格。我实现了五种,每种的秘密在于它如何将像素映射到 token:

def smolvlm_vision_tokens(w, h, max_patches_side=4):
    # pixel-shuffle: each 384x384 patch -> 81 tokens, with a patch budget + thumbnail
    tiles_w = min(math.ceil(w/384), max_patches_side)
    tiles_h = min(math.ceil(h/384), max_patches_side)
    return (tiles_w*tiles_h + 1) * 81

def qwen2vl_vision_tokens(w, h):
    # patch 14px + 2x2 merge -> 1 token per 28x28 px; grows with resolution
    return math.ceil(w/28) * math.ceil(h/28)

def clip_fixed_vision_tokens(w, h):
    return 576                                # LLaVA-1.5: fixed grid, ignores resolution

def deepseek_ocr_vision_tokens(w, h, mode="base"):
    # optical compression: fixed vision-token budget per page
    return {"tiny": 64, "small": 100, "base": 256, "large": 400}[mode]

注意代码中已经显现的结构差异:SmolVLM、CLIP 和 DeepSeek-OCR 具有固定或上限的 token 预算(它们不会随页面无限增长),而 Qwen2-VL 具有可变分辨率(它随图像面积线性增长)。这种差异将主导每个结果。

3、在信任之前验证测试框架

这是我带到每个项目中的规则:我不信任未经外部来源验证的数字。官方 SmolVLM 博客指出,典型图像在 SmolVLM 中花费约 1.2k token,而在 Qwen2-VL 中花费约 16k token——SmolVLM 使用约 7.5% 的 Qwen token。我在 Qwen 中产生 16k 的分辨率下运行了我的测试框架并检查:

Image that yields ~16k tokens in Qwen (~3528x3528px):
  SmolVLM (my harness):  1377 tokens
  SmolVLM/Qwen ratio:    8.6%   (official blog: ~7.5%)

足够接近以供信任——8.6% 与报告的 7.5% 匹配,差异来自我的补丁预算建模的粒度。事实上,我的测试框架的第一个版本是错误的:我将 SmolVLM 建模为无限平铺(每个补丁变成 81 个 token,没有上限),这给出了 Qwen 的 51% 而不是 7.5%。只有与博客的验证才揭示 SmolVLM 实际上会调整图像大小以适应补丁预算——没有这个修正,我所有的 SmolVLM 数字都会膨胀 6 倍。这正是为什么你在发布前要验证。

4、数字:交叉点

我在四个大小递增的文档上运行了基准测试——从一个段落到约 20 页。首先,原始文本成本(已测量):

document                  chars   text tok
short (1 paragraph)         227          60
medium (~1 page)           1816         473
long (~5 pages)            9080        2361
very long (~20 pages)     36320        9441

现在是每种方案的视觉 token 成本,以及压缩比(比率 > 1 表示图像比文本成本更低):

document                   SmolVLM      Qwen2-VL     CLIP        DeepSeek-small
short (1 paragraph)      324( 0.2x)   222( 0.3x)   576( 0.1x)   100( 0.6x)
medium (~1 page)         567( 0.8x)   740( 0.6x)   576( 0.8x)   100( 4.7x)
long (~5 pages)         1053( 2.2x)  3071( 0.8x)   576( 4.1x)   100(23.6x)
very long (~20 pages)   1053( 9.0x) 11877( 0.8x)   576(16.4x)   100(94.4x)

慢慢阅读这个表格,因为它包含了整篇文章的答案。对于文本,所有方案都失败了——比率低于 1,意味着图像比文本花费更多 token。这是有道理的:一个段落是 60 个文本 token,没有视觉编码器可以用少于这个数量来表示整个页面(即使几乎是空的)。编码器的固定开销在少量内容上不划算。

但随着文档增长,固定预算方案开始发挥作用。在 5 页文档上,DeepSeek-OCR small 压缩了 23.6 倍;在 20 页上,压缩了 94.4 倍。固定 CLIP 无论内容如何总是花费 576 个 token,在 20 页上压缩了 16.4 倍。与此同时,Qwen2-VL 保持在约 0.8 倍——它从不压缩,因为它的成本随页面分辨率增长。

这张图是项目的核心。虚线是原始文本(成本 = 文本 token 数量)。任何低于它的方案花费都少于文本。注意三种不同的行为:DeepSeek-OCR(紫色和青色线)和 CLIP(琥珀色)是水平的——恒定成本,所以文档越大,它们越赢。SmolVLM(绿色)上升然后在补丁预算处饱和。而 Qwen2-VL(蓝色)随文本上升,从未决定性地跨越到节省的一侧。每条线与虚线交叉的点是"盈亏平衡点":低于约 300 个文本 token,转为图像不划算;高于它,越来越划算。

5、基准测试揭示的 VLM 设计

出现的模式不是关于"哪个模型最好"——而是关于模型做出的设计选择,这定义了它们的用途。

固定预算模型(DeepSeek-OCR、CLIP)将图像视为固定大小的摘要。它们丢弃分辨率以保持低成本,这非常适合你想要最大压缩并容忍丢失细节的长文档。这是 DeepSeek-OCR 的赌注:激进压缩,接受 <10 倍时 97% 的精度和 20 倍时约 60% 的精度(论文中的数字),并获得将巨大文档塞入小上下文窗口的能力。

可变分辨率模型(Qwen2-VL)保留与图像大小成比例的细节。它们花费更多 token 是因为它们保留了精细信息——当任务需要阅读微小文本、密集表格或压缩会破坏的视觉细节时,这很重要。Qwen 的高成本不是低效;它是不丢弃信息的代价。正如 VISOR 论文所言,减少 token 会创建信息瓶颈,损害细粒度理解任务——这就是为什么不是每个人都想要最压缩的方案。

SmolVLM 故意处于中间位置:有限的补丁预算(节省)但使用像素混洗,比固定 CLIP 保留更多。这是边缘设备的正确选择,因为你没有多余的 token 但仍需要一些保真度——这就是为什么它主导了本地 VLM 领域,这是本项目未来文章的主题。

6、将 token 转换为金钱

压缩比是抽象的;API 账单不是。我采用了处理 10,000 个 5 页文档的具体场景——现实的文档提取工作负载——并将 token 转换为成本,使用每百万 token 2.50 美元的说明性输入价格:

Processing 10,000 5-page documents:
  raw text             2361 tok/doc  ->  $59.02   (baseline)
  Qwen2-VL             3071 tok/doc  ->  $76.78   -30% (more expensive!)
  SmolVLM              1053 tok/doc  ->  $26.32   55% savings
  fixed CLIP            576 tok/doc  ->  $14.40   76% savings
  DeepSeek-OCR base     256 tok/doc  ->   $6.40   89% savings
  DeepSeek-OCR small    100 tok/doc  ->   $2.50   96% savings

两个事实跃然纸上。首先:分词方案的选择将成本从 59 美元移动到 2.50 美元——在相同任务上 24 倍的差异,仅仅通过改变文档的表示方式。在运行数百万文档的管道中,这是项目可行与不可行之间的区别。其次,更违反直觉的是:Qwen2-VL 比原始文本贵 30%。使用错误的方案转为图像不仅未能节省——它实际上提高了成本。"图像压缩"的直觉只对固定预算方案成立;对于可变分辨率方案,作为图像读取是一种悲观化。

这强化了核心信息:没有"使用图像更便宜"。有"使用正确的图像方案,对于超过交叉点的文档,戏剧性地更便宜"。在这些条件之外,你可能支付更多。

7、交叉点,精确地说

对于固定预算方案,盈亏平衡点有一个闭合形式:由于视觉成本是恒定的,当文档的文本超过该预算时,它作为图像才值得。计算:

Crossover point (documents larger than this -> worth going to image):
  DeepSeek-OCR small   ~100 text tokens  (~75 words)
  DeepSeek-OCR base    ~256 text tokens  (~190 words)
  fixed CLIP           ~576 text tokens  (~430 words)

DeepSeek-OCR small 在仅仅约 75 个单词处跨越这条线——意味着实际上任何比段落长的文档在该方案下已经值得转为图像,从成本角度来看。固定 CLIP 需要约 430 个单词(几乎一页)才能收回成本。这是基准测试的可操作数字:你测量你的文档的典型大小,将其与你打算使用的方案的盈亏平衡点进行比较,并立即知道该策略是节省还是浪费。

通常的警告:这是成本盈亏平衡点。价值盈亏平衡点——精度仍然可接受的地方——可能更高,这将是项目下一阶段测量的内容。文档可能高于成本盈亏平衡点(在 token 方面值得)而压缩破坏了精度(实际上不值得)。两条曲线 together 给出了完整的决策。

8、基准测试对其局限性的诚实

我需要明确说明这个数字说什么,因为很容易过度解读。

它测量成本,而不是价值。如果精度保持,压缩 94 倍是很好的,如果模型无法再读取文档则无用。DeepSeek-OCR 报告在 20 倍压缩时精度降至约 60%——意味着我的基准测试显示的 20 页 94.4 倍超出了论文的有用精度范围。压缩数字本身是一半真相;缺失的一半是精度曲线,只有通过运行模型才能获得。这是项目的下一阶段。

它使用文本分词器(GPT-4)作为参考。文档的文本成本在不同分词器之间有所不同——具有不同词汇表的模型会给出不同的计数。我使用 cl100k_base 作为最常见的一个,但确切的压缩比会随另一个分词器略有变化。数量级和交叉点保持不变。

而且视觉方案是规格的模型,而不是真实编码器运行的测量。我根据官方博客验证了 SmolVLM(8.6% 对比报告的 7.5%),这让我对数量级有信心,但真实编码器有智能调整大小和填充细节,会使计数偏移几个百分点。对于目的——决定何时转为图像值得——这种精度足够了;对于你的特定 VLM 的确切计数,你在你的 VLM 上测量。

9、如何运行它,以及项目中的下一步

整个测试框架(token_economics.py)在任何笔记本电脑上运行,无需 GPU,在一秒钟内完成。你将你自己的文档粘贴到 DOCS 字典中,运行它,并查看你的用例的交叉点——因为盈亏平衡点取决于你文档的典型大小。如果你处理短收据,转为图像永远不划算;如果你处理 40 页的合同,它戏剧性地划算。基准测试告诉你你在哪一边。

现在 token 经济学已确立为基线,项目中的下一步是什么:

将其带到 DGX Sparks 并测量精度与成本——在相同文档上实际运行 DeepSeek-OCR、SmolVLM 和 Qwen2-VL,并绘制缺失的曲线:精度与压缩比。这是基准测试从一半真相变为完整决策矩阵的地方。

测量token 剪枝方法(VScan、VisionTrim、基于 RL 的 TPRL)——它们从另一个角度解决问题:不是选择精益的分词方案,而是从昂贵的方案中剪枝冗余 token。VScan 报告在 LLaVA-NeXT-7B 上 10 倍 FLOP 减少,保留 95.4% 的性能(论文中的数字);我想在我自己的机器上测量这一点。

以及本地/边缘角度——那里 token 约束更紧,"小模型 + 精益分词 + 量化"的组合决定了什么能在 Jetson 上运行,什么不能。

运行 token_economics.py,粘贴你的文档,告诉我你的交叉点落在哪里。如果它低于你文档的典型大小,你在浪费 token 读取文本,而你可以作为图像读取——现在你有数字来证明这一点。

完整代码(token_economics.py)和图表:可在 CPU 上复现,无需 GPU。这是成本基线;下一阶段在运行真实 VLM 的同时测量精度。

"""
token_economics.py — benchmark de economia de tokens para ler imagens/documentos
com VLMs, comparando estratégias de representação.

O que é MEDIDO aqui (determinístico, reproduzível):
  - tokens de TEXTO de um documento (via tiktoken, o tokenizador real do GPT-4)
  - tokens de VISÃO que o mesmo conteúdo gastaria sob cada esquema de tokenização
  - razão de compressão texto->visão de cada estratégia

O que NÃO é medido aqui (vem dos papers, marcado como citado):
  - ACURÁCIA de OCR/entendimento (exige rodar o VLM, precisa de GPU)

Esquemas de tokenização visual implementados a partir das especificações públicas:
  - SmolVLM:        81 tokens por patch 384x384
  - Qwen2-VL:       ~28x28 px por token (patch 14, merge 2x2), variável com resolução
  - CLIP fixo:      576 tokens por imagem (LLaVA-1.5, grid 24x24)
  - DeepSeek-OCR:   ~100-256 vision tokens por página (optical compression)
"""
import tiktoken, math, json
from dataclasses import dataclass

ENC = tiktoken.get_encoding("cl100k_base")   # tokenizador GPT-4/GPT-4o-ish

def text_tokens(text: str) -> int:
    """Tokens de texto reais, medidos."""
    return len(ENC.encode(text))

# ---------- esquemas de tokenização VISUAL (das specs públicas) ----------

def smolvlm_vision_tokens(w: int, h: int, max_patches_side: int = 4) -> int:
    """SmolVLM: pixel-shuffle reduz cada patch 384x384 a 81 tokens, e a imagem é
    redimensionada para caber num budget de patches (default ~4x4) + 1 thumbnail global.
    Modelagem validada contra o blog oficial (~1.2k tokens/imagem; ~7.5% do Qwen2-VL)."""
    tiles_w = min(math.ceil(w / 384), max_patches_side)
    tiles_h = min(math.ceil(h / 384), max_patches_side)
    return (tiles_w * tiles_h + 1) * 81   # +1 = thumbnail global

def qwen2vl_vision_tokens(w: int, h: int) -> int:
    """Qwen2-VL: patch 14px, merge 2x2 -> 1 token por bloco de 28x28 px.
    (aproximação da spec; a real tem smart-resize, mas a ordem de grandeza bate)"""
    return math.ceil(w / 28) * math.ceil(h / 28)

def clip_fixed_vision_tokens(w: int, h: int) -> int:
    """LLaVA-1.5 / CLIP ViT-L/14 @336: grid fixo 24x24 = 576 tokens, resolução ignorada."""
    return 576

def deepseek_ocr_vision_tokens(w: int, h: int, mode="base") -> int:
    """DeepSeek-OCR optical compression. Modos publicados (aprox):
       tiny ~64, small ~100, base ~256, large ~400 vision tokens por página."""
    return {"tiny": 64, "small": 100, "base": 256, "large": 400}[mode]

# ---------- render de texto para imagem (para medir dimensões reais) ----------

def rendered_page_dims(text: str, chars_per_line=90, line_px=22, font_w=9,
                       margin=40, page_w=1024):
    """Estima as dimensões em px de uma página renderizada com o texto.
    Retorna (w, h). Layout tipo documento A4 escaneado."""
    lines = 0
    for para in text.split("\n"):
        lines += max(1, math.ceil(len(para) / chars_per_line))
    h = margin*2 + lines*line_px
    return page_w, h

# ---------- benchmark ----------

@dataclass
class Result:
    name: str
    chars: int
    text_tok: int
    vision: dict          # esquema -> (tokens, ratio vs texto)

def benchmark(name: str, text: str) -> Result:
    tt = text_tokens(text)
    w, h = rendered_page_dims(text)
    schemes = {
        "SmolVLM (81/patch)":      smolvlm_vision_tokens(w, h),
        "Qwen2-VL (28px/tok)":     qwen2vl_vision_tokens(w, h),
        "CLIP fixo (576)":         clip_fixed_vision_tokens(w, h),
        "DeepSeek-OCR small":      deepseek_ocr_vision_tokens(w, h, "small"),
        "DeepSeek-OCR base":       deepseek_ocr_vision_tokens(w, h, "base"),
    }
    vision = {k: (v, tt / v if v else float("inf")) for k, v in schemes.items()}
    return Result(name, len(text), tt, vision)

# ---------- documentos de teste (tamanhos variados) ----------

DOCS = {}

# doc curto (~1 parágrafo)
DOCS["curto (1 parágrafo)"] = (
    "O harness determina o desempenho do agente, não o modelo. "
    "Essa é a tese central: Agent = Model + Harness. "
    "A camada de runtime que intermedia cada decisão define o comportamento observável, "
    "mais do que a escolha do modelo base. "
) * 1

# doc médio (~1 página)
DOCS["médio (~1 página)"] = DOCS["curto (1 parágrafo)"] * 8

# doc longo (~5 páginas)
DOCS["longo (~5 páginas)"] = DOCS["curto (1 parágrafo)"] * 40

# doc muito longo (~20 páginas, cenário de long-context)
DOCS["muito longo (~20 páginas)"] = DOCS["curto (1 parágrafo)"] * 160

if __name__ == "__main__":
    results = [benchmark(name, text) for name, text in DOCS.items()]

    print(f"{'documento':<26}{'chars':>8}{'texto tok':>11}")
    print("-"*45)
    for r in results:
        print(f"{r.name:<26}{r.chars:>8}{r.text_tok:>11}")

    print("\n\nTOKENS DE VISÃO por esquema (e razão de compressão texto->visão):")
    print("(ratio > 1 = visão gasta MENOS tokens que texto)\n")
    schemes = list(results[0].vision.keys())
    header = f"{'documento':<26}" + "".join(f"{s.split(' ')[0][:11]:>13}" for s in schemes)
    print(header)
    print("-"*len(header))
    for r in results:
        row = f"{r.name:<26}"
        for s in schemes:
            tok, ratio = r.vision[s]
            row += f"{tok:>6}({ratio:>4.1f}x)"
        print(row)

    # salva json para as figuras
    out = []
    for r in results:
        out.append({
            "name": r.name, "chars": r.chars, "text_tok": r.text_tok,
            "vision": {k: {"tokens": v[0], "ratio": v[1]} for k, v in r.vision.items()},
        })
    json.dump(out, open("/home/claude/bench_results.json", "w"), indent=2, ensure_ascii=False)
    print("\n[salvo bench_results.json]")

# ---------- extensões: custo em dinheiro e break-even ----------

def dollar_cost(tokens: int, n_docs: int, price_per_1m: float = 2.50) -> float:
    """Custo de input em dólares para processar n_docs com `tokens` cada."""
    return tokens * n_docs / 1e6 * price_per_1m

def break_even_tokens(scheme_vision_tokens: int) -> int:
    """Para esquema de budget fixo: o documento vale a pena como imagem
    quando seu texto ultrapassa o budget de vision tokens do esquema."""
    return scheme_vision_tokens

def cost_report(text: str, n_docs: int = 10000, price_per_1m: float = 2.50):
    """Relatório de custo em dinheiro comparando texto puro vs. esquemas de visão."""
    tt = text_tokens(text)
    w, h = rendered_page_dims(text)
    schemes = {
        "texto puro": tt,
        "Qwen2-VL": qwen2vl_vision_tokens(w, h),
        "SmolVLM": smolvlm_vision_tokens(w, h),
        "CLIP fixo": clip_fixed_vision_tokens(w, h),
        "DeepSeek-OCR base": deepseek_ocr_vision_tokens(w, h, "base"),
        "DeepSeek-OCR small": deepseek_ocr_vision_tokens(w, h, "small"),
    }
    base = dollar_cost(tt, n_docs, price_per_1m)
    print(f"Processar {n_docs} documentos (input ${price_per_1m}/1M tokens):")
    for name, toks in schemes.items():
        c = dollar_cost(toks, n_docs, price_per_1m)
        save = "" if name == "texto puro" else f"  ({(1-c/base)*100:+.0f}%)"
        print(f"  {name:<20}{toks:>6} tok/doc  ${c:>8.2f}{save}")

原文链接:Sometimes reading a document as an image costs 24× fewer tokens than reading it as text.

汇智网翻译整理,转载请标明出处