从零手搓 RAG 之文档解析技术

0 阅读9分钟

前言

上一篇,我们把 RAG 的整条链路串了一遍,推荐大家点下面链接阅读 juejin.cn/post/769635…

这篇文章主要讨论文件资料,包括 PDF、Word、PPT、Excel 等格式。后面会重点介绍 PDF 的解析,以及 FastRAG 是怎么对接 Docling 和腾讯云,将解析结果统一为 Markdown。

各种格式的解析方法

格式的差异首先体现在读取方式上。如果每一种格式单独编写一套解析逻辑,代码会变得难以维护。Python 生态有比较成熟的做法,文档加载器(Document Loaders),感兴趣自己研究吧。

FastRAG 把同一个抽象换成了 Go 的写法,解析端口上只有三个方法:

代码:domain/interfaces/parser.go

type IParser interface {
	// Name 配置里的 key,日志用。
	Name() string

	// Formats 该后端支持的扩展名;请求不在集合内直接报参数错误。
	Formats() []string

	// Parse 把 URL 上的文档解析成 markdown。
	Parse(ctx context.Context, req ParseReq) (ParseResult, error)
}

该接口只约定"给我一个文档,给你输出内容",不约定内容中包含哪些元素。

这个接口只接收 URL,不接收文件字节。调用方需要自己上传到对象存储,COS、OSS 等。

多格式文档解析

传 URL 走 POST /api/v1/docs/parse:

curl -X POST http://localhost:8080/api/v1/docs/parse \
  -H 'Content-Type: application/json' \
  -d '{
    "tenant": "demo",
    "kb_no":   "kb_demo",
    "url":     "https://example.com/files/annual-report-2025.pdf",
    "doc_name": "2025 年报"
  }'

各格式对应的处理方式如下表所示。下表列的是当前 FastRAG 配置声明和适配器映射的范围,不代表服务商或开源项目的全部能力:

格式开源这条线(Docling,自部署)商业这条线(腾讯云)怎么传
pdfpdf_backend=docling_parse、do_ocr=true、table_mode=accurateFileType=1URL
docx直接处理FileType=2URL
doc老格式是 OLE 二进制,需要另配依赖,配置里只声明了它、没有保证能跑好FileType=2URL
pptx直接处理FileType=3URL
xlsx直接处理FileType=4URL
xls没有声明FileType=4URL
wps没有声明FileType=8URL
md直接处理FileType=5URL
txt直接处理FileType=6URL
html / htm声明里有映射表里没有URL
png / jpg / jpeg / tiff声明里有FileType=7(连 bmp 也有)URL
csv / json / xml当前接入未支持当前接入未支持自己转 markdown,走 /api/v1/docs

腾讯云一侧是一张枚举映射表:

代码:infrastructure/serviceimpl/parser_tencent.go

var tencentFileType = map[string]int64{
	"pdf": 1, "doc": 2, "docx": 2, "ppt": 3, "pptx": 3,
	"xls": 4, "xlsx": 4, "md": 5, "txt": 6, "wps": 8,
	"png": 7, "jpg": 7, "jpeg": 7, "bmp": 7, "tif": 7, "tiff": 7,
}

它的作用是翻译,把扩展名转换成腾讯云的枚举值。

Docling 则在配置中声明自身支持的扩展名:

"docling": {
  "type": "docling",
  "base_url": "http://127.0.0.1:5001",
  "formats": ["pdf","docx","doc","pptx","xlsx","html","htm","md","txt","png","jpg","jpeg","tiff"],
  ...
}

后端支持的范围其实高度重叠:txt、md、docx、pptx、xlsx 都认,其他服务商可以自己去研究对比。整体来说非常简单,不用太多赘述。

PDF 文档解析

PDF 在业务场景里是很常见的文档格式,广泛应用于商业、学术和个人领域。尽管 PDF 文件的内容在表达图像、文字和表格信息,但其本质上是一系列显示和打印指令的集合。即使是一个仅包含 "你好" 文字的简单 PDF,其文件内容也是一长串的打印指令。

