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… 流程也是:
- 第一步先把视频里说的话转成字幕;
- 第二步按帧把不同字幕截出来,拼成一大段文本;
- 最后让 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 表格怎么切?「主体-属性」断裂
举个例子:一个表格是关于一个主体的属性列举,比如一张冰箱的各种详细属性表。
由于表格够长,常规做法难免会切一刀。这一刀切下去的问题就是:后面的属性被单独存下来,变成了无主体、只有属性的描述。
如果后面这个表格返回只有属性的介绍、没有主体的 chunk 直接给到 LLM,鬼知道这是干什么用的,给谁的属性……
所以做事一定要先把「信息给自己看,能不能成立」放在自己身上,逻辑走得通再往下进行。
目前的父子分片或层级分片可以缓解这块的缺陷:检索回来下面行的栏当作子片,最终返回的还是总片。后面我会讲我具体怎么处理。
3.2 段落怎么切?别把目录结构丢掉
段落怎么切一直是难点。我们一定要回到人的视角:人是怎么能知道这段落在讲什么的?一定是从自己的视角出发。
人类读一份 docx 或者 pdf 会怎么做?肯定先看目录。
如果没有目录,直接看一大堆文字我绝对看不下去;如果要硬看下去,肯定会丢一些关系,对这个文档的理解也没有那么深。
比如上面展示的 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 模型处理。原因很直白:
-
OCR 模型非常强:可以把它看作一个「除了图片提取不够强」的多模态模型,而且幻觉率真的低。
- 提取文字非常强;
- 提取表格非常强(遇到过那种长长的账单表格,不管竖着排还是横着排都能提取);
- 提取文件层级(一级/二级/三级标题都没问题);
- 提取图片位置、甚至把某块图片抠出来,都非常强(如果 OCR 处理图片不好,那就交给 VLM 模型去干,哈哈)。
公司里都是先用 MinerU 去跑,现在 MinerU 又更新版本了,我之前还没试过,新版本肯定更强。现在 MinerU 处理图片时还在积极融合 VLM 模型,是我理想中的多模态模型方向。
-
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:
6.1 MinerU 输出的四种东西
MinerU 的返回包通常包含:
- 抠出来的图片文件夹;
- 页面级细粒度元素结构化列表;
- 版面分析 + OCR 识别的模型中间输出文件;
- 全量中间处理结构化数据;
- 标准 Markdown 格式的最终可读文档。
能一次性输出这么多文件,是因为 MinerU 的处理是一条完整链路:
原始 PDF
▼
模型识别计算(_middle.json / _model_output.txt 中间结果)
▼
元素拆分标注(_content_list.json)
▼
最终可读文档(.md / 全文文本)
从底层技术数据到上层可读内容逐层输出,既能满足普通用户的阅读需求,也能满足开发者的二次开发需求。
6.2 _middle.json 全字段拆解
全量中间处理结构化数据是 MinerU 解析流程里输出的最核心文件。它完整记录了 PDF 从版面检测、OCR 识别到内容结构化的全部底层原始数据,是生成最终 .md 文档、_content_list.json 的数据源。
相比其他输出文件,它的信息维度最全、保留的原始特征最多。下面按结构层级逐字段拆解,并结合《医用低温保存箱说明书》的场景举例:
每个元素的公共字段如下:
| 字段名 | 含义说明 | 取值示例 |
|---|---|---|
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; - 渲染规则:
- 先按
row_idx排序,整理出每一行的所有单元格; - 第一行(is_header=true)作为表头,生成
| 表头1 | 表头2 |; - 第二行生成分隔线
| --- | --- |; - 后续行依次生成表格内容行。
- 先按
- 对应你的说明书:技术参数表、装箱单表格,都是从单元格级结构化数据直接拼成标准 Markdown 表格的。
5. 图片内容:figure 元素 → Markdown 图片引用
- middle.json 里:图片元素
category: "figure",带path和caption; - 渲染规则:转换成
的 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 定位问题根因:
- 先在 Markdown 里找到错误内容的位置,确定属于哪个章节、哪一页;
- 打开 middle.json 对应
page_idx的页面数据; - 找到对应元素,查看原始 OCR 文本、置信度 score、元素边界 bbox;
- 判断是 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:图片素材批量导出
需要提取文档里的所有产品图、原理图、示意图时:
- 从
figure_dets里获取所有图片的本地路径和对应图注; - 批量导出所有图片文件;
- 同时可以对应到 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 结构化:
先看第一步 doc 结构化切分:
看截图,你能看到:清晰的每个段落的结构 title,都固定写在段落正文的前面。
类似下面的形式——不管正文多长,token 量多大,先这样把结构信息存起来:
Doc 16/59: len=1304, titles=['斜槽快速发药系统', '快速发药机(斜槽型)', '2.2.3 补药模组']
--- 内容开始 ---
段落正文
--- 内容结束 ---
第二步,把前面大的段落正文变成小的 chunk:
类似这样的结构:
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 真的切得又完整又准确:
想想如果这个表格很长很长,怎么切分呢:
- 如果丢掉标题"六、基本技术参数";
- 如果丢掉本文主体"快速发药机(斜槽型)"——
完全一团乱麻了。只有一个"出药速度 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 瓶颈情况,后面看看我的方法能不能做一个很好的解答:
能看到这是三个复杂的 pdf 文档,平均下来都是 20 页,里面都是针对"医用低温保存箱"这个实体去进行的参数介绍。其中每篇都是针对名字叫低温保存箱、但是多种不同型号的分列介绍,并且涉及了 24 种参数型号:
可以看到上面是针对这些参数回来的表格,三份文档有同样的表格结构。
8.1 跨文档对比:业务人员想做的智能 Agent
针对上面这三个文档,简单列举几个问题,也是业务人员希望做成的智能 Agent:
- 医用低温保存箱有哪些型号呀?
- 医用低温保存箱各种型号的尺寸、额定电流、耗电量、温度的范围分别都是多少呀?
- 对于型号 DW-30L818、DW-86L829W 和 DW-86L579BPT,这三种的重量都是多少呀?
- DW-30L818 和 DW-86L579BPT 怎么安装呀?
三个文章的安装步骤段落:
上面三张图是三个 pdf 文档中"安装步骤"章节的介绍。
okok,哈哈哈,就简简单单几个小问题。(传统 rag 检索方法大家都是:bm25 关键词匹配 + 向量匹配 + rerank 这些策略)已经给传统 rag 干冒烟了都,我们看看传统 rag 回来的 chunk 片内容大概是怎么样子的。
8.2 传统 RAG 逐题失败分析
问题 1:医用低温保存箱有哪些型号呀?
古法 rag 回来的内容思路就是:先拆分"医用低温保存箱""型号",可以在观察刚刚那三个表格:
对于这种的可以检索回来,因为这个表格给你写得很好,就是"医用低温保存箱",也有"型号"两个字。这是一个写得比较规范一些的表格了。
但是对于这种呢?大多数我们经常碰到的表格还是这种的——无头的、无主体的表格。你检索怎么检索?没有"医用低温保存箱"这个主体,只有个"型号"。但是我们知识库里面一堆一堆的产品型号,还有可能把"医用低温保存箱"由于关键词匹配不上时,匹配的是冰箱的型号。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 怎么安装呀?
这个不是表格类型问题,可以再看看图:
三个文章的安装步骤段落:
这就是目前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. 开源项目展示
还有下篇文章在努力编写了,我会去讲讲我自己搭建的开源项目(全网唯一思路,上面的问题都有在解决和表示),是怎么解决这一系列问题的。先贴几张图吧,卖个关子,希望大家持续关注我的项目和文章,谢谢大家~
项目名称叫:
能主动搜并且看完的,绝对都是真实做过项目的大佬们和想技术进步的并行者。如果你们有自己想解决的相关问题也可以下面留言,一起加油进步,迎接 AGI 的到来。
最后希望大家可以帮我秋招内推内推...