跳到正文
原文
OpenRouter:Blog(Announcements)·· 19 小时前精选AI 评分82

OpenRouter 实测扫描件 OCR 模型,Claude Opus 5.5 准确率居首

Best OCR Model for Scanned Documents, Measured

文章要点
  • 1OpenRouter 用 8 张自制扫描页实测 15 个视觉模型、Mistral OCR 和 Tesseract 的准确率与每千页成本。
  • 2Claude Opus 5.5 以 262/264 项关键值居首,每千页 16.42 美元;Mistral OCR 258/264 项仅 2.02 美元。
  • 3GPT-6 Luna 与 GPT-6.1 Sol 在褪色小票上编造卡号、WiFi 密码和服务员名字,错误率指标看不出此类编造。
  • 4关闭推理让五个受测模型中的四个更快更省且不丢关键值;不同实验室双模型互查抓出 35 个错误词中的 34 个。
决策要点

扫描件识别的差距集中在褪色打印、手写和传真等难页,视觉模型会编造页面没有的值且错误率指标看不出来,选型只能在自有文档上实测。

  • 影响:每千页成本从 0.04 到 16.42 美元,选型对批量文档数字化的预算影响显著;模型编造的卡号、密码流入业务系统会造成数据错误,关键值环节需要校验机制。
  • 参考:可考虑用文中的 transcribe 代码在自己的扫描件上跑一轮候选模型,记录关键值命中数和每千页成本;票据类流水线值得测试关闭推理与双模型互查两个降本组合。
  • 依据:数据来自 OpenRouter 自建的 8 页基准,264 项关键值检查取 3 次运行,成本为实际计费,属平台自测口径,暂无独立验证。
正文 · AI 翻译

把扫描文档转换成可靠文本并非易事。Tesseract 这类经典 OCR 引擎在褪色印刷品、手写字和传真上会失灵。视觉模型能很好地读出这些页面,但有时会写出页面上并不存在的数值。我们用自己编写的文本制作了八个测试页面,因此每个字符都能核对,并将它们通过 OpenRouter 上的 15 个视觉模型和 Mistral OCR 进行处理,外加 Tesseract。

我们测试中最好的 OCR 模型是 Claude Opus 5.5,264 个关键数值中答对 262 个,价格为每 1,000 页 16.42 美元。Mistral OCR 性价比最高:264 个中答对 258 个,价格 2.02 美元。本文展示了测试结果、需要留意的错误,以及捕捉这些错误的代码。

这些结果只是一个起点。我们的八个页面并不是你的文档,所以要真正弄清哪个模型最适合你的工作流程,唯一的办法就是在你自己的扫描件上测试候选模型。

太长不看版

  • 最准确:anthropic/claude-opus-5.5。性价比最高:mistral-ocr。最便宜的良好结果:inclusionai/ling-3.0-flash-vl(关闭推理),264 个中答对 254 个,每 1,000 页 0.04 美元。
  • 在一张褪色的收据上,GPT-6 Luna 和 GPT-6.1 Sol 编造了卡号数字、Wi-Fi 密码和一个服务器名称。错误率体现不出这些,所以要核对你实际会使用的数值。
  • 关闭推理后,我们测试的五个模型中有四个变得更快、更便宜,且关键数值无一丢失。
  • 把两个来自不同实验室的廉价模型意见不一致的词标记出来,在 35 个错误词中捕捉到了 34 个,成本为每 1,000 页 0.62 美元。

我们的测试方法

我们根据已知文本渲染每一页,然后像扫描件那样做退化处理:轻微旋转、模糊、噪点和 JPEG 压缩。五个页面是普通页面:一张收据、一封带有两处故意拼写错误的信、一份勾选了复选框的表单、一份双栏时事通讯和一张工整的手写便条。三个页面被刻意做难:一张褪色的热敏收据、快速潦草的手写字,以及一份低分辨率传真,其中是 12 个由相似字符(0/O、1/I/L、5/S、8/B)构成的序列号,模型无法从上下文猜出它们。

Scanned thermal receipt from Cafe Verdigris, faded towards the bottom, with the card number and wifi password barely legible

