使用高精度RAG实现对表格数据的检索
为什么表格堆砌的文档,RAG表现总是“翻车”?
检索增强生成(RAG)技术已经火了好一阵子,但真正落地的时候,坑其实不少——尤其是当文档里塞满了图片和表格的时候。一个很典型的场景:每次让RAG从表格里提取某个具体数值,结果准确率就直线下降。更让人头疼的是,如果文档里多个表格都和同一主题相关(比如财报里的各种财务数据表),那准确率更是惨不忍睹。那么,问题到底出在哪儿?又该怎么解决?下面就来聊聊一套经过验证的方法。

核心痛点
-
:向量搜索算法很难精准定位到正确的表格。特别是当文档中存在多个结构相似的表格时,检索结果往往“跑偏”。
检索阶段就掉链子
-
:大语言模型(LLM)经常误解表格里的数值,尤其当表格有嵌套列这种复杂结构的时候,错误率更高。原因大概率出在表格格式的不一致性上——原始表格的文本表述杂乱无章,模型根本没法好好理解。
生成阶段也翻车
解决方案:四个关键步骤
要解决这两个问题,逻辑其实很清晰:先得把表格从文档里干干净净地抽出来,再给每个表格配上丰富的上下文说明,然后把它们统一成标准的Markdown格式,最后把这些“加料”的表格块嵌入向量数据库。具体来说,有四个核心环节:
- :老老实实从文档里把表格完整地提取出来,不丢不漏。
精确提取
- :利用LLM分析每个表格以及它周围的文档内容,生成一段有力、相关的描述——让表格“自己介绍自己”。
上下文丰富
- :用LLM把表格都转成统一的Markdown格式。这样一来,无论是嵌入模型还是生成模型,都能轻松理解。
格式标准化
- :将上下文的描述和Markdown格式的表格拼在一起,形成一个“表格块”,优化存储和检索效果。
统一嵌入

