中文PDF语义解析与结构化RAG增强技术

发布时间:2026/6/12 8:47:00
中文PDF语义解析与结构化RAG增强技术 1. 项目概述为什么90%的PDF RAG系统都在“假装理解”你有没有试过把一份30页的财务尽调报告、一份带复杂表格的医疗器械说明书或者一份嵌套了十几层标题的政府招标文件直接丢进某个标榜“支持PDF”的RAG系统里然后问“这份文件里提到的第三类医疗器械注册证有效期是多久”——结果它要么从第一页随便摘一句“有效期五年”要么干脆编造一个“根据第7章第2条”甚至返回“未找到相关信息”这不是你的问题而是绝大多数所谓“PDF RAG”根本没在处理PDF它只是在处理PDF的“尸体”把PDF暴力转成纯文本再扔给大模型硬啃。字体丢失、表格错乱、页眉页脚混入正文、公式变成乱码、图表说明和图本身彻底分离……这些不是边缘case而是PDF的日常。我做过一个测试用5种主流PDF解析工具pdfplumber、PyMuPDF、pdfminer、pypdf、unstructured处理同一份带合并单元格和脚注的学术论文提取出的文本结构一致性只有38%关键数据如表格中的数值、参考文献编号错误率平均达22%。这就像让一个近视500度又没戴眼镜的人靠扫描仪拍一张模糊照片去读《本草纲目》的药性表——他确实“看见”了字但完全不知道哪味药配哪条禁忌。真正的“理解”必须从PDF的原始语义结构出发识别出这是个标题、这是个段落、这是个三线表、这是个脚注引用、这是个跨页的流程图。Stop Wasting PDFs不是喊口号是告诉你别再把PDF当文本切片喂给向量库了得先让机器像人一样“看懂”这份文档的骨架与血肉。这个项目就是一套可落地、可复现、专为中文PDF深度定制的RAG理解流水线——它不追求“能跑”而追求“读懂”。适合正在被PDF折磨的产品经理、需要精准检索技术文档的工程师、处理大量合同与报告的法务/风控人员以及所有厌倦了“AI答非所问”的一线知识工作者。核心关键词已经写在标题里PDF语义解析、RAG结构化增强、中文文档理解、表格与公式保留、多级标题重建。2. 整体设计思路放弃“文本切片”拥抱“文档结构建模”2.1 为什么传统PDF RAG的“文本切片”思路注定失败传统RAG对PDF的处理本质是“降维打击”PDF →OCR或解析→ 纯文本 →分块→ 向量嵌入 → 检索。这个链条里第一步就埋下了所有隐患。PDF不是文本容器它是图形指令集合。Acrobat打开一个PDF实际是在执行一串“画一条线、填一个矩形、放一个文字框、设置字体大小”的绘图命令。所以当pdfminer这类工具试图“提取文本”时它其实是在逆向工程这些绘图指令猜哪个文字框属于哪个逻辑段落。这种“猜”在排版规整的新闻稿里可能有80%准确率但在一份用InDesign精心排版的年度报告里准确率会断崖式下跌。我实测过一份某券商发布的2023年ESG报告PDF其“管理层讨论与分析”章节中关键指标KPI表格被拆成了4个独立文本块每个块里只有一列数据且行序完全错乱而页脚的“数据来源公司年报第X页”被拼接进了正文最后一段导致向量检索时所有关于“数据来源”的查询都会错误地关联到该章节。这就是“文本切片”的原罪它把结构信息Structure和语义信息Semantics全部丢弃只留下最脆弱的字符串序列String Sequence。大模型再强也无法从“净利润 2023年 12.5亿”这样破碎的字符串里推断出“12.5亿”是“净利润”在“2023年”的值更无法知道这个值来自表格的第几行第几列。所以本项目的第一原则绝不以纯文本为中间态。我们的输入是PDF二进制流输出是带有丰富结构标签的JSON文档对象模型DOM中间每一步都服务于“保真还原”。2.2 核心架构三层理解流水线Parsing → Structuring → Embedding我们构建了一个三层流水线每一层解决一个维度的理解问题层层递进不可跳过第一层高保真解析层Parsing Layer这是地基。我们放弃所有“通用PDF解析器”转而采用双引擎协同策略对含文字的页面优先使用PyMuPDF即fitz进行坐标级解析因为它能精确返回每个字符的边界框bbox、字体、大小、颜色对扫描件或文字被图片覆盖的页面则调用PaddleOCR进行高精度OCR并强制开启layout_analysis版面分析模式识别出标题、正文、表格、图片、页眉页脚等区域。关键创新在于我们不把OCR结果当作最终文本而是将其坐标信息与PyMuPDF的矢量坐标对齐生成一个统一的、带坐标的文本元素列表。例如一个表格单元格在PyMuPDF中是一个矩形区域在OCR中是一组带坐标的文字行我们通过IOU交并比算法将它们匹配从而确认“这行OCR文字物理上位于这个PDF矩形框内”。这解决了“OCR识别准但位置错”的老大难问题。第二层语义结构重建层Structuring Layer这是灵魂。拿到带坐标的文本元素后我们不做任何“按换行符切分”的粗暴操作。而是基于视觉线索Visual Cues和语言线索Linguistic Cues进行双重建模视觉线索利用元素的x/y坐标、字体大小、加粗状态、行间距、缩进量训练一个轻量级的XGBoost分类器判断每个元素的类型H1,H2,Paragraph,TableHeader,TableCell,FootnoteRef,FigureCaption。比如一个字体为16pt、加粗、居中、上下行距大的元素99%是H1一个字体为10pt、斜体、位于页面底部、x坐标与正文一致的元素大概率是FootnoteRef。语言线索对疑似标题的元素用一个微调过的bert-base-chinese小模型做标题检测Title Detection输入是该元素文本前后两行文本输出是“是否为标题”及“标题级别”。这解决了视觉线索失效的场景比如一份纯文字的法律条文所有字体大小相同但“第一条”、“第二条”明显是标题。最终我们将所有元素按其逻辑层级而非物理顺序组织成一棵DOM树。一个表格不再是一堆乱序的字符串而是一个table节点其下是thead和tbodytbody里是trtr里是td每个td里还嵌套着sup上标和a脚注链接。这才是“理解”的起点。第三层结构感知嵌入层Embedding Layer这是桥梁。传统做法是把整个DOM树序列化成字符串再嵌入这又回到了“文本切片”的老路。我们的方案是对DOM树的每个节点生成一个“结构化嵌入向量”。具体操作对Paragraph节点我们用bge-m3模型生成其文本内容的嵌入向量对TableCell节点我们不仅嵌入其文本还拼接其结构上下文[父表格ID]_[行号]_[列号]_[是否为表头]再将这个结构化字符串也嵌入并与文本嵌入向量做加权平均对H2节点我们嵌入其文本并额外拼接其在整个文档中的路径H1:年度报告 H2:财务摘要让向量天然携带层级关系。这样当用户问“财务摘要里的净利润是多少”检索系统不仅能匹配到“财务摘要”这个H2节点还能精准定位到其子节点中包含“净利润”一词的Paragraph或TableCell而不是泛泛地匹配到文档里所有出现“净利润”的地方。2.3 为什么选择这套组合而非端到端大模型你可能会问现在不是有Qwen-VL、InternVL这些多模态大模型吗直接把PDF当图像喂进去不就行了理论上可行但实操中是灾难。我用Qwen-VL-7B在A100上跑过一份20页的PDF单次推理耗时142秒显存占用28GB且对表格的数值提取准确率仅61%它把“1,234.56”识别成了“123456”。更重要的是RAG的核心价值是可控、可解释、可审计。当模型回答“根据第5页表格净利润为12.5亿”时我们必须能立刻追溯到这个答案来自哪个具体的TableCell节点它的坐标是多少它的父表格ID是什么。端到端大模型是个黑盒你无法验证它的“依据”是否真实存在。而我们的三层流水线每一个环节的输出都是可检查、可调试、可替换的JSON。今天用bge-m3明天换成text-embedding-3-large只需改一行配置。这种工程上的确定性是业务系统落地的生命线。3. 核心细节解析与实操要点从PDF到结构化DOM的每一步3.1 解析层实操PyMuPDF PaddleOCR 的协同配置与避坑指南PyMuPDFfitz是解析PDF矢量内容的黄金标准但它对扫描件PDF完全无效。PaddleOCR则是OCR领域的佼佼者尤其对中文表格识别效果极佳。二者协同的关键在于坐标系统的统一。PyMuPDF的坐标系原点在左上角y轴向下为正而PaddleOCR的坐标系原点在左上角但其返回的bbox是[x1, y1, x2, y2]左上、右下与PyMuPDF的[x0, y0, x1, y1]左下、右上方向相反。如果直接拼接会导致所有OCR文字“飘”在页面之外。我的解决方案是在PaddleOCR返回结果后立即进行坐标归一化。# 假设 page 是 PyMuPDF 的 Page 对象ocr_result 是 PaddleOCR 的返回 page_width, page_height page.rect.width, page.rect.height for line in ocr_result: # line[0] 是 bbox: [[x1,y1], [x2,y1], [x2,y2], [x1,y2]] (顺时针) # 需要转换为 PyMuPDF 的 [x0, y0, x1, y1] 格式且 y 轴翻转 x0 min([p[0] for p in line[0]]) y0 page_height - max([p[1] for p in line[0]]) # 翻转 y x1 max([p[0] for p in line[0]]) y1 page_height - min([p[1] for p in line[0]]) # 翻转 y normalized_bbox [x0, y0, x1, y1] # 将 normalized_bbox 与 PyMuPDF 的 textpage.search_for() 结果进行 IOU 匹配提示PaddleOCR的layout_analysis模型pp_layout必须单独下载它不是默认内置的。运行pip install paddlepaddle后需手动执行paddleocr --download-model layout。否则layout参数会静默失效你得到的永远是“纯文字”结果没有表格、标题等区域划分。另一个致命坑是字体缺失导致的乱码。很多PDF为了减小体积会将中文字体子集化Subset即只嵌入文档中实际用到的那几十个汉字。PyMuPDF在提取文本时如果遇到未嵌入的字符会返回空格或方块。解决方案不是“重装字体”而是强制启用Unicode解码在page.get_text(dict)时传入flagsfitz.TEXT_PRESERVE_LIGATURES | fitz.TEXT_PRESERVE_WHITESPACE并确保page.get_text(text)作为备用方案。我在处理一份某法院的判决书PDF时发现其“本院认为”四个字因字体子集化而全部丢失但get_text(dict)配合上述flags成功还原了98%的字符。3.2 结构层实操用XGBoost BERT做轻量级结构分类结构重建不是靠规则硬编码而是用数据驱动。我收集了200份涵盖财报、合同、论文、说明书的中文PDF样本人工标注了其中12,000个文本元素的类型H1-H3, Paragraph, TableCell, TableHeader, FootnoteRef, FigureCaption, ListBullet。特征工程是成败关键视觉特征12维字体大小pt、是否加粗bool、是否斜体bool、行高pt、左缩进pt、右缩进pt、x坐标归一化到0-1、y坐标归一化到0-1、宽度归一化、高度归一化、与上一元素的垂直距离pt、与上一元素的水平距离pt。语言特征8维文本长度、中文字符占比、数字字符占比、标点符号数量、是否以“第X条”/“一、”/“1.”开头regex、是否以“”/“。”结尾、TF-IDF向量用前1000高频词训练的前5维PCA。XGBoost模型n_estimators200, max_depth6在测试集上达到了92.3%的宏平均F1-score。但H1/H2的区分仍有难度这时就需要BERT的语义补充。我用HuggingFace的bert-base-chinese在标注数据上微调了一个二分类模型Title vs Not Title并在其后接一个三级分类头H1/H2/Other。输入文本截断为32字符因为标题通常很短。微调后标题级别的识别准确率从XGBoost单独的78%提升到了94%。实操心得不要试图用一个大模型搞定所有事。XGBoost快、准、可解释负责“大部分”BERT慢、贵、语义强只负责“最难啃的骨头”。这是一种务实的工程哲学。3.3 表格重建从像素到语义的魔法表格是PDF解析的终极挑战。PyMuPDF能返回表格的“线框”PaddleOCR能识别“文字”但如何把两者缝合还原出真正的行列结构我的方案是三步走线框提取用page.find_tables()PyMuPDF内置获取所有候选表格区域。它基于检测页面上的横线和竖线精度尚可但会漏掉无边框的“隐形表格”如用空格对齐的报表。文字聚类对步骤1得到的每个表格区域提取其中所有带坐标的文字元素来自PyMuPDF或OCR。然后用DBSCAN聚类算法以x坐标为特征将文字聚成“列”再以y坐标为特征将文字聚成“行”。DBSCAN的eps参数至关重要列聚类用eps15像素行聚类用eps8像素这经过了上百份PDF的调优。单元格填充与校验有了行列簇就形成了一个网格。遍历每个网格单元找出落在其bbox内的所有文字元素按y坐标排序拼接成单元格文本。最后强制校验每一行的单元格数必须相等否则触发“合并单元格”逻辑——如果某单元格为空且其右侧单元格的x坐标紧邻且y坐标重叠度80%则判定为跨列合并。注意这个流程对“斜线表头”如Excel中用斜线分割的表头依然无解。我的妥协方案是将斜线表头区域单独标记为TableHeaderComplex并在嵌入时将其文本与下方普通表头文本拼接例如“产品名称/型号” “规格参数”形成一个复合描述。虽然不完美但比完全丢失信息强得多。4. 实操过程与核心环节实现从零搭建你的PDF理解RAG4.1 环境准备与依赖安装实测兼容性清单所有操作均在Ubuntu 22.04 Python 3.10环境下完成。请严格按此顺序安装避免版本冲突# 1. 安装系统级依赖关键 sudo apt update sudo apt install -y libpoppler-cpp-dev libfreetype6-dev libpng-dev libjpeg-dev # 2. 创建虚拟环境强烈推荐 python3 -m venv pdf-rag-env source pdf-rag-env/bin/activate # 3. 安装核心包注意顺序和版本 pip install --upgrade pip pip install pymupdf1.23.24 # 必须锁定此版本1.24有坐标系bug pip install paddlepaddle-gpu2.4.2.post112 # CUDA 11.2如用CPU则装 paddlepaddle pip install paddleocr2.7.1 # 必须2.7.1旧版无layout_analysis pip install scikit-learn1.3.2 xgboost2.0.3 # 与后续模型训练兼容 pip install transformers4.38.2 torch2.1.2 # BERT微调所需 pip install sentence-transformers2.2.2 # bge-m3嵌入所需 pip install numpy1.24.4 pandas2.0.3 # 数据处理实操心得pymupdf的版本是最大雷区。1.24.x系列在处理某些加密PDF时会崩溃1.23.24是目前最稳定的版本。paddleocr的layout_analysis模型下载后默认缓存路径是~/.paddleocr/如果服务器磁盘空间紧张可在代码中指定PaddleOCR(use_angle_clsFalse, langch, detTrue, recTrue, layoutTrue, layout_path./models/pp_layout_server)将模型放在项目目录下。4.2 核心Pipeline代码详解可直接运行以下是一个精简但完整的pdf_to_structured_dom.py核心脚本已去除日志和异常处理仅保留主干逻辑# pdf_to_structured_dom.py import fitz from paddleocr import PaddleOCR import numpy as np from sklearn.cluster import DBSCAN from xgboost import XGBClassifier import joblib class PDFStructurer: def __init__(self): self.ocr PaddleOCR(use_angle_clsFalse, langch, detTrue, recTrue, layoutTrue) self.xgb_model joblib.load(models/xgb_structure.pkl) # 训练好的XGBoost模型 self.bert_tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) self.bert_model AutoModel.from_pretrained(models/bert_title_classifier) # 微调好的BERT def parse_page(self, page: fitz.Page) - list: 解析单页返回带坐标的文本元素列表 elements [] # 步骤1尝试PyMuPDF矢量解析 text_dict page.get_text(dict, flagsfitz.TEXT_PRESERVE_LIGATURES | fitz.TEXT_PRESERVE_WHITESPACE) for block in text_dict[blocks]: if lines in block: for line in block[lines]: for span in line[spans]: # 提取span的bbox、text、font、size bbox [span[bbox][0], span[bbox][1], span[bbox][2], span[bbox][3]] elements.append({ type: text, text: span[text].strip(), bbox: bbox, size: span[size], flags: span[flags] }) # 步骤2如果elements为空或太少10个说明可能是扫描件调用OCR if len(elements) 10: ocr_result self.ocr.ocr(page.get_pixmap(dpi200).tobytes(), clsFalse) if ocr_result and ocr_result[0]: for line in ocr_result[0]: # 进行坐标归一化代码见3.1节 normalized_bbox self._normalize_ocr_bbox(line[0], page.rect.width, page.rect.height) elements.append({ type: ocr_text, text: line[1][0], bbox: normalized_bbox, confidence: line[1][1] }) return elements def _build_dom_tree(self, elements: list) - dict: 构建DOM树核心是结构分类与层级组织 # 特征工程为每个element计算12维视觉特征 8维语言特征 features self._extract_features(elements) # XGBoost预测基础类型 pred_types self.xgb_model.predict(features) # 对pred_types为Title的元素用BERT微调模型细化级别 for i, t in enumerate(pred_types): if t Title: bert_input self.bert_tokenizer( elements[i][text][:32], truncationTrue, paddingmax_length, return_tensorspt ) with torch.no_grad(): logits self.bert_model(**bert_input).logits pred_level torch.argmax(logits, dim-1).item() pred_types[i] fH{pred_level1} # H1, H2, H3 # 按y坐标排序构建层级树伪代码 sorted_elements sorted(zip(elements, pred_types), keylambda x: x[0][bbox][1]) dom_root {type: Document, children: []} current_h1 None for elem, elem_type in sorted_elements: if elem_type.startswith(H): new_node {type: elem_type, text: elem[text], children: []} if elem_type H1: dom_root[children].append(new_node) current_h1 new_node elif elem_type H2 and current_h1: current_h1[children].append(new_node) return dom_root def run(self, pdf_path: str) - dict: 主入口函数 doc fitz.open(pdf_path) full_dom {type: Root, children: []} for page_num in range(len(doc)): page doc[page_num] elements self.parse_page(page) page_dom self._build_dom_tree(elements) full_dom[children].append(page_dom) return full_dom # 使用示例 if __name__ __main__: structurer PDFStructurer() dom structurer.run(sample_report.pdf) print(json.dumps(dom, ensure_asciiFalse, indent2))这段代码的威力在于它输出的不再是{text: 2023年净利润为12.5亿元}而是{ type: TableCell, text: 12.5亿元, parent_table_id: tbl_financial_summary, row_index: 2, col_index: 1, is_header: false, context_path: [H1:2023年年度报告, H2:财务摘要, Table:主要财务指标] }这个结构才是RAG可以真正“理解”的基石。4.3 嵌入与检索结构化向量的生成与查询有了DOM下一步是生成嵌入。我们不使用单一的bge-m3而是为不同节点类型定制嵌入策略Paragraph节点直接用bge-m3嵌入其text字段。TableCell节点嵌入text [ROW: row_index ][COL: col_index ]并用bge-m3的encode_queries方法专为query优化。H2节点嵌入text | context_path[-2]即其父H1的文本强化层级关联。检索时用户的自然语言查询如“2023年净利润”会被同样处理首先用一个轻量级NER模型spaCy中文版抽取出关键实体“2023年”、“净利润”然后构造一个结构化查询向量[bge_m3(2023年净利润)] [bge_m3(财务摘要)]因为“净利润”大概率出现在“财务摘要”章节下再与所有节点向量做余弦相似度计算。Top-K结果会按节点类型加权TableCell权重1.5H2权重1.2Paragraph权重1.0确保表格数据优先返回。实操心得向量数据库选型上我放弃了Milvus和Weaviate选择了ChromaDB。原因很简单它轻量单文件、易部署、API简洁且对元数据metadata的支持极好。我们可以把node_type、row_index、col_index、context_path全部作为metadata存入Chroma检索时用where条件过滤例如collection.query(query_embeddings..., where{node_type: TableCell, row_index: {$gte: 1}})这比在向量层面做所有事情更高效、更可控。5. 常见问题与排查技巧实录那些文档没告诉你的坑5.1 PDF解析失败的5种典型场景与根治方案问题现象根本原因排查方法根治方案我的实测耗时解析后文本为空PDF被加密即使无密码也可能有权限密码pdfinfo sample.pdf查看Encrypted:字段用qpdf --decrypt input.pdf output.pdf解密或在PyMuPDF中用doc.authenticate()尝试空密码2分钟表格内容全部挤在一行PDF中表格是用“空格”对齐的而非真实表格对象用page.get_text(text)查看原始输出若数字间有多个空格则属此类启用PaddleOCR的layout_analysis并关闭PyMuPDF的get_text(text)只用get_text(dict)提取坐标5分钟中文显示为方块或乱码PDF嵌入字体不全或PyMuPDF未正确映射Unicodepage.get_fonts()查看嵌入字体列表page.get_text(text)输出是否为?在get_text(dict)时添加flagsfitz.TEXT_PRESERVE_LIGATURES或预处理PDFgs -o fixed.pdf -sDEVICEpdfwrite -dPDFSETTINGS/prepress input.pdf8分钟页眉页脚污染正文PyMuPDF无法区分页眉页脚与正文区域用page.get_text(dict)查看所有block的y坐标观察是否有固定高度的重复区域在解析前用page.cropbox裁剪掉顶部20pt和底部20pt的区域page.set_cropbox(fitz.Rect(0, 20, page.rect.width, page.rect.height-20))1分钟公式变成一堆乱码字符PDF中的数学公式是用特殊字体如MTMATH绘制的PyMuPDF无法识别page.get_fonts()中看到MTMATH等字体名放弃文本提取将公式区域截图用LaTeX-OCR模型识别如pix2tex并将识别出的LaTeX代码作为Math节点存入DOM15分钟首次配置5.2 RAG“答非所问”的3个深层原因与调试路径RAG返回错误答案90%不是模型问题而是数据管道的问题。我的调试路径是“从后往前”检查检索结果Retrieval运行collection.query(..., n_results5)打印出返回的5个节点及其metadata。如果里面根本没有包含“净利润”的TableCell说明问题出在嵌入或查询构造上。此时检查TableCell的嵌入向量是否真的包含了“净利润”语义——用bge-m3.encode(净利润)和bge-m3.encode(12.5亿元)算相似度应0.7。如果0.5说明嵌入策略有问题需调整拼接的结构化字符串。检查DOM结构Structuring如果检索结果里有正确的TableCell但其text字段是“12.5”缺少“亿元”说明结构重建时单位被切分到了另一个元素。打开DOM JSON查找该TableCell的children很可能有一个Sup节点存着“亿元”。这说明我们的DOM树构建逻辑没有把Sup作为TableCell的子节点正确挂载。修复点在_build_dom_tree函数中需增加对Sup、Sub等内联元素的识别与挂载逻辑。检查原始解析Parsing如果DOM里连“12.5”都没有只有“12”和“5”说明PyMuPDF或OCR把数字切开了。这通常发生在PDF中“12.5”是用两个独立的文字框绘制的。此时需在parse_page函数中增加一个“文本框合并”步骤对y坐标差2pt、x坐标连续的相邻文字框强制合并其文本。个人体会我花了整整两周时间才把一份某银行的《个人贷款合同》PDF的解析准确率从63%提升到98%。最大的收获不是代码而是建立了一套PDF健康度检查清单每次新PDF进来先跑一遍pdfinfo、pdffonts、pdfimages -list再用fitz.Page.get_text(dict)看原始结构最后用PaddleOCR的layout结果做交叉验证。这比盲目调参高效十倍。5.3 性能瓶颈与优化实战如何让处理速度提升300%默认配置下处理一份50页的PDF需要47秒A100。优化后降至12秒。关键优化点OCR按需启用90%的PDF是文字型无需OCR。我们在parse_page开头先用page.get_text(text)提取100字符若长度50且无乱码则跳过OCR。这节省了70%的OCR调用。批量处理页面PaddleOCR的layout_analysis模型支持batch inference。我们将连续5页的pixmap打包成一个tensor一次性送入模型GPU利用率从35%提升到89%。XGBoost模型量化用sklearn-porter将XGBoost模型导出为C代码再编译为so库调用速度提升4倍。Python层只做IO计算全在C层。向量数据库预热ChromaDB首次查询慢是因为内存映射未建立。我们在服务启动时主动执行一次collection.peek()强制加载索引。最终这套系统在生产环境4核CPU 1x A10 GPU上稳定支撑每秒3个PDF解析请求平均延迟11.3秒。对于“理解”而言这已经足够快——毕竟人类阅读一份50页的报告也需要20分钟。6. 扩展与集成让PDF理解融入你的工作流6.1 与现有RAG框架LlamaIndex / LangChain的无缝对接你不必抛弃现有的LangChain或LlamaIndex栈。我们的结构化DOM可以作为一个全新的Document类型注入。以LlamaIndex为例from llama_index.core import Document from llama_index.core.node_parser import SimpleNodeParser # 将DOM节点转换为LlamaIndex的Node def dom_to_llamaindex_node(dom_node: dict) - Document: # 构造text对TableCell拼接行:2 列:1 | 12.5亿元 if dom_node[type] TableCell: text f行:{dom_node[row_index]} 列:{dom_node[col_index]} | {dom_node[text]} else: text dom_node[text] # 构造metadata包含所有结构信息 metadata { node_type: dom_node[type], context_path: .join(dom_node.get(context_path, [])), page_number: dom_node.get(page_number, 0), bbox: dom_node.get(bbox, []) } return Document(texttext, metadatametadata) # 构建Nodes nodes [dom_to_llamaindex_node(node) for node in dom[children]] parser SimpleNodeParser.from_defaults(chunk_size512) llama_nodes parser.get_nodes_from_documents(nodes) # 后续流程完全不变 index VectorStoreIndex(llama_nodes) query_engine index