我们用两种方式给每份转录文本打分。字符错误率(CER)是与真实文本的编辑距离除以其长度,忽略换行和 Markdown。关键数值是处理流程会用到的 88 个字符串,每页 10 到 15 个,例如总计、日期、卡号数字、序列号以及那些拼错的词。每一个都必须在三次运行中每次都精确出现,这为每个模型提供了 264 项检查。成本为每个请求中计费的 usage.cost。模型来自截至 2026 年 10 月 8 日的 OpenRouter 目录。

哪个 OCR 模型在扫描文档上最准确?

表格按关键数值正确数排序。

路线关键数值正确数CER,全部页面CER,3 个困难页面每 1,000 页成本中位秒数
anthropic/claude-opus-5.5264 个中 262 个0.02%0.06%$16.427.2
mistral-ocr(PDF 引擎)264 个中 258 个0.14%0.26%$2.023.8
anthropic/claude-sonnet-5.5264 个中 256 个1.34%3.49%$7.684.2
google/gemini-3.8-flash264 个中 254 个0.15%0.28%$7.509.3
stepfun/step-5-preview264 个中 254 个0.42%1.11%$5.489.3
inclusionai/ling-3.0-flash-vl264 个中 254 个1.05%2.56%$0.127.2
anthropic/claude-haiku-5.5264 个中 253 个1.19%2.89%$0.463.8
x-ai/grok-4.7264 个中 252 个0.13%0.35%$12.8511.3
z-ai/glm-5.3-flashx264 个中 252 个0.33%0.86%$1.455.5
openai/gpt-6.1-sol264 个中 252 个0.65%1.74%$6.038.4
deepseek/deepseek-v4.1-flash264 个中 248 个0.20%0.48%$0.204.8
qwen/qwen3.8-flash264 个中 248 个0.74%1.49%$0.298.1
qwen/qwen3.8-27b264 个中 246 个0.92%2.31%$1.5410.8
xiaomi/mimo-v2.6-flash264 个中 246 个2.02%5.34%$0.583.1
mistralai/mistral-large-4-0264 个中 240 个4.61%12.19%$6.8047.9
openai/gpt-6-luna264 个中 232 个1.49%3.95%$0.547.8
Tesseract 5.5(本地,1 次运行)88 个中 46 个26.06%64.39%$058.1

在五个普通页面上,每个视觉模型的 CER 都低于 0.3%,且全部 15 个模型都完美转录了工整手写体。差距大部分来自那张褪色的收据。Opus 5.5 和 Mistral OCR 在其上拿到了全部 33 项检查,而 GPT-6 Luna 只拿到 16 项。各模型对图像 token 的计数方式不同(一个中位页面在 DeepSeek 计为 1,070 个提示 token,在 Grok 计为 3,314 个),所以请在你自己的页面上测量成本。

哪个 OCR 模型适合你的工作

如果你的大部分工作是首选原因
每个数值都重要的困难扫描件Claude Opus 5.5264 个关键数值中答对 262 个,每 1,000 页 16.42 美元
大批量处理,价格合理Mistral OCR(mistral-ocr PDF 引擎)264 个关键数值中答对 258 个,每 1,000 页 2.02 美元
最低成本Ling 3.0 Flash VL(关闭推理)264 个关键值中识别出 254 个,每 1,000 页 0.04 美元
含有相似字符的序列号和 IDClaude Haiku 5.5,由 DeepSeek V4.1 Flash 交叉检查这一组合标出了 35 个错词中的 34 个,每 1,000 页 0.62 美元
清晰的打印页面,离线Tesseract 5.5在信件和简报上表现完美,而且免费,但对褪色的收据什么都没返回

当视觉模型读取一张难以辨认的扫描件时,会出什么错?

最严重的错误是一个看起来合理、但页面上并不存在的值。下面是褪色收据底部的印刷原文,随后是 GPT-6 Luna 的一次运行结果:

CONTACTLESS 32.95
CARD ****9026
SERVED BY: PRIYA
WIFI: verdigris-guest / tide2026
NET                                 27.46

CONTACTLESS
CARD **** 9027

SERVED BY: SHEVA
WIFI: verdigris_guest / 1freecoffee

收据上并没有 NET 这一行。卡号以 9026 结尾,姓名和密码都是编造出来的。GPT-6 Luna 在三次运行中都改动了服务器名称和年份。GPT-6.1 Sol 在每次运行中写出了不同的编造密码(flatwhite24、flat9am!、flatleaf)。我们还要求模型写入 [?] 而不是去猜测。但这没有用:七个模型中有四个从未使用该标记。