PDF 的设计目的是让版式在不同设备、软件上保持一致,但对计算机而言,它是一种非结构化的格式,储存的信息无法直接被模型理解。PDF 也不是大模型训练语料中的常见格式,通常需要先转为文本。

PDF 解析:对纯文本,可直接还原为字符串;对包含多种元素的复杂格式,最合适的统一输出格式是 Markdown,它关注内容而非排版,又能表达标题、表格等元素,大模型足够熟悉。

电子档与扫描档

同一个 PDF 后缀,分两种来源,处理方式完全不同。

电子档由 Word、LaTeX、网页这类有结构的源文件导出,里面留了一层文字。解析器按坐标和字体归组,就能大致还原出段落与表格,这条路快、成本低,是业务里最常见的一类。

扫描档是纸扫出来的,本身就是图片,里面只有像素没有文字。要先 OCR 认字,再版面分析判断哪块是标题、哪块是表格,最后重排阅读顺序,每一步都会带进误差。

两种还会混在同一个文件里。

现成的解析方案

电子档库不少。Python 有 PyPDF2、PyMuPDF、pdfminer、pdfplumber 和 papermage,它们大多在 langchain_community.document_loaders 里有现成的加载器,切换起来方便;Go 这边选择就少了,ledongthuc/pdf 能读出文字,但没有版面结构,双栏、图文混排的版面容易读乱,pdfcpu 则偏向 PDF 处理(校验、拆分合并、加水印),不是文本提取库。

扫描档没有文字层,得先 OCR、再做表格识别,才能拿到图像里的元素;而无论哪种文档,之后都要做版面分析、还原阅读顺序,才能得到一份元素齐全、顺序正确的 Markdown。这两步靠规则方法很难稳定做出来,要用深度学习库,比如 Layout-parser、PP-StructureV2、PDF-Extract-Kit、pix2text、MinerU、marker、Docling 等。这些项目对模型和解析流程的封装程度不一,可以按需集成,也可以整体部署,代价是部署偏重。

FastRAG 的实现

FastRAG 没有自己实现复杂的解析过程,把解析整体交给外部服务,项目只当客户端。端口对外只约定一件事:输出 Markdown。用哪个服务商由配置决定。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/f6236340b5794ff9bd3b6e0afed4feaa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oiR5pivQWxsZW4=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjk2Njk0NzEyNjMxOTg2NyJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1792215790&x-orig-sign=d2z6nUKWT4U9paAXE1oH%2Bb%2FCyZs%3D

接口定义前面已经贴过,这里不再重复:FastRAG 的解析端口只承诺返回 Markdown,用哪个后端由配置决定。

解析后端由配置决定,启动时根据 config.parser.default 创建对应实现。

代码:infrastructure/serviceimpl/parser_registry.go

func NewParserRegistry(conf *config.Config) (interfaces.IParserRegistry, error) {
	p, err := conf.Parser.Params("")
	if err != nil {
		return nil, err
	}
	switch p.Type {
	case config.ParserTypeDocling:
		return &parserRegistry{def: newDoclingParser(p)}, nil
	case config.ParserTypeTencent:
		tp, err := newTencentParser(p)
		if err != nil {
			return nil, err
		}
		return &parserRegistry{def: tp}, nil
	default:
		return nil, fmt.Errorf("解析后端 %q 的 type=%q 没有对应实现", p.Name, p.Type)
	}
}

开源 Docling Serve 默认监听 5001。适配器把 URL 丢给它,从返回 document.md_content 里取出 Markdown。这里有几个问题说明下:一是 image_export_mode 必须配成 placeholder,配成 embedded,图片会转成 base64 inline 进 Markdown,最后写进 ES 的 content 字段,既撑大索引又干扰检索;二是 timeout_ms 是首次调用、重试等待和重试共享的一份总预算,不是每次尝试各算一份,不然连着超时两次,还没返回连接就被服务端断了。

