OvisOCR2 + RAG 实战

今天,我会用四项关键挑战来测试 OvisOCR2:公式识别、表格识别、阅读顺序和手写文本。

OvisOCR2 + RAG 实战
AI模型价格对比 | AI工具导航 | ONNX模型库 | Vibe Coding教程 | PLC在线仿真器 | Tripo 3D | Meshy AI | ElevenLabs | KlingAI | ArtSpace | Phot.AI | InVideo

不到一个月前,我做过一期关于无限 OCR 的视频,很多朋友都很喜欢。

之后,一位粉丝向我求助,说他们的 OCR 聊天机器人遇到了一个问题。我猜这是个常见问题,所以决定做一期新视频来帮助他和其他开发者。

如今很多公司都在开发知识库、检索增强生成(RAG)和问答系统。这些技术都依赖一项基础能力:把非结构化文档转换成机器可读的文本。

过去是怎么做的呢?业界普遍采用"流水线"式方案。简单来说,就是多个模型协同工作,每个模型负责一项特定任务。

一个模型分析文档布局,另一个识别文本,第三个识别公式,第四个整理表格。然后把各自的输出合并起来,得到最终结果。

听起来很合理,但在实践中,这种方案存在几个问题:维护成本高、部署复杂,而最要命的是误差传播。

只要一个环节出错,这个错误就可能沿着整条流水线传导下去,往往导致糟糕甚至荒谬的结果。

None

OvisOCR2 用完全不同的思路解决了这个问题:端到端模型。它不依赖多个专用模型协同工作,而是用单个模型处理整份文档。

从某种意义上说,它用一位能从头到尾搞定整个任务的高水平专家,取代了流水线上的多名专家。

你给它一张文档图片,OvisOCR2 就能一次性处理文本、表格、数学公式、图片和复杂的文档布局。标题、段落、公式、HTML 表格、阅读顺序和视觉区域都在单次前向中生成,无需多个处理阶段。

它甚至能用带坐标信息的 HTML 标签捕捉图表和图片,让输出更容易集成到下游应用中,例如论文分析、知识库构建、RAG 流水线和文档数据清洗。

下面我用一个在线聊天机器人做个快速演示,看看它实际效果如何。

观看演示视频

今天,我会用四项关键挑战来测试 OvisOCR2:公式识别、表格识别、阅读顺序和手写文本。

先来看公式识别。我上传了一张包含复杂数学公式的图片。可以看到,模型在提取复杂数学公式方面表现出色。

它能准确捕捉希腊符号、平方根和嵌套分数,同时保持每个公式的原始结构。

即使是跨越多行的长公式,也能干净地生成,括号、运算符和数学关系都得到完整保留。

接下来是表格识别,这是文档 OCR 中最具挑战性的任务之一。表格的格式五花八门——有的有清晰边框,有的完全没有——而且往往包含密集的数字数据,模型很容易读错。

我在多个表格示例上测试了 OvisOCR2,它的准确度确实令人印象深刻,始终能同时保留表格的结构和内容。

另一个重大挑战是理解文档结构和阅读顺序。现代文档往往采用复杂多变的版式,处理起来难度大得多。

你可能会遇到多栏设计、图文混排、折页、彩色印刷、倾斜扫描件或手写批注——这些都会让 OCR 更难。而且正确的阅读顺序并不总是简单的从上到下、从左到右。

OvisOCR2 的突出之处在于,它能分析整个页面,判断哪些内容属于同一部分、哪部分应该先读、信息应该如何被阅读。结果是复杂文档的重建更加自然。

最后,OvisOCR2 在把潦草的连笔手写转换成干净、可读的文本方面也表现出色。它能正确识别难词,并准确捕捉大多数标点,包括逗号和撇号。只是会漏掉句末的少数句号。

速度同样出色,大约 8 秒就能处理超过 1000 个字符。凭借 0.88 的准确率得分,OvisOCR2 处理困难的手写笔记时错误相对较少,是快速把手写内容转成数字文本的可靠工具。

