1. 为什么是 RuoYi + RAGFlow
1.1 从“文档黑洞”到知识库平台
我最早做企业内部知识平台的时候,需求其实特别朴素:把散落在共享盘、企业内部Wiki、甚至各种IM聊天记录里的文档整理起来,让员工能用自然语言提问,系统给答案。试过直接用Elasticsearch做全文检索,发现两个问题:一是对PDF、扫描件这类非结构化文档处理得非常痛苦,二是用户要的是答案而不是一堆文档链接,传统的BM25检索很难直接给出可用结论。
后来接触了RuoYi和RAGFlow的组合。RuoYi作为国内使用率很高的Java快速开发框架,自带用户管理、部门管理、角色权限、代码生成、定时任务这些模块,用来做知识库系统的管理后台非常顺手。RAGFlow则是面向深度文档理解的开源RAG引擎,特别擅长处理复杂格式文档的解析、切分和向量化,两者结合正好把我最头疼的两块都补上了。
1.2 私有化部署的核心诉求
企业内部对知识库的要求,第一点是数据不出内网。文档里包含了产品报价、技术方案、客户信息,谁也不想把这些数据交给外部平台做向量化。RAGFlow支持本地部署,意味着embedding模型也好、后续要接的大模型接口也好,都可以完全放在自己的服务器或者内网环境里。也正因如此,“私有化知识库”这几个字的含金量才真正体现出来。
第二点是权限可控。RuoYi本身的RBAC权限体系已经非常成熟,我可以在后端做知识库的“可见范围”控制,再配合RuoYi目前的用户体系,做到不同部门只能查询本部门的知识资源。而直接裸用RAGFlow的公共页面,权限模型过于简单,和生产环境的要求并不匹配。
1.3 整体集成思路与架构分层
在动手之前,我先把整体架构理清楚,避免在集成过程中东一榔头西一棒子。整个系统分成三层:底层是RAGFlow引擎,负责文档解析、向量化、存储和检索;中间层是RuoYi的后端服务,负责业务逻辑、权限控制和API转发;最上层是RuoYi-Vue提供的管理界面,嵌入知识库管理菜单、文档上传页面、问答测试页面等。
这么做的好处很明显:RuoYi不直接依赖RAGFlow的实现细节,RAGFlow升级或者换用其他RAG引擎时,只需要改动中间层的封装代码。同时在RuoYi里可以统一通过一个RagFlowClient类来调用RAGFlow的API,无论后面的业务是写操作日志、做统计报表还是增加审批流,都是在RuoYi的体系里完成,RAGFlow只承担纯粹的“检索增强”职责。
2. 先把 RAGFlow 部署起来
2.1 环境要求与方案选择
RAGFlow官方推荐的部署方式是Docker Compose。建议部署机器至少有8核CPU、16GB内存,磁盘看知识库体量来定,一般环境下预留100GB干净空间比较稳。我当时用了内网一台ESXi虚拟机,分配了8核16G,跑测试环境很轻松。如果说你的文档量很大,或者要并发处理大量PDF,建议再加一档配置,尤其是内存,RAGFlow解析文档和向量化阶段吃内存比较凶。
模型方面,RAGFlow支持多种部署模式。我这边没有用RuoYi后端直接去外网调大模型API,因为内部网络策略不允许,而是通过内网搭了一个模型网关,把时候到DeepSeek和Qwen的接口转发到内部许可的模型服务上。如果你有专门的模型服务器,或者考虑用Llama这类开源模型做私有化部署,也是完全可行的,关键是要把模型服务地址和Key配置到RAGFlow的环境变量里。这里要说一句大实话:开源模型能不能用,取决于你的文档类型和答案质量要求,我实测下来,国内企业做知识库问答和私有化Agent场景,完全本地化的Llama逐渐趋近于能用的水平,但要想达到较好的回答效果,还是得把embedding模型和chat模型分开调优。
2.2 Docker 部署步骤
克隆仓库后进入docker目录,复制.env文件并修改关键配置:
git clone https://github.com/infiniflow/ragflow.git cd ragflow/docker cp .env .env.local.env里要改的地方主要是模型相关:RAGFLOW_LLM_FACTORY、RAGFLOW_LLM_MODEL对应的大模型服务类型和模型名,还有RAGFLOW_EMBEDDING_MODEL选择向量模型。如果只是先跑通,可以先使用默认配置连一个可用的模型API,后面再统一调整。接着直接启动:
docker compose up -d启动完成之后检查容器状态,重点关注ragflow-server和ragflow-web这两个容器是否正常。访问http://服务器IP:9380,初始化管理员账号。需要注意的是,RAGFlow会把mysql、es、redis这些中间件一并编排起来,端口占用要提前检查,不然会在启动阶段报端口冲突。
2.3 部署后的初始配置
RAGFlow登录之后,第一件事是配置模型供应商。在页面“模型配置”里添加你之后要用的embedding模型和对话模型,然后创建第一个知识库做连通性测试。把一份PDF传进去,观察是否能够完成解析、切片和向量化。这一步强烈建议在写RuoYi集成代码之前做,因为后续所有业务逻辑都依赖RAGFlow的解析和检索结果,如果基础链路的模型参数不对,后面排查起来会非常痛苦。
我在实际操作中会把docker compose的容器日志输出到固定文件,因为RAGFlow的API报错信息往往会出现在日志中,而且ES索引健康状态对检索速度影响很大,一旦ES容器内存不足,知识库的检索就会明显变慢。建议定期执行docker compose ps查看状态,发现问题及时看日志。基础环境稳定之后,再进入RuoYi侧的开发。
3. RuoYi 后端集成设计
3.1 RAGFlow API 的认证方式与调用约定
RAGFlow各个接口都需要在Header里带上Authorization: Bearer 。API Key可以在RAGFlow页面右上角头像菜单里的API Keys中创建。我在RuoYi中把API Key配置在sys_config系统参数表里,而不是硬编码在代码中,这样以后RAGFlow端重置Key时,只需要在若依后台的“参数设置”里改一条配置即可,不用重新发版。
RAGFlow的接口调用路径大致是:/api/v1/kb用于知识库管理,/api/v1/kb/{kb_id}/files用于文档上传与管理,/api/v1/retrieval用于检索问答,/api/v1/chats用于对话助手管理。若依后端做一层统一的HTTP封装,客户端选择RestTemplate或者OkHttp都可以,我习惯用OkHttp,因为连接池和超时配置更灵活,做长耗时的文档上传也比较稳。
3.2 封装统一的 RAGFlow 客户端
为了让RuoYi各业务模块只面对自己的Service接口,我在common模块中建了一个RagFlowClient类,把创建知识库、上传文件、触发解析、检索问答等操作封装成Java方法。
知识库创建的逻辑大概是这样的:
public Long createKnowledgeBase(String name, String embeddingModel) { String url = baseUrl + "/api/v1/kb"; JSONObject body = new JSONObject(); body.put("name", name); body.put("embedding_model", embeddingModel); body.put("chunk_method", "knowledge_graph"); // 携带header:Authorization、Content-Type ... }这里有几个参数要重点解释一下。chunk_method指的是文档切分策略,RAGFlow提供了通用、简历、书籍、paper、手动等多种模式,知识库内容和文档类型强相关。如果是公司内部制度文档和产品说明书,用默认的通用切分模式即可;如果是合同类或者带有大量表格的文档,建议选用手动模式,并自定义分隔符和chunk大小,避免切出来的文本块语义被撕裂。
API返回后会携带知识库ID,我在RuoYi侧建了一张sys_kb_info表,把知识库ID、名称、使用部门、embedding模型、状态等信息关联起来,同时把创建人、创建时间通过RuoYi自带的BaseEntity维护,这样后面做知识库列表和权限过滤时都方便。
3.3 业务模块与数据权限设计
RuoYi的风格是“先有菜单,后有模块”。我在菜单管理里新增了一个“知识库管理”一级菜单,下面挂“知识库列表”“文档管理”“问答测试”三个页面。对应的Controller走RuoYi标准的@PreAuthorize权限注解,例如@PreAuthorize("@ss.hasPermi('kb:info:list')"),这样不同角色的人只能看到自己有权限的菜单入口。
数据权限上,知识库表增加dept_id字段,用RuoYi内部通用的数据权限拦截逻辑,在Service层查询时自动追加本部门的过滤条件。文档上传时通过SecurityUtils.getLoginUser()拿到当前登录用户和部门,存入知识库关联表中,这个操作正好和若依“登录用户信息写入”的机制打通,不需要额外维护会话。
3.4 上传文件与触发解析的流程设计
文档上传这个环节,很多人以为就是把文件POST到RAGFlow就完事了,实际上流程是这样的:先把文件二进制上传到RAGFlow的文档接口,获取到文档ID;然后调用解析接口触发解析;解析完成后状态变为running,后台异步完成chunk、embedding;等到文档状态变为done,才能在知识库的检索结果中出现。
所以在RuoYi里我设计了一个文档上传任务表,上传完成后立即记录file_id和状态字段,同时创建一条定时任务,每隔一段时间轮询解析状态,把倒排索引和向量索引的状态同步回业务表。也可以在RuoYi的定时任务管理里配置一个cron,扫描状态不是done的文档,批量调用RAGFlow的查询接口刷新状态。这样即使用户在上传后关闭了浏览器,后台也会把解析状态自动跟踪到完成。
4. 核心功能开发:从上传到问答的完整链路
4.1 创建知识库并管理文档
在若依后端实现一个知识库创建接口,接收知识库名称、绑定的embedding模型名称、切分策略等参数,内部调用RagFlowClient.createKnowledgeBase创建远端知识库,成功之后再插入本地sys_kb_info表,并回写status为可用。
文档上传接口我采用了分段上传的思路。RAGFlow上传接口本身支持分片上传,前端用若依Upload组件直接选中文件,后端拿到MultipartFile后,先计算文件大小,超过阈值就分片调用,不足阈值的直接整传。整传的代码大致是这样:
@RequestMapping("/upload") public AjaxResult uploadFile(Long kbId, MultipartFile file) { Long ragFileId = ragFlowClient.uploadDocument(kbId, file.getOriginalFilename(), file.getBytes()); // 保存本地记录,状态解析中... }上传之后不要忘记调用解析接口:
ragFlowClient.startParsing(kbId, fileId);解析接口是异步的,立即返回,所以业务层要把文档状态更新为“解析中”,等待轮询结果。
4.2 文件解析与检索质量调优
RAGFlow的解析能力来自DeepDoc引擎,它对复杂排版的PDF、带表格的Office文档识别效果比我预想的好。但我调试时发现,切分策略直接影响问答命中率。例如一份产品手册,每页有页眉页脚,如果切出来的chunk把页眉当成正文,检索的时候这些噪音信息就会干扰排序。
我的建议是:对于内部Word文档,先用手动模式,把分隔符设置成段落标记,并把chunk大小调整到400到800之间,重叠量控制在50左右;对于扫描版PDF,在知识库里选择OCR模式,并确认RAGFlow的OCR服务配置正确;对于代码类文档,则直接选用“paper”模式,主要考虑代码块的完整性,避免把一段完整代码拆得七零八落。这些参数不一定一次到位,我通常用同一批测试问题来对比不同参数下的回答质量,选出最合适的一组固定下来。
4.3 问答接口的对接实现
问答是整个集成中最核心的功能。RAGFlow提供了检索接口,我封装在RagFlowClient中,接口参数至少包含知识库ID和用户问题。在业务层,我会先从RuoYi的用户表中读取当前用户信息,把“用户提问记录”写入本地日志表,再调用检索接口,拿到候选文档段落和生成答案。
伪代码如下:
public QAResult ask(Long kbId, String question) { // 1. 记录用户提问 // 2. 调用 /api/v1/retrieval JSONObject payload = new JSONObject(); payload.put("question", question); payload.put("kb_ids", Arrays.asList(kbId)); payload.put("top_k", 3); // 3. 对返回结果做过滤:过滤score低的段落 // 4. 把答案连同引用来源返回前端 }前端拿到答案后,展示出引用来源的文档名称和页码,这个体验对内部员工来说非常关键,没有引用来源的问答很难被信任。RuoYi-Vue的页面可以直接在对话框中展示这些内容,样式上类似一个简化版的ChatGTP界面,我用的是vue-advanced-chat组件改造,测试下来研发成本很小。
4.4 批量处理与后台任务
企业和知识库运营中对批量导入的要求非常普遍。最开始我们是一个一个文件上传,后来发现运维部门的制度文档一次就是上百份,就在RuoYi的定时任务模块里加了一个“批量文档同步”任务。每天凌晨定时扫描指定目录下的新文件,逐一上传并触发解析,解析完成之后再向知识库管理员推送一条通知。
这里还涉及到一个选择:是否需要把文档内容重新解析后同步到ES?RAGFlow底层已经用了ES做检索,我不建议再复制一份数据到业务库,否则同步复杂度和存储成本都会翻倍。数据同步只需要保存file_id、文档状态、chunk数量这些元数据就够了,真正的内容让RAGFlow自己管理。
我还做了自动清理逻辑:对30天内没有被检索过的知识库或者长期处于“解析失败”状态的文件进行标记,并封装了一个“重新解析”按钮。这个功能虽然不起眼,但实际运维中非常有用,因为生产环境中经常有上传的PDF扫描件质量很差,需要换版本重新解析。
5. 集成过程中踩过的坑与排查实录
5.1 部署阶段的高频问题
第一类是端口冲突。RAGFlow的docker compose默认占用的端口比较多,包括9380、9200等,机器上如果已经跑了其他ES集群,9200端口很容易冲突。排查方式很简单,先执行docker compose up -d看哪个容器没起来,再看日志提示哪个端口被占用,要么换宿主机端口映射,要么调整已有服务的端口。
第二类是模型配置问题。RAGFlow页面上的“模型配置”经常被误以为只要填了就能用,实际上它还要测试连通性。我遇到过端到端看起来都正常、但创建知识库之后文件解析一直pending的情况,最后查日志发现是embedding模型没有正确配置,解析流程在生成向量阶段卡住了。
第三类是Docker宿主机加装系统重启导致的容器状态异常。ES容器在断电或者重启之后,如果不通过docker compose up -d重新拉起,有可能出现索引状态为red的情况,此时知识库检索特别慢甚至直接报错。建议把docker compose的启动命令纳入主机开机启动脚本,并定期巡检容器健康状态。
5.2 接口调用中的权限与路由问题
RuoYi默认的跨域配置只针对前端地址,需要在application.yml里把RAGFlow的地址加入Spring Boot的CORS配置,否则浏览器直接调用若依代理转发时会出现跨域报错。我最初遇到时还以为是前端代码问题,最后发现是Nginx或Spring的跨域配置没有把ragflow域名加进去。
另外,认证信息的传递也是一个隐藏问题。RuoYi前端登录后拿到Token,后端通过Shiro或Security校验,但RAGFlow的API Key和用户的Token是两套体系。我在网关层单独定义了一个RAGFlow-Credential管理器,把RAGFlow的API Key和每个知识库的绑定关系缓存起来,而不是让每个用户各自配置API Key,不然运维端很难追踪谁在调哪个知识库。
5.3 问答效果不佳的排查思路
如果检索出来的答案一直不理想,第一件事不是调模型,而是看检索阶段是否拿到了正确的chunk。我的排查路径是:先直接调RAGFlow的检索接口,查看返回的候选段落列表,如果没有相关段落,说明文件解析的切分有问题或者embedding模型选择不合适;如果有相关段落但答案不准确,再考虑调整生成模型参数和提示词。
RAGFlow提供的知识库详情里可以看到每个chunk的具体内容,我强烈建议在问答测试页加上这个调试视图,在后端把检索命中的chunk详情记录下来,前端在问题答案下方展示。这样产品同学和运维同事在反馈问题时,可以直接把命中的chunk截图甩过来,省去大量来回沟通。
还有一点,对于企业内部某些专业术语特别多的场景,光靠embedding检索不够,可以在知识库配置里加入同义词表或者人工标注的问答对,让RAGFlow在检索时优先命中这些人工维护的高质量条目。RAGFlow支持在知识库里设置“文档模板”和“问答对”相关功能,把高频问题先录入进去,能明显提升问答准确率和响应速度。
5.4 性能与并发优化
RAGFlow的解析和检索都是CPU和内存密集型的。并发上传文件多的时候,解析队列会堆积,我建议在RuoYi侧做一个简单的令牌桶限流,控制同一时间上传和触发的解析数量,防止RAGFlow服务被打爆。如果企业内部并发量很大,可以考虑给RAGFlow的ES单独分配更多内存,或者做横向扩展,但这个投入要结合知识库实际使用量来评估,小规模团队一开始并没必要把架构做那么重。
检索接口的响应时间一般在几百毫秒到数秒之间,主要取决于知识库文档总量和检索策略。如果前端要求低延迟,可以把高频问题和对应答案做成缓存,利用RuoYi的Redis缓存模块,当用户问到相同问题时直接命中缓存,不走RAGFlow。缓存逻辑要注意失效策略,当知识库内容更新时,需要把相关缓存清掉,不然用户会一直得到旧答案。
5.5 数据安全与合规建议
这里想多说一点。私有化知识库的“私有化”不完全是个技术概念,更是一个管理概念。RAGFlow和RuoYi都保存了日志,我在集成时关闭了RuoYi的默认调试日志中对文档内容的打印,避免明文文档数据进日志系统;上传的文档会按照部门做隔离,互不可见;针对高敏级别的文档,直接在RuoYi侧禁止下载,只允许在页面内预览和通过问答查看摘要。
另外,RAGFlow自身的API在调用时不会默认做调用频率审计,我在若依的日志功能里记录了一次完整的调用链:谁、在什么时间、对哪个知识库、查询了什么问题。这个操作日志既能满足内部安全审计的要求,也能在出现数据泄露风险时快速定位。
最后再分享一个小细节:若依框架的验证码。开发环境调试接口时,验证码往往让人烦躁,我一般会在application.yml里把captchaEnabled设为false,等联调结束再打开。这个配置不影响代码逻辑,却能让集成调试阶段省掉不少麻烦。
我在实际项目中体会到,RuoYi和RAGFlow的组合,最舒服的地方在于:RuoYi把复杂的账号、权限、菜单、日志这些通用能力都准备好了,RAGFlow把文档解析和RAG链路这块硬骨头啃下来了,我们要做的只是写一层薄薄的胶水代码。这一层胶水代码看起来简单,但想让它稳定、安全、好用,需要花不少心思在参数调优和场景适配上面。希望这篇实践记录能给准备做私有化知识库的朋友提供一点参考,少走一些弯路。