一、背景
随着多模态大模型理解图片、视频的能力越来越强,如今的大模型可准确识别图片中的文字位置和内容,过去几年传统OCR的需求都可以使用多模态LLM完成。一些厂家还发布了针对OCR场景进行优化的OCR专用模型,例如DeepSeek-OCR。模型可在Huggingface上获取:
https://huggingface.co/deepseek-ai/DeepSeek-OCR
关于vLLM部署模型的更多信息,请参考本站之前博客:(过往这篇博客以Qwen3-VL为例)
https://blog.bitipcman.com/post/vllm-qwen3-vl-on-aws-ec2-gpu/
本文介绍如何在AWS EC2 G6e的GPU实例上,使用vLLM部署DeepSeek-OCR模型,并通过Prompt优化识别结果。
二、vLLM部署DeepSeek-OCR
本文以部署DeepSeek-OCR模型为例,给出基本流程,参数均已适配DeepSeek-OCR。
创建一台EC2实例,关键信息如下:
- 区域选择us-west-2
- 机型 G6e.2xlarge(8vCPU/32GB),1个L40s GPU,配备48GB显存
- 存储 gp3类型,容量100GB
- 选择具有外部网络环境的VPC和子网(用于安装依存性包)
- 镜像 Ubuntu Server 24.04,全名
Deep Learning OSS Nvidia Driver AMI GPU PyTorch 2.8 (Ubuntu 24.04),这个AMI已经包含了GPU驱动和CUDA。
创建完毕后通过SSH登录到EC2上,执行如下命令安装依存性包:
sudo apt update
sudo apt upgrade -y
sudo apt install pipx net-tools curl unzip -y
pipx install huggingface_hub
pipx ensurepath
sudo reboot
重启完成后,从Huggingface下载模型,并安装推理用的vLLM:
hf download deepseek-ai/DeepSeek-OCR --local-dir ./DeepSeek-OCR
curl -LsSf https://astral.sh/uv/install.sh | sh
uv venv --python 3.12 --clear --seed
source .venv/bin/activate
uv pip install vllm bitsandbytes qwen-vl-utils --torch-backend=auto
启动vLLM服务(并设置密钥):
uv run vllm serve /home/ubuntu/DeepSeek-OCR \
--served-model-name DeepSeek-OCR \
--api-key dp-by-vllm-api-key-2025-12 \
--gpu-memory-utilization 0.9 \
--async-scheduling \
--max-model-len 8192 \
--max-num-seqs 32 \
--host 0.0.0.0 \
--port 8080
这里需要注意的是--max-model-len参数设置为8192而不能无限设置的更大,是因为DeepSeek-OCR模型的最大输入长度为8192,这已经能满足图片上传的要求。
三、使用OpenAI规范发起请求
1、图片-to-文本识别
在客户端安装依赖包,并通过环境变量设置密钥。
pip install openai
将如下代码保存为01-ocr-to-text.py。在本地同目录下保存一张待处理的图片名为OCR.jpg。这里图片以一张出租车的机打发票为例。
import base64
from openai import OpenAI
# 读取图片并转换为base64
def encode_image(image_path):
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
# 编码图片
image_base64 = encode_image("OCR.jpg")
client = OpenAI(
# 替换为上一步部署好的EC2的公网IP地址(或者Elastic IP地址)
base_url="http://44.249.109.248:8080/v1",
# api_key="xxxxxxxxxxxxxx" # 上一步在vLLM服务启动时候指定的API KEY,这里不推荐hard-code方式,最好是设置OS环境变量
)
stream = client.chat.completions.create(
model="DeepSeek-OCR",
messages=[
{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}},
{"type": "text", "text": "<|grounding|>Convert the document to markdown."}
]
}
],
stream=True
)
with open("output.txt", "w", encoding="utf-8") as f:
for chunk in stream:
if chunk.choices[0].delta.content is not None:
content = chunk.choices[0].delta.content
print(content, end="", flush=True)
f.write(content)
f.write("\n")
print()
在以上这段代码中,有2个要求是DeepSeek-OCR模型的特点:
- 先输入image,后输入text(prompt提示词)
- 提示词内容以
<|grounding|>开头,表示生成坐标,如果不加这个开头,则直接生成文本,不带坐标。
以设置环境变量的方式设置API Key,然后运行代码。
export OPENAI_API_KEY=dp-by-vllm-api-key-2025-12
python 01-ocr-to-text.py
即可看到模型识别的返回结果,同时在当前目录下生成output.txt文件,里边是包含坐标的信息。
text[[301, 166, 334, 238]]
仟根
text[[336, 268, 615, 296]]
上海海博出租汽车有限公司
image[[338, 262, 597, 390]]
text[[440, 297, 506, 316]]
TAXI
text[[397, 321, 532, 340]]
国家税务总局
text[[411, 342, 529, 368]]
车费发票
text[[385, 366, 544, 383]]
FARE RECEIPT
text[[315, 386, 417, 406]]
发票代码:
text[[342, 405, 618, 432]]
1233777024888215
text[[313, 430, 616, 456]]
发票号码:97233233388
text[[311, 456, 452, 470]]
统一社会信用代码:
text[[310, 473, 465, 485]]
9237083284283916429N
text[[310, 487, 428, 500]]
叫车电话:91111
image[[485, 460, 637, 549]]
text[[309, 504, 453, 516]]
监督电话:612345678
text[[309, 519, 475, 533]]
公司地址:南京西路999号
text[[348, 539, 613, 564]]
车号 BB88888
text[[346, 570, 613, 594]]
证号 114514
text[[346, 598, 613, 622]]
日期 24-08-08
text[[297, 567, 330, 684]]
手写无效
text[[341, 624, 613, 649]]
上车 **1228** 22:**20**
text[[380, 647, 615, 678]]
**23:40**
text[[341, 662, 614, 695]]
下车 4.10元
text[[340, 692, 433, 715]]
单价(元)
text[[511, 709, 614, 733]]
**15.9km**
text[[338, 722, 438, 744]]
里程(km)
text[[486, 738, 614, 761]]
**00:03:26**
text[[337, 753, 384, 770]]
等候
text[[486, 767, 614, 789]]
**100.00元**
text[[337, 782, 409, 803]]
金额(元)
text[[535, 795, 611, 817]]
0元
text[[279, 804, 587, 832]]
含电调费 0元燃油附加费
text[[539, 820, 611, 841]]
0元
text[[273, 826, 405, 845]]
节假日附加费
text[[274, 848, 320, 863]]
卡号
text[[272, 865, 480, 882]]
原额 余额
这就是带有坐标的OCR信息,但人类阅读困难。因此可根据OCR识别的坐标重新将扫描结果拼接为HTML格式用于友好显示。
2、将模型识别的坐标信息排版输出为HTML(可选)
将如下代码保存为02-grounding-to-html.py。这个代码的输入是当前目录下上一步生成的包含坐标信息的output.txt,输出是当前目录下的output.html。
import re
def render_to_html(ocr_output, output_file="output.html"):
html = '''<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>OCR Result</title>
</head>
<body>
<div style="position:relative; width:650px; height:900px; border:1px solid #ccc; font-family: Arial;">
'''
# 先保存原始输出用于调试
with open("debug_output.txt", 'w', encoding='utf-8') as f:
f.write(ocr_output)
lines = ocr_output.strip().split('\n')
i = 0
while i < len(lines):
line = lines[i]
if 'text[[' in line:
# 提取坐标
match = re.search(r'text\[\[(\d+),\s*(\d+),\s*(\d+),\s*(\d+)\]\]', line)
if match:
x1, y1, x2, y2 = match.groups()
# 检查下一行是否是文本内容
if i + 1 < len(lines) and lines[i + 1].strip() and not lines[i + 1].startswith(('text[[', 'image[[')):
text = lines[i + 1].strip()
html += f'<span style="position:absolute; left:{x1}px; top:{y1}px;">{text}</span>\n'
i += 1 # 跳过下一行
i += 1
html += '''</div>
</body>
</html>'''
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html)
print(f"已保存到 {output_file}")
# 读取 output.txt
with open("output.txt", 'r', encoding='utf-8') as f:
ocr_output = f.read()
# 渲染为 HTML
render_to_html(ocr_output, "output.html")
现在运行这个代码02-grounding-to-html.py,即可在当前目录下获得按坐标重新渲染了坐标的HTML文件。我们用浏览器浏览一下这个HTML文件,即可看到渲染效果。如下截图。

