在 RAG(Retrieval-Augmented Generation)工程落地過程中,處理文檔中的表格數據 是一個非常重要但復雜的問題,特別是針對技術文檔、報告、論文等結構化強的資料。比如PDF文檔里的表格數據,如下:
RAG處理表格數據的難點
(1)所攜帶的語義信息是不足的,不利于后面的語義檢索;
(2)標題與數據割裂;
(3)缺少上下文語義;
(4)Embedding 不適配結構化數據;
(5)轉換成純文本后,行列關系消失,難以支持細粒度查詢;
還有其他問題就不一一列舉了。
解決方案
我們該如何切割該PDF文檔呢?又該怎么精準的檢索查詢出表格中的數據呢?
直接對表格做向量存儲索引的檢索通常效果欠佳,可以借助大模型生成表格摘要用于嵌入與檢索。這有利于提高檢索精確度,加強大模型對表格的理解。在檢索階段,通過遞歸檢索出原始的表格用于后面生成。
1、文檔轉成MarkDown
建議在切分文檔之前,將所有非結構化的文檔,比如pdf,word,ppt,txt等都轉成帶有Markdown格式的文檔,這么做的好處很多,以后有空再聊。
(1)PDF轉MarkDown
有很多開源的組件,我常用的是pymupdf4llm 。以下是demo代碼:
import pymupdf4llm
from pathlib import Path# 設置參數
pdf_path = r"D:\Test\muxue\data2\caiwubaogao.pdf" # 替換為您的 PDF 文件路徑
output_md = r"D:\Test\muxue\data2\caiwubaogao.md" # 輸出的 Markdown 文件名
image_dir = r"D:\Test\muxue\data2\images" # 圖片保存目錄
dpi = 300 # 圖片分辨率
image_format = "png" # 圖片格式,可選 "png"、"jpg" 等# 創建圖片保存目錄
Path(image_dir).mkdir(parents=True, exist_ok=True)# 轉換 PDF 為 Markdown,并提取圖片
md_text = pymupdf4llm.to_markdown(doc=pdf_path,write_images=True,image_path=image_dir,image_format=image_format,dpi=dpi
)# 保存 Markdown 內容到文件
with open(output_md, "w", encoding="utf-8") as f:f.write(md_text)print(f"Markdown 內容已保存到 {output_md}")
print(f"圖片已保存到目錄 {image_dir}")
(2)Word轉MarkDown
同樣有很多開源組件可用,我使用mammoth 。示例代碼如下:
import mammoth
import osdef docx_to_markdown_with_images(docx_path, output_md_path=None, image_dir="images"):os.makedirs(image_dir, exist_ok=True)def save_image(image):image_name = image.alt_text.replace(" ", "_") if image.alt_text else "image"ext = {"image/png": ".png","image/jpeg": ".jpg","image/gif": ".gif"}.get(image.content_type, ".bin")filename = f"{image_name}{ext}"image_path = os.path.join(image_dir, filename)# 避免重名counter = 1base_name = filename.rsplit(".", 1)[0]while os.path.exists(image_path):filename = f"{base_name}_{counter}{ext}"image_path = os.path.join(image_dir, filename)counter += 1# 讀取圖片數據,保存 —— **改這里!**with image.open() as img_file:with open(image_path, "wb") as out_file:out_file.write(img_file.read())# 返回 Markdown 中圖片的路徑,注意替換成相對路徑或 URL 時修改這里return {"src": image_path.replace("\\", "/")}with open(docx_path, "rb") as docx_file:result = mammoth.convert_to_markdown(docx_file,convert_image=mammoth.images.img_element(save_image))markdown_text = result.valueif output_md_path:with open(output_md_path, "w", encoding="utf-8") as f:f.write(markdown_text)return markdown_text# 示例
markdown = docx_to_markdown_with_images(r"D:\Test\muxue\data2\caiwubaogao.docx",output_md_path=r"D:\muxue\data2\caiwubaogao.md",image_dir=r"D:\Test\muxue\data2\images"
)
print(markdown)
2、采用特殊的文本切割器
(1)MarkdownNodeParser
LlamaIndex對MarkDown文件切分,有幾個切割器,比較常用的切割器是MarkdownNodeParser,示例代碼如下:
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.core.node_parser import MarkdownNodeParser# 加載 Markdown 文檔
documents = SimpleDirectoryReader(input_dir=r"D:\Test\RAGTest\data\markdown", required_exts=[".md"]).load_data()# 創建 Markdown 節點解析器
node_parser = MarkdownNodeParser.from_defaults(include_metadata=True, # 包含元數據include_prev_next_rel=True, # 包含前后節點關系header_path_separator="/"
)# 將文檔解析為節點列表
nodes = node_parser.get_nodes_from_documents(documents)
這個切割器是根據MarkDown的標題級別進行切割的。
(2)MarkdownElementNodeParser
為了處理表格,我們需要使用另一個切割器--MarkdownElementNodeParser。它會將markdown文檔中的文本、標題、表格等元素分別解析為不同類型的節點:普通文本為TextNode,表格為IndexNode(且“完美表格”會被轉為pandas DataFrame,非標準表格則以原始文本存儲)。解析后,節點類型和內容可直接區分,便于后續檢索和處理。
MarkdownElementNodeParser 與普通的數據分割器的區別主要在于它對其中的表格內容借助大模型生成了內容摘要與結構描述,并構造成索引 Node
(IndexNode),然后在查詢時通過索引 Node 找到表格內容 Node,將其一起輸入大模型進行生成。
from llama_index.core.llms.mock import MockLLM
from llama_index.core.node_parser.relational.markdown_element import MarkdownElementNodeParser
from llama_index.core.schema import Document, TextNode, IndexNode# 示例markdown文本,包含文本、標題和表格
md_text = """
# 第一章這是第一章的內容。| 年份 | 收益 |
| ---- | ---- |
| 2020 | 12000 |
| 2021 | 15000 |## 第二節這是第二節的內容。| 產品 | 數量 | 價格 |
| ---- | ---- | ---- |
| A | 10 | 5 |
| B | 20 | 8 |
"""# 構建Document對象
doc = Document(text=md_text)# 初始化MarkdownElementNodeParser
parser = MarkdownElementNodeParser(llm=MockLLM())# 解析為節點
nodes = parser.get_nodes_from_documents([doc])# 輸出每個節點的類型和內容
for i, node in enumerate(nodes):print(f"Node (i): 類型: {type(node).__name__}")print(f"內容: {getattr(node, 'text', getattr(node, 'table', ''))}\n")
切割+檢索的示例完整代碼如下:
'''
markdown中表格數據的切割和查詢
'''
from llama_index.core import VectorStoreIndex, Settings, SimpleDirectoryReader
from llama_index.llms.openai_like import OpenAILike
from llama_index.embeddings.openai_like import OpenAILikeEmbedding
from llama_index.core.node_parser.relational.markdown_element import (MarkdownElementNodeParser,
)
from llama_index.core.llms.mock import MockLLM# ================== 初始化模型 ==================
def init_models():"""初始化模型并驗證"""# Embedding模型embed_model = OpenAILikeEmbedding(model_name="BAAI/bge-m3",api_base="https://api.siliconflow.cn/v1",api_key="sk-xxx",embed_batch_size=10,)llm = OpenAILike(model="deepseek-ai/DeepSeek-V3",api_base="https://api.siliconflow.cn/v1",api_key="sk-xxx",context_window=128000,is_chat_model=True,is_function_calling_model=False,)Settings.embed_model = embed_modelSettings.llm = llm# 驗證模型test_embedding = embed_model.get_text_embedding("測試文本")print(f"Embedding維度驗證:{len(test_embedding)}")return embed_model, llminit_models()# load documents, split into chunks
documents = SimpleDirectoryReader(r"D:\Test\muxue\data2", required_exts=[".md"]).load_data()# 2. 強大的分割器node_parser = MarkdownElementNodeParser(llm=MockLLM())
nodes = node_parser.get_nodes_from_documents(documents)index = VectorStoreIndex(nodes)from llama_index.core.query_engine import CitationQueryEnginequery_engine = CitationQueryEngine.from_args(index,similarity_top_k=3,# here we can control how granular citation sources are, the default is 512citation_chunk_size=512,
)res = query_engine.query("股本增減變動幅度多大?請使用中文回答")
print(res.response) # LLM 輸出回答
print("------來源---------------")
for node in res.source_nodes:print("相關片段:", node.text)print("片段分數:", node.score)print("片段元數據:", node.metadata)print("="*40)
結果如下:
可以看出能夠精準的查詢出表格中的數據。
相關網址
MarkdownElementNodeParser的測試demo代碼:
https://github.com/run-llama/llama_index/blob/main/llama-index-core/tests/node_parser/test_markdown_element.py