代码:infrastructure/serviceimpl/parser_docling.go

var doclingDefaultOptions = map[string]any{
	"to_formats":        []string{"md"},
	"do_ocr":            true,
	"table_mode":        "accurate",
	"pdf_backend":       "docling_parse",
	"image_export_mode": "placeholder",
	"abort_on_error":    false,
}

商业侧接了腾讯云 OCR 多模态文档解析(其他服务商不赘述),扩展名要映射成腾讯要求的 FileType,就是前面那张 tencentFileType 表。

代码:application/service/ingest/parse.go(节选)

if err := in.Validate(); err != nil {
	return nil, err
}
parser := s.parsers.Get()
if !supportsFormat(parser, in.URL) {
	return nil, apperrors.NewParamError(...)
}
res, err := parser.Parse(ctx, interfaces.ParseReq{
	URL: in.URL, Filename: filenameOf(in.URL),
})

企业内部资料

前面讨论的都是文件。企业内部还有一类资料并不以文件形式存在,它们保存在 ERP、SCRM、HIS 这些业务系统里。

这类资料直接拿来导入,会有下面这些问题:

格式对不上:ERP 里订单、客户、产品、审批各是一张表,靠主外键关联;SCRM 里还有聊天、语音、图片、工单。

语义模型不懂:STATUS=10、CUST_TYPE=A、ORG_ID=203,这些业务含义只有某个系统知道。

数据太脏:测试订单没删、空字段、时间金额单位不一致等。

噪音太多:系统日志、操作记录、临时字段,有用的数据可能被淹没。

权限和合规:手机号、合同金额、员工和财务信息,不控制导入容易泄密。

所以,内部资料不能原样导入知识库,先清洗:统一口径、补上语义、去掉噪音、脱敏等。

路径一:RAG 对接业务方的接口

业务系统提供接口,输出知识库需要的标准字段。其优势是清洗逻辑贴近源系统,源系统口径变更时清洗逻辑一起调整,不易漏掉,权限和脱敏也是最熟悉数据的人来处理。

另外,还有一个好处就是实时性强。

路径二:数据平台统一清洗再回流

如果企业有数据平台,可以把 ERP、SCRM、HIS 的数据汇聚到一起,统一主键、统一口径、脱敏,回流给 RAG 导入。

这两种方案各有优势,并非鱼和熊掌不可兼得,也可以混合使用的。FastRAG 项目不涉及这块开发哈,大家各自去探索。

总结

文件解析选商业付费版还是开源自己搭,需要根据实际情况评估。商业版的好处是省心,云端集群已经解决了运维、硬件和稳定性问题;开源版自己搭,这三个问题就要自己扛。

下面是本地用 Docker 部署 Docling 时测到的资源占用,解析的是一个约 13 MB 的 PDF。没有跑文档解析大概占用是 3G+,大家可以自己测试。

内存:

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/0430d6c2fda647e1b87a2b6cc5199885~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oiR5pivQWxsZW4=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjk2Njk0NzEyNjMxOTg2NyJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1792215786&x-orig-sign=9%2BjO18ysVPDG%2FGmAZcAaoEk1Hk0%3D

CPU:

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/2e29426006e7407db156edd407e8feb4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oiR5pivQWxsZW4=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjk2Njk0NzEyNjMxOTg2NyJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1792215787&x-orig-sign=EJJ7WATWxBgemDW%2FKDOkAHsDUIQ%3D

从这2张图也能看出来,PDF 解析是真的吃资源。

另外大家可能发现,解析本身的逻辑并不难理解,真正的难点是解析增量去重、并发控制和资源消耗。该项目解析本身目前还是同步的,留给大家继续思考。

下一篇讲解 Markdown 的分块策略,欢迎大家订阅观看。

完整代码:PycMono/FastRAG。