本地部署的 OCR + RAG 流水线为何检索质量差、延迟高?
结论
OCR + Embeddings + RAG 是可行的处理流程。检索质量取决于识别文本、文档切分、向量生成和索引查询是否相互匹配,不能仅凭本地部署判断效果差或延迟高的原因。
现有信息不足以确定单一原因。建议分别排查两个问题:相关内容为什么没有被检索到,请求时间又消耗在哪些阶段。直接更换 Embedding 模型或增加硬件,未必能解决问题。
1. 先检查 OCR 输出是否保留了原文含义
OCR 的问题不只有错别字。扫描版 PDF 还可能出现多栏文字交错、表格列错位、标题与正文混排,以及页眉页脚反复插入。这些内容即使能被编码成向量,也可能已经失去原有语义。
从失败查询对应的页面入手,对照原始 PDF、OCR 输出和最终进入索引的文本,检查:
- 原文中的答案是否被完整识别?
- 段落顺序、表格行列和单位是否正确?
- 是否混入大量重复页眉、页脚或乱码?
- 文档本身已有可提取文本时,是否仍进行了不必要的 OCR?
清洗时可以处理多余空白和已确认的重复页眉页脚,但不要直接删除全部标点、数字或特殊符号。这些字符可能承载金额、日期、型号或公式含义。
如果 OCR 提供置信度,可以用它筛选需要复核的页面。不同 OCR 工具的置信度含义和阈值需要分别验证。
2. 检查切分是否破坏了上下文
固定长度切分可能把查询所需的条件和答案分到两个片段中。片段太短会缺少语义;太长则可能混入多个主题,甚至超过 Embedding 模型的输入限制而被截断。
可以按以下顺序调整:
- 优先沿标题、段落、列表和表格边界切分。
- 为片段保留必要的章节标题和文档名称。
- 对跨段落信息设置适量重叠,并检查是否产生大量重复候选。
- 保存文档 ID、页码、章节和片段 ID,方便回到原文核对。
- 检查实际编码后的 token 数,以及超长输入的处理方式。
没有适用于所有文档的固定片段长度。应使用同一组查询比较不同的切分方案,不要只凭片段看起来是否整齐来选择。
3. 核对 Embeddings 和检索配置
相似查询得到不同结果,可能与表达差异、OCR 错字、模型对领域术语的理解不足或索引配置有关。
重点检查:
- 文档与查询是否使用配套的 Embedding 模型和处理方式。
- 模型要求的 query/document 前缀是否正确使用。
- 更换模型后,是否重新生成了文档向量。
- 索引的距离度量、向量归一化方式是否符合模型的使用要求。
- 查询过滤条件是否错误排除了相关文档。
维度相同,并不代表不同模型产生的向量可以混用。
对于编号、缩写、专有名词和精确短语,可以评估将关键词检索与向量检索结合。两类检索分数通常不能直接相加,需要采用经过验证的归一化或排名融合方式。也可以评估用 reranker 对候选结果重排,但它会增加计算开销,而且无法补回候选集中完全缺失的答案。
如果数据固定时,完全相同的查询结果仍频繁变化,还应检查并列分数排序、索引更新状态和近似检索实现,不要只调整提示词。
4. 用小规模对照实验定位问题
建立一组有明确相关文档或片段标注的查询,再做三组对照:
| 对照方式 | 主要定位的问题 |
|---|---|
| 原始 OCR 文本与人工校正文本 | OCR 噪声对检索的影响 |
| 当前切分与按文档结构切分 | 上下文是否被切断或稀释 |
| 精确向量搜索与近似向量搜索 | 近似索引是否损失了相关候选 |
可以在可承受的小数据集上用精确搜索作为参照。如果精确搜索也找不到相关片段,应先检查文本、切分和 Embeddings;如果精确搜索能找到,而近似搜索找不到,再检查索引参数和搜索范围。
可以使用 Recall@k 衡量前 k 个结果覆盖了多少已标注的相关内容。下面的 Python 示例只演示指标计算,不代表实际系统效果:
def recall_at_k(retrieved_ids, relevant_ids, k):
if k <= 0:
raise ValueError("k must be positive")
relevant = set(relevant_ids)
if not relevant:
raise ValueError("relevant_ids must not be empty")
retrieved = set(retrieved_ids[:k])
return len(retrieved & relevant) / len(relevant)
# 示例 ID;实际使用时替换为检索结果和人工标注。
retrieved_ids = ["chunk_12", "chunk_8", "chunk_31"]
relevant_ids = {"chunk_8", "chunk_20"}
score = recall_at_k(retrieved_ids, relevant_ids, k=3)
print(score)
标注和评估必须使用同一粒度:文档级标注不能直接与片段 ID 比较。切分方案变化后,也需要重新确认相关片段的对应关系。
5. 把延迟拆开测量
文档数量增加后系统变慢,先确认增长的是索引构建时间、在线检索时间,还是整个问答时间。
分别记录这些阶段的耗时:
- 离线处理:PDF 解析、OCR、切分、Embedding 生成、索引写入。
- 在线请求:查询 Embedding、索引搜索、元数据读取、reranker、LLM 生成。
如果文档可以预先处理,应尽量将 OCR 和文档 Embedding 放到离线流程,文档更新时再处理对应内容。还要检查离线任务与在线查询是否争抢 CPU、GPU、内存或磁盘资源。
如果瓶颈在向量搜索,检查是否仍在全量扫描、索引是否超出内存,以及过滤条件如何影响搜索。如果瓶颈在 reranker 或 LLM,则应评估候选数量和上下文长度。每次调整都应同时记录检索质量与延迟,避免缩短时间的同时漏掉相关内容。
6. 建议的排查顺序
先选取一组失败查询,沿着“原始页面 → OCR 文本 → 索引片段 → 检索候选 → 最终答案”逐层核对。确认相关内容进入索引后,再检查 Embeddings 和近似索引;确认候选内容正确后,再处理上下文组装和答案生成。
这样可以区分三类问题:信息在入库前已经丢失,检索没有找到相关信息,或找到后没有被正确使用。每次只改变一项配置,用固定查询集比较结果,比较容易判断调整是否有效。
备注:内容仅供参考。