其他反复出现的错误:

  • O 和 0:15 个模型中有 13 个在至少一次运行中把两个传真序列号读错。只有 Claude Sonnet 5.5 和 GPT-6.1 Sol 每次都读对。
  • 拼写修正:这封信的落款是“Senior Assosiate”,提示词要求完全照抄拼写,但 45 份转录中仍有 16 份把它“纠正”了。
  • 勾选框:Claude Sonnet 5.5 和 Haiku 5.5 复制了表单中复选框的标签,却丢掉了哪个框被勾选的信息。

第 1 步:把扫描件发给视觉模型

视觉模型生成转录文本的方式,与语言模型生成任何文本的方式相同,因此它倾向于写出文本通常的表达,而不是页面上实际印刷的内容。提示词要求它完全照抄。这能减轻问题但无法消除:45 份转录中仍有 16 份纠正了信件中故意写错的拼写。你需要在 OPENROUTER_API_KEY 中填入一个 OpenRouter API 密钥。图像以 base64 image_url 部分的形式传入(参见 image inputs),因此 OpenRouter 上的每个视觉模型都接受这一相同的请求。把它保存为 transcribe.py,因为第 3 步和第 4 步会从它导入:

import base64
import mimetypes
import os

import requests

API = "https://openrouter.ai/api/v1/chat/completions"
HEADERS = {"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}"}
PROMPT = ("Transcribe all of the text in this scanned document, in reading order. "
          "Copy spelling, numbers and punctuation exactly as they appear, even if they "
          "look wrong. Return plain text only, with no Markdown and no commentary.")


def transcribe(path, model="deepseek/deepseek-v4.1-flash", reasoning={"effort": "none"}):
    mime = mimetypes.guess_type(path)[0] or "image/jpeg"
    with open(path, "rb") as f:
        image = f"data:{mime};base64," + base64.b64encode(f.read()).decode()
    body = {
        "model": model,
        "max_tokens": 16000,
        "messages": [{"role": "user", "content": [
            {"type": "text", "text": PROMPT},
            {"type": "image_url", "image_url": {"url": image}},
        ]}],
    }
    if reasoning:
        body["reasoning"] = reasoning
    response = requests.post(API, headers=HEADERS, json=body, timeout=300)
    response.raise_for_status()
    data = response.json()
    choice = data["choices"][0]
    if choice["finish_reason"] != "stop":
        raise ValueError(f"finish_reason={choice['finish_reason']}: the transcript is incomplete")
    return choice["message"]["content"], data["usage"]["cost"]


if __name__ == "__main__":
    text, cost = transcribe("receipt.jpg")
    print(text)
    print(f"\n${cost:.5f}")
HARBOR LANE MARKET
1180 Harbor Lane, Unit 4
Portland OR 97209
Tel (503) 555-0148
...
TOTAL 62.86
VISA ****4471 62.86
AUTH 08813C
...

$0.00022

每个字符都准确无误,花费大约百分之二美分。数据 URL 的类型由文件扩展名决定,因此 PNG 和 WebP 扫描件也可以。要比较不同模型,只需更改 model 字符串,并为第 2 步中无法关闭推理的模型传入 reasoning=None。

之所以要有 finish_reason 检查,是因为推理 token 会占用 max_tokens 的额度。在 4,000 的限制下,Gemini 3.8 Flash 在每次运行中都花费 3,841 个 token 对传真进行推理,并在序列号 4 的中途停下。请求仍然返回了 200 OK 和 finish_reason: "length",因此如果没有这项检查,流水线就会在不知不觉中存入被截短的转录。

第 2 步:转录时关闭推理

当模型需要规划答案时,推理会有帮助。从页面上抄写文字并不需要规划,因此多余的 token 大多只会增加成本和时间。这就是 transcribe 默认发送 "reasoning": {"effort": "none"} 的原因(参见 reasoning tokens)。我们对每个允许关闭推理的模型在关闭推理的情况下重跑了完整测试:

