1. 为什么要在 RuoYi 里集成 RAGFlow
1.1 从两个真实痛点说起
做过企业级后台的兄弟大概率都碰过 RuoYi,这套基于 Spring Boot 的权限管理框架在国内中小团队里普及率极高,菜单、角色、数据权限、代码生成器一应俱全,开箱就能跑起一套像模像样的管理系统。但问题也很明显:当业务方提出“我想让系统能回答公司内部文档的问题”时,RuoYi 本身是没有任何 AI 能力的,你只能从零去接一个大模型接口,然后自己处理文档切片、向量化、检索、拼 prompt 这一整套链路。
另一边的 RAGFlow 则是这两年在开源 RAG 领域跑出来的黑马,它把文档解析、OCR、分块、向量化、检索重排、引用溯源这一整套流程做成了可视化平台,尤其是对 PDF 里表格、扫描件的解析效果,比很多同类工具要扎实。但 RAGFlow 自己是个独立的 Web 应用,它的用户体系和 RuoYi 完全不互通,你不可能让员工在 RuoYi 里登录一次,再去 RAGFlow 里登录一次。
所以这个项目的核心诉求就一句话:让 RuoYi 作为统一入口,把 RAGFlow 的知识库问答能力嵌进来,用户体系打通,权限可控,数据不出内网。这套方案适合谁?适合手里已经有 RuoYi 项目、想低成本给系统加一个“企业知识助手”的后端和全栈同学,也适合正在做私有化 AI 落地、被数据合规卡住的团队。
1.2 整体架构怎么摆
在动手之前,先把架构想清楚,不然写到一半会发现用户对不上、会话串了、权限漏了。我采用的是一种“RuoYi 做壳、RAGFlow 做脑”的分层结构,具体分四层:
- 接入层:RuoYi 前端页面,用户在系统内的聊天窗口发起提问。
- 业务层:RuoYi 后端新增一个
rag模块,负责鉴权、会话管理、调用转发、结果落库。 - 能力层:RAGFlow 服务,负责知识库管理、文档解析、检索和答案生成。
- 数据层:MySQL 存会话和消息记录,RAGFlow 自己的存储存向量和文档。
这里有个关键决策:RuoYi 后端到底要不要直接调 RAGFlow 的 HTTP API,还是让前端直连?我的选择是后端转发。原因有三点:第一,RAGFlow 的 API Key 绝对不能暴露在前端,否则等于把知识库大门敞开;第二,后端转发才能做用户身份绑定和会话隔离,谁问的、问了什么、答案是什么,都能落到自己的库里;第三,后续要做敏感词过滤、限流、审计,只有经过后端才有抓手。
提示:如果你的 RAGFlow 和 RuoYi 部署在不同机器,务必确认两者内网互通,且 RAGFlow 的 API 端口不要直接暴露到公网。
2. 环境准备与 RAGFlow 本地化部署
2.1 硬件与系统选型
RAGFlow 对资源是有要求的,尤其是做文档解析和向量检索时。我实测下来,最低配建议 4 核 16G 内存起步,如果文档量大、并发高,32G 内存会更稳。磁盘方面,向量库和原始文档都吃空间,预留 100G 以上比较从容。系统我用的是 Ubuntu 22.04,Windows 11 上通过 WSL2 也能跑起来,但生产环境还是建议 Linux,Docker 的兼容性和性能都更好。
关于“llama 适合国内企业拿来搞知识库问答吗”这个热搜问题,我的看法是:模型选型要看你的场景。RAGFlow 本身支持接入多种模型,包括本地部署的开源模型和云端 API。如果数据敏感度极高,那就本地跑一个中等参数量的模型;如果只是内部一般文档问答,用云端 API 成本更低、效果更稳。RAGFlow 的价值在于它把检索这一层做扎实了,模型可以换,但检索质量决定了答案上限。
2.2 Docker 部署 RAGFlow 的完整步骤
RAGFlow 官方推荐 Docker Compose 部署,我按自己的实操记录整理一遍。先确认 Docker 和 Docker Compose 都装好了:
docker --version docker compose version然后拉取 RAGFlow 的代码仓库,进入 docker 目录。这里要注意,RAGFlow 的镜像比较大,包含了很多解析依赖,第一次拉取会比较慢,耐心等。
git clone https://github.com/infiniflow/ragflow.git cd ragflow/docker在启动之前,先看一下.env文件里的配置。有几个参数必须改:SVR_HTTP_PORT是 RAGFlow 的 Web 端口,默认 80,如果被占用就换成 8080 之类;MYSQL_PORT、MINIO_PORT、ES_PORT这些如果和宿主机已有服务冲突,也要改。我踩过的坑是 Elasticsearch 默认吃内存很凶,如果机器内存不够,可以在 compose 文件里限制它的 JVM 堆大小。
docker compose -f docker-compose.yml up -d启动完成后,用docker ps看一下容器状态,正常情况下会有 ragflow-server、es、mysql、minio、redis 这几个。等 ragflow-server 的日志出现 “Running on” 字样,就可以访问http://你的IP:端口了。第一次登录用默认账号,进去第一件事就是改密码。
注意:RAGFlow 首次启动会初始化数据库和索引,可能需要几分钟,别急着以为部署失败了。
2.3 在 RAGFlow 里建好知识库
部署完别急着写代码,先在 RAGFlow 界面里把知识库建起来,验证一下解析效果。登录后新建一个知识库,上传几份 PDF 或 Word 文档,选择合适的分块方法。RAGFlow 提供了多种解析模板,比如 General、Q&A、Resume 等,普通文档用 General 就行,如果是问答对格式的文档,用 Q&A 模板切出来的效果会好很多。
上传后点解析,等状态变成“已完成”,然后可以在检索测试里输入问题,看看召回的内容对不对。这一步非常关键,如果 RAGFlow 自己的检索都不准,后面接进 RuoYi 也是白搭。我一般会准备 10 到 20 个典型问题做验证,召回率能到 80% 以上再往下走。
解析完成后,去 RAGFlow 的 API 页面生成一个 API Key,这个 Key 后面 RuoYi 后端要用。同时记下知识库的 ID,调用接口时需要指定。
3. RuoYi 后端集成 RAGFlow 的核心实现
3.1 新增 rag 模块与依赖配置
RuoYi 是标准的多模块 Maven 项目,我习惯新建一个ruoyi-rag子模块,保持职责清晰。在父 pom 里加上模块声明,然后在ruoyi-rag的 pom 里引入 HTTP 客户端依赖。这里我用的是 OkHttp,比原生 HttpURLConnection 好用太多,连接池和超时控制都省心。
<dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.12.0</version> </dependency>配置文件里加上 RAGFlow 的连接信息,放在application.yml里:
ragflow: base-url: http://192.168.1.100:8080 api-key: ragflow-xxxxxxxxxxxx dataset-id: xxxxxxxxxxxxxxxx timeout: 60000把 base-url、api-key、dataset-id 都做成可配置项,这样测试环境和生产环境切换只改配置,不用动代码。timeout 设 60 秒是因为 RAGFlow 生成答案有时比较慢,尤其是文档多、检索链路长的时候,设太短会频繁超时。
3.2 封装 RAGFlow 的调用客户端
我封装了一个RagFlowClient类,把创建会话、发起对话、查询会话这几个核心接口包起来。RAGFlow 的对话接口是流式的,返回的是 SSE 格式,这一点要特别注意,不能用普通的同步请求去接。
@Component public class RagFlowClient { @Value("${ragflow.base-url}") private String baseUrl; @Value("${ragflow.api-key}") private String apiKey; private final OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .build(); public String createSession(String userId) throws IOException { String url = baseUrl + "/api/v1/chats"; // 构造请求体,绑定用户 // ... } }这里有个细节:RAGFlow 的会话是可以绑定用户标识的,我建议把 RuoYi 的userId拼进去,比如ruoyi_user_1001,这样在 RAGFlow 侧也能区分不同用户的会话,方便排查问题。关于“ruoyi 在哪里写入登录用户的信息”这个热搜,答案是在SecurityUtils.getUserId()里拿,它从 Spring Security 的上下文里取当前登录用户,你在任何 Service 里都能直接调。
3.3 用户体系打通与会话隔离
用户体系打通是这个项目的核心难点。RuoYi 有自己的sys_user表,RAGFlow 也有自己的用户表,两者不可能直接同步。我的做法是不做用户同步,只做身份映射。RuoYi 用户登录后,后端在调用 RAGFlow 时,用 RuoYi 的 userId 作为 RAGFlow 会话的标识,RAGFlow 侧不需要知道这个用户是谁,只需要知道“这是同一个人的连续对话”。
会话隔离靠的是数据库表。我建了两张表,一张rag_session存会话,一张rag_message存消息:
CREATE TABLE rag_session ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, rag_session_id VARCHAR(64), title VARCHAR(255), create_time DATETIME, update_time DATETIME ); CREATE TABLE rag_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, session_id BIGINT NOT NULL, role VARCHAR(16), content TEXT, reference TEXT, create_time DATETIME );每次用户提问,先查有没有活跃会话,没有就调 RAGFlow 创建一个,把返回的rag_session_id存下来。然后发消息、收答案、落库。这样即使用户换了浏览器,只要还是同一个 RuoYi 账号,历史会话都能拉回来。
提示:
reference字段存 RAGFlow 返回的引用来源,前端可以展示“答案来自哪份文档”,这对企业场景的可信度提升非常大。
3.4 流式响应的处理
RAGFlow 的对话接口返回 SSE,RuoYi 这边如果要做到打字机效果,就得把流式数据透传给前端。我的做法是后端用SseEmitter接收 RAGFlow 的流,边收边推给前端。这里要注意线程池的配置,每个流式请求占一个线程,并发高了容易把 Tomcat 线程打满,所以我单独配了一个线程池来处理 RAG 请求。
@GetMapping("/chat/stream") public SseEmitter chatStream(@RequestParam String question) { SseEmitter emitter = new SseEmitter(120000L); ragExecutor.execute(() -> { try { ragFlowClient.streamChat(question, chunk -> { emitter.send(SseEmitter.event().data(chunk)); }); emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } }); return emitter; }实测下来,流式体验比等完整答案再返回好太多,用户感知的响应速度快了好几倍。如果前端不想做流式,也可以后端收完再一次性返回,但那样等待时间会比较长。
4. 前端对接与交互细节
4.1 RuoYi 前端页面改造
RuoYi 前端是 Vue 的,我在views下新建了一个rag目录,放聊天页面。页面结构很简单:左边是会话列表,右边是消息区,底部是输入框。会话列表调后端接口拉当前用户的会话,点击切换;消息区渲染历史消息和实时流。
流式接收用EventSource或者fetch的 ReadableStream 都行。我用的是 fetch,因为要带 token,EventSource 加请求头不太方便。核心逻辑是读response.body的 reader,逐块解析。
const response = await fetch('/rag/chat/stream?question=' + encodeURIComponent(q), { headers: { 'Authorization': 'Bearer ' + getToken() } }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const text = decoder.decode(value); appendToMessage(text); }4.2 引用来源的展示
RAGFlow 返回的答案里会带引用片段,我把这些片段解析出来,在答案下方用小卡片展示,标明来自哪份文档、哪一页。这个功能在企业场景里特别受欢迎,因为员工能自己判断答案可不可信。实现上就是把reference字段解析成 JSON 数组,前端循环渲染。
4.3 权限控制
不是所有人都该访问所有知识库。我在 RuoYi 的菜单权限里加了一个rag:chat:use权限,只有分配了这个权限的角色才能看到聊天入口。如果后续要做多知识库,可以再细化到rag:dataset:xxx这种粒度,在调用 RAGFlow 时根据用户权限选择不同的 dataset-id。
5. 常见问题与排查技巧实录
5.1 部署与连接类问题
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| RAGFlow 容器启动后访问不了 | 端口冲突或防火墙 | 检查docker ps端口映射,确认宿主机防火墙放行 |
| RuoYi 调 RAGFlow 超时 | 网络不通或解析慢 | 先用 curl 在服务器上直连测试,排除网络问题 |
| 上传文档解析一直失败 | 内存不足或格式不支持 | 看 ragflow-server 日志,确认是否 OOM |
| API 返回 401 | API Key 错误或过期 | 重新在 RAGFlow 界面生成 Key |
我遇到最多的是解析失败,十有八九是内存不够。Elasticsearch 和解析进程都吃内存,16G 的机器如果同时跑其他服务,很容易被拖垮。解决办法是给 ES 限制堆内存,或者把解析任务错峰执行。
5.2 检索质量类问题
检索不准是 RAG 落地最头疼的事。我的经验是分三步排查:第一,看分块是否合理,块太大信息冗余,块太小上下文丢失,一般 300 到 500 字比较合适;第二,看 embedding 模型是否匹配,中英文混排的文档要用支持多语言的模型;第三,看是否需要重排,RAGFlow 支持重排模型,开启后召回精度会明显提升。
还有一个容易被忽略的点:文档质量本身。如果原始 PDF 是扫描件且 OCR 效果差,那后面怎么调都是白费。这种情况我建议先人工校对一遍,或者换更清晰的源文件。
5.3 性能与并发类问题
并发一高,RAGFlow 的响应就会变慢。我的优化手段有几个:一是给 RuoYi 侧的 RAG 请求单独配线程池,避免拖垮主业务;二是对高频问题做缓存,相同问题短时间内直接返回缓存答案;三是 RAGFlow 侧如果资源够,可以横向扩展多个实例,用 Nginx 做负载。
注意:缓存答案要谨慎,如果知识库更新了,缓存必须失效,否则会返回过时信息。我一般给缓存设一个较短的过期时间,比如 10 分钟。
5.4 几个独家避坑技巧
第一个坑是会话 ID 丢失。RAGFlow 创建会话后返回的 ID 一定要及时落库,我有一次因为事务回滚导致会话没存上,用户下次提问又创建了新会话,历史全断了。第二个坑是流式响应的编码。SSE 返回的中文如果编码没处理好会乱码,确保前后端都用 UTF-8。第三个坑是API Key 的权限。RAGFlow 的 Key 是绑定知识库的,如果换了知识库,Key 也要重新生成,别拿旧 Key 去调新库。
6. 后续可扩展的方向
这套集成跑通之后,能扩展的地方其实很多。比如把 RAGFlow 的问答能力接到 RuoYi 的工单系统里,用户提工单时自动推荐相似历史工单的解决方案;再比如做一个“知识库健康度”面板,统计哪些问题问得多、哪些文档从没被召回,反过来指导文档维护。
还有一个方向是多知识库路由。企业里往往有多个部门的知识库,可以在 RuoYi 侧根据用户所属部门,自动路由到对应的 RAGFlow 知识库,用户无感知。这个实现起来不难,就是在调 RAGFlow 时动态换 dataset-id。
我自己在实际操作中的体会是,RAG 项目的成败,七分在数据准备,两分在检索调优,一分在模型。很多人一上来就纠结用哪个大模型,其实把文档整理干净、分块调好,效果自然就上来了。RuoYi 加 RAGFlow 这套组合,最大的价值是让后端同学不用从零造轮子,把精力放在业务和体验上,这才是私有化知识库能真正落地的关键。