XingGraph 前篇:在企业知识库 RAG 里踩过的坑 (少AI,拒绝敷衍)

76 阅读35分钟

XingGraph()

实习做了几个月的企业级知识库RAG,这篇文章是很多坑点和以后的思考。 这篇文章把我对「文件怎么切、chunk 怎么切、MinerU 怎么用 RAG有啥问题」的真实想法和落地做法分享出来。 希望大家可以支持下后面开源的项目


0. 引子:企业知识库的 RAG,大家大概是这么做的

目前企业知识库的 RAG,老登的普遍做法大概都是这样的:

先处理文档格式,再切 chunk,然后 bm25 + 向量混合检索 + rerank。一套下来看起来挺完整,但真正做过真实项目和真实文件的人,一眼就能看出来哪里有问题。这篇文章就从「文档处理」讲到「chunk 切分」,最后再讲讲 MinerU 这套工具链。

1. 文档格式处理:先把所有格式变成文本

现实中需要查询的资料有各种形式(doc、docx、ppt、pdf、png、jpg)。目前多模态模型还不够强,指望 Kimi3 这种很强大的多模态模型,也做不到分析两三白页的 PDF 吧。

所以普遍做法是找 Python 包(比如 python-doc 之类)去处理这些文件:把各种格式都转成纯文本,再用现在比较成熟的 LLM 去处理这些文字。