模型关键值,开启 → 关闭CER,开启 → 关闭每 1,000 页成本,开启 → 关闭中位秒数,开启 → 关闭
mistralai/mistral-large-4-0240 → 2494.61% → 0.15%$6.80 → $1.6847.9 → 5.6
inclusionai/ling-3.0-flash-vl254 → 2541.05% → 0.19%$0.12 → $0.047.2 → 4.0
openai/gpt-6-luna232 → 2321.49% → 1.08%$0.54 → $0.227.8 → 3.8
deepseek/deepseek-v4.1-flash248 → 2490.20% → 0.23%$0.20 → $0.164.8 → 4.5
qwen/qwen3.8-27b246 → 2440.92% → 2.28%$1.54 → $0.7710.8 → 8.9

开启推理时,一个 Mistral Large 4 请求推理了 626 秒且没有返回任何文本(finish_reason: "error")。Qwen3.8 27B 在开启推理时表现更好,所以要用自己的页面来测试。Gemini 3.8 Flash、Grok 4.7 和 GLM 5.3 FlashX 不允许关闭推理,因此要传入 reasoning=None 来省略该字段。否则请求会失败并报错:

400 {"error":{"message":"Reasoning is mandatory for this endpoint and cannot be disabled.","code":400,"metadata":{"provider_name":null}}}

第 3 步:对来自不同实验室的两个模型进行交叉检查

像 O 和 0 这样外形相似的字符在模糊图像中很难区分。每个模型根据自己的训练做出判断,因此来自不同实验室的两个模型很少犯同样的错误。它们转录结果不一致的地方通常就是图像不清晰的地方。我们测试了六个模型中的每一对组合,效果最好的是 Claude Haiku 5.5 加 DeepSeek V4.1 Flash 校验。Haiku 在 2,964 个单词中犯了 35 处错误,DeepSeek(如同第 2 步那样关闭推理)在其中 34 处上不同意,其中 10 处为误报。这对组合每 1,000 页成本为 $0.62,是 Mistral OCR 的三分之一。两者使用相同的 API,所以校验只需多一次调用。调用 Haiku 时使用 reasoning=None,因为我们的表格中就是这样运行的:

import difflib

from transcribe import transcribe  # the function from step 1

a, _ = transcribe("fax.jpg", model="anthropic/claude-haiku-5.5", reasoning=None)
b, _ = transcribe("fax.jpg", model="deepseek/deepseek-v4.1-flash")
a, b = a.split(), b.split()
for tag, i1, i2, j1, j2 in difflib.SequenceMatcher(None, a, b, autojunk=False).get_opcodes():
    if tag != "equal":
        print("check:", " ".join(a[i1:i2]), "vs", " ".join(b[j1:j2]))
check: 8LI5-QS0B5O vs 8LI5-QS0B50
check: 30HY-1OHLG7 vs 30HY-10HLG7

这就是大多数模型都读错的两个序列号。如果两个模型以相同的方式误读同一个字符,就不会被标记出来。这种情况在我们的测试中发生过一次,一个 O 被两个模型都读成了零,所以尽可能对照列表或校验和来验证 ID。这种对比还假设两个模型按相同的顺序读取页面。在三次运行中有一次,两个模型按不同顺序列出了传真的两列,结果每个序列号都显示为不一致。

第 4 步:使用 Mistral OCR 作为专用引擎

Mistral OCR 是一个专门为文档 OCR 训练的模型。在 OpenRouter 上,它作为 file-parser 插件中的一个引擎运行,该插件接受 PDF,价格为每 1,000 页 $2。该插件在你的模型看到文件之前先对其进行 OCR。它将文本作为响应中的文件注释返回,因此你指定的模型只需进行回复。将扫描件包装成单页 PDF,并搭配任意便宜模型发送:

import base64
import html
import io
import os

import requests
from PIL import Image, ImageSequence

from transcribe import API, HEADERS  # from step 1


