背景介绍
前面介绍过 有道 QAnything 源码解析,通过深入了解工业界的知识库 RAG 服务,得到了不少调优 RAG 服务的新想法。
因此本次趁热打铁,额外花费一点时间,深入研究了另一个火热的开源 RAG 服务 RagFlow 的完整实现流程,希望同样有所收获。
项目概述
框架设计
首先依旧可以先从框架图入手,与 常规的 RAG 架构 进行一些比较
可以看到右侧知识库被明显放大,同时最右侧详细介绍了文件解析的各种手段,比如 OCR
, Document Layout Analyze
等,这些在常规的 RAG 中可能会作为一个不起眼的 Unstructured Loader
包含进去,可以猜到 RagFlow 的一个核心能力在于文件的解析环节。
在 官方文档 中也反复强调 Quality in, quality out
, 反映出 RAGFlow 的独到之处在于细粒度文档解析。
另外 介绍文章 中提到其没有使用任何 RAG 中间件,而是完全重新研发了一套智能文档理解系统,并以此为依托构建 RAG 任务编排体系
,也可以理解文档的解析是其 RagFlow 的核心亮点。
源码结构
首先可以看到 RagFlow 的源码结构:
对应模块的功能如下:
- api 为后端的 API
- web 对应的是前端页面
- conf 为配置信息
- deepdoc 对应的就是文件解析模块
从代码结构就能看出文件解析 deepdoc 在 RAGFlow 中一等公民角色
另外相关的技术栈如下:
- Web 服务是基于 Flask 实现,这个在 2024 年来看稍微有一点点过时了
- 业务数据库使用的是 MySQL
- 向量数据库使用的是 ElasticSearch ,奇怪的是公司有自己的向量数据库 infinity 竟然默认没有用上
- 文件存储使用的是 MinIO
正如前面介绍的因为没有使用 RAG 中间件,比如 langchain
或 llamaIndex
,因此实现上与常规的 RAG 系统会存在一些差异
源码解析
文件加载的支持
常规的 RAG 服务都是在上传时进行文件的加载和解析,但是 RAGFlow 的上传仅仅包含上传至 MinIO,需要手工点击触发文件的解析。
根据实际体验,以及网络上的反馈了解到 RAGFlow 的解析相当慢,估计资源开销也比较大,因此也能理解为什么采取二次手工确认的产品方案了。
实际的文件解析通过接口 /v1/document/run
进行触发的,实际的处理是在 api/db/services/task_service.py
中的 queue_tasks()
中完成的,此方法会根据文件创建一个或多个异步任务,方便异步执行。实现如下所示:
def queue_tasks(doc, bucket, name):
def new_task():
nonlocal doc
return {
"id": get_uuid(),
"doc_id": doc["id"]
}
tsks = []
# pdf 文件的解析,根据不同的类型设置单个任务最多处理的页数
# 默认单个任务处理 12 页 pdf,pager 类型的 pdf 一个任务处理 22 页,其他 pdf 不分页
if doc["type"] == FileType.PDF.value:
file_bin = MINIO.get(bucket, name)
do_layout = doc["parser_config"].get("layout_recognize", True)
pages = PdfParser.total_page_number(doc["name"], file_bin)
page_size = doc["parser_config"].get("task_page_size", 12)
if doc["parser_id"] == "paper":
page_size = doc["parser_config"].get("task_page_size", 22)
if doc["parser_id"] == "one":
page_size = 1000000000
if not do_layout:
page_size = 1000000000
page_ranges = doc["parser_config"].get("pages")
if not page_ranges:
page_ranges = [(1, 100000)]
for s, e in page_ranges:
s -= 1
s = max(0, s)
e = min(e - 1, pages)
for p in range(s, e, page_size):
task = new_task()
task["from_page"] = p
task["to_page"] = min(p + page_size, e)
tsks.append(task)
# 表格数据单个任务处理 3000 行
elif doc["parser_id"] == "table":
file_bin =