3、修正识别结果为格式化的JSON文件
上一步将带有坐标的OCR识别结果转成可视化的HTML可发现,坐标还是有错位和行列偏离的,对于人工阅读可能不是明显问题,但是对于后续的计算机程序处理就会出现大问题。这里可再调用DeepSeek-OCR之外的任意一个模型,通过LLM大语言模型进行语义理解,然后获得修正的答案,并最终生成结构化可被后续代码处理的JSON文件。
这一步的调用,无须使用专门的OCR模型,大多数通用的大语言模型都可以满足。本文以AWS平台调用Amazon自己的Nova 2 Lite为例(也可以替换为Qwen等开源模型)。
将如下代码保存为03-revise-to-json.py。
import json
import boto3
import re
# 读取 output.txt 和 output.html
with open("output.txt", "r", encoding="utf-8") as f:
ocr_text = f.read()
with open("output.html", "r", encoding="utf-8") as f:
html_content = f.read()
# 初始化 Bedrock 客户端
bedrock = boto3.client(
service_name='bedrock-runtime',
region_name='us-west-2'
)
# 构建提示词
prompt = f"""请分析以下出租车发票的OCR文本和HTML渲染结果,提取结构化信息。
OCR文本内容:
{ocr_text}
HTML渲染内容:
{html_content}
请提取以下字段并以JSON格式返回(只返回JSON,不要其他说明):
{{
"company": "公司名称",
"invoice_code": "发票代码",
"invoice_number": "发票号码",
"unified_social_credit_code": "统一社会信用代码",
"car_number": "车号",
"certificate_number": "证号",
"date": "日期",
"boarding_time": "上车时间",
"boarding_mileage": "上车里程",
"alighting_time": "下车时间",
"unit_price": "单价(元)",
"distance": "里程(km)",
"waiting_time": "等候时间",
"amount": "金额(元)",
"call_fee": "电调费",
"fuel_surcharge": "燃油附加费",
"holiday_surcharge": "节假日附加费"
}}
注意:请确保金额字段匹配正确,从文本中提取准确的数值。其中上车时间里边的第一个数字是里程,后边带冒号的时间格式的才是上车时间。
"""
# 调用 Nova 2 Lite 模型
response = bedrock.invoke_model(
modelId='us.amazon.nova-2-lite-v1:0',
body=json.dumps({
"messages": [
{
"role": "user",
"content": [
{"text": prompt}
]
}
],
"inferenceConfig": {
"temperature": 0.1,
"max_new_tokens": 2000
}
})
)
# 解析响应
response_body = json.loads(response['body'].read())
result_text = response_body['output']['message']['content'][0]['text']
# 提取JSON内容(处理可能的markdown代码块)
json_match = re.search(r'```json\s*(.*?)\s*```', result_text, re.DOTALL)
if json_match:
result_json = json.loads(json_match.group(1))
else:
# 尝试直接解析
result_json = json.loads(result_text)
# 保存到 output.json
with open("output.json", "w", encoding="utf-8") as f:
json.dump(result_json, f, ensure_ascii=False, indent=2)
print("结构化数据已保存到 output.json")
print(json.dumps(result_json, ensure_ascii=False, indent=2))
返回结果如下:
结构化数据已保存到 output.json
{
"company": "上海海博出租汽车有限公司",
"invoice_code": "1233777024888215",
"invoice_number": "97233233388",
"unified_social_credit_code": "9237083284283916429N",
"car_number": "BB88888",
"certificate_number": "114514",
"date": "24-08-08",
"boarding_time": "22:20",
"boarding_mileage": "1228",
"alighting_time": "23:40",
"unit_price": "4.10",
"distance": "15.9",
"waiting_time": "00:03:26",
"amount": "100.00",
"call_fee": "0",
"fuel_surcharge": "0",
"holiday_surcharge": "0"
}
在当前目录下,就获得了output.json文件。如果需要调整输出格式规范,直接修改Prompt提示词即可。
四、小结
以上过程可以看到,借助多模态大模型,可简单准确地识别扫描件、影印件中的文字,在完成传统OCR的同时还可进行语义理解和校验,以获得符合业务结果的要求。在此过程中,使用了EC2 G6e.2xlarge机型(配备单块48GB显存的L40s GPU)进行模型私有化部署,DeepSeek-OCR模型完成了OCR后,还通过Amazon Nova 2 Lite 模型进行校验,最终获得了准确结果。
如果将这个系统转为生产环境,架构上还需要考虑的问题:
- 业务实现:OCR的结果再好,也存在一定的偶然性,建议仅输入本业务流程需要的特定格式的扫描件,不要任意开放式输入,以免出现非预期的结果;同时,对OCR识别的中间过程的文档进行永久留存和备份,以免出现问题时候的可审计性,能判定是否是最初的模型OCR识别出错;
- 可靠性:如果是关键业务,要部署多台EC2 GPU实例,前边部署ELB负载均衡,以免单机故障;
- 安全性:在EC2或者ELB前方,增加WAF,配置防护规则,拦截可疑IP地址、恶意调用(Bad input等)等攻击,即便不配置WAF,也应该在EC2的安全规则组上配置IP地址白名单,仅允许许可的应用调用。
全文完。
最后修改于 2025-12-08