甚至有的项目在做大模型分析视频时(github.com/browser-use… 流程也是:

  1. 第一步先把视频里说的话转成字幕;
  2. 第二步按帧把不同字幕截出来,拼成一大段文本;
  3. 最后让 LLM 去分析这些字幕。

看似是让大模型处理视频,实则是让大模型处理字幕。哈哈哈哈这太有意思了。

总之,因为多模态模型不够强、也有省 token 的想法,所有方向都是尽量靠近「现有又便宜又强的纯文本 LLM」。

2. Chunk 切分:各种"烂大街"的套路

现在各种应届生简历上写的都是这些:固定切分、bm25+向量混合检索、rerank 检索。一眼看去就是完全没做过真实项目和真实文件。

2.1 固定切分、父子分片、大中小分层

老套路无非是:

  • 固定长度切分,加 overlap 属性;
  • 好一点的做父-子分片大中小分片

这些思想的来源,其实是因为前期 LLM 的"大脑容量"基本限制在 256k 温度左右。父-子分片、分层分片的初衷,就是为了减少一个大 chunk 的存在而已。

但是这种做法会丢掉语义关系,chunk 和 chunk 的关系完全没建立起来

你可能会说父子分片和大中小分片不是建立了 chunk 的关系吗?但你看它的目的,就只是把一个大的 chunk 劈成多个小的 chunk,这些小的 chunk 都是从同一个大 chunk 出来的,完全没建立起和其他 chunk 的联动。

2.2 Late Chunking:想法很强,现实还不落地

jina-ai/late-chunking: Code for explaining and evaluating late chunking (chunked pooling)

真正有想法的,是 jina 的 late chunking:为了防止上下文两句话断掉语义关系,直接把一大段话先整体向量化,再去切 chunk。

我也试过。鉴于目前 jina3 的发展还是不够,做不到把 800k 长的一大段文字全部交给 jina 这种长向量处理模型。所以中间还是得自己手动对原文切一刀——这不就和原来一样了吗?哈哈。

后面我就把 late chunking 这个优势主动丢了……所以还是等时间吧。

3. 实习中遇到的三连坑

我在实习中遇到很多问题,下面这些是我针对具体场景的思考。如果你还有更多场景,欢迎一起交流。

3.1 表格怎么切?「主体-属性」断裂

举个例子:一个表格是关于一个主体的属性列举,比如一张冰箱的各种详细属性表。

由于表格够长,常规做法难免会切一刀。这一刀切下去的问题就是:后面的属性被单独存下来,变成了无主体、只有属性的描述。

image.png

image-1.png

image-2.png

如果后面这个表格返回只有属性的介绍、没有主体的 chunk 直接给到 LLM,鬼知道这是干什么用的,给谁的属性……

所以做事一定要先把「信息给自己看,能不能成立」放在自己身上,逻辑走得通再往下进行。

目前的父子分片或层级分片可以缓解这块的缺陷:检索回来下面行的栏当作子片,最终返回的还是总片。后面我会讲我具体怎么处理。

3.2 段落怎么切?别把目录结构丢掉

段落怎么切一直是难点。我们一定要回到人的视角:人是怎么能知道这段落在讲什么的?一定是从自己的视角出发。

人类读一份 docx 或者 pdf 会怎么做?肯定先看目录。

如果没有目录,直接看一大堆文字我绝对看不下去;如果要硬看下去,肯定会丢一些关系,对这个文档的理解也没有那么深。

image-4.png

image-5.png

image-6.png

image-17.png

image-18.png

image-20.png

image-21.png

image-22.png

比如上面展示的 pdf 或者 docx,结构非常清晰、目录一目了然。这种情况怎么切分 chunk?

大部分人的做法是简单按固定 chunk 大小硬切,把这种很清晰的段落结构丢掉了。明明这么完整的一份文件,连目录都给你保留出来了,切 chunk 的时候还要把它丢掉。

结果就是:

  • 后面碎掉的段落 A 和段落 B 被瞎塞到一起;
  • 或者段落 C 因为太长被拆成十几份 chunk;
  • 检索时只会命中其中一个小 chunk,这个 chunk 属于哪个段落?不知道;它代表什么内容?也不知道。只知道它是一个 chunk。

所以这段我想表达的是:对于这种长文本、且已经把目录结构(这段到底在讲什么)整理好的文件,不要无脑直接拆。否则只会切成满满的没有主题、没有段落关联的碎 chunk。

3.3 页码怎么保留?无脑 PDF + OCR

页码的保留,用 pdf + OCR 就能做到。但在我尝试里,docx、doc、ppt 都会遇到问题:

人类看到一份 docx,以为它是一份 docx 文件,实际上它的内容是一个 XML 文件。在不同软件里渲染效果都有差别——WPS 和 Word 打开同一份 docx 都会出偏差,要么图片乱飞,要么段落字体或格式没保留住。

所以如果要保留页码,无脑选 PDF 就好了。按照下面说的 MinerU 的处理方式,会把一份原始文档切出各种"佐料"(比如剥离出来的图片、定位、页码)——页码都会保留下来。

4. 为什么说 PDF 是最好的载体

目前来看,最好的方式就是拿出 PDF 来喂给 OCR 模型处理。原因很直白:

  1. OCR 模型非常强:可以把它看作一个「除了图片提取不够强」的多模态模型,而且幻觉率真的低。

    • 提取文字非常强;
    • 提取表格非常强(遇到过那种长长的账单表格,不管竖着排还是横着排都能提取);
    • 提取文件层级(一级/二级/三级标题都没问题);
    • 提取图片位置、甚至把某块图片抠出来,都非常强(如果 OCR 处理图片不好,那就交给 VLM 模型去干,哈哈)。

    公司里都是先用 MinerU 去跑,现在 MinerU 又更新版本了,我之前还没试过,新版本肯定更强。现在 MinerU 处理图片时还在积极融合 VLM 模型,是我理想中的多模态模型方向。

  2. PDF 天生保留页码:docx、ppt 我试过都很难保留页码。所以一旦发现 python 包处理 docx、doc、ppt 都一般,直接把原文件存成 PDF 再丢给 OCR,效率最高。

5. 核心思想:先带入 LLM 视角,再谈优化

整篇文章我一直会强调一个要点:要把企业级知识库做好,不管遇到什么问题,先把 LLM 代入你自己的视角。

你是自己读到这块 chunk 都提炼不出什么信息,那就别怪 LLM 笨——再聪明的 LLM 也做不到无中生有。

所以如果大家发现 LLM 怎么给我的回复怎么这么差劲。不妨在把数据交给 LLM 之前,先把 LLM 拿到的输入、上下文存档。如果我自己看到这段上下文,我会骂街的,然后就这些内容让我做任务,谁能做好。

这时候 LLM 和人类唯一的区别就是:人类能骂街,LLM 只能憋着一句"处理不出来"。所以不管做哪个环节,都要拿起自己的视角:你自己怎么读懂一份文件的,就把这个方法教给 LLM。LLM 的文本处理能力,绝对不比人差。

6. MinerU:一份文档处理链路走通

MinerU 的输出格式比方法很结构化、很完整,需要根据你自己的任务去调整对它输出的处理方式。

先说一个很多人忽略的关键点:MinerU 返回的格式非常结构化和完整,需要根据自己任务的不同去调整怎么处理这份 MinerU 的内容。

比如拿一份 PDF 给 MinerU:

image-7.png

6.1 MinerU 输出的四种东西

MinerU 的返回包通常包含:

  1. 抠出来的图片文件夹
  2. 页面级细粒度元素结构化列表
  3. 版面分析 + OCR 识别的模型中间输出文件
  4. 全量中间处理结构化数据
  5. 标准 Markdown 格式的最终可读文档

image-9.png

能一次性输出这么多文件,是因为 MinerU 的处理是一条完整链路:

原始 PDF
   ▼
模型识别计算(_middle.json / _model_output.txt 中间结果)
   ▼
元素拆分标注(_content_list.json)
   ▼
最终可读文档(.md / 全文文本)

从底层技术数据到上层可读内容逐层输出,既能满足普通用户的阅读需求,也能满足开发者的二次开发需求。

6.2 _middle.json 全字段拆解

全量中间处理结构化数据是 MinerU 解析流程里输出的最核心文件。它完整记录了 PDF 从版面检测、OCR 识别到内容结构化的全部底层原始数据,是生成最终 .md 文档、_content_list.json 的数据源。

相比其他输出文件,它的信息维度最全、保留的原始特征最多。下面按结构层级逐字段拆解,并结合《医用低温保存箱说明书》的场景举例:

image-10.png

image-11.png

image-12.png

image-16.png

image-15.png

image-14.png

每个元素的公共字段如下:

字段名含义说明取值示例
category_id元素类型的数字编码0=正文,1=一级标题,2=二级标题,5=表格,6=图片,7=页眉,8=页脚
category元素类型的文本描述text正文 / title标题 / table表格 / figure图片 / header页眉 / footer页脚
bbox元素在页面上的坐标边界框格式为 [x1, y1, x2, y2],代表元素左上角和右下角的归一化坐标
score模型对该元素类型识别的置信度0~1,越高越准,例如 0.97
order人类自然阅读顺序的序号从 0 开始递增,代表从左到右、从上到下的阅读顺序
text该元素对应的 OCR 识别文本内容仅文本/标题类元素有,例如"产品特点"

结合说明书内容,最常见的元素类型及特有信息如下:

1. 标题类元素(title):对应说明书里的"注意事项""产品特点""产品安装"等章节标题。

特有字段:

  • level:标题层级,1级=大章节主标题,2级=子章节标题,对应 Markdown 里 ###

示例:

{
  "category": "title",
  "level": 1,
  "text": "注意事项",
  "bbox": [0.03, 0.05, 0.12, 0.09],
  "order": 0
}

2. 正文文本类元素(text):对应段落说明、条款内容、操作步骤等普通文字,是占比最高的元素类型。

特有字段:

  • lines:逐行拆分的 OCR 原始结果,每行包含行级坐标、行文本、行内拆字的置信度。
  • 作用:保留了换行、行距等排版细节,最终 Markdown 的段落合并就是基于此字段生成的。

示例:说明书里 本产品用于提供-40℃~-86℃储存环境… 这段正文,会被拆成多行 OCR 结果,最终合并成完整段落打到 md 里。

3. 表格类元素(table):这是 _middle.json 最核心的价值部分——它不是简单的文本拼接,而是保留了单元格级的拓扑结构,这是 _content_list.json 和最终 md 都不具备的底层信息。

表格顶层字段:

字段名含义说明
table_id表格在本页的唯一编号
row_count表格总行数
col_count表格总列数
has_header是否包含表头(true/false)

核心子字段 cells(单元格数组),每个单元格都包含独立的结构信息:

字段名含义说明
row_idx单元格所在行号,从 0 开始计数
col_idx单元格所在列号,从 0 开始计数
row_span跨行数,普通单元格为 1,合并单元格大于 1
col_span跨列数,普通单元格为 1,合并单元格大于 1
bbox单元格自身的坐标边界框
text单元格内的 OCR 识别文本
is_header是否为表头单元格(true/false)

对应说明书里的型号技术参数表:

  • row_count = 6(1 行表头 + 5 行型号数据);
  • col_count = 6(型号、有效容积 → 最大功率、稳定运行功率、重量、外形尺寸);
  • 第一行所有单元格 is_header = true
  • 每个型号(如 DW-86L579BPT)对应一行,每个参数对应一个独立单元格,都有精确的行号列号。

4. 图片类元素(figure):对应产品结构图、控制面板示意图、制冷原理图、安全标识图标等。

特有字段:

  • figure_id:图片在本页的唯一编号;
  • image_path:MinerU 自动提取出的图片本地保存路径;
  • caption:图注 / 图片说明文字(如"图 1 冷却水连接示意图")。

示例:说明书里的三色状态灯板示意图、电控箱开关示意图,都会被识别为 figure 元素,同时自动把图片提取成本地文件并记录路径。

5. 页眉 / 页脚元素(header/footer):对应每页的页码、品牌标识、底部版权文字等。

  • 特点:会被明确标记为非正文内容,生成最终 Markdown 时会被排除在正文主体之外。
  • 示例:每页底部的 "38""39" 页码、"海尔生物医疗" 页脚小字。

6. 列表类元素(list):对应"1. xxx""・xxx"这类有序/无序列表。

特有字段:

  • list_type:ordered 有序 / unordered 无序;
  • items:列表项数组,每个项有独立的文本和层级。

示例:安全注意事项里每条带 "・" 的条款、安装步骤的 1.2.3. 步骤,都会被识别成列表结构并保留层级。

6.3 结合说明书看 _middle.json 的典型内容

结合这份《医用低温保存箱使用说明书》,_middle.json 里会有这些典型数据:

  • 封面页:识别出"合格证""Haier Biomedical""医用低温保存箱使用说明书"等不同层级标题,品牌 logo 识别为图片;
  • 目录页:识别为有序列表结构,每条目录对应章节名和页码;
  • 注意事项页:多个无序列表项,每个安全条款一个文本块,安全警示图标识别为图片;
  • 参数表格页:完整的单元格级表格结构,装箱单、技术参数表都保留精确的行列关系;
  • 原理图页:制冷原理图、接线图识别为图片元素,附图表注文字和提取的图片文件路径。

6.4 这个文件的核心价值

  • 信息最完整:包含版面坐标、识别置信度、单元格拓扑、图片路径等所有底层原始信息,比最终 Markdown 和 content_list 维度更全;
  • 支持二次开发:可以基于它自定义提取指定表格、按页面区域提取内容、批量导出所有图片、转换成自定义格式;
  • 可用于排查问题:如果最终文档有识别错误,可以回溯到 middle.json 查看原始 OCR 置信度、版面检测是否准确,定位错误根因。

6.5 标准 Markdown 最终文档 .md

这份 .md 是 MinerU 整个解析链路的最终面向用户的产物,目标是把 PDF 完整还原成可编辑、可阅读、格式统一的纯文本文档,纯粹服务于人的阅读和二次编辑。

还原质量方面,结合这份说明书它做到了:

  • 完整的章节层级:严格按照原始 PDF 的目录结构,还原了「注意事项 → 产品特点 → 产品安装 → 使用方法 → 清洁维护 → 保修说明」的完整全书结构,一级/二级子章节层级分明;
  • 表格完整转换:所有技术参数表、装箱单、故障排查表、报警类型对照表都转换成标准 Markdown 表格,行列对应关系和原 PDF 一致,不需要手动重新排版;
  • 文本内容全量保留:从安全警示条款、操作步骤、参数数值到注意事项说明,所有正文文字都完整按出;
  • 列表格式还原:原 PDF 的有序/无序列表、带层级的说明项,都转换成 Markdown 对应的列表语法。

格式规范特点

  • 标题分级:用 ###### 对应原文档章节层级,生成标准文档大纲;
  • 表格:采用标准 | 列名 | 列名 | Markdown 表格语法,支持直接复制到 Notion、Obsidian、语雀、Typora 等所有 Markdown 工具中正常渲染;
  • 段落与空行:按照原文档的段落逻辑自动分段,段落间保留空行,符合中文阅读习惯;
  • 特殊内容:加粗、引用等格式会根据原文档的排版做对应转换,重点内容保留强调效果。

6.6 middle.json → Markdown:渲染规则的对应关系

上面这两个产物是最能拿出来直接用的两个。一句话总结:

middle.json 是生产原料,Markdown 文档是加工后的成品;Markdown 里所有内容,都来自 middle.json 的结构化数据,是经过一套「渲染规则」转换后的产物。

转换核心链路:

原始 PDF
  → 版面检测 + OCR 识别
  → 生成 middle.json(全量结构化数据)
  → 执行「Markdown 渲染规则」
  → 输出最终 .md 文档

下面按 middle.json 的字段,拆解每一步转换的对应关系:

1. 标题层级:category+level → markdown # 号

  • middle.json 里:每个标题有 category: "title" + level: 1/2/3(一二三级标题);
  • 渲染规则:level=1 → # 标题,level=2 → ## 标题,以此类推;
  • 对应你的说明书:「注意事项」是 # 一级标题,「安全标签」是 ## 二级标题,和原 PDF 排版层级完全对应。

2. 正文段落:text + lines → 自然段落

  • middle.json 里:文本块有 category: "text",内部 lines 数组是逐行 OCR 结果;
  • 渲染规则:按 order 阅读顺序,把相邻正文行合并成一个完整段落,遇到换行/分段自动拆分;
  • 对应你的说明书:安全注意事项里的长段说明文字,就是由多个行级 OCR 结果合并成的完整段落。

3. 列表内容:list 类型 → 有序/无序列表

  • middle.json 里:列表元素 category: "list"list_type 区分有序/无序,items 是每个列表项;
  • 渲染规则:有序列表转 1. 内容,无序列表转 - 内容,多层级自动缩进;
  • 对应你的说明书:安装步骤的 1/2/3、安全条款的・项,都是通过这个逻辑转换来的。

4. 表格内容:cells 单元格矩阵 → Markdown 表格

  • middle.json 里:表格元素有完整的 cells,每个单元格带 row_idx/col_idx/text/is_header
  • 渲染规则:
    1. 先按 row_idx 排序,整理出每一行的所有单元格;
    2. 第一行(is_header=true)作为表头,生成 | 表头1 | 表头2 |
    3. 第二行生成分隔线 | --- | --- |
    4. 后续行依次生成表格内容行。
  • 对应你的说明书:技术参数表、装箱单表格,都是从单元格级结构化数据直接拼成标准 Markdown 表格的。

5. 图片内容:figure 元素 → Markdown 图片引用

  • middle.json 里:图片元素 category: "figure",带 pathcaption
  • 渲染规则:转换成 ![图注](图片路径) 的 Markdown 图片语法;
  • 对应你的说明书:产品结构图、制冷原理图、控制面板示意图,会以图片引用的形式插入到 Markdown 对应位置。

6. 过滤与清洗:剔除页眉页脚

  • middle.json 里会标记 category: "header"(页眉)和 category: "footer"(页脚)的元素;
  • 渲染规则:生成 Markdown 时直接过滤这些非正文内容,只保留主体正文,保证文档干净;
  • 对应你的说明书:每页底部的页码、顶部的品牌小字都会被过滤,不会出现在最终 Markdown 里。

6.7 两者如何结合使用

这两个文件定位完全不同,搭配起来可以覆盖「日常阅读 → 内容校验 → 数据提取 → 定制化输出」的全场景。

场景 1:日常阅读 & 编辑——只用 Markdown

普通查阅、整理文档内容、做笔记归档的时候,直接用 Markdown 即可。格式清晰、打开即用,完全满足日常阅读和编辑需求,不需要碰底层的 middle.json。

  • 对应场景:查阅低温保存箱的参数、操作步骤、保修条款,直接打开 Markdown 搜索、复制、标注。
场景 2:识别错误排查——用 middle.json 回溯

当你发现 Markdown 里有识别错误(参数数字错了、表格串行、文字缺漏),用 middle.json 定位问题根因:

  1. 先在 Markdown 里找到错误内容的位置,确定属于哪个章节、哪一页;
  2. 打开 middle.json 对应 page_idx 的页面数据;
  3. 找到对应元素,查看原始 OCR 文本、置信度 score、元素边界 bbox;
  4. 判断是 OCR 识别错误,还是版面分析把元素拆分错了,或者表格行列判定错误。

举个例子:如果 Markdown 里型号 DW-86L579BPT 的参数写错了,去 middle.json 找到对应表格的对应单元格,查看原始 OCR 文本和置信度,就能知道是识别偏差,还是原 PDF 本身印刷问题。

场景 3:精准批量提取数据

从 Markdown 直接复制粘贴效率低还容易错,正确的做法是:

  • 从 middle.json 里直接读取 table_dets 的单元格数据,按行列导出成 Excel/CSV,或写入数据库;
  • 把提取后的结果再写入 Markdown,整理成规整的汇总内容。
  • 对应:想做"全型号参数对比汇总表",不用手动从 Markdown 抄 6 个型号的参数,直接从 middle.json 表格结构数据里批量导出,又快又准。
场景 4:定制化文档排版

默认 Markdown 是通用标准的,如果你有特殊排版需求(标题加粗、表格加备注、只保留指定章节、调整章节顺序),可以基于 middle.json 自定义渲染:

  • 只需"技术参数 + 故障排查"两部分,就从 middle.json 里筛选对应章节,只渲染这两部分;
  • 要把所有安全注意事项改成红色加粗提示,可以基于元素类型批量加格式,不用手动 1 个 1 个改。
场景 5:图片素材批量导出

需要提取文档里的所有产品图、原理图、示意图时:

  1. figure_dets 里获取所有图片的本地路径和对应图注;
  2. 批量导出所有图片文件;
  3. 同时可以对应到 Markdown 里的上下文位置,知道每张图属于哪个章节。

比如做培训 PPT 要用到控制面板示意图、制冷原理图,直接从 middle.json 批量提取,比从 PDF 里截图清晰度更高,还自带图注。

场景 6:跨文档定位 + 原文溯源

做「点击文档跳转到对应 PDF 页面」的交互,或知识库的原文溯源时:

  • Markdown 负责展示可读内容;
  • middle.json 提供每个元素对应的 page_idx(页码)和 bbox(位置),给每个段落/表格加上原文锚点,点击就能跳到 PDF 对应页面的对应位置。

7. 我是怎么用上这些原材料的

上面介绍的都是 MinerU 处理完一份文档会给到什么原材料、怎么去用它。怎么用这堆切好的菜去做饭,考验的是工程师自己的技术和业务场景判断。

真的非常强大。不知道以后多模态模型怎么发展,如果能完整复刻现有能力都十分了。但鉴于 transformer 的遗忘属性:大脑容量又能有多大;处理时长又得多长;随着文档长度要不要动态调整……目前最优选择还是 minerU 之类的 OCR 模型。哈哈哈哈。

7.1 文章段落处理:doc 结构化 + chunk 结构化

回到之前说的:对于文档里很清晰的目录结构,切 chunk 的时候要保留结构信息。

我处理文档的思路分两步:第一步 doc 结构化,第二步 chunk 结构化

整体流程:

Markdown 文件
    │
    ▼
Step 1: MarkdownExtractor.extract()
    │  按标题切分为 Document[]
    │
    ▼
Step 2: RecursiveCharacterTextSplitter.split_documents()
    │  把每个 Document 再切成 Chunk[]
    │
    ▼
输出:Document[](每个 chunk 带标题上下文)

先说 doc 结构化:

image-25.png

image-53.png

image-27.png

先看第一步 doc 结构化切分:

image-28.png

image-29.png

image-31.png

看截图,你能看到:清晰的每个段落的结构 title,都固定写在段落正文的前面

类似下面的形式——不管正文多长,token 量多大,先这样把结构信息存起来:

Doc 16/59: len=1304, titles=['斜槽快速发药系统', '快速发药机(斜槽型)', '2.2.3 补药模组']
--- 内容开始 ---
段落正文
--- 内容结束 ---

第二步,把前面大的段落正文变成小的 chunk:

image-32.png

image-33.png

类似这样的结构:

Chunk 68/76: len=229, titles=['斜槽快速发药系统', '快速发药机(斜槽型)', '4.4.2 手动出药'], has_sep=True
--- 内容开始 ---
chunk内容
--- 内容结束 ---

那这里的 chunk 也是会保留自己的目录行的,包括整个文件的题目、当前章节的标题目录。

有人会问了:为什么 chunk 还要再把前面提取出来的 doc 再切一遍?目的就是学习父子分片的思想:

  • 这里的 chunk 就相当于子片部分,长度自己可以调整,一般来说一个小 chunk 最多卡在 2048 长度这样,然后按照完整格式切一下,不要中间断句或者表格给切断这样;
  • 有了父子分片的思想,后面检索的时候既能保证短小,还能保证检索回整节原文(建立 doc 和他下面分好的 chunk 之间的关系,然后动态拿父片);
  • 又能在保证短小长度的同时,保留住章节结构信息。
  • 为后面让LLM去读取这片chunk的时候,可以去改提示词符合这份改动

一个小小的改动一举多得,防止了以往"看到一个片,不知道他是文章啥章节的内容"的问题,也是在模拟人类读取文章:先确定段落结构,再去读段落,更好理解了。

7.2 表格:先把主体找回来

回到 3.1 说的表格问题:主体和后续属性内容断裂之后再处理,语义关联会丢,形成零散的、只有属性、不知道在描述什么主体的 chunk。

所以解决方法是:用 MinerU 把整张表格完整拿下来,再让它输出成清晰的 HTML 格式。

先看看 MinerU 切好的表格结构,可以观察到,MinerU 真的切得又完整又准确:

image-34.png

image-35.png

想想如果这个表格很长很长,怎么切分呢:

  • 如果丢掉标题"六、基本技术参数";
  • 如果丢掉本文主体"快速发药机(斜槽型)"——

完全一团乱麻了。只有一个"出药速度 8~10秒/处方",鬼知道这是关于啥的。

所以要保留上面 doc 和 chunk 的切分逻辑:

  • 在 doc 阶段,完整保留整个长表格;
  • 在 chunk 阶段,保留完好表格逻辑的同时,切分时保留好 doc 和 chunk 的关系:用 chunk 能找回来原 doc,并且小 chunk 片也会带上文章主体和本段落标题名。

实际落地的效果就是下面这样:

Chunk 75/76: len=232, titles=['斜槽快速发药系统', '快速发药机(斜槽型)', '六、基本技术参数'], has_sep=True
--- 内容开始 ---
<table>
  <tr><td>外观尺寸(mm)</td><td>4690(L)×2750(W)×2850(H)(主机尺寸)</td></tr>
  <tr><td>电源</td><td>AC380 V±10% 50/60Hz</td></tr>
  <tr><td>功率</td><td>8000W</td></tr>
  <tr><td>储药槽数</td><td>1000-1200种(依据药品实际尺寸)</td></tr>
  <tr><td>储药量</td><td>12000-15000盒</td></tr>
  <tr><td>储药单元</td><td>长度1600mm</td></tr>
  <tr><td>出药速度</td><td>8~10秒/处方</td></tr>
  <tr><td>补药速度</td><td>2000盒/小时</td></tr>
</table>
--- 内容结束 ---

以上就是我目前对于切片这块的思考和实践了。可以看到其实没有什么新东西:利用起来原来的父子分片,只是自己加了「保留好完整的段落结构」这一个改动,对于原来的缺点就已经有很强的补充了。

因为目前在切分时,都是按照这种「先有标题、再有正文」的逻辑,包括后面写提示词的时候,也会把这串结构详细说给 LLM 听。这已经可以说是我目前了解到的最好的分片策略了,有更好的希望大家补充一起学习。

8. Naive RAG 的困境:跨文档场景实测

ok,上面这些都是 Naive RAG 的逻辑,根本没有跳脱出 RAG 的真实困境。大家现在都知道了:这种死板的策略,在单个文档、少量文档、少量 chunk 的情况下表现还可以;但到了海量 chunk、海量段落混淆的时候,回来的 chunk 片简直是一团糟,这怎么能满足生产中做好一个外挂的企业知识库呢。

先真实列举一些遇到过的 RAG 瓶颈情况,后面看看我的方法能不能做一个很好的解答:

image-36.png

image-37.png

image-38.png

能看到这是三个复杂的 pdf 文档,平均下来都是 20 页,里面都是针对"医用低温保存箱"这个实体去进行的参数介绍。其中每篇都是针对名字叫低温保存箱、但是多种不同型号的分列介绍,并且涉及了 24 种参数型号:

image-42.png

image-43.png

image-40.png

image-39.png

可以看到上面是针对这些参数回来的表格,三份文档有同样的表格结构。

8.1 跨文档对比:业务人员想做的智能 Agent

针对上面这三个文档,简单列举几个问题,也是业务人员希望做成的智能 Agent:

  1. 医用低温保存箱有哪些型号呀?
  2. 医用低温保存箱各种型号的尺寸、额定电流、耗电量、温度的范围分别都是多少呀?
  3. 对于型号 DW-30L818、DW-86L829W 和 DW-86L579BPT,这三种的重量都是多少呀?
  4. DW-30L818 和 DW-86L579BPT 怎么安装呀?

三个文章的安装步骤段落: image-44.png

image-45.png

image-46.png

上面三张图是三个 pdf 文档中"安装步骤"章节的介绍。

okok,哈哈哈,就简简单单几个小问题。(传统 rag 检索方法大家都是:bm25 关键词匹配 + 向量匹配 + rerank 这些策略)已经给传统 rag 干冒烟了都,我们看看传统 rag 回来的 chunk 片内容大概是怎么样子的。

8.2 传统 RAG 逐题失败分析

问题 1:医用低温保存箱有哪些型号呀?

古法 rag 回来的内容思路就是:先拆分"医用低温保存箱""型号",可以在观察刚刚那三个表格:

image-47.png

对于这种的可以检索回来,因为这个表格给你写得很好,就是"医用低温保存箱",也有"型号"两个字。这是一个写得比较规范一些的表格了。

image-48.png

但是对于这种呢?大多数我们经常碰到的表格还是这种的——无头的、无主体的表格。你检索怎么检索?没有"医用低温保存箱"这个主体,只有个"型号"。但是我们知识库里面一堆一堆的产品型号,还有可能把"医用低温保存箱"由于关键词匹配不上时,匹配的是冰箱的型号。chunk和chunk没有建立起来关联

“大哥,我要的是那种医院保存的保温箱,给我检索回来个家用冰柜,你就按照他两者都有共有的属性就检索回来了,这怎么落地生产?”

问题 2:医用低温保存箱各种型号的尺寸、额定电流、耗电量、温度的范围分别都是多少呀?

这个问题和上面的类似:如果返回给 LLM 最后分析的 chunk 片能是这三个完整的表格,LLM 肯定能给你分析得明明白白的。

怕就怕是我要的是"医用低温保存箱",检索回来的是"保存箱、低温柜"的别的内容。

还有这个问题的一个难点就是:对于表格,"医用低温保存箱"和"额定电流、耗电量、温度"这三个属性不是属于同一个 chunk 里面。当表格长的情况下,两者被切开了,导致后面检索回来一堆"额定电流、耗电量、温度"——但是我要的主体呢?

给我各种家用电器的额定电流、耗电量、温度,要这些片干什么?你这让 LLM 怎么分析。

问题 3:对于型号 DW-30L818、DW-86L829W 和 DW-86L579BPT,这三种的重量都是多少呀?

和上面的问题其实一样,这个难点是直接看的具体型号 DW-30L818、DW-86L829W 和 DW-86L579BPT,能找到对于这三个型号的表格或者信息内容,原来的rag只能解决这些型号和后面的技术参数共存在一个chunk,如果给拆分开不同的chunk就很难解决了

问题 4:DW-30L818 和 DW-86L579BPT 怎么安装呀?

这个不是表格类型问题,可以再看看图:

三个文章的安装步骤段落: image-44.png

image-45.png

image-46.png

这就是目前rag做不到的,检索回来的信息都是零碎的chunk,不知道这部分是谁的的信息内容,没有一个chunk和chunk的关联性

图片中完全没写这是谁的安装步骤,这是哪个型号的安装步骤呢?就四个大字《安装步骤》给到你,你知道按照哪个安装吗???

换成你肯定骂街了,所以这就是我一直强调的:把自己的视角带入到 LLM 中。如果你也做不到,你怎么指望 LLM 做呢?就算他再聪明,也不能知道他是谁的安装步骤啊。

9. 知识库内容数据增加:业务增量不重切原文

对于技术参数部分,如果明天业务人员想补充一下对于某某型号,多加一个型号参数 DW-30L6666。但是又因为技术文档更新不了或者不好更新、pdf 不好编辑或者别的原因。

(首先,这个业务人员加的这个参数或者新加的能力肯定不能是很大的改动;如果是很强的改动,一大片文章内容的增加只能去改动原文了。)

就比如刚刚那些低温冷藏柜的属性:业务人员想多加一个型号 DW-30L6666,这个型号的功能是和 DW-30L818 的效果是一样的,就是他的各种参数或者安装方法什么的都是一样,只是去多加一个型号别名吧。

总不能在原始的 pdf 里面又重新修改表格,然后在数据库中把原来的 chunk 删掉,又重新把新的 chunk 添加进入数据库吧?这完全不符合逻辑操作。

10. 检索链路展示:数据飞轮

现在企业都在做数据飞轮,这是什么意思?就是要让所有的 agent 链路、让 AI 执行的流程透明化、数据流转可视化。在执行整条链路的时候,可以把整条链路的跳转清晰展示出来,让业务人员能直接看到:

  • AI 到底理解清楚我的意思没?
  • AI 去运转的流程确实是按照我的思路去跑吗?
  • AI 最后在输出我内容前拿到的是什么样子的上下文,才输出这个语句的呢?

能不能在看到这些链路不清晰的地方、或者完全 AI 跑错的场景处,给继续优化,让整个数据的流转用一次减少一次的问题存在,让整个 AI 链路越转越通畅、越用越好用。随着 LLM 能力的提升,再换到更强的模型时,直接能套着我的这套流程和数据,变得更加贴合我的业务场景去运转。

目前搭建的是类似于 langsmith、langfuse 这类的可观测平台。我自己也玩过 phenoix 这个平台,还是有很长远的应用场景的,只是在我部门没部署起来这部分,没见过真正业务中会出什么问题。

这些都是我自己简单的理解呀,是更加理想化的说法。

就拿刚刚上面的例子:如果还是按照以往——简单的切 chunk、然后检索回来 chunk。在询问的时候,这怎么能形成一个链路呀?

比如询问"对于型号 DW-30L818、DW-86L829W 和 DW-86L579BPT,这三种的重量都是多少呀?"检索回来的一堆堆零碎的片,难道真指望开发或者业务人员对这一堆零碎的 chunk 片去慢慢看吗?格式都是乱七八糟的,这是关于这三个型号的内容吗?是包含了我想要的重量信息吗?和我原文一致吗?

这费时费力的,谁也不想去修复和上报,根本转不起来这个数据飞轮。哈哈哈。

11. 结尾反思

后面还有很多问题就不列举了,就是老生常谈的:传统的 rag 没有一个树状链路,没有一个跳跃的链路结构。所有的片是同一层级一把抓,就算父子分片,这也是把大片转小片,大片和大片的关系也没联络起来。

上面这些几种问题,一般浑水摸鱼的开发人员根本没心思慢慢潜心研究到底怎么切片、怎么检索、当前的检索能预估得到什么结果,都不会去考虑。只是简单按照网上大家都这样做、营销号和机构里面都这样教,我就这样搞,能随便交付就行了。

完全没理解到现在的 agent 或者 AI 应用开发已经和传统开发不一样了,不是以往 CRUD 传统的早已模板化的思路。不思进取的很快就被淘汰。

还按照这样的流程去交付,只会搞得:

  • 业务人员觉得 AI 处理实际业务能力一般,开发人能力一般;
  • 开发人员觉得业务复杂,AI 做不了;
  • 甲方一直提需求,售前和产品经理一直被夹在其中,摸不清楚 AI 的能力边界能不能满足;
  • 开发人员只觉得让 AI 干所有天马行空的要求,这做不了那做不了,导致一直保持互相拉扯和对战的地步。

AI 还在进步,业务侧更偏向 AI,谁都想自己系统有 AI 功能。开发人员工作因为 AI 能力,开发交付迅猛增速,还是要多脑子活络一些:可以多主动和业务聊聊,他是怎么分析这块内容的,甲方是想要什么能力让 AI 去做,有没有一个流程示范,然后再把这份能力模拟到自己身上、模拟到 AI 上,这才能做好 AI 应用开发。

这篇文章可以表示我这段时间实习的收获和发现的种种问题,我这里有些是组内做的,有些是自己的尝试。只是一个实习的 AI 小白天马行空的思路,有很多不对的地方希望大家指正呀~

12. 开源项目展示

还有下篇文章在努力编写了,我会去讲讲我自己搭建的开源项目(全网唯一思路,上面的问题都有在解决和表示),是怎么解决这一系列问题的。先贴几张图吧,卖个关子,希望大家持续关注我的项目和文章,谢谢大家~

项目名称叫:

image-54.png

image-55.png

image-56.png

image-57.png

image-58.png

image-59.png

image-60.png

image-61.png

image-62.png

image-63.png

能主动搜并且看完的,绝对都是真实做过项目的大佬们和想技术进步的并行者。如果你们有自己想解决的相关问题也可以下面留言,一起加油进步,迎接 AGI 的到来。

可以看看我的项目,点点star,提提意见~~ xing-kj/XingGraph: Knowledge-graph RAG for LLM agents: ontology-driven graph building (RDF/OWL), structured-doc chunking with title attribution, multi-subject WIKI-completion retrieval, model-hop traversal, traceable answers on Neo4j. Built for traceable, low-hallucination LLM memory.

最后希望大家可以帮我秋招内推内推...