1、OvisOCR2 有什么独特之处?

这个模型最引人注目的特点是它的规模:只有 8 亿参数。在如今参数动辄数百亿甚至上千亿的时代,这个规模相当小巧。

该模型基于 Qwen3.5–0.8B。它的训练数据结合了真实文档与合成数据,训练过程整合了 SFT、RL 和 OPD。

可以这样理解:SFT 教会模型如何解析文档并格式化输出。随后 RL 提升这些输出的整体质量,而 OPD 则基于模型的实际生成过程提供更强的教师监督。这有助于缩小训练与推理之间的差距。

None

除了 OmniDocBench,官方报告还显示,OvisOCR2 在 PureDocBench 上取得了最高的 Avg3 得分 75.06。PureDocBench 包含三条赛道:干净、数字降质和真实降质。

这些赛道覆盖文本、公式、表格和阅读顺序,使该基准更能代表扫描、压缩和拍摄等真实部署场景。

关于它的局限性,官方报告承认,复杂而真实的文档仍可能出现内容缺失、输出重复,或表格与公式结构错误。对于关键业务应用,仍需人工复核。

2、OvisOCR2 vs PaddleOCR vs GOT-OCR2.0

None

选择哪个 OCR 模型,最终取决于你需要处理的文档类型。

对于包含多栏版式和密集表格的复杂英文文档,OvisOCR2 以其准确性与效率的平衡而脱颖而出。

它的端到端结构化输出和轻量级 0.8B 参数量,使其在这些场景下特别实用。

对于更通用的 OCR 任务,GOT-OCR2.0 提供了更灵活的方案。它更小的参数量和更快的推理速度,非常适合直接了当的文本提取。

对于中文文档,PaddleOCR 是个很强的选择。它针对中文文本识别做了专门优化,并通过多阶段流水线提供细粒度的控制。

归根结底,没有一个模型对每类文档都是最优的。正确的选择取决于文档类型、语言和所需的准确度。

这些模型也可以组合到同一条流水线中,让每个模型处理它最擅长的环节。

3、开始写代码

让我们一步步探索,揭开构建一个强大的推理应用背后的答案。我们要先安装支持该模型所需的库。为此,我们执行一次 pip install:

pip install torch torchvision transformers pillow einops addict easydict pymupdf psutil langchain langchain-community langchain-openai faiss-gpu sentence-transformers openai

下一步是常规操作:导入相关库(其作用会在后面逐步显现),并做一些基本配置。

OvisOCR2:把文档和图片转换成结构化、AI 友好的数据(如 JSON 和 Markdown),准确度行业领先——为 AI 应用提供动力。

import os
import torch
import tempfile
import fitz  # PyMuPDF
from transformers import AutoModel, AutoTokenizer
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain.docstore.document import Document

于是,我构建了这个 SimpleRAG 系统,用 OvisOCR2 做文本提取,用 OpenAI 生成查询。下面我带你过一遍我写的这套东西。

在初始化中,我设置了核心组件——使用 HuggingFace 的 BGE 嵌入做向量表示,用 GPT-4o 作为聊天模型,temperature 设为 0 以保证回答一致。我初始化了向量库和问答链的占位符,稍后会构建它们。

然后我开始把 OvisOCR2 模型和分词器从 HuggingFace 加载到 GPU 内存。

当我调用 extract_text_from_images() 时,我传入一个图片或 PDF 文件列表。系统会逐个处理每个文件。

如果文件是 PDF,我先把每一页转换成图片(PNG),保存到一个临时文件夹。然后把所有页发给 OvisOCR2,让它读取整份文档的文本。

如果文件本身是图片,我直接把它发给 OvisOCR2。这种模式对版式复杂、包含表格和表单的文档效果很好。

OCR 完成后,模型把提取出的文本保存到临时文本或 Markdown 文件中。然后我读取这些文件,把所有文本合并成一个大字符串。

最后,我把文本存成 LangChain Document,并把原始文件路径保存为元数据。这样以后搜索或提问时,我就知道文本来自哪份文档。

