向量检索搞不定 PDF?试试 Docling + 层级树 + Gemma 3.5 Flash 这套结构化方案
- 问题:AI 读不懂复杂文档
- Daya:用文档结构代替向量搜索
- Docling:把 PDF 变成结构化 Markdown
- 层级标题树:结构化的核心创新
- Gemma 3.5 Flash:轻量模型的逆袭
- 测试与调试:TestSprite 的实战
- 完整工作流
- 总结与思考
问题:AI 读不懂复杂文档
你给 AI 一份 PDF 让它分析财务报表,结果数字全是错的。让它总结合同第七条,它给你总结了一条完全不相干的条款。
这是大多数 RAG 系统的通病——不是 AI 能力不够,是检索层面出了问题。
目前主流方案是把文档拆成文本块,转成向量,用余弦相似度匹配。但 PDF 有表格、图表、多栏排版、数学公式。简单文本提取出来就是一团乱麻,向量检索在这种场景下经常找到”看起来相似但实际错误的”内容。
相似度不等于推理。
Daya:用文档结构代替向量搜索
Daya 这个开源项目给出的答案是:不依赖向量搜索,而是构建文档的层级结构。文档不再是扁平的文本集合,而是像课本一样有目录层级。检索时按结构定位,不靠语义相似度猜。
这套方案的核心组件有三个:
| 组件 | 作用 | 来源 |
|---|---|---|
| Docling | PDF 文档解析,保留布局结构 | IBM Research 开源库 |
| 层级标题树 | 文档结构化索引,替代向量数据库 | Daya 项目自建 |
| Gemma 3.5 Flash | 智能推理 + Agent 执行 | Google DeepMind |
Docling:把 PDF 变成结构化 Markdown
Docling 是 IBM Research 开发的开源文档解析库,能把 PDF、Word、PPT 等格式转换为结构化 Markdown,保留表格、图表、标题层级、页面编号等布局信息。
与常见的 PDF 解析器不同,Docling 不会把一段跨栏的文本切成混乱的碎片。它理解文档的视觉布局——哪段是标题、哪段是正文、表格的单元格如何对应、图片在什么位置。
Daya 在此基础上做了增强:通过 VLMEnricherPipeline(视觉语言模型增强管道),自动提取文档中的图表并进行视觉标注,把”一堆像素”变成可检索的结构化描述。
层级标题树:结构化的核心创新
如果说 Docling 解决了”把 PDF 变成干净文本”的问题,层级标题树解决的是”如何高效定位信息”的问题。
实现的核心是Tree Builder,它把 Docling 解析出的扁平内容重建为层级树。我们来拆开看几个关键函数。
页面映射:解决 PPT 转 PDF 的页码偏移
def build_slide_page_map(original_path, processing_path):
"""把 PPT 的幻灯片编号和 PDF 的页码做对齐"""
# 如果是旧版 .ppt,先用 LibreOffice 转成 .pptx
# 计算 PPT 页数和 PDF 页数的偏移量
# 返回 {pdf页码: 幻灯片编号} 的映射字典
PPT 导出为 PDF 时经常产生额外页面(标题页、隐藏幻灯片、备注页),导致页码错位。这个函数通过计算偏移量来建立映射——PDF 第 3 页可能对应幻灯片第 1 页。
标题树生成:按前缀模式自动分层
def build_heading_tree(flat_headings):
"""将扁平的标题列表重建为嵌套层级树"""
# 按前缀类型(纯文本/编号/圆点/小写字母)自动分配层级
# 用栈式扫描确定父子关系
# 遇到顶级纯文本标题时重置层级映射
这个函数的巧妙之处在于不依赖字体大小或标题等级——Docling 并不总提供一致的数据。它通过前缀检测来动态判断层级:编号标题可能是二级,圆点标题可能是三级。一种全新的前缀类型自动分配下一个层级。
每次遇到新的顶级纯文本标题,它会重置前缀层级映射,防止不同章节的标题被错误嵌套。
内容补全:多级回退查文本
def build_ideal_output(tree_headings, section_texts, total_pages, figures):
"""为每个标题节点补充正文内容"""
# 递归遍历标题树
# 多级回退找文本:
# 1. 优先用 VLM 标注(图表描述)
# 2. 次优用节点直接文本
# 3. 按页面范围从 Markdown 中找
# 4. 最后从临近页面模糊匹配
# 图表作为子节点插回标题树
这套回退逻辑确保了即使 Docling 的解析不完美(比如某个标题下的文本没被正确提取),系统仍有办法找到内容。
最有意思的设计是图表插入逻辑:VLM 标注后的图表节点不是作为孤立的根节点存在,而是通过 doc_order 找到最近的父标题,作为子节点挂入标题树。这样”Figure 1”就自然地属于它出现的章节之下。
Gemma 3.5 Flash:轻量模型的逆袭
Google DeepMind 的 Gemma 3.5 Flash 在这个场景中扮演推理引擎的角色。
从历史定位来看,Flash 系列一直是”快但不够聪明”——适合快速生成草稿,复杂推理要交给 Pro。但 Gemma 3.5 Flash 在几个关键指标上发生了逆转:
- MCP Atlas(Agent 评测):83.6%
- Terminal-Bench 2.1(编码评测):76.2%
- 两项均超越了 Gemma Pro
对于一个轻量模型,这意味着它能在保持速度的同时进行复杂的多步推理——不需要在速度和质量之间二选一。
更重要的是,一个模型同时搞定速度和智能,就不需要在不同模型之间切换了。这个设计成本归零的价值,比 benchmark 数字大得多。
测试与调试:TestSprite 的实战
项目作者在开发过程中遇到了几个棘手的问题:ChromaDB 的 SQLite 锁定问题、Streamlit 重启后索引丢失、Python set 推导式导致的检索逻辑错误、Markdown 代码块导致的 JSON 解析失败。
这些 Bug 暴露了本地 RAG 系统常见的可靠性问题——解决它们需要的不只是 LLM 的编码能力,还要有人-in-the-loop 的调试工具链。
作者用 TestSprite 进行了系统化的测试和修复:它能自动生成测试代码、项目描述、代码总结,定位问题所在并给出修复建议。这展示了现代 AI 编程的一个趋势:不再靠一个人硬啃所有 Bug,而是让 AI Agent 帮你筛查和修复。
完整工作流
整个 Daya 方案的端到端流程可以概括为:
输入文件(PDF/PPT/DOCX)
↓
LibreOffice(非 PDF 格式自动转换)
↓
Docling + VLMEnricherPipeline(文档解析 + 视觉标注)
↓
build_heading_tree(提取标题 → 重建层级树)
↓
build_ideal_output(内容补全 + 图表插入)
↓
ChromaDB(向量存储,用于语义检索兜底)
↓
Gemma 3.5 Flash(推理 + 生成答案)
其中层级树的 JSON 结构和按页面标记的 Markdown 是检索的核心数据源。检索时,先基于语义搜索在 ChromaDB 中找到相关块,再通过页面引用从标题树中拉取更大上下文,最后交给 Gemma 推理生成答案。
总结与思考
谁应该关注这套方案
如果满足以下条件之一,Daya 方案值得试:
- 金融/法律/医疗——文档中包含大量表格、图表、跨页引用,对检索准确性要求高
- 企业内部文档——不是标准格式的 HTML 或纯文本,而是大量 PDF、PPT、扫描件
- 对幻觉敏感——向量检索的”看起来相关但实际不对”在这种场景下不可接受
对比传统方案的差异
| 维度 | 标准 RAG(向量) | Daya(结构化) |
|---|---|---|
| 文档处理 | 简单文本分块 | 布局感知 + 视觉标注 |
| 检索方式 | 余弦相似度 | 层级结构定位 + 语义兜底 |
| 图表处理 | 丢失 | VLM 标注后结构化 |
| 页码引用 | 无 | 精确到页面 |
| 多栏/公式 | 易出错 | Docling 原生支持 |
| 实现复杂度 | 低 | 中等 |
个人看法
做 RAG 的人往往陷入一个思维定式:”向量数据库是标配”。但实际工程中,最可靠的信息定位方式不是数学相似度,而是文档本身的结构。
如果你打开一份 PDF,你是去搜关键词、看目录、翻页码,而不是做”语义匹配”的——文档被设计成有结构是有原因的,为什么不把这种结构用到检索中?
Daya 的核心洞察就在于此。它不是要替代向量搜索,而是把结构检索作为第一优先级,语义搜索作为兜底方案。对于文档规整的场景(PDF/法律文件/合同/论文),这比纯向量方案可靠得多。
当然,这套方案对文档质量有要求——太乱的扫描件、没有明确层级结构的文档,效果会打折扣。但我认为对于大部分企业场景,结构化永远优于”盲匹配”。
代码已开源:https://github.com/RoneyBABA/DAYA