RAG 表格问答为什么总出错?


在这里插入图片描述

一、一个"正确"的错误答案:当财务审核遇上 RAG

凌晨两点,财务审核系统的告警响了。

QA 团队刚跑完一轮回归测试,RAG 问答模块在 200 份季报上的准确率从 92% 跌到了 67%。不是模型崩了,也不是检索挂了——系统给出的每个答案都"有模有样":有数字、有引用、格式规范。但核对原文后,开发团队发现了一堆让人后背发凉的错误:

用户问:“Q2 收入是多少?”
系统答:“3.2 亿”,并附上引用。
开发核对 PDF 原文——那个引用指向的是**“Q2 成本 3.2 亿”**。收入实际 4.8 亿。

两个数字都在文档里,OCR 没认错字,向量检索也精准命中了包含 “Q2” 的 Chunk。但答案错了。而且错得隐蔽、错得理直气壮——因为系统真的"读到了"那个数字,只是搞错了它属于谁。

这类错误的诡异之处在于:它不像幻觉那样答非所问,也不像检索失败那样直接丢出"我不知道"。它给出的是一个结构正确但语义错误的答案——有数字、有引用,只是数字和问题的对应关系错了。所以团队的第一反应很自然:调 Embedding、改 Chunk、压 Prompt。但这些动作优化的是"找不找得到",而这场事故的根因是"找到了但认错了"。

折腾两周后,准确率只回升了 3 个百分点。

问题根本不在下游。

回到那份季报的原始 PDF:它是一个标准的合并单元格表格——"收入"和"成本"作为父表头,各自横跨 Q1、Q2 两列。

原始 PDF 表格截图

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

OCR 识别结果

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

解析后的文本输出

RAG 的答案质量上限,不是在生成阶段决定的,而是在入库的表格解析阶段就被焊死了。 后续检索再精准、生成再流畅,系统也只是在一个已经断裂的语义地基上盖房子。

本文将沿着这条链路,系统拆解表格解析质量如何影响 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 🌟 🌟 🌟🌟 🌟🌟☆ 良好

评分说明:

星级 含义
🌟🌟🌟 优秀 — 内容完整、结构正确、层级关系保留
🌟🌟 良好 — 内容基本识别,但存在顺序混乱或格式问题
🌟☆ 及格边缘 — 能解析但格式处理画蛇添足,可能引入错误
🌟 较差 — 内容丢失严重或结构完全断裂

关键结论:

  • xParse 在三个测试维度均获得不错的表现,是唯一在复杂层级导航、元数据关联和跨页长表场景下均表现稳定的工具。
  • LlamaParse 在简单层级导航表现尚可,但在元数据脱钩和跨页表格场景下评分下滑,对中文复杂表格的适配仍有优化空间。
  • MinerU 在元数据识别上表现突出,但在跨页长表场景下出现严重的样式丢失和内容排序失真。
  • PaddleOCR 作为开源基线工具,在复杂表格结构保留上存在明显短板,更适合简单版面的 OCR 场景。

为什么当前 RAG 管道普遍忽略表格结构?

这个问题之所以普遍存在,不是某一个工具的缺陷,而是三个层面的系统性盲区:

技术栈断层。 LangChainLlamaIndex 等 RAG 框架的文档加载器,底层通常调用的是基础 PDF 解析能力,表格结构信息在加载环节就丢失了——框架不知道这是一张表,更不知道它有几层表头、有没有合并单元格。后续的 Splitter 和 Retriever 在一个"无结构文本"的世界里工作,根本感知不到表格的存在。

评测盲区。 RAG 的常用评测指标(忠实度、相关性、上下文召回率等)对表格结构错误不敏感。一个数字被挂错表头,在评测中不会触发"忠实度"扣分,因为模型确实引用了原文中的某个数字。这套评测体系天然偏向"是否引用了原文内容",而非"引用的是否是正确的那一条"。

