GPT-4V / Claude Vision 在文档理解中的实践——比 OCR 强在哪
GPT-4V / Claude Vision 在文档理解中的实践——比 OCR 强在哪
适读人群:做文档处理、合同分析、表格提取等场景的 AI 开发者 | 阅读时长:约 16 分钟 | 核心价值:搞清楚多模态模型处理文档的真实能力边界和工程实现
去年接了一个合同审查的项目,甲方是做供应链的公司,每天有几百份采购合同需要人工审查,想用 AI 替代一部分。
项目启动阶段,我做了一个技术选型对比测试,测了三个方案:
- 传统 OCR + 规则提取
- OCR + LLM 理解
- 直接用 GPT-4V / Claude Vision 处理文档图像
结果出来的时候,我改变了一些之前的判断。
这篇文章就是基于那次实测的总结,加上后来几个类似项目里积累的经验。
为什么要用 Vision 而不是直接解析文字
这个问题先讲清楚,因为很多人的第一反应是:文档不就是提取文字嘛,OCR 做不好吗?
OCR 处理的是"文字",而文档传递的是"信息"。这两者的差距就是 Vision 模型的价值所在。
举几个在合同审查里的真实例子:
例子一:带批注的扫描件
一份合同 PDF 是扫描件,某些条款旁边有手写的"√"或"×"标注,有的条款被划掉,有的被圈出来。
OCR:能提取文字,但不知道哪些被划掉了,哪些有标注。你拿到的是所有条款的混排,无法判断批注含义。
GPT-4V:能看到"这个条款被用红笔圈出来了,旁边写了'需重点确认'",理解批注的语义。
例子二:复杂表格里的合并单元格
一份货物清单,有多级表头,有合并单元格,有些单元格里有换行。
OCR:提取出一堆文字,表格结构完全丢失,你不知道哪个数字对应哪个货物哪个字段。
GPT-4V:能理解表格结构,知道"第三行的数量是 500 对应的货物是螺丝 M6x12"。
例子三:条款的排版层级
合同里有一级条款(1. 2. 3.)、二级条款(1.1 1.2)、三级条款(1.1.1)。不同层级用不同的缩进和字体表示。
OCR:文字都提取了,但层级关系在纯文本里很难保留,尤其是原始 PDF 排版不规范的时候。
GPT-4V:通过视觉理解排版,能还原层级结构。
实测数据
我在合同审查项目里做的对比测试,这里说一下关键指标:
测试集:200 份合同,包含:
- 电子 PDF(原生文字):60 份
- 扫描件(高质量):80 份
- 扫描件(低质量,有噪点):60 份
测试任务:提取合同甲乙双方名称、合同金额、履行期限、违约金条款
测试结果(正确率):
任务 | OCR+规则 | OCR+LLM | GPT-4V直接处理
--------------------|-----------|----------|---------------
提取甲乙方名称 | 91% | 96% | 97%
提取合同金额 | 85% | 94% | 95%
提取履行期限 | 78% | 91% | 93%
提取违约金条款 | 45% | 82% | 88%
处理低质量扫描件 | 52% | 71% | 84%
处理手写批注 | 8% | 12% | 76%结论很明显:
- 纯文字提取任务,OCR+LLM 和 GPT-4V 差距不大
- 涉及文档理解(违约金条款通常分散在多个段落)、低质量图像、手写内容,GPT-4V 有明显优势
处理速度和成本:
方案 | 每份合同耗时 | 每份成本(约)
------------------|--------------|---------------
OCR+规则 | 2-5秒 | 0.01元
OCR+LLM | 5-15秒 | 0.3-0.8元
GPT-4V (GPT-4o) | 8-20秒 | 0.5-1.5元
Claude Vision | 10-25秒 | 0.6-1.8元GPT-4V 比 OCR 贵 50-150 倍,但比专人审查便宜几个数量级。对这个客户来说,每份合同少于 2 元的成本完全可以接受。
工程实现
基础调用示例
import anthropic
import base64
from pathlib import Path
def analyze_contract_with_vision(pdf_path: str, questions: list[str]) -> dict:
"""
用 Claude Vision 分析合同文档
Args:
pdf_path: PDF 文件路径(转换成图片处理)
questions: 要提取的信息列表
Returns:
提取结果字典
"""
client = anthropic.Anthropic()
# PDF 转图片(需要 pdf2image 库)
images = convert_pdf_to_images(pdf_path)
# 构建包含图片的消息
content = []
# 添加所有页面的图片
for i, image in enumerate(images):
image_data = encode_image_to_base64(image)
content.append({
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": image_data,
}
})
content.append({
"type": "text",
"text": f"(以上是第 {i+1} 页)"
})
# 添加提取指令
questions_text = "\n".join([f"{i+1}. {q}" for i, q in enumerate(questions)])
content.append({
"type": "text",
"text": f"""请从上面的合同文档中提取以下信息:
{questions_text}
请用 JSON 格式返回,字段名使用英文,值使用原文。
如果某项信息在合同中不存在,值设为 null。
如果信息不确定或有歧义,在值后面加备注说明。"""
})
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=2048,
messages=[{"role": "user", "content": content}]
)
# 解析 JSON 响应
import json
response_text = response.content[0].text
# 提取 JSON 部分(模型可能在 JSON 前后有解释文字)
json_match = re.search(r'\{.*\}', response_text, re.DOTALL)
if json_match:
return json.loads(json_match.group())
return {"error": "解析失败", "raw": response_text}
def convert_pdf_to_images(pdf_path: str, dpi: int = 150) -> list:
"""将 PDF 转换为图片列表"""
from pdf2image import convert_from_path
images = convert_from_path(
pdf_path,
dpi=dpi, # 150 DPI 在质量和文件大小间取平衡
fmt='jpeg',
jpegopt={'quality': 85}
)
return images
def encode_image_to_base64(image) -> str:
"""将 PIL Image 编码为 base64"""
import io
buffer = io.BytesIO()
image.save(buffer, format='JPEG', quality=85)
return base64.b64encode(buffer.getvalue()).decode('utf-8')处理多页文档的策略
多页文档一次性全部送给模型会遇到两个问题:token 限制、成本过高。
实际工程里我用的是分层处理策略:
def process_long_document(pdf_path: str, task: str) -> str:
"""
长文档的分层处理策略
"""
images = convert_pdf_to_images(pdf_path)
total_pages = len(images)
if total_pages <= 10:
# 10 页以内:一次性处理
return process_pages_batch(images, task)
# 超过 10 页:先做目录索引,再按需读取
# 第一步:生成文档摘要和页面索引
doc_index = build_document_index(images)
# 第二步:根据任务确定需要重点读取的页面
relevant_pages = identify_relevant_pages(doc_index, task)
# 第三步:精读相关页面
selected_images = [images[i] for i in relevant_pages]
return process_pages_batch(selected_images, task)
def build_document_index(images: list) -> dict:
"""
快速扫描文档,建立页面索引
用低分辨率图片,节省成本
"""
# 降低分辨率做快速扫描
thumbnail_images = [img.resize((img.width // 2, img.height // 2)) for img in images]
# 批量处理,每批 5 页
index = {}
batch_size = 5
for i in range(0, len(thumbnail_images), batch_size):
batch = thumbnail_images[i:i+batch_size]
batch_index = quick_scan_pages(batch, start_page=i)
index.update(batch_index)
return index
def quick_scan_pages(images: list, start_page: int) -> dict:
"""快速扫描一批页面,返回每页的主要内容摘要"""
client = anthropic.Anthropic()
content = []
for j, image in enumerate(images):
image_data = encode_image_to_base64(image)
content.append({
"type": "image",
"source": {"type": "base64", "media_type": "image/jpeg", "data": image_data}
})
content.append({"type": "text", "text": f"(第 {start_page + j + 1} 页)"})
content.append({
"type": "text",
"text": "请为每一页生成一行简短摘要(20字以内),格式:页码: 内容摘要"
})
response = client.messages.create(
model="claude-haiku-4-5", # 用便宜的模型做索引
max_tokens=500,
messages=[{"role": "user", "content": content}]
)
# 解析返回的索引
index = {}
for line in response.content[0].text.split('\n'):
if ':' in line:
parts = line.split(':', 1)
try:
page_num = int(parts[0].strip()) - 1
index[page_num] = parts[1].strip()
except ValueError:
pass
return index表格提取的专项处理
表格提取是 Vision 最有价值的场景之一,专门做一下优化:
def extract_table_from_image(image, table_description: str = "") -> list[dict]:
"""
从图片中提取表格数据
返回结构化的列表,每个元素是一行数据
"""
client = anthropic.Anthropic()
image_data = encode_image_to_base64(image)
prompt = f"""请提取图片中的表格数据。
{"表格描述:" + table_description if table_description else ""}
要求:
1. 用 JSON 数组格式返回,每行数据是一个对象
2. 使用表头作为字段名
3. 如果有合并单元格,将合并的内容重复填入每个单元格
4. 数字保持原格式,不要转换
5. 如果表格有多级表头,用下划线连接各层级(如:一级_二级)
只返回 JSON,不要其他解释。"""
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=4096,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_data}},
{"type": "text", "text": prompt}
]
}]
)
import json
try:
return json.loads(response.content[0].text)
except json.JSONDecodeError:
# 容错处理
json_match = re.search(r'\[.*\]', response.content[0].text, re.DOTALL)
if json_match:
return json.loads(json_match.group())
return []Vision 的真实局限
说了优势,也得讲清楚局限,不然你会在不合适的场景上踩坑。
限制一:页数多、成本贵
一个 100 页的 PDF,用 GPT-4V 完整分析,成本可能达到几十元。如果你每天要处理几千份文档,成本模型要仔细算。
解法:分层处理,只把关键页面送给 Vision。
限制二:表格超大时会截断
如果一张图里有个几十列的宽表,模型有时候会数不清列的对应关系,或者在列很多的情况下漏掉某些列。
解法:对超宽表格做切割,分段处理后合并。
限制三:高度一致的格式用规则更稳定
如果你的文档格式非常固定(比如政府发的标准化表格),规则或 OCR 方案的准确率可能并不低于 Vision,但成本低得多。
Vision 最适合处理格式不统一、有复杂视觉元素的文档。
限制四:幻觉问题
模型有时候会"创造"它实际上没看到的内容。对于合规要求高的场景(比如合同金额提取),必须有置信度验证机制,或者对关键字段做二次确认。
def extract_with_confidence(image, field: str) -> dict:
"""带置信度验证的字段提取"""
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=500,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": encode_image_to_base64(image)}},
{"type": "text", "text": f"""请提取文档中的"{field}"。
用 JSON 格式返回:
{{
"value": "提取的值",
"confidence": "high/medium/low", // high=文档中明确写了,medium=需要推断,low=不确定
"source": "原文中的具体位置或原句"
}}"""}
]
}]
)
return json.loads(response.content[0].text)合同审查那个项目最终上线,用的是分层策略:
- 电子 PDF → 直接解析文字 + LLM 理解(便宜、快)
- 高质量扫描件 → Vision 处理
- 低质量扫描件或有手写批注 → Vision + 人工复核
系统把人工审查从每份 30 分钟降到了 5 分钟(人工只看 AI 标注的重点),对这个规模的公司来说,ROI 很清晰。
Vision 的价值不在于全面取代 OCR,在于处理 OCR 处理不好的场景。搞清楚边界,才能用对地方。
