接手这个需求的时候,我脑子里第一反应是:RuoYi 这种传统后台管理系统和 RAGFlow 这种 AI 原生应用,完全是两个世界的产物。但实际做完之后我发现,恰恰是这种“混搭”最能解决企业里的真实问题——审批流、用户管理、权限控制这些活儿交给 RuoYi,文档解析、向量检索、生成问答这些聪明活儿交给 RAGFlow,两边各干各的擅长事,中间用一套标准接口串起来。这篇博文就完整记录一下我的集成过程,从方案选型到部署细节再到排坑实录,给同样想在私有不落地 AI 能力的朋友一条能直接照抄的路。
先交代背景:我手上这个项目是典型的 Java 技术栈,后台用的若依前后端分离版(Spring Boot 2.5 + Vue 3),业务方提了一个很实际的需求——把公司几百份技术文档、规章制度、项目复盘变成“能聊天的知识库”,员工在内部系统里直接提问就能得到答案,而且答案必须能追溯到原文。调研一圈之后我选了 RAGFlow 而不是 Dify 或 FastGPT,原因后面细说,这里先给结论:RAGFlow 的文档解析能力是目前开源项目里最强的,尤其是对付 PDF 里那些排版混乱、带表格、带图片的内容,DeepDoc 的版面识别确实能打。
下面从头拆解整个集成方案。
1. 整体设计与方案选型
1.1 为什么是 RuoYi + RAGFlow,而不是别的组合
先解决“为什么”的问题。市面上能聊的 RAG 开源项目不少,Devin、FastGPT、Dify、RAGFlow、QAnything,我都跑过一遍。选 RAGFlow 有几个硬理由:
一是文档解析质量。企业内部知识库最头疼的就是 PDF 扫描件、表格、复杂排版。Dify 在这块做得比较浅,基本靠 LangChain 那套切分逻辑,遇到双栏排版或者带边框表格就抓瞎。RAGFlow 内置了 DeepDoc 模型,能先做版面检测再提取内容,实测同一个 PDF,RAGFlow 抽出来的 Markdown 比 Dify 干净太多。
二是知识库的引用溯源做得规范。RAGFlow 每个回答都会带引用片段和页码,这对企业场景是刚需——员工问完答案,理想要能点开出处核对原文,不然 AI 胡说八道全都赖你。
三是部署相对轻。RAGFlow 官方提供 Docker Compose 一键部署,不像某些方案要额外维护一堆微服务组件。虽然它也有 Elasticsearch、MySQL、MinIO 这些依赖,但至少在安装层面做得比较省心。
RuoYi 这边没什么好说的,国内做 Java 后端的几乎没人不知道这套脚手架。用户体系、RBAC 权限、菜单管理、代码生成器都给你备齐了,拿来即用。我要做的就是在它基础上加一层“AI 能力适配层”,把知识库功能挂进若依的管理体系里。
1.2 整体架构思路
整个集成链路是这样的:
RuoYi 前端(Vue3) → RuoYi 后端(Spring Boot) ↓ HTTP JSON AI 适配层(自研) ↓ HTTP JSON RAGFlow API(端口 9380) ↓ DeepDoc 解析 + Embedding + 向量检索用户在前端上传文件,若依后端先走一遍原有的权限校验,确认这个人有“知识库管理”权限,然后在本地落一份文件备份(防止 RAGFlow 出问题时数据不丢),再通过 HTTP 调用 RAGFlow 的接口,把文件转交过去创建文档、触发解析。问答环节也一样,用户在若依的对话窗口里提问,后端先带上用户身份信息转发给 RAGFlow 的会话接口,拿到结果后流式返回前端。
这套设计的核心原则是:若依永远只做“编排”和“权限控制”,RAGFlow 永远只做“认知处理”。两边不共享数据库,不共享缓存,通过 API 松耦合。好处是以后想换掉 RAGFlow 换成别的引擎,适配层改改就行,若依那边一行代码不用动。
1.3 目录结构与工程改造范围
在 RuoYi 源码的基础上我新增了一个模块叫ruoyi-ai,主要包含这几块:
AiConfig.java:配置 RAGFlow 的地址、API Key、请求超时时间KnowledgeBaseController.java:知识库管理接口(增删改查)DocumentController.java:文档上传、删除、解析状态查询ChatController.java:问答对话接口RagFlowClient.java:封装 RAGFlow 的 HTTP 调用SseEmitterController.java:流式输出的 Web 接口
改造范围控制在最小。不动若依原有的登录逻辑、菜单权限、代码生成器,只在pom.xml里加一个okhttp依赖用来做 HTTP 调用,前端新建一个views/ai/knowledge页面。这样升级若依版本或者拉官方更新的时候,冲突会非常少。
2. 核心环节拆解:RuoYi 侧的集成改造
2.1 登录用户信息的获取与传递
热搜词里有条“ruoyi在哪里写入登录用户的信息”,这问题我在集成时也碰到了。如果你熟悉若依的源码,应该知道登录成功后用户信息存在SecurityContextHolder里,具体类是LoginUser。我在后端写了一个工具方法:
public LoginUser getLoginUser() { return SecurityUtils.getLoginUser(); }SecurityUtils是若依自带的工具类,底层就是从SecurityContextHolder拿Authentication,再强转成LoginUser。拿到之后,用户 ID、用户名、角色权限都好办了。
在我设计的知识库体系里,权限隔离是这么做的:每创建一个知识库,t_knowledge_base表记录create_by;每上传一个文档,记录归属知识库 ID 和上传人 ID;问答时,先根据当前用户 ID 去查他有权限的知识库列表,再在调用 RAGFlow 时指定这些知识库的 ID。这样就能做到不同部门的人问的是各自知识库里的内容,互不干扰。
> 注意:若依的前后端分离版本用的是 JWT 鉴权,请求头里带 Authorization 字段。 > RAGFlow 不认识你 RuoYi 的 token,所以后端调用 RAGFlow 时要用它自己的 API Key 做身份认证。 > 两边身份体系各管各的,中间靠适配层做映射,这是集成的关键认知。2.2 验证码与白名单的正确处理方式
“ruoyi vue 去掉验证码”这个搜索词我太熟悉了,很多人在集成 AI 功能时会顺手把登录验证码关掉,理由是“内部系统没必要”。我的建议是:联动 AI 功能可以关,但别全局关。
若依的验证码逻辑比较简单,登录接口login会主动读取系统配置里的captchaEnabled。你可以在数据库sys_config表里把sys.account.captchaEnabled改成false,这是最正规的开关。但如果你只想让某些接口不校验验证码,就得动CaptchaController或过滤器链路了,容易造成安全隐患,不推荐。
另外一个容易踩的坑是RuoYi 的接口白名单配置。若依默认把/login、/captchaImage等路径放行了,但如果你想给 RAGFlow 的回调接口单独开一个免登录入口,一定要在SecurityConfig里加白名单:
.antMatchers("/ai/callback/**").permitAll()除此之外,所有 AI 相关接口都必须走若依的登录鉴权,防止内部知识库接口裸奔。
2.3 HTTP 客户端封装
RuoYi 本身用的是 Spring 自带的 RestTemplate,但我在封装 RAGFlow 调用时选了 OkHttp。原因很简单:RAGFlow 的问答接口是 SSE 流式返回的,RestTemplate 处理流式响应太别扭,OkHttp 原生支持 EventSource,代码写起来干净多了。
OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .build(); Request request = new Request.Builder() .url(RAGFLOW_BASE_URL + "/api/v1/chats/" + chatId + "/completions") .addHeader("Authorization", "Bearer " + apiKey) .addHeader("Content-Type", "application/json") .post(RequestBody.create(json, MediaType.parse("application/json"))) .build();超时时间我调了好几次。首次连接 30 秒是因为 RAGFlow 在处理一个“冷文档”时需要现场跑 embedding,慢的时候可能要十几秒;读取超时 60 秒是因为正常生成一个几百字的回答也要一两分钟。如果你用默认超时,大概率会频繁报 SocketTimeoutException。
3. RAGFlow 部署与文件解析细节
3.1 Docker 部署要点
RAGFlow 官方推荐用 Docker Compose 部署,但这玩意儿部署起来并不是一路绿灯。我是在一台 8C16G 的 Ubuntu 20.04 服务器上部署的,过程里整理了这么几个关键点:
git clone https://github.com/infiniflow/ragflow.git cd ragflow/docker cp .env.example .env docker compose -f docker-compose.yml up -d.env文件里有几个参数必须改:
SVR_HTTP_PORT:默认 9380,如果你服务器这个端口被占了,改成别的并记住,后面集成要用MYSQL_PASSWORD、MINIO_PASSWORD:默认密码必须改,这些都是内网服务,但安全意识别丢RAGFLOW_IMAGE:指定镜像版本,我用的infiniflow/ragflow:v0.15.0,这个版本解析稳定性比较好
RAGFlow 默认会依赖 Elasticsearch,这一块内存占用非常夸张。我实测 8G 内存的服务器跑起来之后,ES 吃掉 3G,MySQL 吃掉 1G,RAGFlow 的主服务再吃 2G,剩下的给系统别的进程已经捉襟见肘了。所以如果机器只有 8G,建议把 ES 的ES_JVM_OPTS调小一点:
environment: - ES_JVM_OPTS=-Xms2g -Xmx2g再低就不建议了,ES 内存不够会直接 OOM。
3.2 创建知识库与配置解析方式
RAGFlow 后台(端口 9380 对应 Web 界面)操作路径是这样的:登录进去,点“知识库” → “创建知识库” → 选 Embedding 模型 → 保存。
Embedding 模型的选择是个大坑。RAGFlow 默认列表里有BAAI/bge-large-zh-v1.5这种国产模型,也有 OpenAI 的 embedding。国内企业做私有化部署,OpenAI 基本用不了,网络和合规都是问题,老老实实选 bge 系列。
答一下“llama 适合国内企业拿来搞知识库问答和私有化 agent 部署吗”——这个问题我被问过很多次。如果你的团队懂模型微调、有 GPU 资源、且对推理速度有耐心,Llama 3 中文版是可用的。但对大多数企业内部知识库场景,尤其是文档处理为主的需求,我强烈建议别一上来就上 Llama。原因是中文效果需要额外调优,部署成本高,而 RAGFlow 默认推荐的Qwen系列或者国产开源模型(比如Qwen2.5-7B-Instruct)在中文语义理解上完全不输 Llama,API 接入还省事。我自己在用的方案是:Embedding 用 bge-large-zh,生成模型用本地的 Qwen2.5-7B,通过 Ollama 起服务,RAGFlow 侧配置 OpenAI-compatible 的模型接口指过去。
创建知识库时有个细节:知识库名称和 Embedding 模型一旦创建不能改。我当时建库时手滑选了英文模型BAAI/bge-large-en-v1.5,搞出来的检索效果很差,中文文档几乎匹配不上。只能删库重建,白白浪费了重新解析一遍文档的时间。切记,中文文档库必须选中文 Embedding 模型。
3.3 文档解析:从上传到入库的等待
文件解析是 RAGFlow 的核心亮点,也是最容易出问题的环节。上传一个 PDF 到知识库之后,RAGFlow 后台会显示“解析中”状态。解析流程是:
- DeepDoc 做版面分析,识别哪些区域是标题、段落、表格、图片
- OCR 识别扫描件里的文字(用的 PaddleOCR)
- 表格转成 Markdown 格式
- 段落做切分,按语义生成 chunk
- 每个 chunk 跑 embedding 模型,转成向量存入 ES
“ragflow文件解析”和“ragflow 教程 批量处理文件”这两个热搜词说明大家对这块需求很大。批量处理场景我有个经验:RAGFlow 解析文件吃 CPU 和内存,一次性丢 50 个文件进去会把 ES 打挂。稳妥的做法是分组上传,每次 10 个文件左右,观察解析状态队列不积压再加量。
> 注意:上传 PDF 前最好先检查一下是否是“假 PDF”──图片扫描件还是文本型 PDF。 > 纯扫描件必须靠 OCR,解析时间翻倍;文本型 PDF 解析速度很快。 > 提前用 pdfinfo 或 macOS 的“预览”看一眼能否选中文字,心里有个数。3.4 解析效果的验收方法
有时候解析过程成功了,但结果不能用。我一般会打开知识库里某个文档的“解析结果”,仔细看两点:
一是有没有把表格拆得七零八落。RAGFlow 在处理带边框的表格时一般能整张提取成 Markdown 表格,但如果是无边框的“伪表格”(用空格、Tab 排出来的),它会有概率误判成普通文本,这时候回答引用出来的内容就是乱的。
二是 chunk 有没有过度切分。DeepDoc 默认的切分逻辑按段落走,如果某一段特别长,它可能切出上百个 token 的 chunk,这会稀释检索 Top-K 的精度。遇到这种文档,我的办法是:原文里先人工把大段落拆成几个小段落再加粗小标题,重新解析后效果立竿见影。
4. 核心流程落地:文件上传到问答闭环
4.1 上传流程的实现细节
整个文件上传链路我画一下(这里用文字描述,不画图了):
前端 → RuoYi 后端/ai/document/upload→ 本地磁盘保存 → 记录数据库 → 调用 RAGFlow 创建文档接口 → 调 RAGFlow 开始解析接口 → 返回解析任务 ID → 前端轮询后端查状态 → 解析完成展示。
@PostMapping("/upload") public AjaxResult upload(MultipartFile file, Long kbId) { // 1. 权限校验:当前用户是否有这个知识库的操作权限 // 2. 保存文件到本地 /ruoyi/upload/ai/YYYY/MM/dd/ // 3. 向数据库 t_ai_document 插入文档记录,状态为 0(待解析) // 4. 调用 ragFlowClient.createDocument(kbId, file) // 5. 调用 ragFlowClient.startParse(docId) // 6. 更新状态为 1(解析中) return AjaxResult.success(); }有个问题值得单独说:RAGFlow 的 document API 对文件大小有要求,超过 100MB 的文件会直接拒绝。企业里的 PDF 如果带大量高清图片,很容易超限。我这边写了一个前置判断,超过 80MB 就提示用户拆分成小文件再传。
解析状态查询这里,RAGFlow 提供两个接口:GET /api/v1/datasets/{dataset_id}/documents/{doc_id}拿单个文档状态,还有个批量接口可以一次传多个 DOC 的 ID。我前端轮询用的是批量接口,每 5 秒查一次,因为单文档接口轮询 200 次还不如一次拉全部文档状态过来对比。
4.2 问答接口:会话保持是关键
RAGFlow 的问答 API 结构比较特别,它不是每次请求都得传知识库 ID,而是要靠“会话(Chat)”这个概念。你需要先调POST /api/v1/chats创建一个会话,指定这个会话关联哪些知识库,拿到chat_id之后再调POST /api/v1/chats/{chat_id}/completions发问题。
这一步我踩过坑:一开始我以为每次提问都新建会话,传完问题就丢掉。结果发现 RAGFlow 的会话有上下文记忆能力,同一个chat_id连续追问才能带上历史对话信息。后面做了改造,在t_ai_chat_session表里记录用户与会话的对应关系,同一个用户在前端“连续对话”时走同一个chat_id,点击“新建对话”才重新创建会话。
@PostMapping("/chat") public void chat(@RequestBody ChatRequest req, HttpServletResponse response) { String chatId = chatSessionService.getOrCreateChatSession(req.getUserId(), req.getKbIds()); // 设置 SSE 响应头 response.setContentType("text/event-stream"); response.setCharacterEncoding("UTF-8"); // 调用 RAGFlow 的 completions 接口 RagFlowClient.streamChat(chatId, req.getQuestion(), callback); }4.3 流式输出的正确姿势
RAGFlow 的/completions接口走 SSE,服务端会持续推送数据处理,最终输出长这样:
data: {"code": 0, "data": {"answer": "这是一个片段"}} data: [DONE]在 RuoYi 后端处理流式返回,我遇到过中文乱码问题。核心点是:响应头的Content-Type必须是text/event-stream; charset=UTF-8,且不能手动调response.getWriter()和response.getOutputStream()混用。这两个输出流只能选一个用,混用会报IllegalStateException。
Vue 前端这边用fetch的ReadableStream读取流式数据最方便。axios 对流式响应支持不太好,除非你装@microsoft/fetch-event-source这个库,否则推荐直接上原生 fetch。
const response = await fetch('/ai/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question: this.question, kbIds: this.selectedKbs }) }); const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); while (true) { const { done, value } = await reader.read(); if (done) break; const text = decoder.decode(value, { stream: true }); this.answer += text; }4.4 引用溯源功能展示
RAGFlow 比普通 RAG 框架强的地方在于回答会附带引用。流式返回完答案之后,紧接着会收到一个包含reference字段的 JSON,里面有文档名称、页码、原始内容片段。我在前端做了一个折叠面板,展示“参考来源”,用户点开就能看到原文片段和文档跳转链接。这个功能对内部知识库的信任度提升非常明显——员工愿意用 AI 问答的前提是知道 AI 的依据是什么。
5. 常见问题与排查实录
5.1 部署与启动环节
问题一:RuoYi 调 RAGFlow 接口报 Connection Refused
排查思路:先确认 RAGFlow 容器起来没有,docker ps看进程,如果 RAGFlow 服务端口起来了,再用curl在 RuoYi 服务器上直接请求http://RAGFlow_IP:9380/api/v1/datasets测通不通。八成是网络组策略挡掉了,或者.env里配的端口没有映射出来。还有种情况是 Docker 容器内部用的端口和宿主机端口不一致,注意检查docker compose ps打印出来的实际映射。
问题二:内存不足导致 RAGFlow 服务反复重启
RAGFlow 的 docker-compose 里depends_on的依赖关系不会自动解决资源竞争。ES 还没起来,RAGFlow 主服务先启动连不上 ES,就自动退出;Docker 的restart: always又把它拉起来,形成死循环。我解决的办法是先把核心依赖单独启动,手动等 ES 健康检查过了再启动主服务:
docker compose up -d elasticsearch mysql minio # 等 30 秒 curl localhost:9200/_cluster/health # 确认 ES 状态 green docker compose up -d ragflow5.2 文档解析环节
问题三:PDF 表格解析出来是乱的
这个我吃过不少亏。RAGFlow 的 DeepDoc 对有线框的表格识别率很高,但遇到跨页表格、合并单元格这种复杂结构还是会翻车。我的经验是:核心表格类文档,先用 WPS 或 Acrobat 的“导出表格”功能把表格单独转成 Excel 或 CSV 再进知识库,文字描述部分保留 PDF。这样混合入库反而比一股脑丢 PDF 效果好。RAGFlow 对 xlsx 文件的解析不比对 PDF 弱,这点是我实测出来的。
问题四:解析进度一直卡在 50% 不动
大概率是某个文件里包含了超高分辨率的图片,OCR 跑得太久。到容器里看日志:
docker logs -f ragflow-server看到OCR timeout之类的字眼就明白了。解决方法是:把大图文件换掉,或者把 RAGFlow 的OCR_TIMEOUT配置调大。RAGFlow 在.env里没有直接暴露这个参数,得改代码里的配置,嫌麻烦的话直接在“文档设置”里把 OCR 引擎换成本地 CPU 版本,牺牲点速度换稳定。
5.3 检索与问答环节
问题五:问题总是答非所问,引用片段乱七八糟
先别怀疑模型能力。多数情况是 Embedding 模型选错了。检查一下你知识库创建时选的 Embedding 是不是中文模型,如果选的是英文模型,中文文档检索结果肯定乱。第二个常见原因是 chunk 切得太碎,检索 Top-K 找回的片段残缺不全。到 RAGFlow 后台把 chunk 重叠度调大一点,或者重解析文档前先在原文里把段落结构理清楚。
问题六:回答速度很慢,一个简单问题要等十几秒
链路分三段排查:一是 RAGFlow 解析文档时有没有大量未完成的任务在排队,排队的任务会抢占模型推理资源;二是 Embedding 模型是不是跑在 CPU 上,如果是,换个 GPU 机器或者用云上推理;三是生成模型本身延迟,Qwen2.5-7B 在纯 CPU 环境下生成一个 200 字回答就要 10 秒以上,这很正常。想提速就是从模型规模上做减法,7B 换 1.8B,或者上 GPU。
5.4 前端交互与权限
问题七:非管理员用户看不到知识库菜单
这是若依的菜单权限机制,需要在系统管理 → 菜单里把 AI 知识库的菜单分配给对应角色。光配置前端显示不够,后端接口的@PreAuthorize注解也要对应放开权限码。我这边新建了ai:kb:list、ai:kb:add、ai:kb:chat这种权限标识,跟若依的角色权限体系完全打通。
6. 一些经验和后续扩展方向
6.1 通用集成经验
整个项目做下来,我最深的两点体会:一是不要把 AI 能力做得太“特化”。很多人一上来就想让 RAGFlow 直接嵌入若依的菜单里,做成一个“智能客服”页面,这样反而限制死了应用场景。我做的是把知识库、文档、会话这些原子能力封装成通用接口,后续接 OA 审批助手、接项目归纳总结、接合同审核,都是往适配层上加逻辑的事,不用另起炉灶。二是监控体系一定要早搭。RAGFlow 容器挂了、ES 负载炸了、模型推理变慢了,这种事情一旦发生你肯定是最后一个知道的。我在若依里做了个简单的定时任务,每两分钟探活一次 RAGFlow 的核心 API,挂了就往钉钉群发告警。这个成本很低,但救了我好几次。
6.2 后续可以扩展的方向
如果你顺着这条链路继续往下做,有几条路是比较自然的:一是把 RAGFlow 返回的引用数据回流到若依数据库,做知识库热度和命中率的统计分析;二是接入若依的工作流模块,让知识库接口变成审批流程里的一个节点,比如合同审核时自动调知识库里面的历史模板做比对;三是把多个知识库按部门隔离做成“知识空间”,RuoYi 这边用部门数据权限控制谁能看到哪些知识库,RAGFlow 那边在会话层做多知识库路由。
6.3 选型之外的补充思考
看热搜词有“dify ragflow weknora 开源版 企业功能比较”,最后补一点我对这三者的判断。Dify 的强项是工作流编排,适合做复杂 Agent 应用,但文档解析和知识库召回质量比 RAGFlow 弱一些。FastGPT 的界面和文档体验不错,社区也活跃,但底层的解析链路和 RAGFlow 比还是差点意思。如果你的核心诉求就是“把企业内部文档变成可检索、可问答的知识库”,RAGFlow 目前是最省心的开源方案。如果后续要往“对话式业务流程自动化”方向走,再考虑引入 Dify 和 RAGFlow 串联。部署一套 RAGFlow 的成本很低,但要真正让它在你公司的文档土壤里长出东西来,坑还有不少,希望这篇记录能给你省下几天的排查时间。