实战:让RAG读懂Meta财报
目标
下面详细拆解每一步,包括完整的代码实现和最终效果对比。注意,本文只展示上下文化表格块的方案;实际在完整版笔记中,还包含了与非上下文化表格块的对比实验,差异相当明显。
第一步:精确提取——从PDF里把表格“抠”出来
用Unstructured.io的partition_pdf函数,并选用hi_res策略(高分辨率模式),能有效识别文档中的表格元素。同时设置chunking_strategy="by_title",确保文本按标题切分,避免不同章节的内容混在一起。
!apt-get -qq install poppler-utils tesseract-ocr
%pip install -q --user --upgrade pillow
%pip install -q --upgrade unstructured["all-docs"]
%pip install kdbai_client
%pip install langchain-openai
%pip install langchain
%pip install langchain-community
%pip install pymupdf
%pip install --upgrade nltk
import os
from getpass import getpass
import openai
from openai import OpenAI
from unstructured.partition.pdf import partition_pdf
from unstructured.partition.auto import partition
from langchain_openai import OpenAIEmbeddings
import kdbai_client as kdbai
from langchain_community.vectorstores import KDBAI
from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI
import fitz
nltk.download('punkt')
设置OpenAI API密钥(需要提前在环境变量中配置,或运行时手动输入):
if "OPENAI_API_KEY" in os.environ:
KDBAI_API_KEY = os.environ["OPENAI_API_KEY"]
else:
OPENAI_API_KEY = getpass("请输入 OPENAI API 密钥: ")
os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY
下载Meta 2024年第二季度财报PDF(文档中表格密集,非常适合做测试):
!wget 'https://s21.q4cdn.com/399680738/files/doc_news/Meta-Reports-Second-Quarter-2024-Results-2024.pdf' -O './doc1.pdf'
用partition_pdf提取元素,指定高分辨率策略和分块参数:
elements = partition_pdf(
'./doc1.pdf',
strategy="hi_res",
chunking_strategy="by_title",
max_characters=2500,
new_after_n_chars=2300,
)
看看提取了什么:
from collections import Counter
display(Counter(element.__class__ for element in elements))
输出:Counter({CompositeElement: 17, Table: 10})。这里17个是文本块,10个是表格元素——还算干净利落。
第二步 & 第三步:上下文丰富 + 格式标准化
先随便看一个表格元素的原始内容,就能理解为什么RAG表现差:
print(elements[-2])
输出是一长串混杂了数字和文字的字符串,没有任何结构。如果直接用这个去嵌入、检索,向量搜索算法根本分不清这个表格到底在描述什么。
所以,需要两件事:第一,给每个表格生成一段详细的描述性上下文;第二,把表格转成漂亮的Markdown格式。
首先,提取整个PDF的文本作为文档上下文:
def extract_text_from_pdf(pdf_path):
text = ""
with fitz.open(pdf_path) as doc:
for page in doc:
text += page.get_text()
return text
pdf_path = './doc1.pdf'
document_content = extract_text_from_pdf(pdf_path)
然后,定义一个函数,传入表格的原始内容和文档上下文,让LLM同时输出表格描述和Markdown格式的表格:
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def get_table_description(table_content, document_context):
prompt = f"""
Given the following table and its context from the original document,
provide a detailed description of the table. Then, include the table in markdown format.
Original Document Context:
{document_context}
Table Content:
{table_content}
Please provide:
1. A comprehensive description of the table.
2. The table in markdown format.
"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "You are a helpful assistant that describes tables and formats them in markdown."},
{"role": "user", "content": prompt}
]
)
return response.choices[0].message.content
接下来,遍历所有表格元素,用上述函数替换原始文本:
for element in elements:
if element.to_dict()['type'] == 'Table':
table_content = element.to_dict()['text']
result = get_table_description(table_content, document_content)
element.text = result
print("Processing complete.")
处理之后的表格块会长什么样?以下是一个示例输出(为方便阅读已转成Markdown):
### Detailed Description of the Table
The table presents segment information from Meta Platforms, Inc. for both revenue and income (loss) from operations. The data is organized into two main sections:
1. **Revenue**: This section is subdivided into two categories: "Advertising" and "Other revenue". The total revenue generated from these subcategories is then summed up for two segments: "Family of Apps" and "Reality Labs". The table provides the revenue figures for three months and six months ended June 30, for the years 2024 and 2023.
2. **Income (loss) from operations**: This section shows the income or loss from operations for the "Family of Apps" and "Reality Labs" segments, again for the same time periods.
The table allows for a comparison between the two segments of Meta's business over time, illustrating the performance of each segment in terms of revenue and operational income or loss.
### The Table in Markdown Format
```markdown
### Segment Information (In millions, Unaudited)
|| Three Months Ended June 30, 2024 | Three Months Ended June 30, 2023 | Six Months Ended June 30, 2024 | Six Months Ended June 30, 2023 |
|----------------------------|----------------------------------|----------------------------------|------------------------------- |-------------------------------|
| **Revenue:** ||| | |
| Advertising| $38,329| $31,498| $73,965 | $59,599 |
| Other revenue| $389 | $225 | $769| $430|
| **Family of Apps** | $38,718| $31,723| $74,734 | $60,029 |
| Reality Labs | $353 | $276 | $793| $616|
| **Total revenue**| $39,071| $31,999| $75,527 | $60,645 |
|||| | |
| **Income (loss) from operations:** ||| | |
| Family of Apps | $19,335| $13,131| $36,999 | $24,351 |
| Reality Labs | $(4,488) | $(3,739) | $(8,334)| $(7,732)|
| **Total income from operations** | $14,847| $9,392 | $28,665 | $16,619 |
```
对比一下最初的纯文本,现在表格有了清晰的描述和结构化的Markdown格式。嵌入模型能从中获取到表格的主题、列含义以及数值的上下文关系,向量检索的命中率会大幅提升。
第四步:统一嵌入——把“加料”的表格存进向量库
现在所有元素(包括文本块和增强后的表格块)都准备好了,接下来要把它们嵌入并存入向量数据库。这里选用KDB.AI作为向量数据库,因为它速度快、对复杂查询支持好。
首先,生成所有元素的嵌入:
from unstructured.embed.openai import OpenAIEmbeddingConfig, OpenAIEmbeddingEncoder
embedding_encoder = OpenAIEmbeddingEncoder(
config=OpenAIEmbeddingConfig(
api_key=os.getenv("OPENAI_API_KEY"),
model_name="text-embedding-3-small",
)
)
elements = embedding_encoder.embed_documents(elements=elements)
接着,把元素整理成Pandas DataFrame,方便存入KDB.AI:
import pandas as pd
data = []
for c in elements:
row = {}
row['id'] = c.id
row['text'] = c.text
row['metadata'] = c.metadata.to_dict()
row['embedding'] = c.embeddings
data.append(row)
df = pd.DataFrame(data)
连接到KDB.AI云服务(需要先在KDB.AI申请API密钥和端点):
KDBAI_ENDPOINT = (
os.environ["KDBAI_ENDPOINT"]
if "KDBAI_ENDPOINT" in os.environ
else input("KDB.AI endpoint: ")
)
KDBAI_API_KEY = (
os.environ["KDBAI_API_KEY"]
if "KDBAI_API_KEY" in os.environ
else getpass("KDB.AI API key: ")
)
session = kdbai.Session(api_key=KDBAI_API_KEY, endpoint=KDBAI_ENDPOINT)
定义向量表的模式。嵌入列需要设置向量索引参数:维度数(text-embedding-3-small输出1536维)、索引类型(这里用最简单的flat)、距离度量(L2欧几里得距离)。
schema = {
'columns': [
{'name': 'id', 'pytype': 'str'},
{'name': 'text', 'pytype': 'str'},
{'name': 'metadata', 'pytype': 'dict'},
{'name': 'embedding', 'vectorIndex': {'dims': 1536, 'type': 'flat', 'metric': 'L2'}}
]
}
删除可能已存在的同名表,然后创建新表:
KDBAI_TABLE_NAME = "Table_RAG"
if KDBAI_TABLE_NAME in session.list():
session.table(KDBAI_TABLE_NAME).drop()
table = session.create_table(KDBAI_TABLE_NAME, schema)
将DataFrame插入表中:
table.insert(df)
现在,所有元素已经安家在向量数据库,随时可以检索。
打造RAG管道——用LangChain把一切串起来
定义嵌入模型和向量检索器:
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import KDBAI
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vecdb_kdbai = KDBAI(table, embeddings)
构建RAG链,使用gpt-4o作为生成模型,检索时返回前5个相似文档:
qabot = RetrievalQA.from_chain_type(
chain_type="stuff",
llm=ChatOpenAI(model="gpt-4o"),
retriever=vecdb_kdbai.as_retriever(search_kwargs=dict(k=5)),
return_source_documents=True,
)
def RAG(query):
print(query)
print("-----")
return qabot.invoke(dict(query=query))["result"]
效果实测——真金不怕火炼
以下是五个有代表性的提问和结果:
示例1:简单数值提取
RAG("what is the 2024 GAAP advertising Revenue in the three months ended June 30th? What about net cash by operating activities")
结果
正确。
示例2:跨表格查询
RAG("what is the three month costs and expenses for 2023?")
结果
正确。
示例3:资产负债表数据
RAG("At the end of 2023, what was the value of Meta's Goodwill assets?")
结果
正确。
示例4:复杂嵌套表格(此处若用非上下文化表格块,几乎必然出错)
RAG("What is the research and development costs for six months ended in June 2024")
结果
注意,这个表格在原始PDF中具有嵌套列,如果没有上下文描述和Markdown格式,LLM很容易混淆列标签。上下文化表格块在此类场景下优势明显。
示例5:开放性推理问答
RAG("Given a sentiment score between 1 and 10 for the outlook? Explain your reasoning")
结果
一些权衡与思考
当然,为表格增加上下文描述不是免费的午餐。每张表格都需要额外调用一次LLM来生成描述,这会增加成本和延迟。对于只有少数简单表格的场景,或许并不值得这么做。实验表明,当表格结构简单(仅有几行几列)时,直接用原始表格文本效果也不差。但当表格开始出现嵌套列、跨页合并单元格、或数值含义依赖上下文时,非上下文化的表格块就会频频翻车。示例4就是一个典型。
所以,是否要采用上下文化表格块,取决于你的文档中表格的复杂度和数量。如果表格是核心信息载体,而且数据量大、结构复杂,那么这个方案绝对值得投入。
总结
表格丰富的文档对RAG管道是一块硬骨头,但并非啃不动。通过精确提取、上下文丰富、格式标准化和统一嵌入这一组合拳,能有效解决检索不一致和生成不准确两大痛点。Meta财报的例子清晰展示了使用增强表格块之后,生成响应的质量和可靠性显著提升。随着RAG技术不断演进,这种将表格“拟人化”的预处理思路,或许会成为处理结构化数据类文档的标配方法。
-
- 关于宇宙的好的网名有哪些
- 角色扮演 | 1
- 网名