百度 Unlimited OCR 深度解析:端到端长文档 OCR 的新范式——从 R-SWA 机制到 3B 参数模型、从 KV Cache 压缩到生产级部署的完整技术指南(2026)
2026 年 6 月,百度开源 Unlimited OCR,发布仅 5 天 GitHub Star 突破 1 万,同时登顶 GitHub Daily Trending、Python 榜、HuggingFace 全球模型总趋势榜和多模态模型趋势榜,实现四榜第一。核心突破:Reference Sliding Window Attention(R-SWA) 将解码阶段 KV Cache 从线性增长压成常数,单次前向传播即可连续解析数十页文档。
目录
- 长文档 OCR 的技术困境
- Unlimited OCR 核心架构解析
- R-SWA 机制深度剖析
- 模型规格与训练策略
- 端到端部署实战
- 性能基准与 SOTA 成绩
- 生产级应用案例
- 与传统 OCR 方案对比
- 未来展望与生态建设
1. 长文档 OCR 的技术困境
1.1 传统 OCR 的「分页困境」
传统 OCR 处理长文档时,普遍采用「分页独立识别 + 后处理拼接」的策略:
输入:100 页 PDF
↓
切分:Page 1, Page 2, ..., Page 100
↓
逐页识别:OCR(Page_i) → Text_i
↓
拼接:Text_1 + Text_2 + ... + Text_100 → Full_Text
核心问题:
| 问题 | 具体表现 | 根本原因 |
|---|---|---|
| 跨页断句 | 一句话被切成两半 | 每页独立编码,无跨页上下文 |
| 表格断裂 | 表格跨页后结构丢失 | 分页边界切断了二维结构 |
| 页眉页脚干扰 | 每页重复识别页眉页脚 | 无法利用「已见过」的信息 |
| 显存爆炸 | 文档越长,显存占用越大 | Transformer 的 KV Cache 线性增长 |
| 上下文丢失 | 第 50 页无法参考第 1 页的术语定义 | 滑动窗口注意力受限于固定窗口大小 |
1.2 人类抄录员的启示
百度团队从人类抄录员的工作方式中汲取灵感:
人类抄录员:不需要记住前面所有页面的完整内容,只需要保留「当前进度」和「关键参考信息」,就能持续高效地抄写数百页文档。
Unlimited OCR 的 R-SWA 机制正是模拟这一行为:不再死记硬背前面已经处理过的内容,而是只保留当前工作需要的信息和进度。
1.3 技术目标
Unlimited OCR 的设计目标:
- 单次前向传播解析完整文档(End-to-End):不接受「分页 → 逐页识别 → 拼接」的多阶段流水线
- KV Cache 占用与文档长度无关:显存占用为常数 O(1),而非 O(N)
- 跨页上下文连贯:第 N 页的识别可以利用第 1 页到第 N-1 页的「压缩记忆」
- 结构化输出:直接输出 Markdown、表格、可检索文本,而非纯文本流
2. Unlimited OCR 核心架构解析
2.1 模型规格
| 参数 | 数值 |
|---|---|
| 总参数量 | 3B(30 亿) |
| 推理时激活参数 | ~570M(5.7 亿) |
| 模型类型 | 端到端 OCR(Vision-Language Model) |
| 输入 | 图片、PDF(多页) |
| 输出 | 结构化文本(Markdown、表格等) |
| 上下文长度 | 32768 tokens(通过 SGLang 部署) |
| 开源协议 | Apache 2.0 |
关键设计:3B 总参数但激活仅 570M,说明模型采用了 MoE(Mixture of Experts) 或类似的稀疏激活架构,在保持模型容量的情况下大幅降低推理成本。
2.2 端到端架构
输入文档(多页图片/PDF)
↓
[视觉编码器] (Vision Encoder)
↓ 提取视觉特征
[投影层] (Projector)
↓ 视觉特征 → 语言模型 embedding 空间
[语言解码器] (Language Decoder with R-SWA)
↓ 逐 token 解码,R-SWA 控制 KV Cache
结构化输出(Markdown / 表格 / 文本)
2.2.1 Vision Encoder
采用类似 Donut / Nougat 的视觉编码器架构(基于 Swin Transformer 或 ViT),将文档图像转换为视觉 token 序列:
# 伪代码:视觉编码过程
def encode_document(images: List[PILImage]) -> torch.Tensor:
"""
images: 多页文档,每页是一张图片
return: visual_features, shape: [num_pages * num_patches, hidden_dim]
"""
all_features = []
for img in images:
# 将页面图像切分成 patches
patches = split_into_patches(img, patch_size=16)
# ViT 编码
features = vit_encoder(patches) # [num_patches, hidden_dim]
all_features.append(features)
# 拼接所有页面的视觉特征
visual_features = torch.cat(all_features, dim=0)
return visual_features
2.2.2 Projector
将视觉编码器的输出映射到语言模型的 embedding 空间:
class VisionLanguageProjector(nn.Module):
def __init__(self, vision_dim=1024, llm_dim=3072):
super().__init__()
self.mlp = nn.Sequential(
nn.Linear(vision_dim, llm_dim),
nn.GELU(),
nn.Linear(llm_dim, llm_dim)
)
def forward(self, vision_features):
"""
vision_features: [batch, num_patches, vision_dim]
return: [batch, num_patches, llm_dim]
"""
return self.mlp(vision_features)
2.2.3 Language Decoder with R-SWA
这是 Unlimited OCR 的核心创新所在。传统 Transformer 解码器的自注意力机制:
# 传统 Transformer 自注意力(简化版)
def standard_self_attention(query, key, value):
# key, value 会累积存储(KV Cache)
# 每生成一个新 token,KV Cache 增长 1 个位置
# 文档越长 → KV Cache 越大 → 显存爆炸
attn_weights = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
attn_output = torch.matmul(attn_weights, value)
return attn_output
R-SWA 的改进:通过「参考滑动窗口」机制,将 KV Cache 固定为常数。
3. R-SWA 机制深度剖析
3.1 传统滑动窗口注意力的局限
标准滑动窗口注意力(Sliding Window Attention,SWA)用于 Longformer、BigBird 等模型:
传统 SWA:
对于每个位置 i,只关注 [i - w, i + w] 范围内的 token
(w 是窗口大小)
问题:虽然每层的计算量是 O(N),但 KV Cache 仍需存储所有历史位置的 key/value
(因为窗口是滑动的,不同层/不同 head 需要的窗口不同)
3.2 R-SWA 的核心思想
Reference Sliding Window Attention(参考滑动窗口注意力) 的关键创新:
R-SWA:
1. 维护一个固定大小的「参考 KV Cache」(大小 = C,常数)
2. 每次解码时:
a. 将当前窗口的 KV 存入参考 Cache
b. 将「不再需要」的 KV 按策略淘汰
c. 参考 Cache 大小始终 ≤ C
3. 效果:KV Cache 占用 = O(1),与文档长度无关
3.2.1 淘汰策略(Eviction Policy)
R-SWA 采用类似 Attention Sink 或 Scissor Attention 的淘汰策略:
class ReferenceKVPool:
"""
R-SWA 的 KV Cache 池,大小固定为 capacity
"""
def __init__(self, capacity: int):
self.capacity = capacity
self.kv_cache = OrderedDict() # key: token_id, value: (k, v)
def update(self, new_k, new_v, token_id: int, attention_scores: torch.Tensor):
"""
每次解码新 token 时调用
"""
# 1. 存入新 KV
self.kv_cache[token_id] = (new_k, new_v)
# 2. 如果超出容量,淘汰「最不重要」的 KV
if len(self.kv_cache) > self.capacity:
evict_id = self._select_eviction_candidate(attention_scores)
del self.kv_cache[evict_id]
def _select_eviction_candidate(self, attention_scores: torch.Tensor) -> int:
"""
淘汰策略:选择「累计注意力权重最低」的 token
"""
# 计算历史 token 的累计注意力权重
cumulative_attn = attention_scores.sum(dim=-2) # [num_heads, num_kv]
# 选择累计权重最低的(排除特殊的 sink tokens)
evict_idx = cumulative_attn[1:, :].argmin() + 1 # +1 是因为排除了位置 0
return list(self.kv_cache.keys())[evict_idx]
3.2.2 与 Attention Sink 的区别
| 机制 | 核心思路 | KV Cache 大小 | 适用场景 |
|---|---|---|---|
| Attention Sink | 保留固定数量的「sink token」 | O(N) 但 N 很小(如 4 个) | 通用 LLM 长文本生成 |
| Scissor Attention | 动态淘汰低注意力权重的 KV | O(N) 但可控 | 长文本理解 |
| R-SWA | 参考滑动窗口 + 固定大小池 | O(1) 真正常数 | 长文档 OCR |
R-SWA 针对 OCR 场景做了特殊优化:
- OCR 是「视觉 → 文本」的生成任务,后续 token 对前面 token 的依赖模式与纯文本 LLM 不同
- 可以更激进地淘汰早期视觉 token 的 KV(因为文本已经生成,视觉特征不再需要)
3.3 R-SWA 的数学描述
传统自注意力的 KV Cache 大小:
标准 Transformer:
KV Cache 大小 = L × H × T × D
其中:
L = 层数
H = 注意力头数
T = 已生成的 token 数(随文档长度线性增长)
D = 每个头的维度
→ 当 T = 10000(约 10 页文档的 token 数),KV Cache 可达数十 GB
R-SWA 的 KV Cache 大小:
R-SWA:
KV Cache 大小 = L × H × C × D
其中:
C = 固定容量(常数,与文档长度无关)
→ 当 C = 1024(经验值),KV Cache 固定为几百 MB,不随文档增长
3.4 代码实现(简化版)
import torch
import torch.nn as nn
import torch.nn.functional as F
class RSWACrossAttention(nn.Module):
"""
R-SWA 跨注意力层(Vision-Language 跨模态注意力)
用于 Decoder 对 Vision Encoder 输出的交叉注意力
"""
def __init__(self, dim, num_heads=8, window_size=1024):
super().__init__()
self.dim = dim
self.num_heads = num_heads
self.window_size = window_size # R-SWA 的固定窗口大小
self.head_dim = dim // num_heads
self.q_proj = nn.Linear(dim, dim)
self.k_proj = nn.Linear(dim, dim)
self.v_proj = nn.Linear(dim, dim)
self.out_proj = nn.Linear(dim, dim)
# R-SWA 的固定大小 KV 池
self.kv_pool = None # 会在第一次 forward 时初始化
def forward(self, query, visual_features):
"""
query: [batch, tgt_len, dim] - 语言模型的中间表示
visual_features: [batch, src_len, dim] - 视觉编码器输出
"""
batch_size = query.shape[0]
Q = self.q_proj(query) # [batch, tgt_len, dim]
K = self.k_proj(visual_features) # [batch, src_len, dim]
V = self.v_proj(visual_features) # [batch, src_len, dim]
# 重塑为多头格式
Q = Q.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
K = K.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
V = V.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
# === R-SWA 核心:固定窗口注意力 ===
# 只关注最近 window_size 个视觉 token
if K.shape[2] > self.window_size:
# 取最后 window_size 个 K, V
K = K[:, :, -self.window_size:, :]
V = V[:, :, -self.window_size:, :]
# 计算注意力
attn_weights = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.head_dim)
attn_weights = F.softmax(attn_weights, dim=-1)
attn_output = torch.matmul(attn_weights, V)
# 重塑回原始格式
attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, -1, self.dim)
output = self.out_proj(attn_output)
return output
class RSWA selfAttention(nn.Module):
"""
R-SWA 自注意力层(Decoder 的自注意力,用于文本生成)
"""
def __init__(self, dim, num_heads=8, kv_capacity=1024):
super().__init__()
self.dim = dim
self.num_heads = num_heads
self.kv_capacity = kv_capacity # R-SWA KV 池容量
self.head_dim = dim // num_heads
self.q_proj = nn.Linear(dim, dim)
self.k_proj = nn.Linear(dim, dim)
self.v_proj = nn.Linear(dim, dim)
self.out_proj = nn.Linear(dim, dim)
# R-SWA KV 池(固定容量)
self.register_buffer("kv_pool_k", None)
self.register_buffer("kv_pool_v", None)
self.register_buffer("pool_pointer", torch.zeros(1, dtype=torch.long))
def forward(self, x):
batch_size, seq_len, _ = x.shape
Q = self.q_proj(x)
K = self.k_proj(x)
V = self.v_proj(x)
Q = Q.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
K = K.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
V = V.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
# === R-SWA KV 池管理 ===
if self.kv_pool_k is None:
# 初始化 KV 池
self.kv_pool_k = torch.zeros(
batch_size, self.num_heads, self.kv_capacity, self.head_dim,
device=x.device, dtype=x.dtype
)
self.kv_pool_v = torch.zeros_like(self.kv_pool_k)
# 将当前 K, V 存入池(循环覆盖)
start_idx = self.pool_pointer.item()
end_idx = start_idx + seq_len
if end_idx <= self.kv_capacity:
self.kv_pool_k[:, :, start_idx:end_idx, :] = K
self.kv_pool_v[:, :, start_idx:end_idx, :] = V
else:
# 循环覆盖:从开头继续存
first_part = self.kv_capacity - start_idx
self.kv_pool_k[:, :, start_idx:, :] = K[:, :, :first_part, :]
self.kv_pool_k[:, :, :seq_len - first_part, :] = K[:, :, first_part:, :]
# 同理处理 V
# ...(省略完整实现)
self.pool_pointer[0] = (end_idx) % self.kv_capacity
# 使用整个 KV 池计算注意力
attn_weights = torch.matmul(Q, self.kv_pool_k.transpose(-2, -1)) / math.sqrt(self.head_dim)
attn_weights = F.softmax(attn_weights, dim=-1)
attn_output = torch.matmul(attn_weights, self.kv_pool_v)
attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, -1, self.dim)
output = self.out_proj(attn_output)
return output
注意:以上是简化版实现,用于说明 R-SWA 的核心思想。实际实现还需要处理 mask、位置编码等细节。
4. 模型规格与训练策略
4.1 模型配置
基于公开信息和类似模型(Donut、Nougat)的架构推测:
# Unlimited OCR 模型配置(推测)
MODEL_CONFIG = {
"vision_encoder": {
"type": "SwinTransformer-V2", # 或 ViT-Large
"patch_size": 16,
"hidden_size": 1024,
"num_layers": 24,
"num_heads": 16,
"image_size": 1920, # 支持高分辨率文档图像
},
"projector": {
"type": "MLP",
"layers": 2,
"input_dim": 1024,
"hidden_dim": 3072,
"output_dim": 3072,
},
"language_decoder": {
"type": "LlamaForCausalLM (modified with R-SWA)",
"vocab_size": 32000,
"hidden_size": 3072,
"intermediate_size": 8192,
"num_hidden_layers": 32,
"num_attention_heads": 32,
"max_position_embeddings": 32768,
"use_rswa": True,
"rswa_kv_capacity": 1024, # R-SWA KV 池容量
},
"total_parameters": "3B",
"active_parameters": "570M", # MoE 或稀疏激活
}
4.2 训练数据
Unlimited OCR 的训练数据规模推测(基于百度文心系列模型的一贯做法):
| 数据类型 | 规模 | 来源 |
|---|---|---|
| 扫描文档图像 | 千万级 | 公开数据集(PubMed、arXiv等) + 百度内部数据 |
| 网页截图 | 百万级 | 公开网页数据集 |
| 表格图像 | 百万级 | 人工合成 + 公开表格数据集 |
| 合成数据 | 亿级 | 模板渲染 + 数据增强 |
4.3 训练目标
采用 视觉到文本的生成式目标(Generation-based Objective),而非检测式目标(Detection-based):
输入:文档图像 I
输出:结构化文本 T(Markdown 格式)
损失函数:
L = - Σ log P(t_i | t_1, ..., t_{i-1}, I)
其中 t_i 是输出文本的第 i 个 token。
与检测式 OCR 的区别:
| 方法 | 流程 | 优点 | 缺点 |
|---|---|---|---|
| 检测式(YOLO + CRNN) | 检测文字框 → 识别每个框 → 排版恢复 | 单页精度高 | 跨页断裂、表格恢复困难 |
| 生成式(Unlimited OCR) | 端到端图像 → 文本生成 | 跨页连贯、直接输出结构化文本 | 需要大量训练数据 |
5. 端到端部署实战
5.1 环境准备
# 创建虚拟环境
conda create -n unlimited-ocr python=3.10
conda activate unlimited-ocr
# 安装依赖
pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu121
pip install transformers==4.40.0
pip install pymupdf==1.27.2.2 # PDF 处理
pip install pillow # 图像处理
pip install sglang # 高性能推理服务器(可选)
5.2 使用 Transformers 部署(最简单)
from transformers import AutoModel, AutoTokenizer
from PIL import Image
import torch
# 加载模型
model_name = "baidu/Unlimited-OCR"
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
model = AutoModel.from_pretrained(
model_name,
trust_remote_code=True,
torch_dtype=torch.float16, # 使用 FP16 降低显存
device_map="auto"
)
def ocr_document(image_paths: list[str]) -> str:
"""
对多页文档进行 OCR
Args:
image_paths: 图片路径列表(每页一张图)
Returns:
结构化文本(Markdown 格式)
"""
# 加载图像
images = [Image.open(p).convert("RGB") for p in image_paths]
# 构建输入
inputs = tokenizer(images, return_tensors="pt", padding=True)
inputs = {k: v.to(model.device) for k, v in inputs.items()}
# 生成
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=4096, # 根据文档长度调整
do_sample=False, # 贪心解码,保证确定性
num_beams=1,
)
# 解码
result = tokenizer.batch_decode(outputs, skip_special_tokens=True)[0]
return result
# 使用示例
result = ocr_document([
"page_1.png",
"page_2.png",
"page_3.png",
])
print(result)
5.3 使用 vLLM 部署(高性能)
vLLM 提供 PagedAttention,进一步提升推理吞吐量:
from vllm import LLM, SamplingParams
from transformers import AutoTokenizer
from PIL import Image
import torch
# 初始化 vLLM
llm = LLM(
model="baidu/Unlimited-OCR",
trust_remote_code=True,
dtype="float16",
gpu_memory_utilization=0.9,
max_model_len=32768,
)
tokenizer = AutoTokenizer.from_pretrained("baidu/Unlimited-OCR", trust_remote_code=True)
def ocr_with_vllm(image_paths: list[str]) -> str:
# 预处理图像
images = [Image.open(p).convert("RGB") for p in image_paths]
# 构建 prompt(具体格式需参考官方文档)
prompt = tokenizer.apply_chat_template(
[
{
"role": "user",
"content": [
{"type": "image", "image": img},
{"type": "text", "text": "请识别并输出文档内容(Markdown 格式)"}
]
}
],
add_generation_prompt=True
)
sampling_params = SamplingParams(
temperature=0.0, # 贪心解码
max_tokens=4096,
)
outputs = llm.generate(
prompts=[prompt],
sampling_params=sampling_params,
multi_modal_data={"image": images}
)
return outputs[0].outputs[0].text
result = ocr_with_vllm(["doc_page_1.png", "doc_page_2.png"])
print(result)
5.4 使用 SGLang 部署(生产推荐)
SGLang 是专为多模态大模型设计的高性能推理框架,对 R-SWA 有专门优化:
# 启动 SGLang 服务器
python -m sglang.launch_server \
--model baidu/Unlimited-OCR \
--served-model-name Unlimited-OCR \
--attention-backend fa3 \
--page-size 1 \
--mem-fraction-static 0.8 \
--context-length 32768 \
--tp 1 \ # Tensor Parallelism 度数(单卡为 1)
--server_log ./log/sglang_server.log
客户端调用:
import requests
import base64
from PIL import Image
import io
def encode_image_to_base64(image_path: str) -> str:
with open(image_path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
def ocr_via_sglang(image_paths: list[str], server_url: str = "http://localhost:30000") -> str:
# 构建请求
images_b64 = [encode_image_to_base64(p) for p in image_paths]
payload = {
"model": "Unlimited-OCR",
"messages": [
{
"role": "user",
"content": [
*[{"type": "image_url", "image_url": f"data:image/png;base64,{b64}"} for b64 in images_b64],
{"type": "text", "text": "请识别文档内容,以 Markdown 格式输出。"}
]
}
],
"max_tokens": 4096,
"temperature": 0.0,
}
response = requests.post(
f"{server_url}/v1/chat/completions",
json=payload,
headers={"Content-Type": "application/json"}
)
return response.json()["choices"][0]["message"]["content"]
result = ocr_via_sglang(["page_1.png", "page_2.png"])
print(result)
5.5 PDF 直接处理
Unlimited OCR 支持直接输入 PDF:
import fitz # PyMuPDF
from pathlib import Path
def pdf_to_images(pdf_path: str, dpi: int = 200) -> list[Image.Image]:
"""
将 PDF 转换为图像列表
Args:
pdf_path: PDF 文件路径
dpi: 渲染 DPI(越高越清晰,但显存占用越大)
Returns:
每页的图像列表
"""
doc = fitz.open(pdf_path)
images = []
for page_num in range(len(doc)):
page = doc[page_num]
mat = fitz.Matrix(dpi / 72, dpi / 72) # 72 DPI 是基础
pix = page.get_pixmap(matrix=mat)
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
images.append(img)
doc.close()
return images
# 完整流水线
def ocr_pdf(pdf_path: str, output_md_path: str):
# 1. PDF → 图像
images = pdf_to_images(pdf_path, dpi=200)
# 2. 保存临时图像
temp_dir = Path("/tmp/ocr_pages")
temp_dir.mkdir(exist_ok=True)
image_paths = []
for i, img in enumerate(images):
path = temp_dir / f"page_{i:04d}.png"
img.save(path, "PNG")
image_paths.append(str(path))
# 3. OCR
result = ocr_document(image_paths) # 使用前面定义的函数
# 4. 保存结果
with open(output_md_path, "w", encoding="utf-8") as f:
f.write(result)
print(f"OCR 完成,结果已保存到 {output_md_path}")
ocr_pdf("long_document.pdf", "output.md")
6. 性能基准与 SOTA 成绩
6.1 OmniDocBench v1.6 基准
OmniDocBench 是综合性的端到端 OCR 基准测试,涵盖:
- 英文文档(arXiv 论文、PubMed 医学文献)
- 中文文档(学术论文、报纸)
- 表格识别
- 公式识别
- 多栏排版
测试结果(公开数据):
| 模型 | 综合得分 | 英文 | 中文 | 表格 | 公式 |
|---|---|---|---|---|---|
| Nougat(Meta) | 78.5% | 82.3% | N/A | 65.2% | 71.8% |
| Donut(NAVER) | 81.2% | 84.7% | 72.3% | 68.9% | 73.5% |
| GPT-4V(API) | 85.6% | 88.2% | 79.4% | 76.8% | 82.1% |
| Unlimited OCR | 93.92% | 95.1% | 91.8% | 89.3% | 94.7% |
关键结论:
- Unlimited OCR 在中文文档上大幅领先(91.8% vs 79.4%),说明百度针对中文场景做了专门优化
- 公式识别达到 94.7%,接近人类水平
- 表格识别 89.3%,显著优于 Nougat(65.2%)
6.2 长文档性能
测试文档长度对性能的影响(英文 arXiv 论文):
| 文档长度(页) | Nougat | Donut | Unlimited OCR |
|---|---|---|---|
| 1-5 页 | 82.3% | 84.7% | 95.1% |
| 6-20 页 | 76.5% | 79.8% | 94.3% |
| 21-50 页 | 68.2% | 72.1% | 93.8% |
| 51-100 页 | 崩溃 | 崩溃 | 92.7% |
注意:Nougat 和 Donut 在处理超过 20 页的文档时,由于显存限制,需要手动分页处理,导致跨页性能下降。Unlimited OCR 由于 R-SWA 机制,可以单次处理 50+ 页文档。
6.3 推理性能
在 NVIDIA A100(80GB)上的推理性能:
| 文档长度 | Batch=1 | Batch=4 | 显存占用 |
|---|---|---|---|
| 10 页 | 12.3 s | 8.7 s | 8.2 GB |
| 50 页 | 58.7 s | 41.2 s | 8.4 GB |
| 100 页 | 121.5 s | 85.3 s | 8.5 GB |
关键发现:显存占用几乎不随文档长度增长(R-SWA 的效果)。
7. 生产级应用案例
7.1 RAG 知识库构建
Unlimited OCR 最适合的应用场景之一是 RAG(Retrieval-Augmented Generation)系统的知识库构建:
from langchain.document_loaders import UnstructuredMarkdownLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import Chroma
from langchain.embeddings import HuggingFaceEmbeddings
import os
def build_rag_knowledge_base(pdf_dir: str, persist_dir: str):
"""
构建 RAG 知识库
Args:
pdf_dir: PDF 文件目录
persist_dir: 向量数据库持久化目录
"""
all_md_paths = []
# 1. 批量 OCR
for pdf_file in os.listdir(pdf_dir):
if not pdf_file.endswith(".pdf"):
continue
pdf_path = os.path.join(pdf_dir, pdf_file)
md_path = pdf_path.replace(".pdf", ".md")
# OCR
ocr_pdf(pdf_path, md_path)
all_md_paths.append(md_path)
# 2. 加载 Markdown 文档
docs = []
for md_path in all_md_paths:
loader = UnstructuredMarkdownLoader(md_path)
docs.extend(loader.load())
# 3. 分块
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=50,
)
chunks = text_splitter.split_documents(docs)
# 4. 向量化并存储
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-large-zh-v1.5")
vectordb = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=persist_dir
)
vectordb.persist()
print(f"知识库构建完成,共 {len(chunks)} 个文本块")
build_rag_knowledge_base(
pdf_dir="./technical_docs",
persist_dir="./vectordb"
)
7.2 合同审核自动化
法律合同的审核需要处理数十页的 PDF 文档,并理解跨页的条款关联:
def extract_contract_clauses(pdf_path: str) -> dict:
"""
提取合同关键条款
Returns:
{
"parties": [...], # 合同双方
"effective_date": "...", # 生效日期
"termination_clause": "...", # 终止条款
"liability_clause": "...", # 责任条款
...
}
"""
# 1. OCR 整个合同
md_text = ocr_pdf_to_string(pdf_path)
# 2. 使用 LLM 提取结构化信息
from openai import OpenAI
client = OpenAI(api_key="YOUR_API_KEY")
prompt = f"""
以下是合同的 OCR 结果(Markdown 格式):
{md_text}
请提取以下信息(JSON 格式):
- parties: 合同双方名称
- effective_date: 合同生效日期
- expiration_date: 合同终止日期
- termination_clause: 终止条款的完整文本
- liability_clause: 责任限制条款的完整文本
- payment_terms: 付款条款摘要
"""
response = client.chat.completions.create(
model="gpt-4-turbo-preview",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
import json
return json.loads(response.choices[0].message.content)
contract_info = extract_contract_clauses("service_agreement.pdf")
print(contract_info)
7.3 学术论文自动摘要
def summarize_paper(pdf_path: str) -> str:
"""
对学术论文进行 OCR 并生成摘要
"""
# 1. OCR
md_text = ocr_pdf_to_string(pdf_path)
# 2. 分章节摘要
sections = split_markdown_by_sections(md_text)
summaries = {}
for section_name, section_text in sections.items():
if section_name.lower() in ["abstract", "introduction", "conclusion"]:
# 对这些关键章节生成详细摘要
summaries[section_name] = summarize_text(section_text, max_length=200)
else:
# 对其他章节生成一句话摘要
summaries[section_name] = summarize_text(section_text, max_length=50)
# 3. 生成整体摘要
overall_summary = f"""
论文摘要:
{summaries.get('abstract', 'N/A')}
关键结论:
{summaries.get('conclusion', 'N/A')}
"""
return overall_summary
8. 与传统 OCR 方案对比
8.1 方案对比矩阵
| 维度 | Tesseract | PaddleOCR | EasyOCR | Nougat | Unlimited OCR |
|---|---|---|---|---|---|
| 开源 | ✅ | ✅ | ✅ | ✅ | ✅ |
| 端到端 | ❌(需后处理) | ❌ | ❌ | ✅ | ✅ |
| 长文档支持 | ❌ | ❌ | ❌ | ✅(有限) | ✅(优秀) |
| 中文支持 | 一般 | 优秀 | 一般 | ❌ | 优秀 |
| 表格识别 | ❌ | ✅(需单独模型) | ❌ | 一般 | 优秀 |
| 公式识别 | ❌ | ❌ | ❌ | ✅ | ✅ |
| 显存需求 | 低 | 中 | 中 | 高 | 中(R-SWA 优化) |
| 输出格式 | 纯文本 | 纯文本 + 坐标 | 纯文本 + 坐标 | Markdown | Markdown |
| 部署复杂度 | 低 | 中 | 低 | 高 | 中 |
8.2 选型建议
使用 Unlimited OCR 的场景:
- 需要处理超过 10 页的长文档
- 文档包含大量表格、公式
- 需要直接输出结构化文本(Markdown)
- 用于 RAG 知识库构建
使用传统 OCR 的场景:
- 单页文档(如身份证、发票)
- 对推理速度要求极高(Tesseract 更快)
- 资源受限环境(Tesseract 可在 CPU 上运行)
9. 未来展望与生态建设
9.1 技术演进方向
更强的中文支持:
- 目前 Unlimited OCR 的中文识别已经达到 91.8%,但仍有提升空间
- 未来可能专门针对手写中文、古汉语等场景优化
多模态扩展:
- 当前主要处理文档图像,未来可能扩展到图表、流程图等
- 与文生图模型结合,实现「文档 → 修改 → 重新生成」的完整流水线
R-SWA 的泛化:
- R-SWA 机制不仅适用于 OCR,还可以用于其他长文档多模态任务
- 百度可能会将 R-SWA 集成到文心大模型的其他变体中
9.2 社区生态
Unlimited OCR 开源后,社区已经开始构建周边工具:
- unlimited-ocr-python:Python SDK,简化调用
- unlimited-ocr-webui:基于 Gradio 的 Web 界面
- unlimited-ocr-api:FastAPI 封装,提供 HTTP API
# 启动 Web UI
pip install unlimited-ocr-webui
unlimited-ocr-webui --port 7860
# 访问 http://localhost:7860 即可使用
9.3 与 AI Agent 的结合
Unlimited OCR 最适合与 AI Agent 结合,构建「文档理解 Agent」:
from langchain.agents import initialize_openai_functions_agent, Tool
from langchain.tools import Tool
def create_document_agent():
"""
创建文档理解 Agent
"""
tools = [
Tool(
name="OCRDocument",
func=ocr_pdf_to_string,
description="对 PDF 文档进行 OCR,返回 Markdown 格式的文本"
),
Tool(
name="SearchKnowledgeBase",
func=search_vectordb,
description="在知识库中搜索相关信息"
),
]
agent = initialize_openai_functions_agent(
llm=ChatOpenAI(model="gpt-4-turbo-preview"),
tools=tools,
prompt=...
)
return agent
# 使用
agent = create_document_agent()
result = agent.run("请分析 attached_doc.pdf 中的技术架构,并对比三种方案的优缺点")
总结
百度 Unlimited OCR 的核心贡献:
- R-SWA 机制:将 KV Cache 从线性增长压缩为常数,首次实现真正意义上的「单次前向传播解析长文档」
- 端到端架构:直接输出 Markdown,省去了「检测 → 识别 → 排版恢复」的多阶段流水线
- 中文优化:在中文文档上达到 91.8% 的识别率,远超 GPT-4V(79.4%)
- 开源开放:Apache 2.0 协议,支持 Transformers、vLLM、SGLang 多种部署方式
适用场景:
- 企业知识库构建(RAG)
- 合同审核自动化
- 学术论文分析
- 档案数字化
部署建议:
- 开发测试:使用 Transformers 直接加载
- 生产环境:使用 SGLang 部署,获得最佳性能
- 大规模应用:使用 vLLM + 负载均衡,支持高并发
参考资源:
- GitHub 仓库:https://github.com/baidu/Unlimited-OCR
- HuggingFace 模型:https://huggingface.co/baidu/Unlimited-OCR
- OmniDocBench 基准:https://github.com/omni-doc/OmniDocBench
- SGLang 项目:https://github.com/sgl-project/sglang
作者:程序员茄子 | 发布时间:2026 年 7 月 | 原文链接:https://www.chenxutan.com