认知错位。 部分 RAG 开发者认为表格解析是 OCR 的问题:OCR 准确率够高,表格就能用。但 OCR 解决的是字符级归属,表格解析要解决的是字段级归属。一个字都没识别错,表格依然可能完全不可用。


四、解决痛点:入库前修复表格结构,RAG 管道能改变什么

通过上述实测,我们发现:**解析阶段的结构丢失,无法通过后期的 RAG 优化(调 Embedding、改 Prompt、换模型)来弥补。

入库前做好表格解析,RAG 能改变什么?

要系统性解决这三个破坏路径,需要在解析阶段就做三件事:

解法一:输出保留完整表格结构

解析结果应该保留表头层级、数据行归属、合并单元格关系。这样 RAG 的 Splitter 可以基于表格结构做智能切分,而非暴力断句。模型在回答时能区分"表头"和"数据",不再把脚注和科目名称混淆。

解法二:元数据与表格主体保持关联

单位、币种等元数据不应被切片策略从表格主体上剥离。RAG 检索到表格数据时,单位说明应随同返回。模型回答时能带上单位,输出"1350 百万元"而非"1350"。

解法三:跨页表格按文档级对象处理

跨页长表在解析阶段就应该完成拼接,作为一张完整表格输出。RAG 的 Splitter 不需要在一张表中间下刀,长表后半段的数据行不再丢失表头。


五、与下游工具结合:xParse 如何无缝接入你的 RAG 管道

xParse 的核心价值不仅在于解析准确,更在于输出格式专为 RAG 设计。以下是 xParse 与主流 RAG 框架的集成方案。

与 LangChain 集成

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"混淆

与 LlamaIndex 集成

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 百万元。"

与 Unstructured 生态结合

如果你的 RAG 管道已经基于 Unstructured 构建,xParse 可以作为前置解析器替换默认的 partition_pdf,直接输出 Unstructured 兼容的文档元素列表:

from unstructured.partition.auto import partition_elements
# 替换为 xParse 输出 -> 转换为 Unstructured Element 格式
# 保留表格的 Table 类型和 Header 层级

把 xParse 装进 Codex

我们需要在本地先安装Codex并进行对应配置,在输入框
输入命令:
安装一下这个 skills:https://github.com/intsig-textin/xparse-skills
在这里插入图片描述
在这里插入图片描述

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

在这里插入图片描述


六、结论:选对解析方案,比调检索策略更直接

RAG 出错,根因往往不在模型,而在入在这里插入代码片库前的表格解析。通过 4 款工具的实测对比,我们验证了:

  1. 表格结构保留能力是第一优先级。PyMuPDF 等基础工具在复杂表格场景下几乎必然导致结构拍平,不适合直接用于金融、法律等对表格精度要求高的 RAG 场景。如果表头层级无法还原,再精妙的检索策略也无济于事。
  2. LlamaParse 在 RAG 原生集成上体验最好,API 设计简洁,但在超复杂表头、跨页边界及元数据关联场景下仍有明显优化空间。适合对集成效率要求高、表格复杂度中等的场景。
  3. MinerU 在元数据识别上表现突出,但跨页长表的结构连续性处理不足,需要额外工程投入来拼接续表关系。适合单页表格密集、元数据丰富的文档。
  4. PaddleOCR 作为开源方案,在表格识别完整性上表现尚可,但在单位/币种等精细元数据提取上稳定性不足,且缺乏跨页合并能力。适合作为基础识别引擎配合自定义后处理流程。
  5. xParse 在解析精度(表头层级还原、跨页拼接、元数据关联)和 RAG 友好性(结构化 JSON 输出、直接单元格级检索、上下文连续性保持)之间取得了最佳平衡。对于金融财报、法律合同、科研文献等复杂表格密集的场景,能够显著降低 RAG 幻觉风险。

RAG 的表格问答质量,入库前就决定了上限。


如果你的业务涉及财报、合同、研报等复杂文档,可以试试xParse,以更高质量的文档解析能力,为 RAG 提供更可靠的数据基础。

注册赠送1000页体验额度:https://www.textin.com/register/code/RZEVT4

更多推荐