本文摘要:扫描PDF无文本层,OCR乱码经embedding后合法入库,检索全通而生成质量静默下降。转换后接一道规则质检,检查字符异常与碎片词,在向量库前拦下明显错误。
一、问题与结论
一条已跑通的 RAG 管线:markitdown把公司内部手册的扫描版 PDF 转成 Markdown,切块后 embedding 进向量库,用户提问能命中、接口全绿,但大模型偶尔在答案里夹进 “司司同同意” 这类字串。链路上没有任何报错。
结论先行:这是静默失败。扫描件没有文本层,内容藏在图片里,转换结果取决于 OCR 对低质量图像的识别;识别出的错字格式完全合法,embedding 模型只编码语义、不判断内容对错,向量库照单全收,乱码一路穿到生成结果。修复点不在检索侧,而在转换与入库之间:加一道文本质量门禁。
扫描件转 Markdown 的典型输出缺陷分三类(前两类为条件推理,未实测):
- 表格被拆散:单元格逐行输出、行列关系丢失,检索能命中片段但答案结构错;
- 图片丢失:Markdown 仅保留引用或直接丢弃,图内文字不进文本,检索根本命中不了;
- 中文乱码:错字混入但排版正确,是唯一不显形、直接污染向量库的一类。
二、排查与选择依据
先画出失败链路,确认每个环节为何都"合法":
扫描PDF(无文本层) → markitdown 转换(依赖 OCR 环节,低质扫描产生错字) → Markdown 输出(乱码混入,格式合法) → embedding(只编码语义,不校验内容正确性) → 向量库(合法入库,检索命中) → LLM 生成(拼入乱码,答案质量静默下降)排查时要区分两种输入:文本型 PDF(含可复制文本层)转换路径短、结果稳定,是很多成功案例的来源;扫描型 PDF(纯图片)必须经过 OCR,失败模式完全不同,不能把前者的经验套用到后者。另需注意 markitdown 是"文件/办公文档 → Markdown"的通用转换工具,不是为 RAG 质量把关设计的组件,入库前的校验属于调用方自行组合的责任(支持格式与 OCR 相关安装变体,以仓库 README 所述为准)。
替代方案与取舍
| 方案 | 选择条件 | 代价与边界 |
|---|---|---|
| 转换后质检门禁(本文) | 管线已用markitdown,需最低成本先拦坏数据 | 规则粗糙,漏检细粒度错字;只拦不修,不提升 OCR 质量 |
| 预处理扫描件(去噪、二值化、提高分辨率) | 批量扫描件质量普遍偏差、DPI 偏低 | 多一条图像处理链路,耗时;不解决生僻字与手写 |
| 换带 OCR/版面分析的解析库 | 需要表格结构、版面块与置信度信息 | 依赖更重、接入成本更高(以各自文档为准) |
python-docx/mammoth读 docx 原件 | 输入其实有 docx 源文件 | 只覆盖 docx,不处理 PDF 扫描件 |
什么时候不该用本文方案:合同、法规这类高价值文档,或乱码频发到质检频繁告警时——质检只是入口闸门,不产生更准的文本,应换 OCR 后端重跑或转人工审校;输入若是文本型 PDF,本来就不需要 OCR,直接转换即可,不必引入这层规则。
三、关键原理
markitdown 的 README 将其定位为转换工具,"支持 PDF"只说明格式在清单内,不等于能正确还原扫描件——图像里的字必须先被识别成文本,识别质量受扫描分辨率、语言与所装依赖影响(具体依赖变体以仓库当前文档为准)。因此输出可能完整、结构正确,内容却已出错。
下游两环同样不设防:embedding 是语义编码器,乱码段落照样得到向量;向量库按条目存储,不校验文本是否可读。三类失败里,表格拆散和图片丢失会造成"查不到",容易被当成检索调优问题;中文乱码会造成"查到了但答错",最难定位。判断依据不是接口状态码,而是内容质量指标——这一环缺失,正是问题的全部来源。
四、可运行示例
环境:Python 3.11,pip install markitdown。输入:一页 150 DPI 的中文扫描 PDF,存为scanned_sample.pdf。操作步骤:先用 CLI 转换,再用 API 转换并接质检。
markitdown scanned_sample.pdf>out.md"""扫描 PDF → markitdown → Markdown → 质检 → 再决定是否入库 环境: Python 3.11 依赖: pip install markitdown """importrefrommarkitdownimportMarkItDown md=MarkItDown()result=md.convert("scanned_sample.pdf")markdown_text=result.text_content# 属性名以仓库 README 示例为准defdetect_quality_issues(text:str)->dict:"""规则粗检,价值在于引入质量门禁这一步,不是完美方案"""issues={}u_fffd=text.count("\ufffd")ctrl=len(re.findall(r"[\u0000-\u001F]",text))ifu_fffdorctrl:issues["replace_char"]=f"U+FFFD×{u_fffd},控制符×{ctrl}"fragments=re.findall(r"\b[a-zA-Z]\b",text)iflen(fragments)>10:issues["fragment_words"]=f"单字母独立词×{len(fragments)},疑似 OCR 碎片"iflen(text.strip())<50:issues["too_short"]=f"输出仅{len(text.strip())}字符,OCR 可能未生效"garbled=re.findall(r"[^\x00-\x7F\u4e00-\u9fff,。、;:!?\s]{6,}",text)ifgarbled:issues["garbled_runs"]=f"疑似乱码连续串×{len(garbled)}"returnissues quality=detect_quality_issues(markdown_text)ifquality:print("质检未通过,暂不入向量库:")fork,vinquality.items():print(f" [{k}]{v}")else:print("质检通过,可进入 embedding 环节")常见失败:转换后输出接近空字符串,too_short触发。原因是扫描件无文本层、且 OCR 相关可选依赖未安装,转换只走了文本提取路径。修复方法:按仓库 README 给出的安装变体补齐依赖(具体以 README 为准),或先对扫描件做图像预处理再转换。
五、验证结果与边界
预期输出:文本型 PDF 或 OCR 正常时打印质检通过,可进入 embedding 环节;扫描件含错字、控制符或乱码串时打印质检未通过,暂不入向量库及命中的规则项,并阻断 embedding 调用。
实际输出:本文示例的拦截行为基于规则推导,未在本机跑通真实扫描件,属未验证结果;上线前请用自己的一批扫描 PDF 跑一遍,观察误报(正常段落被拦)与漏检(错字段落放行)比例,再决定阈值。规则本身不覆盖图片丢失与表格拆散——那两类缺陷需要单独检查 Markdown 里的图片引用与表格结构是否完整。若乱码率高到质检频繁告警,说明闸门已在正确位置,但根因在 OCR 输入质量,质检替代不了换后端或人工审校。
参考资料
- microsoft/markitdown