基于 AnythingLLM 搭建本地知识库时,扫描版 PDF 和图片型文档经常出现索引成功但检索不出内容的情况。这不是向量数据库的问题,而是文档在进入分块与嵌入之前缺少一道关键的 OCR 文本提取。即使能够提取出文字,如果分块策略过于粗暴,也会把表格、段落的语义单元切碎,最终导致大模型只能看到片段信息。

一、AnythingLLM 默认文档解析的短板
AnythingLLM 的优势在于把文档加载、切分、向量化和对话流程整合在一起,但它的默认解析器并不会自动识别扫描件。对于包含文本层的 PDF,解析器可以读取文字;对于扫描生成的图片型 PDF,提取结果常常是空字符串或者毫无意义的小片段。用户看到的现象是上传完成后文档状态正常,可提问时却显示没有匹配内容,或者引用的来源块与问题完全无关。
复杂排版也会放大解析问题。双栏论文、带页眉页脚的合同、跨页表格等文档在抽取文本后,阅读顺序可能被打乱。例如左侧栏的内容和右侧栏的内容被交错拼在一起,表格单元格被拆成零散行列,这种原始文本进入分块环节后,再好的嵌入模型也很难学到正确的上下文关系。
因此,优化思路应该前移到两个阶段:第一,识别页面是否缺少文本层,并用 OCR 生成高质量文本;第二,按照文档结构和语义边界重新分块,而不是依赖默认的固定长度切分。
二、OCR 预处理:为扫描文档重建文本层
处理扫描版 PDF 的常用流程是先判断每一页的文本提取结果。如果一页的文字长度少于某个阈值,就把该页渲染成图片,再调用 OCR 引擎识别。这样既能保留原本文本页的精确文字,又不会浪费计算资源去 OCR 所有页面。PyMuPDF 适合读取页面文本,pdf2image 负责把页面转成图片,Tesseract 或 PaddleOCR 完成识别。
下面是一段可以直接运行的 Python 脚本。它遍历 PDF 每一页,当文本量不足时自动执行 OCR,并把中文和英文合并识别。
import fitz
import pytesseract
from pdf2image import convert_from_path
pdf_path = "scan.pdf"
doc = fitz.open(pdf_path)
for page_index in range(len(doc)):
page = doc[page_index]
text = page.get_text().strip()
if len(text) < 20:
images = convert_from_path(
pdf_path,
first_page=page_index + 1,
last_page=page_index + 1,
dpi=300,
)
ocr_text = ""
for image in images:
ocr_text += pytesseract.image_to_string(image, lang="chi_sim+eng")
text = ocr_text
print(f"第 {page_index + 1} 页字符数:{len(text)}")
对于中文扫描件和包含复杂表格的资料,推荐使用 PaddleOCR 或 PP-Structure。Tesseract 的优势是部署简单,但在中文手写体、密集表格和低分辨率图片上识别率不稳定。OCR 完成后,建议把结果保存为 Markdown 或纯文本文件,并保留页码标记,例如在每页开头写入 <!-- 第 1 页 --> 这样的注释。这样后续在 AnythingLLM 中检索命中时,还能追溯到原始页,方便人工复核。
依赖安装命令如下:
pip install PyMuPDF pdf2image pytesseract paddleocr paddlepaddle
OCR 结果不要直接作为二进制 PDF 再次上传,除非已经生成带文本层的双层 PDF。直接导入文本文件可以避免 AnythingLLM 再次对图片进行无效解析,也能让你在导入前检查清洗质量。
三、分块策略:从固定窗口到结构感知
文档经过 OCR 后会产生大段纯文本,接下来要决定如何切分。默认的固定长度分块虽然效率高,但经常把一句话、一个表格或一个段落从中间截断。检索系统只能返回被切断的小块,大模型没有足够的上下文来推理答案。更合理的方式是使用递归分隔符,优先按段落、换行、句号、空格等自然边界切分,只有当前片段仍然超过阈值时才使用更小的分隔符。
下面这个示例使用 LangChain 的递归文本拆分器,它对中英文混排文档同样有效。chunk_size 控制每个块的最大字符数,chunk_overlap 保留相邻块之间的重叠内容,避免关键信息恰好落在边界上。
import os
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=80,
separators=[os.linesep * 2, os.linesep, "。", " ", ""],
)
full_text = "这里是 OCR 后的完整文档内容"
chunks = splitter.create_documents([full_text])
for idx, chunk in enumerate(chunks):
print(idx, len(chunk.page_content), chunk.page_content[:60])
如果你的文档以 Markdown 标题组织,最好使用结构感知分块。可以先解析标题层级,把同一标题下的段落尽量合并,只有超过 chunk_size 时才继续切分。这样每个块通常带有一个明确主题,检索时更容易命中用户意图。对于表格,可以把表头和每一行合并成一个独立块,不要只按行切分,否则某一列的数据会失去含义。
下表对比了三种常见分块方式的特点:
| 分块方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 固定长度分块 | 实现简单、速度较快 | 容易切断段落和表格 | 纯文本、无结构的普通资料 |
| 递归分块 | 优先保留自然边界 | 对复杂表格仍可能切碎 | 文章、报告、说明文档 |
| 结构感知分块 | 语义完整,检索质量高 | 需要额外解析文档结构 | 合同、论文、带标题的规范文档 |
四、导入 AnythingLLM 前的验证与调参
优化完 OCR 和分块后,不要直接替换生产环境,应该先用一组固定问题做小范围评测。准备十到二十个有明确答案的问题,分别对旧文档和新文档进行检索,记录是否命中正确块、引用内容是否完整。常见问题是 chunk_size 设置过大导致检索粒度粗糙,或者 chunk_overlap 设置过小让答案落在边界时无法拼回上下文。
对于合同、论文、说明书等文档,建议 chunk_size 控制在 400 到 700 个字符,overlap 设置为 chunk_size 的百分之十到百分之二十。扫描质量较差的资料可以先做图像增强,例如提高 DPI、去噪和纠偏,再进行 OCR。预处理后的文本文件可以直接拖入 AnythingLLM 的文档目录,让系统重新完成嵌入。最终目标是让每个检索块都包含一个完整、可独立理解的信息单元,这样 RAG 的回答质量才会稳定提升。
AnythingLLMOCR文档分块修改时间:2026-09-19 12:03:17