OCR实战:扫描文档如何高效接入LLM与RAG应用

📅 2026/8/27 7:22:06
OCR实战:扫描文档如何高效接入LLM与RAG应用
大家好欢迎来到我的技术专栏。这段时间在做一个企业内部知识库问答系统最头疼的不是大模型调优反而是“喂给大模型的数据”这一步。企业内部大量文档是 PDF 扫描件、高清图片、表单截图这些文件有个共同痛点文字看得见但复制不了。尤其是扫描版 PDF你在阅读器里选中文字却什么都复制不出来。这种文档直接喂给 LLM 是不可能被理解的模型只能看到一堆空白。解决办法就是 OCROptical Character Recognition光学字符识别。本文会完整拆解一套“OCR 提取文本 → 清洗整理 → 喂给 LLM”的实战方案包含环境配置、代码示例、开源工具选型和常见坑点。不管你是刚接触 OCR 的新手还是已经在做 RAGRetrieval-Augmented Generation检索增强生成项目的开发者这篇教程都能帮你少走很多弯路。1. 背景与核心概念1.1 为什么 LLM 需要 OCR很多刚接触大模型应用开发的朋友会问一个问题LLM 不是能处理文本吗为什么还要 OCR这里要澄清一个概念LLM 处理的是纯文本不是图片也不是 PDF。虽然多模态模型如 GPT-4V、Qwen-VL可以直接“看图说话”但在实际生产场景中成本高多模态接口按图片调用计费一个几十页的 PDF 下来成本不低。不稳定多模态模型在做版面分析时对复杂的表格、印章、手写混排内容准确率不够稳定。不灵活后续要做 embedding 向量化、全文检索仍然需要先把图片转换成文本。所以最实用的路线是先用 OCR 把不可复制的文档转成可复制的文本再进入 LLM 应用链路。1.2 OCR 解决什么问题OCR 的核心任务是从图片中检测并识别文字。根据文档复杂程度可以分为几个层级场景说明典型文档纯文字截图背景干净、文字清晰代码截图、聊天记录扫描文档纸张纹理、噪声明显扫描版合同、论文复杂版面表格、图片、多栏混排财报、报纸、简历手写文字字迹变形、笔画粘连手写笔记、表单不同场景对 OCR 工具的要求差别很大。本文主要聚焦前两类也是大多数知识库项目中遇到最多的场景。1.3 与 OCR 容易混淆的概念OCR 不等于 PDF 解析PDF 分为文本型 PDF可以直接提取文本和扫描型 PDF本质是图片必须 OCR。部分 PDF 解析库如 PyMuPDF 的get_text()只能处理文本型 PDF对扫描型 PDF 输出空白。OCR 不等于文档解析文档解析除了文字识别还涉及版面分析、表格还原、段落合并。OCR 是文档解析的基础环节但不是全部。明白这些边界之后我们进入技术选型环节。2. 技术选型主流 OCR 方案对比在做技术选型之前先看一下目前常用的 OCR 开源方案。2.1 PaddleOCRPaddleOCR 是百度飞桨开源的 OCR 工具集是目前中文场景下口碑最好的开源方案。它支持中英文识别、版面分析、表格识别、关键信息抽取并且模型体积可控支持 CPU 和 GPU 推理。优点中文识别准确率高对中英文混排支持很好。支持 80 种语言。文档方向分类、版面分析、表格识别一体化。模型可裁剪、可量化部署灵活。缺点依赖 PaddlePaddle 框架安装包体积稍大。部分高级功能如表格还原需要额外安装组件。2.2 Tesseract OCRTesseract 是经典的开源 OCR 引擎由惠普实验室开发后来由 Google 维护。它在英文场景表现不错但中文识别准确率与 PaddleOCR 相比有明显差距。优点历史悠久社区成熟适配多平台。轻量通过命令行即可使用。支持多种语言包。缺点中文识别效果一般。对复杂版面支持较弱。预训练模型的精度提升空间有限。2.3 商业 OCR 服务百度 OCR、阿里云 OCR、腾讯 OCR 等商业服务识别精度高对复杂版面支持更好但需要按量付费而且涉及将文档内容发送到云端的问题。对安全要求高的企业内部文档要慎重评估数据合规风险。2.4 本文选型结论我的建议是个人学习、原型验证优先选 PaddleOCR。中文场景为主PaddleOCR 更合适。英文场景为主Tesseract 也够用。生产环境并要求数据本地化PaddleOCR 自建服务。对精度极其敏感、文档版面极其复杂可以考虑商业 OCR但要做好成本与合规评估。下面以 PaddleOCR 为主Tesseract 作为补充完整演示整个流程。3. 环境准备与安装3.1 环境说明本文演示环境如下版本需要根据你的项目实际情况调整操作系统Ubuntu 20.04 / Windows 10 / macOS均可Python3.9 或 3.10PaddlePaddleCPU 版PaddleOCR最新稳定版其他 Python 库fastapi、uvicorn、langchain、openai如果你的机器有 NVIDIA GPU可以安装 GPU 版 PaddlePaddle识别速度会明显提升。3.2 安装 PaddlePaddle创建一个虚拟环境然后安装 PaddlePaddle。CPU 版安装命令如下python -m venv ocr_llm_env source ocr_llm_env/bin/activate # Windows 下为 ocr_llm_env\Scripts\activate pip install paddlepaddle如果你用的是 NVIDIA GPU 机器可以访问 PaddlePaddle 官网选择对应的安装命令这里以 CPU 版为例。3.3 安装 PaddleOCR安装 PaddleOCR 本体pip install paddleocr此外还需要安装 shapely、PyMuPDF 等依赖库pip install shapely PyMuPDF3.4 安装 Tesseract备用方案Tesseract 在 Windows 下需要单独下载安装包安装时勾选你要用的语言包尤其是中文语言包。macOS 下可以直接用 Homebrew 安装brew install tesseract brew install tesseract-langUbuntu 下使用 apt 安装sudo apt update sudo apt install tesseract-ocr sudo apt install tesseract-ocr-chi-sim3.5 验证安装python -c from paddleocr import PaddleOCR; print(PaddleOCR OK)如果没有任何报错说明 PaddleOCR 安装成功。首次运行时会自动下载模型文件需要保持网络畅通。4. 基础实战用 PaddleOCR 识别图片中的文字4.1 最小示例识别一张图片我们先从一个最简单的例子开始识别一张包含中文文字的图片。# 文件路径ocr_basic.py from paddleocr import PaddleOCR # 初始化 OCR 引擎 # use_angle_cls 开启方向分类可以自动纠正旋转 180/90 度的图片 ocr PaddleOCR(use_angle_clsTrue, langch) # 识别图片 img_path ./test.png result ocr.ocr(img_path, clsTrue) # 打印识别结果 for idx, line in enumerate(result[0]): text line[1][0] confidence line[1][1] print(f第 {idx 1} 行: {text}, 置信度: {confidence:.2f})运行python ocr_basic.py预期输出效果如下具体内容随图片文字变化第 1 行: 这是一段测试文字, 置信度: 0.98 第 2 行: 用于验证 OCR 识别效果, 置信度: 0.97这里说一下参数含义langch指定中文识别模型。use_angle_clsTrue开启方向分类器对扫描时放歪的图片效果明显。clsTrue在ocr()方法中启用方向分类。4.2 识别 PDF 扫描件PDF 扫描件本质上是图片的集合。先用 PyMuPDF 把 PDF 的每一页转成图片再逐页 OCR。# 文件路径ocr_pdf.py import fitz # PyMuPDF from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch) def pdf_to_images(pdf_path, zoom2.0): 将 PDF 每页转换为高分辨率图片 images [] doc fitz.open(pdf_path) for page_num in range(len(doc)): page doc.load_page(page_num) # zoom 参数控制渲染分辨率2.0 表示放大两倍 mat fitz.Matrix(zoom, zoom) pix page.get_pixmap(matrixmat) image_path fpage_{page_num 1}.png pix.save(image_path) images.append(image_path) doc.close() return images def ocr_pdf(pdf_path): images pdf_to_images(pdf_path) all_text [] for idx, img_path in enumerate(images): result ocr.ocr(img_path, clsTrue) page_text [] if result[0]: for line in result[0]: page_text.append(line[1][0]) all_text.append(f 第 {idx 1} 页 \n \n.join(page_text)) return \n\n.join(all_text) if __name__ __main__: text ocr_pdf(scanned_document.pdf) with open(output.txt, w, encodingutf-8) as f: f.write(text) print(OCR 完成结果已保存到 output.txt)几个关键点zoom 参数很重要PDF 渲染分辨率过低会严重影响识别效果。一般建议 2.0 到 3.0对应约 144 到 216 DPI。不要直接对 PDF 调 OCRPaddleOCR 输入是图片必须先转图片。分页处理PDF 是流式结构内存不足时逐页处理更稳定。4.3 识别图片中的表格PaddleOCR 提供了表格识别能力可以把表格结构还原为 HTML 或 Excel。这里用PPStructure完成版面分析与表格识别。# 文件路径ocr_table.py from paddleocr import PPStructure import json # 初始化版面分析引擎支持中文 engine PPStructure(langch, show_logTrue) # 识别表格图片 img_path ./table.png result engine(img_path) # 遍历识别结果 for item in result: res item.get(res, {}) # 如果是表格类型输出还原后的 HTML if item.get(type) table: table_html res.get(html, ) print(检测到表格HTML 内容) print(table_html)这是我在实际项目中很常用的一段代码。企业里的报表、财务表、产品参数表PPStructure 都能较好地还原成结构化数据后面可以直接转成 DataFrame 或入库。5. 进阶实战把 OCR 接入 LLM 应用链路OCR 识别出来文本只是第一步真正要把文本“喂给 LLM”还需要处理清洗、分块、向量化、检索等环节。下面完整演示一个基于 FastAPI LangChain 的 OCR → LLM 问答服务。5.1 项目结构ocr_llm_demo/ ├── app.py # FastAPI 服务入口 ├── ocr_service.py # OCR 服务封装 ├── llm_service.py # LLM 服务封装 ├── requirements.txt # 依赖清单 ├── uploads/ # 上传文件目录 └── data/ # OCR 结果存储目录5.2 OCR 服务封装我们先写一个独立的 OCR 服务模块方便在 Web 服务里调用。# 文件路径ocr_service.py import os import uuid import fitz from paddleocr import PaddleOCR from typing import List class OCRService: def __init__(self, lang: str ch): self.ocr PaddleOCR(use_angle_clsTrue, langlang) def extract_text_from_image(self, image_path: str) - str: 从单张图片提取文本 result self.ocr.ocr(image_path, clsTrue) lines [] if result and result[0]: for line in result[0]: lines.append(line[1][0]) return \n.join(lines) def extract_text_from_pdf(self, pdf_path: str, zoom: float 2.0) - str: 从扫描版 PDF 提取文本 images self._pdf_to_images(pdf_path, zoom) all_text [] for idx, img_path in enumerate(images): page_text self.extract_text_from_image(img_path) all_text.append(f 第 {idx 1} 页 \n{page_text}) return \n\n.join(all_text) def _pdf_to_images(self, pdf_path: str, zoom: float) - List[str]: PDF 转图片 doc fitz.open(pdf_path) image_paths [] for page_num in range(len(doc)): page doc.load_page(page_num) mat fitz.Matrix(zoom, zoom) pix page.get_pixmap(matrixmat) # 使用 UUID 避免并发写文件冲突 img_name fpage_{uuid.uuid4().hex}.png img_path os.path.join(uploads, img_name) pix.save(img_path) image_paths.append(img_path) doc.close() return image_paths # 全局单例避免每次请求都重新加载模型 ocr_service OCRService()这里有一个优化点模型对象要全局复用。PaddleOCR 初始化时要加载模型文件耗时较长如果每次请求都重新实例化性能会很差。5.3 LLM 服务封装LLM 服务封装的核心作用是把 OCR 结果组织成 LLM 友好的输入。# 文件路径llm_service.py import os from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.vectorstores import FAISS from langchain_openai import OpenAIEmbeddings class LLMService: def __init__(self): # 文本分块器按长度分割并保留少量重叠 self.splitter RecursiveCharacterTextSplitter( chunk_size800, chunk_overlap100, separators[\n\n, \n, 。, , , , ] ) # Embedding 模型配置 self.embeddings OpenAIEmbeddings( modeltext-embedding-ada-002, api_keyos.getenv(OPENAI_API_KEY) ) def text_to_chunks(self, text: str): 把 OCR 文本切分为合理的 chunk return self.splitter.split_text(text) def build_vector_store(self, text: str, save_path: str ./data): 构建向量存储用于后续检索 chunks self.text_to_chunks(text) if not chunks: return None vector_store FAISS.from_texts(chunks, self.embeddings) vector_store.save_local(save_path) return vector_store这个封装做了三件事文本分块LLM 有上下文长度限制OCR 出来的长文档必须切块。向量化将文本块转为向量供相似度检索使用。持久化向量库保存到本地避免重复计算。5.4 FastAPI 服务入口把 OCR 和 LLM 串起来提供一个可上传文档并返回问答结果的 API。# 文件路径app.py import os import uuid from fastapi import FastAPI, UploadFile, File from ocr_service import ocr_service from llm_service import LLMService from pydantic import BaseModel app FastAPI(titleOCR It - OCR for LLM) llm_service LLMService() # 确保目录存在 os.makedirs(uploads, exist_okTrue) os.makedirs(data, exist_okTrue) class QueryRequest(BaseModel): question: str document_id: str app.post(/upload) async def upload_document(file: UploadFile File(...)): 上传文档并执行 OCR返回 document_id # 保存上传文件 file_ext os.path.splitext(file.filename)[-1].lower() doc_id uuid.uuid4().hex file_path os.path.join(uploads, f{doc_id}{file_ext}) with open(file_path, wb) as f: f.write(await file.read()) # 执行 OCR if file_ext .pdf: text ocr_service.extract_text_from_pdf(file_path) elif file_ext in [.png, .jpg, .jpeg, .bmp]: text ocr_service.extract_text_from_image(file_path) else: return {error: 不支持的文件类型} # 构建向量存储 vector_store llm_service.build_vector_store(text, save_pathf./data/{doc_id}) return { document_id: doc_id, text_length: len(text), preview: text[:500] } app.post(/query) async def query_document(req: QueryRequest): 根据 document_id 加载向量库检索相关内容并返回答案 vector_store_path f./data/{req.document_id} if not os.path.exists(vector_store_path): return {error: document_id 不存在} # 加载向量库并检索 from langchain_community.vectorstores import FAISS vector_store FAISS.load_local( vector_store_path, llm_service.embeddings, allow_dangerous_deserializationTrue ) docs vector_store.similarity_search(req.question, k3) # 组织上下文 context \n\n.join([doc.page_content for doc in docs]) # 调用 LLM 生成答案 # 这里以 OpenAI 为例实际项目中可以替换为本地模型或任意 LLM API from openai import OpenAI client OpenAI(api_keyos.getenv(OPENAI_API_KEY)) response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是一个严谨的文档问答助手只能根据提供的文档内容回答问题。}, {role: user, content: f文档内容\n{context}\n\n问题{req.question}} ] ) return { answer: response.choices[0].message.content, source_chunks: [doc.page_content for doc in docs] } if __name__ __main__: import uvicorn uvicorn.run(app, host0.0.0.0, port8000)5.5 运行与测试启动服务uvicorn app:app --host 0.0.0.0 --port 8000用 curl 测试上传文档curl -X POST http://localhost:8000/upload \ -F file./scanned_report.pdf预期返回{ document_id: 3f9a8c2b1d5e4f7a8b0c1d2e3f4a5b6c, text_length: 18345, preview: 2024年第一季度业务报告…… }然后测试问答curl -X POST http://localhost:8000/query \ -H Content-Type: application/json \ -d {document_id: 3f9a8c2b1d5e4f7a8b0c1d2e3f4a5b6c, question: 本季度营收是多少}5.6 结果说明整个链路是上传 PDF → OCR 提取文本。文本分块 → 向量化 → 存入 FAISS。用户提问 → 检索最相关的 top-k 文本块。将文本块作为上下文拼接给 LLM → 生成回答。这种方式解决了“LLM 无法直接读取扫描件”的问题同时通过检索机制让 LLM 只需要关注相关片段不超长、不跑题。6. 常见问题与排查思路6.1 PaddleOCR 安装失败问题现象常见原因解决思路pip 安装 paddleocr 报错Python 版本不兼容确认 Python 版本为 3.9/3.10安装时找不到 paddlepaddle未先安装 PaddlePaddle先执行pip install paddlepaddle导入时报 DLL 加载失败Windows缺少 VC 运行库安装 Visual C Redistributable模型下载超时网络问题手动下载模型文件放入~/.paddleocr/目录6.2 OCR 识别效果差问题现象常见原因解决思路中文识别乱码语言参数未设置确认langch扫描件文字模糊图片分辨率太低提高 PDF 渲染 zoom 到 3.0识别结果顺序错乱多栏排版文档误识别用 PPStructure 的版面分析功能文字带阴影/倾斜扫描质量差先做图像预处理灰度化、二值化、纠偏表格数据错位表格线检测失败使用PPStructure专用表格引擎6.3 LLM 回答质量差问题现象常见原因解决思路回答内容与文档无关向量检索不到相关内容增加 top-k 数量或减小 chunk_size回答中文字乱序OCR 顺序错乱导致语义断裂开启版面分析按阅读顺序重组文本上下文超长报错chunk 数量过多减小 chunk_size 或只拼接前 k 个文档文档太长构建向量慢PDF 页数多导致 OCR 慢分批处理按页建立索引6.4 性能问题排查思路如果你发现 OCR 服务速度很慢按以下顺序排查确认是否在用 CPU 推理PaddleOCR 在 CPU 上速度有限。检查是否每次请求都在重新加载模型正确做法是全局单例。检查 PDF 渲染 zoom 是否过高4.0 以上会显著增加计算量。尝试paddle.inference模式或 TensorRT 加速。7. 最佳实践与工程建议7.1 数据质量优先OCR 是数据血缘的最上游。如果 OCR 阶段识别错误后面所有环节都会被污染。建议保留原图OCR 输出的文本和原图页码建立映射关系方便追溯。置信度过滤PaddleOCR 返回的置信度低于 0.6 的行单独记录人工复核。定时抽样验证定期抽检识别结果防止新文档格式导致质量下降。7.2 文本清洗与规范化OCR 出来的文本与正常文本有明显差异常见的清洗策略包括去除 OCR 产生的多余空格和换行。统一全角半角字符。纠正常见 OCR 错别字如“O”识别为“0”。合并被错误断开的段落。移除页眉页脚等重复干扰信息。# 文件路径text_cleaner.py import re def clean_ocr_text(text: str) - str: OCR 文本基础清洗 # 统一换行 text text.replace(\r\n, \n).replace(\r, \n) # 去除多余空格 text re.sub(r[ ], , text) # 合并人工断行中文场景 text re.sub(r(\S)\n(\S), r\1\2, text) # 去除重复的页眉页脚 lines text.split(\n) seen set() cleaned [] for line in lines: if line.strip() and line.strip() in seen: continue seen.add(line.strip()) cleaned.append(line) return \n.join(cleaned)7.3 生产环境的成本与性能在真实业务中OCR 往往不是瓶颈但也不是免费的优先做缓存同样的文档不要重复 OCR用文档 MD5 做缓存键。异步处理长文档 OCR 耗时几秒到几十秒不要用同步接口使用任务队列。批量预取如果已知文档池提前 OCR 并建好索引比用户上传时再处理体验好得多。按需加载模型如果 GPU 资源紧张可以只对高分辨率图片启用方向分类器。7.4 安全与权限边界如果使用商业 OCR API确认文档是否含有敏感数据必要时脱敏后上传。OCR 服务不要直接暴露公网至少加 API Key 认证。上传的原始文档应定期清理或设置访问控制。向量库中包含大量原文信息同样要保护不要随意下载。7.5 与 LangChain / RAG 的集成注意点很多 RAG 项目默认用文本解析器读 PDF遇到扫描件就会失败。正确的集成方式是先用 OCR 把扫描 PDF 转成纯文本。对 OCR 文本做清洗和段落重排。再交给文本分块器切分。最后向量化入库。7.6 模型选择建议项目阶段推荐方案原型验证PaddleOCR FastAPI中文文档大量场景PaddleOCR PPStructure GPU英文为主Tesseract 或 PaddleOCR复杂版面、手写体商业 OCR 服务评估合规后手机端/边缘设备PaddleOCR 移动端模型或 ONNX 导出8. 最后想说的工程心得OCR 与 LLM 的组合解决的是“非结构化数据进入 AI 系统”的第一步。很多人在做 LLM 应用时把精力全放在 prompt 和模型选择上却忽略了数据入口的准确性。实际上一个扫描版的 PDF 如果 OCR 垃圾进后面 RAG 再花哨垃圾也只会被检索出来当“依据”输出。数据管道的地基永远比上层建筑重要。动手建议先拿 10 份不同类型的文档截图、扫描件、报表跑一遍 PaddleOCR看看识别效果和自己预期的差距。如果这部分搞定了再考虑接入 FastAPI 和向量库。如果你在实践过程中遇到其他坑欢迎在评论区留言我们一起讨论。