class SimpleRAG:
    def __init__(self):
        self.embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-en-v1.5")
        self.llm = ChatOpenAI(model="gpt-4o", temperature=0)
        self.vectorstore = None
        self.qa_chain = None
        model_name = "ATH-MaaS/OvisOCR2"
        self.tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
        self.model = AutoModel.from_pretrained(
            model_name,
            trust_remote_code=True,
            use_safetensors=True,
            torch_dtype=torch.bfloat16,
        ).eval().cuda()

    def _pdf_to_images(self, pdf_path, dpi=300):
        doc = fitz.open(pdf_path)
        tmp_dir = tempfile.mkdtemp(prefix="pdf_ocr_")
        mat = fitz.Matrix(dpi / 72, dpi / 72)
        paths = []
        for i, page in enumerate(doc):
            out = os.path.join(tmp_dir, f"page_{i+1:04d}.png")
            page.get_pixmap(matrix=mat).save(out)
            paths.append(out)
        doc.close()
        return paths

    def _read_output_text(self, output_path):
        """Read all .md or .txt files written by save_results=True."""
        texts = []
        for fname in sorted(os.listdir(output_path)):
            if fname.endswith((".md", ".txt")):
                with open(os.path.join(output_path, fname), "r", encoding="utf-8") as f:
                    texts.append(f.read())
        return "\n\n".join(texts)

    def extract_text_from_images(self, image_paths: list):
        docs = []
        for path in image_paths:
            is_pdf = path.lower().endswith(".pdf")
            tmp_out = tempfile.mkdtemp(prefix="ocr_out_")
            if is_pdf:
                pages = self._pdf_to_images(path)
                self.model.infer_multi(
                    self.tokenizer,
                    prompt="<image>Multi page parsing.",
                    image_files=pages,
                    output_path=tmp_out,
                    image_size=1024,
                    max_length=32768,
                    no_repeat_ngram_size=35,
                    ngram_window=1024,
                    save_results=True,
                )
            else:
                self.model.infer(
                    self.tokenizer,
                    prompt="<image>document parsing.",
                    image_file=path,
                    output_path=tmp_out,
                    base_size=1024,
                    image_size=640,
                    crop_mode=True,
                    max_length=32768,
                    no_repeat_ngram_size=35,
                    ngram_window=128,
                    save_results=True,
                )
            text = self._read_output_text(tmp_out)
            if not text.strip():
                text = "No text found"
            docs.append(Document(page_content=text, metadata={"source": path}))
        return docs

build_index 中,我从所有图片中提取文本,用 Recursive Character TextSplitter 把文档切成 1000 字符的块、重叠 200 字符,用 BGE 嵌入创建 FAISS 向量库,并设置一个 RetrievalQA 链,使用 GPT-4o 且每次查询检索最相关的 3 个块。

对于 query,我只需把问题传给问答链,由它处理检索和生成,返回答案。

def build_index(self, image_paths: list):
      docs = self.extract_text_from_images(image_paths)
      text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
      splits = text_splitter.split_documents(docs)
      self.vectorstore = FAISS.from_documents(splits, self.embeddings)
      self.qa_chain = RetrievalQA.from_chain_type(
          llm=self.llm,
          retriever=self.vectorstore.as_retriever(search_kwargs={"k": 3}),
      )
def query(self, question: str):
      return self.qa_chain.invoke(question)

# Usage
rag = SimpleRAG()
rag.build_index(["your_pic.jpg"])  # also accepts .pdf
answer = rag.query("extract all the table?")
print(answer)

4、结束语

这项工作有力地展示了专用 OCR、通用多模态模型和元素级奖励是如何协同工作的。

它为论文处理、数据清洗、代码与公式解析,提供了一个更精简、更一致的开源方案。0.8B 的参数量也让它非常适合本地部署,并且它已经适配了 vLLM 推理。


原文链接: OvisOCR2 + RAG: Revolutionize Complex Data Extraction (Open-Source)

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