def mistral_ocr(path):
    if path.lower().endswith(".pdf"):  # scanned PDFs go as they are
        with open(path, "rb") as f:
            raw = f.read()
    else:  # wrap an image in a PDF in memory, one page per frame of a multipage TIFF
        pages = [p.convert("RGB") for p in ImageSequence.Iterator(Image.open(path))]
        buf = io.BytesIO()
        pages[0].save(buf, "PDF", save_all=True, append_images=pages[1:])
        raw = buf.getvalue()
    pdf = "data:application/pdf;base64," + base64.b64encode(raw).decode()
    pdf_name = os.path.splitext(os.path.basename(path))[0] + ".pdf"
    body = {
        "model": "upstage/solar-mini4",  # any cheap text model; it only has to reply
        "max_tokens": 5,
        "messages": [{"role": "user", "content": [
            {"type": "text", "text": "Reply OK."},
            {"type": "file", "file": {"filename": pdf_name, "file_data": pdf}},
        ]}],
        "plugins": [{"id": "file-parser", "pdf": {"engine": "mistral-ocr"}}],
    }
    response = requests.post(API, headers=HEADERS, json=body, timeout=300)
    response.raise_for_status()
    data = response.json()
    parts = data["choices"][0]["message"]["annotations"][0]["file"]["content"]
    texts = [p["text"] for p in parts if p.get("type") == "text"]  # skip extracted image parts
    text = "\n".join(t for t in texts if not t.startswith(("<file", "</file")))
    return html.unescape(text), data["usage"]["cost"]


text, cost = mistral_ocr("receipt_faded.jpg")
print(text)
print(f"\n${cost:.5f}")
CAFE VERDIGRIS
22 Quayside Row
Bristol BS1 4QA
VAT No. GB 318 7742 09

27/09/2026 08:17 TILL 2 CHK 0193
...
|  CONTACTLESS | 32.95  |
|  CARD ***9026 |   |

SERVED BY: PRIYA
WIFI: verdigris-guest / tide2026

$0.00202

褪色收据上的每个值都正确返回,包括卡号数字和密码。Mistral OCR 的设计目标是保留版式,因此其输出是 Markdown:收据行变成表格,标题得到 ##,& 返回为 &amp;。这就是该函数要反转义 HTML 实体的原因。如果你需要纯文本,就去掉 Markdown;如果接下来由 LLM 读取结果,就保留它。

在 OCR 方面,LLM 比 Tesseract 更好吗?

是的,除非是干净的打字页面。Tesseract 5.5 逐行读取字符形状,因此在清晰的印刷体上表现出色,而在噪声和手写体上则表现不佳。它完美转录了信件和新闻简报。对于褪色收据它什么都没返回,在凌乱手写体上只识别出 11 个值中的 1 个,在传真上识别出 15 个中的 4 个。

常见问题

哪个 LLM 最适合 OCR?

在我们的测试中是 Claude Opus 5.5。Gemini 3.8 Flash 和 Grok 4.7 的错误率几乎同样低,但漏掉了更多序列号。DeepSeek V4.1 Flash 和 Ling 3.0 Flash VL 接近前者的表现,每 1,000 页成本不到 $0.20。

如何对扫描的 PDF 进行 OCR?

像第 4 步那样,使用 mistral-ocr 引擎发送它。或者,将每一页渲染为图像交给视觉模型,或者将 PDF 直接发送给能读取 PDF 的模型。免费的文本解析器无法处理扫描件,因为扫描件没有文本层。

最准确的 OCR 模型是什么?

Claude Opus 5.5 在 264 个关键值中答对了 262 个,在我们全部八份扫描件上的字符错误率为 0.02%。Mistral OCR 位居第二,答对 264 个中的 258 个,成本仅为前者的八分之一。

使用 LLM 进行 OCR 要花多少钱?

在我们的测试中,成本从每 1,000 页 $0.04(Ling 3.0 Flash VL,关闭推理)到 $16.42(Claude Opus 5.5)不等。Mistral OCR 的价格为每 1,000 页 $2。各模型计算图像 token 的方式不同,所以请在你自己的页面上测量成本。

Mistral OCR 比视觉模型更好吗?

在我们的测试中,它的性价比最高:以每 1,000 页 $2.02 的价格提取了 264 个键值中的 258 个,仅次于 Claude Opus 5.5。它返回保留版式的 Markdown,而视觉模型则按你要求的任意格式返回结果。

LLM 能读懂手写字吗?

能。全部 15 个视觉模型都完美转录了我们工整的便条。对于潦草的手写字,大多数模型的字符准确率保持在 99% 以上,出错的地方主要是人名(把 “Achterberg” 认成了 “Ackterberg”)。

后续步骤

来源:OpenRouter:Blog(Announcements) · openrouter.ai