
凌晨两点,财务审核系统的告警响了。
QA 团队刚跑完一轮回归测试,RAG 问答模块在 200 份季报上的准确率从 92% 跌到了 67%。不是模型崩了,也不是检索挂了——系统给出的每个答案都"有模有样":有数字、有引用、格式规范。但核对原文后,开发团队发现了一堆让人后背发凉的错误:
用户问:“Q2 收入是多少?” 系统答:“3.2 亿”,并附上引用。 开发核对 PDF 原文——那个引用指向的是**“Q2 成本 3.2 亿”**。收入实际 4.8 亿。
两个数字都在文档里,OCR 没认错字,向量检索也精准命中了包含 “Q2” 的 Chunk。但答案错了。而且错得隐蔽、错得理直气壮——因为系统真的"读到了"那个数字,只是搞错了它属于谁。
这类错误的诡异之处在于:它不像幻觉那样答非所问,也不像检索失败那样直接丢出"我不知道"。它给出的是一个结构正确但语义错误的答案——有数字、有引用,只是数字和问题的对应关系错了。所以团队的第一反应很自然:调 Embedding、改 Chunk、压 Prompt。但这些动作优化的是"找不找得到",而这场事故的根因是"找到了但认错了"。
折腾两周后,准确率只回升了 3 个百分点。
问题根本不在下游。
回到那份季报的原始 PDF:它是一个标准的合并单元格表格——"收入"和"成本"作为父表头,各自横跨 Q1、Q2 两列。

但解析器输出时,两组 Q2 被拍平成了四个孤立的字符串:

从这一刻起,“3.2 亿"和"4.8 亿"都失去了它们的"姓氏”——系统知道有个 Q2 是 3.2 亿,但不知道它是"成本"的 Q2,还是"收入"的 Q2。

RAG 的答案质量上限,不是在生成阶段决定的,而是在入库的表格解析阶段就被焊死了。 后续检索再精准、生成再流畅,系统也只是在一个已经断裂的语义地基上盖房子。
本文将沿着这条链路,系统拆解表格解析质量如何影响 RAG 问答,并通过实测对比不同解析方案的效果。
要理解表格识别出错的原因,需要先做一个区分:字符级归属 vs. 字段级归属。OCR 解决的是前者——这个字符在图像的哪个位置;表格解析要解决的还有后者——这个数值属于哪个字段。
RAG 系统对字段级归属的依赖,远高于字符级归属。一个字识别错了,模型有时还能靠上下文纠正;但一个数值挂错了字段,模型只会自信地给出错误答案。
以下三个破坏路径,根因都在于:字段级归属在解析阶段断裂,导致 RAG 检索和生成出现系统性偏差。
xParse(TextIn · 文档解析)
地址:https://www.textin.com/register/code/RZEVT4
简介:国内商业化文档解析服务,面向 RAG 场景优化,对有线/无线复杂表格识别能力较强,支持 PDF、图片、Word 等多格式在线解析,可直接输出 Markdown,提供网页试用通道。
PaddleOCR(百度开源版面分析)
地址:https://aistudio.baidu.com/paddleocr
简介:百度飞桨开源 OCR + PP-Structure 版面分析工具,AI Studio 提供在线体验 Demo,侧重中文图文识别,能够检测表格、段落、标题等版面元素,适合作为开源方案基线对比。
MinerU(书生 · 浦语开源 PDF 提取)
地址:https://mineru.net/OpenSourceTools/Extractor
简介:上海人工智能实验室开源 PDF 解析工具,广泛应用于国内 RAG 开发社区,主打 PDF 转结构化 Markdown,重点优化表格、公式提取,内置免登录网页在线解析工具。
LlamaParse(LlamaIndex 海外 RAG 专用解析)
地址:https://cloud.llamaindex.ai/parse
简介:海外面向知识库检索场景设计的商用解析平台,主打布局感知解析,优化文档元素上下文关联,官方提供网页端上传测试,是海外 RAG 主流文档预处理方案。
很多 RAG 工具提取 PDF 表格时,会直接把二维结构抻成一段连续文本。表头、数据行、脚注全糊在一起,变成一大块扁平的 Markdown。塞进向量库之后,模型检索到的就是这段"去结构化"的文本——它能读到字,但看不出哪是表头、哪是数据、哪是注释。 于是经常出现这种幻觉:比如当用户问"这张表涉及哪些会计科目",模型把科目行下面的小字脚注也当成了科目名,一并列了出来。因为在拍平后的文本里,那条注释就紧挨着科目名称,模型根本无从判断它们其实不是同一个层级的东西。
我们使用以下表格进行解析。该表格格式较为复杂,包含不同主体的内容,我们来看下效果:
xParse 输出:解析内容完整,准确层级结构与原文顺序一致。
解析程度:🌟🌟🌟

PaddleOCR 解析结果: 整体表现是不错的,只有一处比较细微的问题,在解读的时候只有研发投入的单位是错误的。
解析程度:🌟🌟☆

MinerU: 整体的表现也是比较不错的,也是一处轻微的问题,在表格的最前面新增了一行。
解析程度:🌟🌟

