RuoYi 集成 RAGFlow 实战:Java 后端实现 RAG 知识库问答与流式输出

发布时间:2026/9/26 14:45:14
RuoYi 集成 RAGFlow 实战:Java 后端实现 RAG 知识库问答与流式输出 1. 从零到一为什么要在 RuoYi 里集成 RAGFlow很多做 Java 后端的兄弟都有过这种经历公司内部文档散落在各个角落产品需求文档在 Confluence接口文档在 Swagger运维手册在某个人的本地 Markdown 里新人来了问个问题老员工得翻半天聊天记录。老板一句“搞个内部知识库问答吧”任务就落到你头上了。我最初的想法很简单RuoYi 这套后台管理框架大家都很熟权限、用户、菜单、日志全都现成的直接在上面加一个“智能问答”模块后端调一下大模型的 API 不就完事了。真动手才发现事情没那么简单。直接把用户问题丢给大模型它不知道你公司内部的业务术语不知道你们系统的部署架构更不知道上个月刚改过的接口字段。回答要么是泛泛而谈要么干脆胡编乱造。这就是 RAG检索增强生成要解决的问题。核心思路不复杂用户提问时先从私有知识库里检索出最相关的文档片段把这些片段作为上下文一起塞给大模型让它基于真实资料来回答。RAGFlow 就是一套开箱即用的 RAG 引擎它把文档解析、切片、向量化、检索、重排这些脏活累活全包了还提供了标准的 HTTP API 和兼容 OpenAI 的接口。那为什么是 RuoYi RAGFlow 这个组合我的考量是这样的RuoYi 负责“人和权限”也就是谁可以问、能问哪些库、问答记录怎么审计RAGFlow 负责“知识和检索”也就是文档怎么存、怎么切、怎么找。两者通过 API 解耦各干各擅长的事。RuoYi 不用去碰向量数据库和 Embedding 模型这些它不擅长的东西RAGFlow 也不用关心用户体系和权限控制。这个架构对国内企业来说落地成本最低因为 RuoYi 的二次开发资料铺天盖地RAGFlow 又支持本地化部署数据不出内网。这篇是系列的第二篇上一篇聊了 RAGFlow 的本地化部署和基础配置这一篇重点讲 RuoYi 后端怎么跟 RAGFlow 打通包括 API 封装、流式输出、会话管理、文件上传这些实际开发中一定会踩到的坑。如果你正在做类似的事情或者公司正好有这个需求下面的内容可以直接抄作业。2. 整体架构设计与技术选型考量2.1 前后端与 RAGFlow 的职责边界划分在动手写代码之前先把边界划清楚不然后面会越写越乱。我的划分原则是RuoYi 管“业务态”RAGFlow 管“知识态”。RuoYi 这边负责的东西包括用户登录鉴权用 Sa-Token 或者 Spring Security 都行看项目原有配置、知识库的权限分配哪个部门能访问哪个知识库、问答会话的创建和归档、聊天记录的持久化存储、以及前端交互的 SSE 流式推送。RAGFlow 那边负责文档的上传和解析、文本切片和向量化、相似度检索和重排、调用大模型生成回答。这里有个关键决策点会话状态到底存哪边。RAGFlow 的对话 API 是支持传入conversation_id的它自己会维护多轮对话的上下文。但我建议在 RuoYi 这边也存一份会话记录原因有两个一是审计需求公司要知道谁在什么时候问了什么二是容错万一 RAGFlow 服务重启或者会话过期RuoYi 这边还能根据历史记录重建上下文。所以我的做法是双写RAGFlow 的conversation_id存在 RuoYi 的会话表里每次提问时带上同时把问答内容也落库。另一个决策点是文件存储。热词里有人问“图片存放 MinIO 还是存放到 RAGFlow”这个问题很实际。我的建议是原始文档存 MinIO 或者你们现有的对象存储RAGFlow 只存解析后的切片和向量。原因很简单RAGFlow 的定位是检索引擎不是文件管理系统。你把原始文件也丢给它后期迁移或者备份会很麻烦。RuoYi 这边上传文件到 MinIO拿到 URL 后再调 RAGFlow 的文档解析接口把文件流或者可访问的 URL 传过去。这样文件的生命周期管理还在 RuoYi 体系内RAGFlow 挂了也不影响文件本身。2.2 为什么选择 HTTP API 而不是 SDK 直连RAGFlow 提供了 Python SDK用起来确实方便几行代码就能调通。但 RuoYi 是 Java 技术栈你不可能为了调一个知识库在 Java 项目里嵌一个 Python 运行时那运维会疯掉。所以只能走 HTTP API。好在 RAGFlow 的 API 设计得比较规范核心接口就那么几个创建对话、提问、获取对话历史、上传文档、触发解析。认证方式也简单在请求头里带Authorization: Bearer API_KEY就行。这个 API Key 在 RAGFlow 的 Web 界面里可以生成每个知识库可以配不同的 Key方便做权限隔离。这里有个细节要注意RAGFlow 的 API 默认返回是流式的SSE如果你用普通的RestTemplate或者HttpClient去调会一直阻塞到全部生成完才返回用户体验很差。所以必须用支持流式读取的客户端。我选的是 OkHttp因为它对 SSE 的支持比较成熟而且 RuoYi 项目里通常已经间接依赖了 OkHttp比如通过 Hutool 或者某些工具库不用额外引入太多东西。2.3 技术栈版本与依赖清单为了避免版本兼容问题我把这次实践用到的关键版本列一下都是实测跑通的组合组件版本说明RuoYi3.8.x基于 Spring Boot 2.7.xJDK1.8 / 111.8 实测可用11 更推荐RAGFlow0.15.x本地化部署Docker 方式OkHttp4.12.0流式请求核心依赖Hutool5.8.xJSON 处理和 HTTP 工具Sa-Token1.37.x如果项目用 Sa-Token 做鉴权MinIO8.5.x文件存储可选依赖引入很简单在pom.xml里加 OkHttp 就行。如果你用的是 RuoYi-Vue 版本Hutool 和 Sa-Token 通常已经有了没有的话补一下。dependency groupIdcom.squareup.okhttp3/groupId artifactIdokhttp/artifactId version4.12.0/version /dependency dependency groupIdcom.squareup.okhttp3/groupId artifactIdokhttp-sse/artifactId version4.12.0/version /dependency注意OkHttp 4.x 要求 Kotlin 标准库如果你的项目是纯 Java 且不想引入 Kotlin 依赖可以用 OkHttp 3.14.xAPI 基本一致。我实测 4.12.0 在 Spring Boot 2.7 下没问题Kotlin 标准库会被自动带入不影响打包。3. RuoYi 后端集成 RAGFlow 的核心实现3.1 配置管理与 API Key 的安全存放先把配置项理清楚。我习惯在application.yml里加一个独立的配置块不要跟数据库、Redis 的配置混在一起后期好维护。ragflow: base-url: http://192.168.1.100:9380 api-key: ragflow-xxxxxxxxxxxxxxxx default-dataset-id: xxxxxxxxxxxxxxxx connect-timeout: 30 read-timeout: 300 stream-timeout: 600这里有几个点要说明。base-url是 RAGFlow 服务的地址本地部署的话就是内网 IP 加端口默认 9380。api-key千万不要硬编码在代码里也不建议直接明文写在 yml 里提交到 Git。我的做法是用环境变量覆盖或者用 RuoYi 自带的配置加密功能。如果公司有配置中心Nacos、Apollo那就更好了直接放配置中心。read-timeout和stream-timeout要设大一点。RAGFlow 在检索和生成的时候如果知识库文档多、问题复杂响应时间可能到几十秒。普通接口设 30 秒就够了但流式接口我设了 600 秒防止长回答被截断。这个值根据你们实际文档量和模型速度调整我见过最慢的一次生成用了将近两分钟所以宁可设大点。default-dataset-id是默认知识库的 ID在 RAGFlow 界面创建知识库后URL 里能看到。这个 ID 后面调检索接口要用。3.2 封装 RAGFlow 客户端工具类接下来写一个RagFlowClient工具类把 HTTP 调用封装起来。不要在每个 Service 里直接写 OkHttp 代码那样重复且难维护。Component public class RagFlowClient { Value(${ragflow.base-url}) private String baseUrl; Value(${ragflow.api-key}) private String apiKey; private final OkHttpClient client; public RagFlowClient() { this.client new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(300, TimeUnit.SECONDS) .build(); } private Request buildRequest(String path, String jsonBody) { MediaType JSON MediaType.parse(application/json; charsetutf-8); RequestBody body RequestBody.create(jsonBody, JSON); return new Request.Builder() .url(baseUrl path) .addHeader(Authorization, Bearer apiKey) .addHeader(Content-Type, application/json) .post(body) .build(); } }这个类里我用了构造器初始化OkHttpClient而不是用Bean注入。原因是流式请求和普通请求的超时时间可能不一样后面可以针对流式场景单独建一个 Client。如果你习惯用配置类也可以抽一个OkHttpConfig把不同超时时间的 Client 都注册成 Bean。buildRequest方法统一处理了认证头和 Content-Type这样每个具体接口只需要传路径和 JSON 体就行。注意Authorization头的格式是Bearer加 API Key中间有个空格这个很容易漏。3.3 创建对话与获取会话 IDRAGFlow 的对话接口需要先创建一个 conversation拿到conversation_id后才能提问。这个设计跟 OpenAI 不太一样OpenAI 是你自己维护 messages 数组RAGFlow 是服务端帮你维护上下文。public String createConversation(String datasetId, String name) { String json JSONUtil.createObj() .set(dataset_id, datasetId) .set(name, name) .toString(); Request request buildRequest(/api/v1/conversations, json); try (Response response client.newCall(request).execute()) { String respBody response.body().string(); JSONObject obj JSONUtil.parseObj(respBody); if (obj.getInt(code) 0) { return obj.getJSONObject(data).getStr(id); } throw new RuntimeException(创建对话失败: respBody); } catch (IOException e) { throw new RuntimeException(RAGFlow 连接异常, e); } }这里返回的id就是conversation_id存到 RuoYi 的会话表里。每次用户新建一个聊天窗口就调一次这个接口。如果用户只是刷新页面应该复用之前的conversation_id而不是重新创建否则上下文就丢了。实操心得RAGFlow 的 conversation 是有过期时间的默认好像是 24 小时还是 7 天具体看版本。如果用户第二天回来继续问可能会发现上下文没了。我的处理方式是在 RuoYi 这边存最近 N 轮问答如果检测到 conversation 失效就用历史记录重新拼一个上下文或者干脆新建一个 conversation 并把历史摘要作为第一条消息发过去。3.4 流式问答接口的实现与 SSE 推送这是整个集成里最核心也最容易出问题的部分。RAGFlow 的/api/v1/conversations/{conversation_id}/completions接口返回的是 SSE 流数据格式大概是这样的data: {code: 0, data: {answer: 根据, reference: {...}}} data: {code: 0, data: {answer: 文档, reference: {...}}} data: {code: 0, data: {answer: 内容..., reference: {...}}}每个 chunk 里有一个answer字段是增量文本。你要做的是把这些增量拼起来同时通过 SSE 推给前端。public void streamChat(String conversationId, String question, SseEmitter emitter) { String json JSONUtil.createObj() .set(question, question) .set(stream, true) .set(dataset_ids, CollUtil.newArrayList(defaultDatasetId)) .toString(); Request request buildRequest( /api/v1/conversations/ conversationId /completions, json); client.newCall(request).enqueue(new Callback() { Override public void onFailure(Call call, IOException e) { emitter.completeWithError(e); } Override public void onResponse(Call call, Response response) { try (BufferedReader reader new BufferedReader( new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8))) { String line; StringBuilder fullAnswer new StringBuilder(); while ((line reader.readLine()) ! null) { if (line.startsWith(data:)) { String data line.substring(5).trim(); if ([DONE].equals(data)) break; JSONObject obj JSONUtil.parseObj(data); String answer obj.getJSONObject(data) .getStr(answer); if (StrUtil.isNotBlank(answer)) { fullAnswer.append(answer); emitter.send(SseEmitter.event() .data(answer)); } } } // 流结束后保存完整回答 saveChatRecord(conversationId, question, fullAnswer.toString()); emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } } }); }这段代码有几个关键点。第一用enqueue异步执行不要用execute阻塞 Tomcat 线程否则并发一高线程池就满了。第二读取流的时候用BufferedReader按行读因为 SSE 是行分隔的。第三data:后面的内容要trim()RAGFlow 有时候会在冒号后面加空格。第四遇到[DONE]要跳出循环这是流结束的标志。前端那边用EventSource接收就行RuoYi-Vue 默认用的是 Axios但 Axios 不支持 SSE需要单独用EventSource或者fetch加ReadableStream。这个在前端篇再细说。注意SseEmitter的超时时间要设够默认是 30 秒长回答会被切断。创建的时候传一个较大的值比如new SseEmitter(600000L)。另外如果用了 Nginx 反向代理要配置proxy_buffering off否则 SSE 会被缓冲前端收不到实时推送。3.5 文档上传与解析触发知识库的文档管理也在 RuoYi 这边做界面用户上传文件后RuoYi 先存 MinIO然后调 RAGFlow 的文档上传接口。RAGFlow 上传文档的接口是/api/v1/datasets/{dataset_id}/documents用multipart/form-data格式。这里有个坑RAGFlow 支持传文件流也支持传 URL。如果你传 URL它需要能访问到那个地址。MinIO 如果是内网地址RAGFlow 容器可能访问不到所以最稳妥的方式还是传文件流。public String uploadDocument(String datasetId, MultipartFile file) { RequestBody fileBody RequestBody.create( file.getBytes(), MediaType.parse(file.getContentType())); MultipartBody body new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart(file, file.getOriginalFilename(), fileBody) .build(); Request request new Request.Builder() .url(baseUrl /api/v1/datasets/ datasetId /documents) .addHeader(Authorization, Bearer apiKey) .post(body) .build(); // ... 执行请求拿到 document_id }上传成功后拿到document_id还需要调一次解析接口/api/v1/datasets/{dataset_id}/chunks来触发解析。RAGFlow 的解析是异步的调完接口后文档状态会变成RUNNING解析完成后变成DONE。RuoYi 这边可以做一个定时任务轮询文档状态解析完成后更新本地记录。实操心得RAGFlow 解析 PDF 和 Word 的效果差异很大。PDF 如果是扫描件需要 OCR解析时间会很长而且准确率取决于 OCR 质量。Word 文档解析效果最好因为结构清晰。建议在 RuoYi 上传界面加一个提示告诉用户优先上传 Word 或者文本类文件。另外RAGFlow 的解析参数切片大小、重叠长度可以在知识库配置里调默认是 512 token 切片对于技术文档来说有点大我一般调到 256检索精度会高一些。4. 会话管理与数据持久化设计4.1 数据库表结构设计RuoYi 这边需要两张表一张存会话一张存消息。会话表关联用户和 RAGFlow 的conversation_id消息表存具体的问答内容。CREATE TABLE kb_conversation ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, ragflow_conversation_id varchar(64) DEFAULT NULL, dataset_id varchar(64) DEFAULT NULL, title varchar(255) DEFAULT NULL, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE kb_message ( id bigint(20) NOT NULL AUTO_INCREMENT, conversation_id bigint(20) NOT NULL, role varchar(16) NOT NULL, content text, reference text, create_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_conversation_id (conversation_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;reference字段存 RAGFlow 返回的引用信息就是回答参考了哪些文档片段。这个字段很有价值前端可以展示“本回答参考了以下文档”增强可信度。RAGFlow 的返回里reference是一个 JSON 对象包含chunks数组每个 chunk 有content、document_name、similarity等字段。直接存 JSON 字符串就行不用拆表。4.2 多轮对话上下文的维护策略RAGFlow 自己会维护上下文你只要传conversation_id就行。但这里有个问题RAGFlow 的上下文窗口是有限的如果对话轮次太多早期的内容会被截断。对于需要长期记忆的场景我的做法是在 RuoYi 这边做一个摘要。具体来说当会话消息超过 10 轮时把前 5 轮的内容调一次大模型生成摘要存到会话表的summary字段里。下次提问时如果检测到 RAGFlow 的上下文可能不够用就把摘要作为 system prompt 的一部分传过去。这个策略有点复杂一般场景用不上但如果你做的是客服或者技术支持场景用户可能会连续问几十轮那就需要了。注意RAGFlow 的conversation_id是跟 dataset 绑定的。如果你有多个知识库用户切换知识库时应该新建一个 conversation而不是复用。否则检索范围会混乱。4.3 问答记录的审计与导出RuoYi 自带的日志功能可以记录操作日志但问答内容比较长放操作日志里不合适。我单独做了一个导出功能管理员可以按时间范围、用户、关键词导出问答记录为 Excel。这里用到了 Java POI热词里有人问“Java POI Word 能生成图表吗”答案是能但比较麻烦需要用到 XWPFChart 相关的类。不过问答记录导出用 Excel 就够了POI 的 SXSSFWorkbook 处理大数据量导出很稳。导出的时候注意脱敏如果问答内容涉及敏感信息导出前要过滤。这个看公司合规要求我一般会在导出界面加一个“是否包含敏感词过滤”的选项。5. 常见问题与排查技巧实录5.1 连接与认证类问题问题一failed to connect to the docker api at npipe这个报错跟 RAGFlow 本身没关系是 Docker Desktop 在 Windows 下的配置问题。如果你在 Win11 上部署 RAGFlowDocker Desktop 需要开启“Expose daemon on tcp://localhost:2375 without TLS”选项或者用 WSL2 后端。我实测 WSL2 后端最稳npipe 那个方式经常抽风。问题二api_key_required或api key is required in authorization header检查三个地方API Key 是否在 RAGFlow 界面正确生成、请求头格式是否是Bearer加 Key中间有空格、Key 是否跟知识库匹配。RAGFlow 的 API Key 是分知识库的用 A 知识库的 Key 去调 B 知识库的接口会报这个错。问题三api error: 400 this models maximum context length is 1048576 tokens这个报错说明你传给大模型的上下文太长了。RAGFlow 在检索后会拼接多个文档片段如果片段太多或者切片太大就会超限。解决办法是调小知识库的切片大小或者减少检索返回的片段数量top_k参数。我一般把top_k设为 3 到 5切片大小 256这样上下文长度可控。5.2 流式输出类问题问题四前端收不到流式数据要等很久才一次性显示九成是 Nginx 缓冲的问题。在 Nginx 配置里加location /api/ragflow/ { proxy_pass http://backend; proxy_buffering off; proxy_cache off; proxy_set_header Connection ; proxy_http_version 1.1; chunked_transfer_encoding off; }另外检查后端SseEmitter的超时时间以及 OkHttp 的readTimeout是否够大。问题五流式输出到一半断了可能是 RAGFlow 服务端超时也可能是网络中断。我的处理方式是在前端加一个重试机制如果流断了用最后收到的内容作为上下文重新发起一次请求让模型接着写。这个体验比直接报错好很多。5.3 文档解析类问题问题六上传的 PDF 解析出来是乱码RAGFlow 对 PDF 的解析依赖底层的 PDF 解析库如果 PDF 是扫描件或者用了特殊字体解析效果会很差。解决办法是先用 OCR 工具把 PDF 转成文本再上传文本文件。或者用 RAGFlow 的 OCR 功能但需要额外配置 OCR 模型。问题七解析一直卡在 RUNNING 状态检查 RAGFlow 的容器日志看是不是 Embedding 模型加载失败或者显存不够。本地部署的话Embedding 模型和 LLM 模型都需要显存如果显卡内存小建议把 Embedding 模型换成小一点的比如 bge-small-zh。5.4 常见问题速查表问题现象可能原因排查方向401 未授权API Key 错误或缺失检查请求头 Authorization404 接口不存在路径拼写错误或版本不匹配对照 RAGFlow API 文档流式无输出Nginx 缓冲或超时设置关闭 proxy_buffering回答不准确切片太大或 top_k 太小调整知识库解析参数解析失败文件格式不支持或编码问题转成 UTF-8 文本重试上下文超限检索片段过多减小 top_k 或切片大小会话丢失conversation 过期重建会话并注入历史摘要独家避坑技巧RAGFlow 的 API 返回里code为 0 表示成功非 0 表示失败。但有些版本在流式返回中即使出错也会返回code: 0错误信息藏在data里。所以解析响应时除了看code还要检查data里有没有error字段。这个坑我踩过排查了半天才发现是模型调用失败但接口返回了 200。6. 性能优化与扩展方向6.1 接口响应速度优化RAGFlow 的检索和生成是两个阶段检索通常很快几百毫秒生成取决于模型速度。如果用的是本地部署的模型生成速度可能只有几 token 每秒一个长回答要等很久。优化方向有几个一是换更快的模型比如用 DeepSeek 的 API 或者智谱的 API热词里有人问“DeepSeek API 如何调用”其实跟 OpenAI 格式兼容RAGFlow 里配置一下就行。二是开启流式输出让用户先看到部分内容感知上快很多。三是在 RuoYi 这边加缓存对于常见问题如果知识库内容没变可以直接返回缓存答案。缓存的 key 可以用问题的 MD5value 存答案和引用设一个合理的过期时间。6.2 多知识库与权限隔离如果公司有多个部门每个部门有自己的知识库那就需要在 RuoYi 这边做权限控制。我的做法是在kb_conversation表里加一个dataset_id字段创建会话时根据用户所属部门分配对应的知识库。RAGFlow 那边每个知识库用独立的 API Key这样即使前端传错了 dataset_id后端也会因为 Key 不匹配而拒绝。6.3 后续可以扩展的功能目前这个集成已经能满足基本的问答需求但还有几个方向可以继续做。一是智能体AgentRAGFlow 支持配置 Agent可以调用外部工具比如查数据库、调接口。热词里有人问“RAGFlow 怎么做智能体”这个在 RAGFlow 的界面里配置就行RuoYi 这边只需要把 Agent 的 ID 传过去。二是多模态RAGFlow 新版本支持图片检索如果知识库里有架构图、流程图可以开启图片解析问答时能返回相关图片。三是问答评价在 RuoYi 前端加一个点赞点踩的功能把评价数据存下来后期可以用来微调模型或者优化检索策略。我个人在实际操作中的体会是RuoYi 和 RAGFlow 的集成难点不在代码本身而在对 RAGFlow 各种参数和行为的理解。官方文档写得比较简略很多细节要靠自己试。建议在开发阶段把 RAGFlow 的日志级别调到 DEBUG能看到检索了哪些片段、相似度是多少这对调优非常有帮助。另外不要一上来就追求完美先把主流程跑通再逐步优化切片策略和提示词效果会好很多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询