1. 为什么要在 RuoYi 里塞进一套 RAGFlow
很多团队做企业级后台,第一反应就是拿 RuoYi 起手。权限、菜单、部门、数据字典、代码生成器,一套下来省掉至少两周的脚手架工作量。但真到了"让系统能回答业务问题"这一步,RuoYi 本身是给不了答案的——它是个优秀的后台管理框架,不是一个知识检索系统。
于是就有了两条路:一条是自己在 RuoYi 里手搓向量检索,从文档切分、embedding、向量库到 prompt 拼装全自己写;另一条是找一个成熟的知识库引擎,把它当成一个"外挂大脑"接进来。RAGFlow 属于后者,它把文档解析、分块、向量化、检索、重排、引用溯源这一整条链路都封装好了,尤其是对 PDF、Word、Excel 这类复杂版式的解析能力,比大多数自研方案要扎实得多。
这篇要聊的就是第二条路:用 RuoYi 做业务外壳和权限中枢,用 RAGFlow 做知识检索内核,两者通过 API 打通,搭出一套能私有化部署、数据不出内网的知识库问答系统。适合谁看?适合手里已经有一套 RuoYi 项目、被业务方追着要"智能问答"功能、又不想把公司文档传到外部服务的后端同学。整套东西跑通之后,你在 RuoYi 的页面上传文档、建知识库、发起提问,答案带着原文出处返回,全程数据都在自己的服务器上。
需要先说明一点:RuoYi 有多个版本分支,RAGFlow 也在持续迭代,下面的接口路径和字段名以我实际跑通的版本为准,你那边如果版本不同,对着官方接口文档微调即可,思路是一致的。
2. 先把两边的角色边界划清楚
动手之前必须想明白一件事:谁负责什么。这个边界如果一开始没划清,后面会陷入"到底该在 RuoYi 存文档还是该在 RAGFlow 存文档"的反复纠结。
2.1 RuoYi 负责的那一半
RuoYi 在这套架构里承担的是"业务与权限层",具体包括:
- 用户与登录态:谁在提问、属于哪个部门、能看到哪些知识库,这些都由 RuoYi 的
sys_user、sys_dept、sys_role体系来管。RAGFlow 自己那套账号体系在企业内网里其实用不上,我们只需要它提供一个稳定的 API Key。 - 知识库的业务归属:一个知识库属于哪个部门、哪些角色可见、谁有上传权限,这些元数据存在 RuoYi 的 MySQL 里,RAGFlow 那边只存"知识库实体"本身。
- 文件上传入口:用户从 RuoYi 页面上传文件,RuoYi 负责接收、落盘、记录业务信息,然后再转发给 RAGFlow 做解析。
- 对话界面与会话记录:前端聊天窗口、历史会话列表、消息落库,都在 RuoYi 侧完成。
2.2 RAGFlow 负责的那一半
RAGFlow 是"检索与生成层",它管的是:
- 文档解析:把 PDF、Word、PPT、Excel、TXT、Markdown 等格式拆成可检索的文本块,这一步是它的核心价值,尤其是带表格、带多栏排版的 PDF。
- 向量化与索引:调用 embedding 模型把文本块转成向量,存进它内置的向量库。
- 检索与重排:根据用户问题召回相关文本块,做重排后拼成上下文。
- 调用大模型生成答案:把上下文和问题一起送给 LLM,返回带引用的回答。
2.3 为什么不让 RuoYi 直接调大模型
有人会问:既然 RuoYi 都能调 API,为什么不干脆跳过 RAGFlow,自己拼 prompt 调大模型?原因在于检索质量。自己手搓的方案,文档切分策略往往很粗糙,PDF 里的表格会被切得七零八落,检索出来的上下文驴唇不对马嘴,大模型再强也答不对。RAGFlow 在文档解析和分块上投入了大量工程,这是它值得被引入的理由。把专业的事交给专业的组件,RuoYi 专注它擅长的权限和业务编排,这才是合理的分工。
提示:如果你的文档全是结构规整的纯文本,自研检索也不是不行;但只要涉及扫描件、复杂表格、多栏 PDF,RAGFlow 的解析优势会非常明显。
3. 部署 RAGFlow 时那些文档没写清楚的细节
RAGFlow 官方推荐用 Docker Compose 部署,命令看起来很简单,但实际跑起来有几个坑,我一个个说。
3.1 硬件与系统前提
RAGFlow 对资源的要求不低,尤其是内存。它内部跑着 Elasticsearch(或 Infinity)、MySQL、Redis、MinIO 等一堆组件,最低建议 16GB 内存,低于这个数容器会频繁被 OOM Killer 干掉。CPU 建议 4 核以上,磁盘留出至少 50GB,因为向量索引和原始文件都占空间。
如果你是在 Windows 11 上做本地验证,最省事的做法是装 Docker Desktop 并开启 WSL2 后端,把项目放在 WSL 的文件系统里而不是 Windows 挂载盘里——放在/mnt/c/...下会因为跨文件系统 IO 导致容器启动慢得离谱,这个坑我踩过,换成 WSL 内部路径后启动时间从几分钟降到几十秒。
3.2 部署命令与关键配置
拉取代码后进入 docker 目录,核心是改.env文件。几个必须关注的参数:
# .env 中需要重点确认的项 # 对外暴露的 HTTP 端口,默认 80,如果被占用改成别的 SVR_HTTP_PORT=9380 # 镜像源,国内环境建议换成可访问的镜像仓库 RAGFLOW_IMAGE=infiniflow/ragflow:v0.15.0 # 时区,避免日志时间对不上 TZ=Asia/Shanghai改完之后:
docker compose -f docker-compose.yml up -d启动完成后用docker logs -f ragflow-server看日志,等到出现类似 "Running on all addresses" 的字样,说明服务起来了。第一次启动会拉取镜像并初始化数据库,耐心等几分钟。
3.3 模型接入的两种选择
RAGFlow 需要两类模型:embedding 模型和对话模型。这里有个关键决策——用云端 API 还是本地部署。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 云端 API | 接入快、效果好、不占本地资源 | 数据要出内网、按量计费 | 对数据敏感度不高的场景 |
| 本地模型 | 数据完全不出内网 | 需要 GPU、效果依赖模型选型 | 强合规要求的企业内网 |
如果走本地路线,embedding 可以用 BGE 系列,对话模型可以用 Qwen 系列的开源版本,通过 Ollama 或 vLLM 起服务,然后在 RAGFlow 的模型设置里填本地推理服务的地址。注意 embedding 模型一旦选定,中途不要换,因为换模型意味着所有已索引的向量都要重新生成,代价很大。
注意:RAGFlow 里配置模型时,base_url 要填容器能访问到的地址。如果模型服务跑在宿主机上,容器里不能用
localhost,得用宿主机的内网 IP 或者 Docker 的host.docker.internal。
4. 打通 RuoYi 与 RAGFlow 的接口层
这一节是整篇的核心。RuoYi 要调 RAGFlow,本质就是发 HTTP 请求,但里面有几个容易翻车的点。
4.1 认证方式:API Key 怎么拿怎么用
RAGFlow 的 API 认证用的是 Bearer Token。登录 RAGFlow 的 Web 界面,在右上角头像菜单里找到 API 设置,生成一个 API Key。这个 Key 要配置到 RuoYi 的配置文件里,绝对不要硬编码在 Java 代码里,也不要提交到 Git 仓库。
在 RuoYi 的application.yml里加一段:
ragflow: base-url: http://192.168.1.100:9380 api-key: ragflow-xxxxxxxxxxxxxxxx connect-timeout: 10000 read-timeout: 120000read-timeout 要设得大一点,因为文档解析和问答生成都是耗时操作,默认的几秒根本不够,会直接超时。
4.2 用 RestTemplate 还是 OkHttp
RuoYi 默认集成了 RestTemplate 的封装,但说实话,处理 RAGFlow 这种长耗时、大响应体的接口,我更推荐用 OkHttp 或者 Hutool 的 HttpUtil。原因有两个:一是连接池管理更灵活,二是超时控制更细粒度。
下面是一个基于 OkHttp 的调用封装示例:
@Component public class RagFlowClient { private final OkHttpClient client; private final String baseUrl; private final String apiKey; public RagFlowClient(@Value("${ragflow.base-url}") String baseUrl, @Value("${ragflow.api-key}") String apiKey, @Value("${ragflow.read-timeout}") long readTimeout) { this.baseUrl = baseUrl; this.apiKey = apiKey; this.client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(readTimeout, TimeUnit.MILLISECONDS) .build(); } public String post(String path, String jsonBody) throws IOException { RequestBody body = RequestBody.create( jsonBody, MediaType.parse("application/json")); Request request = new Request.Builder() .url(baseUrl + path) .addHeader("Authorization", "Bearer " + apiKey) .addHeader("Content-Type", "application/json") .post(body) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException("RAGFlow 调用失败: " + response.code()); } return response.body().string(); } } }4.3 创建知识库与上传文档的完整链路
RAGFlow 的接口大致分这么几类:数据集(dataset,也就是知识库)管理、文档管理、检索、对话。RuoYi 侧要做的编排是:
- 用户在 RuoYi 页面点"新建知识库",RuoYi 调 RAGFlow 的创建数据集接口,拿到返回的
dataset_id,存进 RuoYi 自己的业务表。 - 用户上传文件,RuoYi 先把文件存到本地或对象存储,记录业务信息,然后调 RAGFlow 的上传接口把文件传过去。
- 上传后要手动触发解析,RAGFlow 不会自动解析,这一步很多人会漏掉,导致文档传上去了但检索不到。
- 解析是异步的,RuoYi 侧要么轮询解析状态,要么用回调,解析完成后文档才真正可检索。
这里有个实操细节:RAGFlow 上传文档的接口用的是multipart/form-data,不是 JSON。用 OkHttp 构造的时候要换成MultipartBody,别用上面那个 JSON 方法。而且文件流要重新读,如果同一个InputStream被读两次会报错,建议先落盘再读文件。
4.4 检索与问答接口的调用姿势
问答接口是用户感知最直接的。RAGFlow 提供两种模式:一种是纯检索(返回召回的文本块),一种是带生成的对话(返回答案和引用)。企业知识库问答一般用后者。
调用对话接口时,关键参数有:
question:用户问题dataset_ids:限定在哪些知识库里检索,这个由 RuoYi 根据用户权限动态传入stream:是否流式返回,前端要打字机效果就设 true
权限控制就卡在dataset_ids这一步。RuoYi 在调用前先查当前用户能访问哪些知识库,把对应的dataset_id列表传进去,这样即使用户绕过前端直接调接口,也拿不到没权限的知识库内容。这是把权限做在 RuoYi 侧的核心价值。
5. 权限模型怎么和知识库对齐
这是 RuoYi 集成 RAGFlow 时最需要动脑子的地方,也是很多教程一笔带过的地方。
5.1 知识库的可见性设计
我建议在 RuoYi 里建一张知识库业务表,字段大致如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| kb_name | varchar | 知识库名称 |
| ragflow_dataset_id | varchar | 对应 RAGFlow 的数据集 ID |
| dept_id | bigint | 归属部门 |
| visibility | tinyint | 0 私有 1 部门可见 2 全公司可见 |
| create_by | varchar | 创建人 |
| status | tinyint | 启用状态 |
可见性判断逻辑放在 Service 层:私有库只有创建人可见,部门库同部门可见,全公司库所有人可见。查询用户可访问的知识库时,把这三个条件用 OR 拼起来。
5.2 为什么不在 RAGFlow 里做权限
RAGFlow 本身也有团队和权限的概念,但它的权限模型是围绕它自己的账号体系设计的,跟 RuoYi 的部门、角色、数据权限对不上。硬要在两边做映射,维护成本极高,而且一旦 RuoYi 的组织架构调整,两边就不同步了。正确做法是:RAGFlow 只当它是一个无状态的检索服务,所有权限判断都在 RuoYi 侧完成,通过 dataset_ids 参数来收口。这样 RuoYi 的组织架构怎么变,权限逻辑都只在一处维护。
5.3 会话记录的归属
用户和知识库的对话历史,存在 RuoYi 的 MySQL 里,而不是依赖 RAGFlow 的会话管理。原因很简单:RAGFlow 的会话数据是跟它的账号绑定的,而我们的用户体系在 RuoYi。自己存会话记录,还能顺便做审计、做敏感词过滤、做问答质量分析。
会话表设计上,一条会话关联一个用户和一个知识库,会话下面挂多条消息,每条消息记录角色(用户/助手)、内容、引用的文档块 ID、耗时等信息。引用信息很重要,前端要展示"答案来自哪份文档的哪一段",这个数据从 RAGFlow 的响应里取。
6. 实测中冒出来的那些坑
前面讲的是"应该怎么做",这一节讲"实际做的时候会怎么翻车"。这些都是我在真实项目里踩出来的。
6.1 文档解析卡在"未解析"状态
最常见的问题:文件上传成功,但状态一直是未解析。排查链路是这样的:
先看 RAGFlow 的容器日志,docker logs ragflow-server,如果看到 embedding 相关的报错,多半是模型配置有问题——要么 API Key 失效,要么本地模型服务没起来。如果日志里是解析超时,那可能是文件太大或者格式太复杂,PDF 里的扫描件需要 OCR,而 OCR 模型没配置好。
还有一个隐蔽的坑:RAGFlow 的解析任务是有并发限制的,一次传几十个文件,后面的会排队,看起来像卡住了,其实是在等。这种情况耐心等或者分批传就行。
6.2 中文检索召回不准
如果发现问中文问题召回的相关文档很少,检查两个地方:一是 embedding 模型是不是对中文友好的,有些英文模型对中文语义的捕捉很差;二是分块大小,RAGFlow 默认的 chunk size 对中文来说可能偏大,一个块里塞了太多内容,向量表达反而模糊。可以适当调小分块大小,或者开启它的自动分块优化。
6.3 跨域与前端直连的诱惑
前端同学可能会想:能不能让浏览器直接调 RAGFlow 的接口,省得 RuoYi 后端转发?千万别这么干。一是 API Key 会暴露在前端,二是权限控制就彻底失效了,三是跨域配置会很麻烦。所有对 RAGFlow 的调用必须经过 RuoYi 后端,这是安全底线。
6.4 大文件上传超时
RuoYi 默认的上传大小限制和 Nginx 的client_max_body_size都可能拦住大文件。一个几十兆的 PDF 传不上去,先查这两个配置。另外 RAGFlow 侧对单文件大小也有限制,超大文件建议先拆分。
7. 让问答效果再上一个台阶的几个调优点
系统跑通只是及格线,真正让业务方满意的是回答质量。分享几个我实测有效的调优点。
7.1 检索参数不是越大越好
RAGFlow 检索时有top_k、similarity_threshold这些参数。很多人以为召回越多越好,把 top_k 调到几十,结果上下文里塞了一堆不相关的内容,大模型反而被干扰,答案质量下降。我的经验是 top_k 控制在 3 到 8 之间,配合相似度阈值过滤掉低分块,效果比无脑调大要好。
7.2 给知识库分门别类
不要把所有文档都堆进一个知识库。按业务线、按文档类型分开建库,检索时根据问题意图路由到对应的库,召回精度会明显提升。比如"报销制度"的问题就只在行政制度库里检索,不要去财务库里捞。
7.3 引用溯源要展示到位
用户对 AI 答案天然有戒心,把引用来源展示清楚能极大提升信任度。前端在答案下面列出引用的文档名和片段,用户点开能看原文,这个体验比单纯给一段答案强太多。RAGFlow 的响应里带了引用信息,RuoYi 侧解析出来存好、传给前端就行。
7.4 定期重建索引
文档更新后,旧索引不会自动失效。如果知识库内容变动频繁,要建立定期重建索引的机制,或者在文档更新时触发对应块的重新解析。否则用户会问出"为什么答案还是老版本的"这种问题。
8. 关于选型的一点个人看法
最后聊聊选型。市面上做知识库问答的开源方案不止 RAGFlow 一家,各有各的定位。RAGFlow 的优势在于文档解析扎实、部署相对独立、API 清晰,适合把它当成一个纯粹的检索后端来接。它的短板是自带的界面偏工程化,直接给业务用户用体验一般,所以拿 RuoYi 做一层业务外壳是合理的搭配。
至于大模型选云端还是本地,我的建议是分阶段:验证阶段用云端 API 快速跑通链路,确认业务价值后再评估是否迁移到本地模型。一上来就折腾本地部署,很容易在环境问题上耗掉大量时间,还没看到效果就先泄气了。
这套 RuoYi + RAGFlow 的组合,我前后在几个项目里落地过,最深的体会是:集成的难点从来不在技术本身,而在于把权限、数据归属、用户体验这几件事想清楚。接口调通可能一天就够了,但把权限模型设计对、把引用溯源做扎实、把解析失败的兜底处理好,才是真正花时间的地方。如果你正准备动手,建议先把第 5 节的权限模型想明白再写代码,能省掉后面大量的返工。