LlamaParse解析结果: 相对 PaddleOCR、MinerU、xParse 来说,效果有点差表格错乱,而且有明显的表格错位。
解析程度:🌟

做 RAG 的都知道,表格里最值钱的信息往往不在格子里,而在表头上方那行"单位"。 但解析系统只管格子里的内容,不注意单位或者币种,要么直接丢了。 最后模型读到 1350,却不知道是百万元。用户问收入多少,模型说 1350,实际是 13.5 亿——差了三个数量级。
原图如下:

xParse: 单位、币种识别准确,与表格数据关联完整;输出层级清晰,结构保留完好。
解析程度:🌟🌟🌟

MinerU: 对单位和币种的解析同样较为优秀,并无问题。
解析程度:🌟🌟🌟

PaddleOCR: 金额和符号的识别质量一般,存在乱码、识别失败及空格等问题。
解析程度:🌟☆

LlamaParse: Markdown 模式下无法解析出数据。

在 Text 模式下虽解析出部分数据,但内容严重失真,错误较多。
解析程度:🌟

RAG 的 Chunk 切分策略通常基于段落边界或固定 Token 数,不会识别表格的连续性。一张跨页长表可能被切成多个 Chunk:表头在 Chunk 1,前半段数据在 Chunk 2,后半段数据在 Chunk 3。当检索器召回 Chunk 3 时,模型看到的是一堆没有表头的数据行。
面对用户提问,模型检索到了对应的数据行,但无法判断每个数值属于哪一列,只能猜测或拒答。
xParse: 在处理跨页长表时表现尤为出色。
解析程度:🌟🌟🌟

MinerU: 单表可用,续表结构丢失、内容顺序错乱。。
解析程度:🌟☆

PaddleOCR: : 表格识别完整,但内容组织欠佳,未实现类似 xParse 的合并与排序效果。
解析程度:🌟🌟☆

LlamaParse: : 表格识别完整,但格式还原不够忠实。存在过度合并现象,部分本不连贯的内容被强行拼接,反而破坏了原有结构。并且有部分无用的空格。 解析程度:🌟🌟

工具 | 破坏路径一(表格拍平/层级导航) | 破坏路径二(单位/币种元数据) | 破坏路径三(跨页长表/上下文断裂) | 综合表现 |
|---|---|---|---|---|
xParse | 🌟🌟🌟 | 🌟🌟🌟 | 🌟🌟🌟 | 🌟🌟🌟 优秀 |
MinerU | 🌟🌟 | 🌟🌟🌟 | 🌟☆ | 🌟🌟中等 |
PaddleOCR | 🌟🌟☆ | 🌟☆ | 🌟🌟☆ | 🌟🌟 中等 |
LlamaParse | 🌟 | 🌟 | 🌟🌟 | 🌟🌟☆ 良好 |
评分说明:
星级 | 含义 |
|---|---|
🌟🌟🌟 | 优秀 — 内容完整、结构正确、层级关系保留 |
🌟🌟 | 良好 — 内容基本识别,但存在顺序混乱或格式问题 |
🌟☆ | 及格边缘 — 能解析但格式处理画蛇添足,可能引入错误 |
🌟 | 较差 — 内容丢失严重或结构完全断裂 |
关键结论:
这个问题之所以普遍存在,不是某一个工具的缺陷,而是三个层面的系统性盲区:
技术栈断层。 LangChain、LlamaIndex 等 RAG 框架的文档加载器,底层通常调用的是基础 PDF 解析能力,表格结构信息在加载环节就丢失了——框架不知道这是一张表,更不知道它有几层表头、有没有合并单元格。后续的 Splitter 和 Retriever 在一个"无结构文本"的世界里工作,根本感知不到表格的存在。
评测盲区。 RAG 的常用评测指标(忠实度、相关性、上下文召回率等)对表格结构错误不敏感。一个数字被挂错表头,在评测中不会触发"忠实度"扣分,因为模型确实引用了原文中的某个数字。这套评测体系天然偏向"是否引用了原文内容",而非"引用的是否是正确的那一条"。
认知错位。 部分 RAG 开发者认为表格解析是 OCR 的问题:OCR 准确率够高,表格就能用。但 OCR 解决的是字符级归属,表格解析要解决的是字段级归属。一个字都没识别错,表格依然可能完全不可用。
通过上述实测,我们发现:**解析阶段的结构丢失,无法通过后期的 RAG 优化(调 Embedding、改 Prompt、换模型)来弥补。
入库前做好表格解析,RAG 能改变什么?
要系统性解决这三个破坏路径,需要在解析阶段就做三件事:
解析结果应该保留表头层级、数据行归属、合并单元格关系。这样 RAG 的 Splitter 可以基于表格结构做智能切分,而非暴力断句。模型在回答时能区分"表头"和"数据",不再把脚注和科目名称混淆。
单位、币种等元数据不应被切片策略从表格主体上剥离。RAG 检索到表格数据时,单位说明应随同返回。模型回答时能带上单位,输出"1350 百万元"而非"1350"。
跨页长表在解析阶段就应该完成拼接,作为一张完整表格输出。RAG 的 Splitter 不需要在一张表中间下刀,长表后半段的数据行不再丢失表头。
xParse 的核心价值不仅在于解析准确,更在于输出格式专为 RAG 设计。以下是 xParse 与主流 RAG 框架的集成方案。
from langchain_core.documents import Document
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
import json
# 1. xParse 输出结构化 JSON
with open("xparse_output.json") as f:
parsed_doc = json.load(f)
# 2. 将表格转换为结构化文档(每个单元格一个 Document,携带完整路径)
documents = []
for element in parsed_doc["elements"]:
if element["type"] == "table":
table_meta = element.get("metadata", {})
for row in element["data"]:
for col_path, value in flatten_headers(row, element["headers"]).items():
# col_path 示例: "收入 > Q2"
content = f"{col_path}: {value} (单位: {table_meta.get('unit', '未知')})"
documents.append(Document(
page_content=content,
metadata={
"type": "table_cell",
"table_path": col_path,
"unit": table_meta.get("unit"),
"page": element.get("page")
}
))
# 3. 智能切片:表格数据按单元格切分,文本按段落切分
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
# 表格数据已是结构化单元格,无需再切分;文本部分正常处理
# 4. 入库
vectorstore = Chroma.from_documents(documents, OpenAIEmbeddings())
retriever = vectorstore.as_retriever()
# 5. 构建 RAG 链
from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI
qa = RetrievalQA.from_chain_type(
llm=ChatOpenAI(model="gpt-4"),
retriever=retriever,
return_source_documents=True
)
# 查询:每个检索结果都携带完整表头路径
result = qa.invoke({"query": "收入 Q2 是多少?"})
# 检索结果示例: "收入 > Q2: 1350 (单位: 百万元)"
# 模型明确知道这是"收入"下的 Q2,不会与"成本 Q2"混淆from llama_index.core import Document, VectorStoreIndex, StorageContext
from llama_index.core.node_parser import SentenceSplitter
import json
# 1. 加载 xParse 输出
with open("xparse_output.json") as f:
xparse_data = json.load(f)
# 2. 构建 LlamaIndex 文档节点
nodes = []
for element in xparse_data["elements"]:
if element["type"] == "table":
# 表格转换为带元数据的节点
table_text = f"表格(第{element['page']}页):\n"
table_text += f"单位: {element['metadata'].get('unit', '未知')}\n"
# 保留层级结构作为文本
for row in element["data"]:
row_texts = []
for parent, children in row.items():
if isinstance(children, dict):
for child, val in children.items():
row_texts.append(f"{parent} {child}: {val}")
else:
row_texts.append(f"{parent}: {children}")
table_text += " | ".join(row_texts) + "\n"
nodes.append(Document(
text=table_text,
metadata={
"element_type": "table",
"page": element["page"],
"headers": json.dumps(element["headers"])
}
))
# 3. 构建索引(LlamaIndex 自动处理节点关系)
index = VectorStoreIndex(nodes)
# 4. 查询
query_engine = index.as_query_engine()
response = query_engine.query("主营业务成本 Q2 是多少?")
# 由于文本中明确保留了 "成本 Q2: 1100" 的层级关系
# 模型不会与 "收入 Q2: 1350" 混淆
print(response) # "主营业务成本 Q2 为 1,100 百万元。"如果你的 RAG 管道已经基于 Unstructured 构建,xParse 可以作为前置解析器替换默认的 partition_pdf,直接输出 Unstructured 兼容的文档元素列表:
from unstructured.partition.auto import partition_elements
# 替换为 xParse 输出 -> 转换为 Unstructured Element 格式
# 保留表格的 Table 类型和 Header 层级我们需要在本地先安装Codex并进行对应配置,在输入框
输入命令:
安装一下这个 skills:https://github.com/intsig-textin/xparse-skills


配置 API Key 和一些基础环境后,输入命令开始解析:

RAG 出错,根因往往不在模型,而在入在这里插入代码片库前的表格解析。通过 4 款工具的实测对比,我们验证了:
PyMuPDF 等基础工具在复杂表格场景下几乎必然导致结构拍平,不适合直接用于金融、法律等对表格精度要求高的 RAG 场景。如果表头层级无法还原,再精妙的检索策略也无济于事。LlamaParse 在 RAG 原生集成上体验最好,API 设计简洁,但在超复杂表头、跨页边界及元数据关联场景下仍有明显优化空间。适合对集成效率要求高、表格复杂度中等的场景。PaddleOCR 作为开源方案,在表格识别完整性上表现尚可,但在单位/币种等精细元数据提取上稳定性不足,且缺乏跨页合并能力。适合作为基础识别引擎配合自定义后处理流程。xParse 在解析精度(表头层级还原、跨页拼接、元数据关联)和 RAG 友好性(结构化 JSON 输出、直接单元格级检索、上下文连续性保持)之间取得了最佳平衡。对于金融财报、法律合同、科研文献等复杂表格密集的场景,能够显著降低 RAG 幻觉风险。RAG 的表格问答质量,入库前就决定了上限。
如果你的业务涉及财报、合同、研报等复杂文档,可以试试xParse,以更高质量的文档解析能力,为 RAG 提供更可靠的数据基础。