上一篇介绍了整体架构与选型思路。本篇记录一下实现层面:RAGFlow 私有化部署(无 GPU 环境)、8 个功能模块的数据表设计与核心逻辑,以及 Java 后端对接 RAGFlow API 的完整代码。重点讲解知识库的五种开放范围权限设计。
一、RAGFlow 私有化部署
1.1 硬件要求
以实际开发环境为例:CPU 10 核 / 内存 32G / 无 GPU(笔记本),完全可运行。
| 资源 | 本项目实际 | 说明 |
|---|---|---|
| CPU | 10 核 | 文档解析阶段为主要开销 |
| 内存 | 32G(Docker 分配 8~12G 即可) | RAGFlow 服务 + 解析任务 |
| GPU | 无 | 向量化走 API,本地无推理负担 |
| 磁盘 | SSD 500G | 文档原件 + 向量库 |
关键认知:RAG 方案不做模型训练与微调,算力消耗集中在"文档解析 + 向量化"的一次性过程,日常问答阶段只做检索 + 生成,因此对硬件要求远低于训练/微调方案,只不过经过实践,CPU模式下的文档切片解析速度确实一般,正式环境需要GPU加速。
1.2 Docker Compose 部署ragFlow
bash
# 克隆项目
git clone https://github.com/infiniflow/ragflow.git
cd ragflow/docker
# 配置环境变量
cp .env.example .env
vim .env
# 启动(首次拉取镜像,v0.27.0 建议锁定镜像 tag)
docker compose up -d
ragFlow启用以后登录页如下:

1.3 模型配置(开发环境我用的是硅基流动 API)
RAGFlow 支持配置 OpenAI 兼容的模型供应商。开发阶段选择硅基流动(SiliconFlow)线上 API,主要原因是简化本地环境、提升开发效率------不需要在笔记本上部署嵌入模型和对话模型,即可快速验证业务逻辑。
1.4 模型选择
三个模型各司其职:
| 模型 | 类型 | 作用 |
|---|---|---|
| BAAI/bge-m3 | 向量化 | 把文档切块转成向量,支持多语言与长文本(免费) |
| BAAI/bge-reranker-v2-m3 | 重排序 | 检索结果精排,显著提升准确率(免费) |
| deepseek-ai/DeepSeek-V4-Flash | 对话生成 | 基于检索结果生成答案 |
二、若依侧:8 个功能模块的设计
2.1 模块菜单结构
bash
AI知识库问答
├── 模型管理 # 三类模型统一维护
├── 知识库管理 # 知识库 CRUD + 开放范围
├── 知识库文档 # 文档上传/解析/状态
├── RAG提示词 # 助手系统提示词模板
├── 对话助手管理 # 助手 = 知识库 + 模型 + 提示词
├── RAG会话明细 # 每次问答的详细记录
├── 用户会话管理 # 按用户维度管理会话
└── AI智能问答 # 最终用户工作台
2.2 模型管理
数据表 kb_model:
| 字段 | 类型 | 说明 |
|---|---|---|
| model_name | varchar(100) | 模型名称,如 BAAI/bge-m3 |
| model_type | varchar(20) | 模型类型:向量化模型 / 重排序模型 / 大语言模型 |
| platform | varchar(50) | 模型平台:硅基流动、本地、OpenAI 等 |
| deploy_type | varchar(20) | 部署方式:公有云 / 本地 |
| api_base_url | varchar(200) | API 地址 |
| api_key | varchar(200) | 密钥(密文存储) |
| status | char(1) | 是否启用 |
界面提供**"验证"按钮**:点击后实际调用一次模型 API(向量化模型跑一次 embedding、大模型跑一次补全),把连通性和响应时间直接反馈到界面上,避免配错模型到问答环节才发现。
------三类模型(重排序/向量化/大语言)各维护一条记录,全部启用,平台为"硅基流动",部署方式"公有云",每条记录支持验证/修改/删除。
2.3 知识库管理 + 开放范围(核心权限设计)
整个 AI 模块共 10 张业务表,统一 rag_ 前缀,与若依 sys_ 体系隔离,方便单独备份与维护:
| 表名 | 中文名 | 归属模块 | 职责 |
|---|---|---|---|
| rag_llm_model | RAG大模型配置 | 模型管理 | LLM / Embedding 模型与密钥维护 |
| rag_knowledge_base | RAG知识库管理 | 知识库管理 | 知识库主表,含开放范围 pub_area |
| rag_knowledge_base_auth | 知识库权限表 | 知识库管理 | 指定用户 / 部门 / 角色授权明细 |
| rag_document | RAG知识库文档 | 知识库文档 | 文档元数据 + 解析 / 向量化状态 |
| rag_parse_task | RAG文档解析任务队列 | 知识库文档 | 解析任务异步队列 |
| rag_prompt_template | RAG提示词模板表 | RAG提示词 | 系统 / 用户提示词模板 |
| rag_chat_bot | RAG对话助手配置 | 对话助手管理 | 助手 = 知识库集合 + 模型 + 提示词 + 检索参数 |
| rag_chat_bot_user_sort | RAG对话助手用户排序表 | 对话助手管理 | 每用户独立助手卡片顺序 |
| rag_conversation | RAG用户会话表 | 用户会话管理 | 会话主表(对应 RAGFlow conversation) |
| rag_conversation_msg | RAG会话消息明细 | RAG会话明细 | 问答明细 + 引用来源 + token 消耗 |
通用设计规范(所有主表统一遵守):
- 带
del_flag逻辑删除、is_enable启用开关、create_by / create_time / update_by / update_time审计字段,与若依BaseEntity对齐; - 对接 RAGFlow 的表(知识库 / 文档 / 助手 / 会话)主键一律用 varchar(64) 直接存 RAGFlow 侧 ID,避免本地自增主键与远端 ID 的映射表;
- 状态类字段用数字或短字符串枚举,注释里写明含义(如
parse_status:0 待解析 / 1 解析中 / 2 成功 / 3 失败)。
2.4 模型管理
数据表 rag_llm_model(RAG大模型配置):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| model_name | varchar(128) | 模型标识名称,传给 RAGFlow API,如 BAAI/bge-m3 |
| model_alias | varchar(128) | 前端显示别名 |
| model_type | varchar(32) | 模型类型:llm 大模型 / embedding 向量化模型 |
| model_platform | varchar(64) | 模型平台:ollama / openai / qwen / 硅基流动等 |
| api_key | varchar(300) | API KEY |
| base_url | varchar(300) | API 地址(OpenAI 兼容接口) |
| deploy_mode | char(1) | 部署方式:1 云 / 2 本地 |
| description | varchar(512) | 描述 |
| is_enable | char(1) | 是否启用 |
| del_flag | char(1) | 逻辑删除 |
| create_by / create_time / update_by / update_time | varchar(64) / datetime | 若依审计字段 |
界面提供**"验证"按钮**:点击后实际调用一次模型 API(向量化模型跑一次 embedding、大模型跑一次补全),把连通性和响应时间直接反馈到界面上,本质是参考ragFlow的模型验证功能。
------向量化 / 大语言模型各维护一条记录,全部启用,平台为"硅基流动",部署方式"公有云",每条记录支持验证/修改/删除。
2.5 知识库管理 + 开放范围(核心权限设计)
数据表 rag_knowledge_base(RAG知识库管理):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | varchar(64) | 主键(对应 RAGFlow dataset_id) |
| kb_name | varchar(128) | 知识库名称 |
| kb_desc | varchar(512) | 知识库描述 |
| avatar | varchar(255) | 知识库封面图标 |
| chunk_method | varchar(32) | RAGFlow 分片解析模板,默认 General |
| business_type | tinyint | 业务分类:1 通用文档库 / 2 网页抓取库 / 3 FAQ问答库(本地业务,不传给 RAGFlow) |
| embedding_model | varchar(128) | 绑定向量化模型 |
| chunk_size | int | 分片大小 token(对应 chunk_token_num) |
| chunk_overlap | int | 分片重叠值 |
| rag_status | tinyint | 知识库状态:0 未初始化 / 1 正常 / 2 同步中 / 3 异常失败 |
| rag_msg | varchar(1024) | 状态备注 / 异常信息 |
| total_doc_num | int | 文档总数量(冗余) |
| total_chunk_num | bigint | 向量分片总数(冗余) |
| pub_area | varchar(30) | 开放范围:private 私有 / public 公开 / assign_user 指定用户 / assign_dept 指定部门 / assign_role 指定角色 |
| is_enable | char(1) | 是否启用 |
| del_flag | char(1) | 逻辑删除 |
| create_by / create_time / update_by / update_time | varchar(64) / datetime | 若依审计字段 |
开放范围是知识库权限的核心,创建知识库时直接配置:
| 开放范围(pub_area) | 含义 | 可见人群 |
|---|---|---|
| private | 私有 | 仅创建人可见可用 |
| public | 公开 | 所有登录用户 |
| assign_user | 指定用户 | 仅勾选的用户可见 |
| assign_dept | 指定部门 | 部门内成员可见(含子部门) |
| assign_role | 指定角色 | 拥有指定角色的人可见 |
权限明细用一张授权表 rag_knowledge_base_auth(知识库权限表)实现,通过 auth_subject_type 区分主体类型------相比"每个类型一张关联表",新增 / 取消授权只插删一条记录,后端只写一套代码:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| resource_id | varchar(64) | 关联知识库 ID(rag_knowledge_base.id) |
| pub_area | varchar(20) | 开放范围冗余(private / public / assign_user / assign_dept / assign_role) |
| auth_subject_type | varchar(20) | 授权主体类型:user 用户 / dept 部门 / role 角色 |
| auth_subject_id | varchar(64) | 授权主体 ID:用户 ID / 部门 ID / 角色 ID |
| create_by / create_time / update_time | varchar(64) / datetime | 若依审计字段 |
唯一索引 uk_resource_subject(resource_id, auth_subject_type, auth_subject_id):同一知识库对同一主体只允许一条授权,天然防重复。
------新增知识库表单:知识库名称、向量化模型、分片解析模板、开放范围(私有/公开/指定用户/指定部门/指定角色单选)、封面、描述。
2.6 知识库文档管理
数据表 rag_document(RAG知识库文档):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | varchar(64) | 主键(对应 RAGFlow 文档 ID) |
| kb_id | varchar(64) | 关联知识库 rag_knowledge_base.id |
| doc_name | varchar(256) | 文档名称 |
| doc_type | varchar(32) | 文档类型:pdf / docx / txt / md / url |
| file_path | varchar(512) | 文件存储路径 / 网页 url |
| file_size | bigint | 文件大小(字节) |
| page_count | int | PDF 页数 |
| parse_status | tinyint | 解析状态:0 待解析 / 1 解析中 / 2 解析成功 / 3 解析失败 |
| vector_status | tinyint | 向量化状态:0 待向量化 / 1 向量化中 / 2 成功 / 3 失败 |
| status_msg | varchar(1024) | 解析 / 向量化失败日志 |
| chunk_count | int | 文档分片向量数量(冗余) |
| source | varchar(64) | 来源:upload 上传 / crawl 网页抓取 |
| is_enable | char(1) | 是否启用 |
| del_flag | char(1) | 逻辑删除 |
| create_by / create_time / update_by / update_time | varchar(64) / datetime | 若依审计字段 |
这里有两个关键点:
- 主键直接用 RAGFlow 返回的文档 ID,省掉本地 ID 与远端 ID 的映射表;
- 增加
status_msg(失败原因留痕)、page_count(PDF 页数)、chunk_count(分片冗余)等运维字段。
文档上传后进入解析任务队列 rag_parse_task(RAG文档解析任务队列):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 任务 ID |
| kb_id | varchar(64) | 知识库 ID |
| doc_id | varchar(64) | 文档 ID |
| doc_name | varchar(255) | 文档名称 |
| status | char(1) | 任务状态:0 待执行 / 1 执行中 / 2 成功 / 3 失败 |
| error_msg | varchar(1000) | 失败原因 |
| remark | varchar(500) | 备注 |
| create_by / create_time / update_by / update_time | varchar(64) / datetime | 若依审计字段 |
上传接口只做两件事:落库 rag_document + 插入一条 rag_parse_task 任务,立即返回 ;后端定时任务(@Scheduled)轮询 status = '0' 的任务,逐个调用 RAGFlow 解析接口,完成后回写 rag_document.parse_status / vector_status。异步削峰,还有一个同时大批量上传、解析文档的问题,我也做了处理,后面单独详细讲一下。
页面提供:上传文档 / 批量删除 / 全部解析三个核心操作,左侧为知识库列表(切换知识库查看各自文档),右侧为文档表格,实时展示解析状态。
------知识库 001 下 6 份文档,pdf/docx 混合,全部"解析成功",支持按文档名称/类型/解析状态/是否启用筛选,禁用的文档不参与rag检索。
2.7 RAG 提示词
数据表 rag_prompt_template(RAG提示词模板表):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| template_name | varchar(128) | 模板名称 |
| template_type | varchar(32) | 模板类型:rag_qa RAG问答 / summary 摘要 / translate 翻译 |
| system_prompt | longtext | 系统提示词(角色约束,核心 prompt) |
| user_prompt | longtext | 用户前置模板,可留空 |
| description | varchar(512) | 模板描述说明 |
| is_default | tinyint | 是否默认模板:0 否 / 1 是 |
| is_enable | char(1) | 是否启用 |
| del_flag | char(1) | 逻辑删除 |
| create_by / create_time / update_by / update_time | varchar(64) / datetime | 若依审计字段 |
维护助手的系统提示词模板,比如限定回答范围、要求引用来源、禁止编造等。提示词作为独立模块管理,方便运营人员随时调整而不用改代码;rag_chat_bot.prompt_template_id 引用本表 id,一个模板可被多个助手复用。
2.8 对话助手管理
数据表 rag_chat_bot(RAG对话助手配置):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | varchar(64) | 主键(即 RAGFlow chat_assistant id) |
| bot_name | varchar(128) | 助手名称 |
| bot_desc | varchar(512) | 助手描述 |
| prompt_template_id | bigint | 关联提示词模板 rag_prompt_template.id |
| kb_code_list | text | 绑定知识库 kb_code 集合,JSON 数组 "kb01","kb02" |
| llm_model | varchar(128) | 选用大模型名称 |
| temperature | decimal(3,2) | 温度参数,默认 0.10 |
| max_tokens | int | 最大输出 token,默认 1024 |
| top_n | int | 检索返回 topN 分片数量,默认 3 |
| similarity_threshold | decimal(3,2) | 相似度阈值,默认 0.20 |
| is_stream | tinyint | 是否流式输出:0 否 / 1 是 |
| refine_multiturn | char(1) | 是否开启多轮查询改写 |
| rerank_enable | char(1) | 是否开启 rerank 重排序 |
| is_enable | char(1) | 是否启用助手 |
| del_flag | char(1) | 逻辑删除 |
| create_by / create_time / update_by / update_time | varchar(64) / datetime | 若依审计字段 |
助手 = 知识库集合 + 大模型 + 提示词 + 检索参数的绑定关系。这一块有三个关键点:
- 一个助手可绑定多个知识库 (
kb_code_list存 JSON 数组),问答时多库联合检索; - 检索参数入库 (
top_n/similarity_threshold/rerank_enable/refine_multiturn/temperature/max_tokens),前端可调,后端组装 RAGFlow 请求体时透传,为了降低使用门槛,最初的设计是做一个默认最优配置,将对话助手的大量配置隐藏,让用户能以极低的学习成本来使用这套知识库检索系统; - 助手 ID 即 RAGFlow chat_assistant ID,本地零映射。
业务上可以建多个助手:制度问答助手、技术文档助手、档案查询助手......每个助手面向不同知识库和场景。助手卡片在首页的排序由 rag_chat_bot_user_sort(RAG对话助手用户排序表)维护:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 用户 ID(sys_user.user_id) |
| chat_bot_id | varchar(64) | 对话助手 ID(rag_chat_bot.id) |
| sort_no | int | 排序号(越小越靠前,从 1 开始) |
| create_time / update_time | datetime | 创建 / 更新时间 |
唯一索引 uk_user_bot(user_id, chat_bot_id):每用户对同一助手只有一条排序记录,前端拖拽调整时 upsert,实现每用户独立卡片顺序。
------3 个助手(通用知识问答/对话助手002/我的私人对话助手),均绑定知识库001、使用 deepseek-ai/DeepSeek-V4-Flash。
2.9 RAG 会话明细 + 用户会话管理
数据表 rag_conversation(RAG用户会话表):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | varchar(64) | 主键(即 RAGFlow conversation_id) |
| bot_id | varchar(64) | 关联助手 rag_chat_bot.id |
| user_id | varchar(64) | 若依系统用户账号 |
| conv_title | varchar(256) | 会话标题,AI 自动生成或用户修改,默认"新对话" |
| is_enable | char(1) | 会话是否有效:N 关闭 / Y 正常 |
| del_flag | char(1) | 逻辑删除 |
| create_time | datetime | 会话创建时间 |
| update_time | datetime | 会话最后更新时间 |
数据表 rag_conversation_msg(RAG会话消息明细):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| conv_id | varchar(64) | 关联会话 rag_conversation.id |
| role | varchar(20) | 角色:user / assistant |
| question | text | 用户问题(role=user 有效) |
| answer | longtext | AI 回答内容(role=assistant 有效) |
| reference | text | 引用知识库文档来源 JSON 数组 |
| token_cost | int | 消耗 token 数量 |
| cost_ms | bigint | 请求耗时毫秒 |
| create_time | datetime | 消息时间 |
设计要点:
- 会话主键直接用 RAGFlow 的 conversation_id,若依侧与 RAGFlow 侧一一对应,多轮续聊时直接透传,无映射成本;
- 查询按
conv_id走索引(idx_conv_id),会话列表按user_id + del_flag过滤。
这两个模块的价值:可审计、可复盘。每次问答的问题、答案、引用来源等都留痕,方便排查问题或扩展统计功能。
三、Java 后端对接 RAGFlow API
其实后端的设计思路也很容易理解,首先在配置文件配置ragFlow的相关信息,然后写一个对接ragFlow API接口的工具类,然后按需调用就行了。
3.1 配置
bash
ragflow:
base-url: http://127.0.0.1:90
api-key: ragflow-2rl-xxxxGoqxxxp0Bqa-xxxxx-xxxx
#sse超时,毫秒
sse-timeout: 600000
3.2 核心API工具类(知识库增删改、文档增删解析、创建对话助手等)
这个类其实本质就是把ragFlow的API调用都落实到java类中,完整的API登录ragFlow都可以看到,如下代码可供参考。

bash
package com.ruoyi.rag.controller;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.rag.domain.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Component
public class RagFlowApiClient {
@Value("${ragflow.base-url}")
private String baseUrl;
@Value("${ragflow.api-key}")
private String apiKey;
private final RestTemplate restTemplate = new RestTemplate();
private HttpHeaders getHeader() {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + apiKey);
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
/**
* 获取multipart表单请求头,用于文件上传/web抓取
*/
private HttpHeaders getMultipartHeader() {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + apiKey);
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
return headers;
}
/**
* 创建知识库
* @param ragKnowledgeBase 知识库名称
* @return ragflow kb_id
*/
public String createKnowledgeBase(RagKnowledgeBase ragKnowledgeBase) {
String url = baseUrl + "/api/v1/datasets";
Map<String,Object> body = new HashMap<>();
body.put("name", ragKnowledgeBase.getKbName());
body.put("description", ragKnowledgeBase.getKbDesc());
body.put("embedding_model", ragKnowledgeBase.getEmbeddingModel());
body.put("chunk_method", ragKnowledgeBase.getChunkMethod());
HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body),getHeader());
ResponseEntity<String> resp = restTemplate.postForEntity(url,entity,String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if (!Objects.equals(code, 0)) {
String msg = json.getString("message");
throw new RuntimeException("创建知识库失败:" + Objects.toString(msg,"未知错误"));
}
JSONObject data = json.getJSONObject("data");
return data.getString("id");
}
/**
* 删除单个dataset
* @param datasetId ragflow dataset_id
*/
public void deleteSingleDataset(String datasetId) {
if(datasetId == null || datasetId.isBlank()){
return;
}
String url = baseUrl + "/api/v1/datasets";
Map<String,Object> body = new HashMap<>();
// 就算删一个,也要包进数组
body.put("ids", List.of(datasetId));
HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
ResponseEntity<String> resp = restTemplate.exchange(
url,
HttpMethod.DELETE,
entity,
String.class
);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if (!Objects.equals(code, 0)) {
String msg = json.getString("message");
throw new RuntimeException("删除知识库失败:" + Objects.toString(msg,"未知错误"));
}
}
//==================== 文档上传相关 ====================
/**
* type=local 上传本地文件(File对象,支持多文件)
* @param datasetId 知识库id
* @param files 文件列表
* @return 返回新建文档id集合
*/
public List<String> uploadLocalDocuments(String datasetId, List<File> files) {
if(datasetId == null || datasetId.isBlank()){
throw new IllegalArgumentException("datasetId不能为空");
}
if(files == null || files.isEmpty()){
throw new IllegalArgumentException("待上传文件不能为空");
}
String url = baseUrl + "/api/v1/datasets/" + datasetId + "/documents";
MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
for (File file : files) {
FileSystemResource resource = new FileSystemResource(file);
form.add("file", resource);
}
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(form, getMultipartHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if (!Objects.equals(code, 0)) {
String msg = json.getString("message");
throw new RuntimeException("RAGFlow上传文档失败:" + Objects.toString(msg,"未知错误"));
}
JSONArray dataArray = json.getJSONArray("data");
List<String> docIds = new ArrayList<>();
for (int i = 0; i < dataArray.size(); i++) {
JSONObject item = dataArray.getJSONObject(i);
docIds.add(item.getString("id"));
}
return docIds;
}
/**
* type=local 适配前端 MultipartFile 上传
* @param datasetId 知识库id
* @param multipartFiles spring接收的文件
* @return 文档id列表
*/
public List<RagDocument> uploadLocalMultipartFiles(String datasetId, List<MultipartFile> multipartFiles) {
if(StringUtils.isBlank(datasetId)){
throw new IllegalArgumentException("datasetId不能为空");
}
if(multipartFiles == null || multipartFiles.isEmpty()){
throw new IllegalArgumentException("待上传文件不能为空");
}
String url = baseUrl + "/api/v1/datasets/" + datasetId + "/documents";
MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
for (MultipartFile mf : multipartFiles) {
ByteArrayResource resource;
try {
resource = new ByteArrayResource(mf.getBytes()){
@Override
public String getFilename() {
return mf.getOriginalFilename();
}
};
} catch (Exception e) {
throw new RuntimeException("读取上传文件字节失败", e);
}
form.add("file", resource);
}
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(form, getMultipartHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if (!Objects.equals(code, 0)) {
String msg = json.getString("message");
throw new RuntimeException("RAGFlow上传文档失败:" + Objects.toString(msg,"未知错误"));
}
JSONArray dataArray = json.getJSONArray("data");
List<RagDocument> resultList = new ArrayList<>();
for (int i = 0; i < dataArray.size(); i++) {
JSONObject item = dataArray.getJSONObject(i);
RagDocument ragDoc = new RagDocument();
// ✅ RagFlow文档ID直接存入主键id
ragDoc.setId(item.getString("id"));
// 知识库id
ragDoc.setKbId(datasetId);
//文档名称
ragDoc.setDocName(item.getString("name"));
//文件大小,字节
Long size = item.getLong("size");
ragDoc.setFileSize(size);
//pdf页数
Long pageCount = item.getLong("page_count");
ragDoc.setPageCount(pageCount);
//来源 local本地上传
ragDoc.setSource("local");
//上传完成未启动解析,待解析
ragDoc.setParseStatus(0); //0待解析 1解析中 2解析成功 3解析失败
ragDoc.setIsEnable("Y");
ragDoc.setDelFlag("0");
//截取文件后缀作为docType
String fileName = item.getString("name");
if(fileName != null && fileName.contains(".")){
String suffix = fileName.substring(fileName.lastIndexOf(".")+1).toLowerCase();
ragDoc.setDocType(suffix);
}
resultList.add(ragDoc);
}
return resultList;
}
/**
* type=web 抓取网页生成文档
* @param datasetId 知识库id
* @param docName 文档名称
* @param crawlUrl 网页地址
* @return 文档id
*/
public String uploadWebDocument(String datasetId, String docName, String crawlUrl) {
if(datasetId == null || datasetId.isBlank()){
throw new IllegalArgumentException("datasetId不能为空");
}
String url = baseUrl + "/api/v1/datasets/" + datasetId + "/documents?type=web";
MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
form.add("name", docName);
form.add("url", crawlUrl);
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(form, getMultipartHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if (!Objects.equals(code, 0)) {
String msg = json.getString("message");
throw new RuntimeException("RAGFlow抓取网页文档失败:" + Objects.toString(msg,"未知错误"));
}
JSONArray dataArray = json.getJSONArray("data");
JSONObject item = dataArray.getJSONObject(0);
return item.getString("id");
}
/**
* 查询ragflow单文档信息,使用list接口带id过滤
* @param datasetId 知识库dataset_id
* @param docId ragflow文档id
* @return null=ragflow侧还未生成该文档;返回JSONObject文档对象
*/
public JSONObject getDocumentInfo(String datasetId, String docId) {
if(StringUtils.isAnyBlank(datasetId,docId)){
return null;
}
String encodeDocId = URLEncoder.encode(docId, StandardCharsets.UTF_8);
String url = baseUrl + "/api/v1/datasets/" + datasetId + "/documents?id=" + encodeDocId + "&page=1&page_size=1";
HttpEntity<Void> httpEntity = new HttpEntity<>(null, getHeader());
ResponseEntity<String> resp;
try {
resp = restTemplate.exchange(url, HttpMethod.GET, httpEntity, String.class);
}catch (Exception e){
return null;
}
JSONObject json = JSON.parseObject(resp.getBody());
if(!Objects.equals(json.getInteger("code"),0)){
return null;
}
JSONObject dataObj = json.getJSONObject("data");
JSONArray docsArr = dataObj.getJSONArray("docs");
if(docsArr == null || docsArr.isEmpty()){
return null;
}
return docsArr.getJSONObject(0);
}
/**
* type=empty 创建空文档
* @param datasetId 知识库id
* @param docName 文档名称
* @return 文档id
*/
public String createEmptyDocument(String datasetId, String docName) {
if(datasetId == null || datasetId.isBlank()){
throw new IllegalArgumentException("datasetId不能为空");
}
String url = baseUrl + "/api/v1/datasets/" + datasetId + "/documents?type=empty";
Map<String,Object> body = new HashMap<>();
body.put("name", docName);
HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if (!Objects.equals(code, 0)) {
String msg = json.getString("message");
throw new RuntimeException("RAGFlow创建空文档失败:" + Objects.toString(msg,"未知错误"));
}
JSONArray dataArray = json.getJSONArray("data");
JSONObject item = dataArray.getJSONObject(0);
return item.getString("id");
}
/**
* 【内置分块流水线】启动文档解析(普通知识库绝大多数用这个)
* POST /api/v1/datasets/{dataset_id}/chunks
* @param datasetId 知识库id
* @param docIdList document_ids列表
*/
public void parseDocuments(String datasetId, List<String> docIdList) {
if(StringUtils.isBlank(datasetId)){
throw new IllegalArgumentException("datasetId不能为空");
}
if(docIdList == null || docIdList.isEmpty()){
throw new IllegalArgumentException("文档id列表不能为空");
}
String url = baseUrl + "/api/v1/datasets/" + datasetId + "/chunks";
Map<String,Object> body = new HashMap<>();
body.put("document_ids", docIdList);
HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body),getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url,HttpMethod.POST,entity,String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if(!Objects.equals(code,0)){
String msg = json.getString("message");
throw new RuntimeException("RAGFlow启动解析任务失败:"+ Objects.toString(msg,"未知错误"));
}
}
/**
* 停止解析(内置流水线)
* DELETE /api/v1/datasets/{dataset_id}/chunks
* @param datasetId
* @param docIdList
*/
public void stopParseDocuments(String datasetId, List<String> docIdList){
if(StringUtils.isBlank(datasetId)){
throw new IllegalArgumentException("datasetId不能为空");
}
if(docIdList == null || docIdList.isEmpty()){
throw new IllegalArgumentException("文档id列表不能为空");
}
String url = baseUrl + "/api/v1/datasets/" + datasetId + "/chunks";
Map<String,Object> body = new HashMap<>();
body.put("document_ids", docIdList);
HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body),getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url,HttpMethod.DELETE,entity,String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if(!Objects.equals(code,0)){
String msg = json.getString("message");
throw new RuntimeException("RAGFlow停止解析失败:"+ Objects.toString(msg,"未知错误"));
}
}
/**
* 删除知识库下的单个文档
* DELETE /api/v1/datasets/{dataset_id}/documents
* 注意:RAGFlow删除文档走的是集合路径(body传ids数组),单个文档路径不支持DELETE(会返回405)
* @param datasetId 知识库id
* @param docId 文档id
*/
public void deleteDocument(String datasetId, String docId){
if(StringUtils.isBlank(datasetId)){
throw new IllegalArgumentException("datasetId不能为空");
}
if(StringUtils.isBlank(docId)){
throw new IllegalArgumentException("文档id不能为空");
}
String url = baseUrl + "/api/v1/datasets/" + datasetId + "/documents";
Map<String,Object> body = new HashMap<>();
// 就算删一个,也要包进数组
body.put("ids", List.of(docId));
HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.DELETE, entity, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if(!Objects.equals(code,0)){
String msg = json.getString("message");
throw new RuntimeException("RAGFlow删除文档失败:"+ Objects.toString(msg,"未知错误"));
}
}
/**
* Create chat assistant 创建聊天助手 POST /api/v1/chats
* @param name 助手名称【必填】
* @param icon base64头像,可以null
* @param dataset_ids 绑定知识库uuid列表 List<String>,可以null
* @param llm_id 模型id,可以null
* @param llmSetting llm_setting对象,助手大模型推理参数配置,可以null
* llm_setting包含属性说明:
* "model_type": string 模型类型标识,仅支持"chat"和"image2text";不传或其他值默认当作"chat"。
* "temperature": float 控制模型输出随机性,数值越低回答越保守,越高越有创造性,默认0.1。
* "top_p": float 核采样阈值,从概率最高的词汇中做采样,截断低概率词汇,默认0.3。
* "presence_penalty": float 存在惩罚,对对话中已出现过的词施加惩罚,减少内容重复,默认0.4。
* "frequency_penalty": float 频率惩罚,降低高频重复用词倾向,默认0.7。
* @param promptConfig prompt_config对象,用于定义大模型需要遵守的指令,可以null
* prompt_config包含属性说明:
* "system": string 系统提示词内容。
* "prologue": string 用户打开对话时展示的开场白。
* "parameters": object[] 系统提示词中使用的自定义变量数组。注意:
* knowledge 为保留变量,代表检索出来的知识库片段;
* system内所有变量需要使用大括号包裹。
* "empty_response": string 用户提问未检索到知识库内容时返回的兜底回答;留空则允许模型自由生成回答。
* "quote": boolean 是否展示引用来源切片,默认true。
* "tts": boolean 是否开启语音朗读TTS。
* "refine_multiturn": boolean 是否开启多轮问题改写优化。
* "use_kg": boolean 是否启用知识图谱检索。
* "reasoning": boolean 是否开启模型推理思考模式。
* "cross_languages": list[string] 跨语言检索支持的语言列表。
* "web_search_provider": string 联网搜索服务商,可选值 "tavily"、"querit",不传默认tavily。
* "tavily_api_key": string Tavily联网搜索API密钥。
* "querit_api_key": string Querit联网搜索API密钥,使用该参数时web_search_provider必须指定为querit。
* "toc_enhance": boolean 是否开启目录增强检索(针对带目录长文档优化检索)。
* @param similarityThreshold float 检索相似度阈值,可以null
* @param vectorSimilarityWeight float 向量相似度权重,可以null
* @param topN int 检索结果topN,可以null
* @param topK int 检索结果topK,可以null
* @param rerank_id 重排模型id,可以null
* @return 返回RAGFlow返回完整data JSONObject,取出id即为chatAssistantId
*/
public JSONObject createChatAssistant(String name,
String icon,
List<String> dataset_ids,
String llm_id,
RagChatBotLlmSetting llmSetting,
RagChatBotPromptConfig promptConfig,
Float similarityThreshold,
Float vectorSimilarityWeight,
Integer topN,
Integer topK,
String rerank_id) {
if(StringUtils.isBlank(name)){
throw new IllegalArgumentException("chat assistant name不能为空");
}
String url = baseUrl + "/api/v1/chats";
Map<String,Object> body = new HashMap<>();
body.put("name", name);
if(StringUtils.isNotBlank(icon)){
body.put("icon", icon);
}
if(dataset_ids != null){
body.put("dataset_ids", dataset_ids);
}
if(StringUtils.isNotBlank(llm_id)){
body.put("llm_id", llm_id);
}
if(llmSetting != null ){
body.put("llm_setting", llmSetting);
}
if(promptConfig != null){
body.put("prompt_config", promptConfig);
}
if(similarityThreshold != null){
body.put("similarity_threshold", similarityThreshold);
}
if(vectorSimilarityWeight != null){
body.put("vector_similarity_weight", vectorSimilarityWeight);
}
if(topN != null){
body.put("top_n", topN);
}
if(topK != null){
body.put("top_k", topK);
}
if(StringUtils.isNotBlank(rerank_id)){
body.put("rerank_id", rerank_id);
}
HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if(!Objects.equals(code,0)){
String msg = json.getString("message");
String detail = Objects.toString(msg,"未知错误");
throw new RuntimeException("创建对话助手失败:" + detail + ",返回完整响应:" + json);
}
return json.getJSONObject("data");
}
/**
* Get chat assistant GET /api/v1/chats/{chatId} 查询单个助手
* @param chatId chat助手id
* @return data原始JSONObject
*/
public JSONObject getChatAssistant(String chatId) {
if(StringUtils.isBlank(chatId)){
throw new IllegalArgumentException("chatId不能为空");
}
String url = baseUrl + "/api/v1/chats/" + chatId;
HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if(!Objects.equals(code,0)){
String msg = json.getString("message");
throw new RuntimeException("RAGFlow查询chat assistant失败:"+ Objects.toString(msg,"未知错误"));
}
return json.getJSONObject("data");
}
/**
* Delete chat assistant DELETE /api/v1/chats/{chatId} 删除单个助手
*/
public void deleteChatAssistant(String chatId) {
if(StringUtils.isBlank(chatId)){
throw new IllegalArgumentException("chatId不能为空");
}
String url = baseUrl + "/api/v1/chats/" + chatId;
HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.DELETE, entity, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if(!Objects.equals(code,0)){
String msg = json.getString("message");
throw new RuntimeException("RAGFlow删除chat assistant失败:"+ Objects.toString(msg,"未知错误"));
}
}
/**
* Delete chat assistants 批量删除助手 DELETE /api/v1/chats
* @param ids 要删除chatId列表;如果deleteAll=true,ids传null
* @param deleteAll 是否删除全部当前用户助手
*/
public void batchDeleteChatAssistant(List<String> ids, Boolean deleteAll) {
String url = baseUrl + "/api/v1/chats";
Map<String,Object> body = new HashMap<>();
if(ids != null){
body.put("ids", ids);
}
if(deleteAll != null){
body.put("delete_all", deleteAll);
}
HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.DELETE, entity, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if(!Objects.equals(code,0)){
String msg = json.getString("message");
throw new RuntimeException("RAGFlow批量删除chat assistant失败:"+ Objects.toString(msg,"未知错误"));
}
}
/**
* List chat assistants GET /api/v1/chats 查询助手列表
* @param page 页码,null默认1
* @param pageSize 每页大小,null默认30
* @param orderby 排序字段 create_time / update_time
* @param desc 是否降序 null默认true
* @param keywords 名称模糊搜索
* @param ownerIds 租户id过滤
* @param id 精确chatId匹配
* @param name 精确名称匹配
* @return 返回完整返回体data对象,包含chats数组、total
*/
public JSONObject listChatAssistant(Integer page,
Integer pageSize,
String orderby,
Boolean desc,
String keywords,
List<String> ownerIds,
String id,
String name) {
StringBuilder sb = new StringBuilder(baseUrl + "/api/v1/chats?");
if(page != null){
sb.append("page=").append(page).append("&");
}
if(pageSize != null){
sb.append("page_size=").append(pageSize).append("&");
}
if(StringUtils.isNotBlank(orderby)){
sb.append("orderby=").append(URLEncoder.encode(orderby, StandardCharsets.UTF_8)).append("&");
}
if(desc != null){
sb.append("desc=").append(desc).append("&");
}
if(StringUtils.isNotBlank(keywords)){
sb.append("keywords=").append(URLEncoder.encode(keywords, StandardCharsets.UTF_8)).append("&");
}
if(ownerIds != null && !ownerIds.isEmpty()){
for(String oid : ownerIds){
sb.append("owner_ids=").append(URLEncoder.encode(oid, StandardCharsets.UTF_8)).append("&");
}
}
if(StringUtils.isNotBlank(id)){
sb.append("id=").append(URLEncoder.encode(id, StandardCharsets.UTF_8)).append("&");
}
if(StringUtils.isNotBlank(name)){
sb.append("name=").append(URLEncoder.encode(name, StandardCharsets.UTF_8)).append("&");
}
//去掉末尾&
String url = sb.toString();
if(url.endsWith("&")){
url = url.substring(0, url.length()-1);
}
HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if(!Objects.equals(code,0)){
String msg = json.getString("message");
throw new RuntimeException("RAGFlow查询chat assistant列表失败:"+ Objects.toString(msg,"未知错误"));
}
return json.getJSONObject("data");
}
/**
* 流式版本,stream=true,返回BufferedReader读取SSE原始行(移除Flux、WebClient)
*/
public BufferedReader chatCompletionsV2Stream(String chatId,
List<Map<String, Object>> messages,
String llmId,
String sessionId) {
if (StringUtils.isBlank(chatId)) {
throw new IllegalArgumentException("chatId不能为空");
}
if (messages == null || messages.isEmpty()) {
throw new IllegalArgumentException("messages对话消息列表不能为空");
}
String url = baseUrl + "/api/v1/chat/completions";
Map<String, Object> body = new HashMap<>();
body.put("chat_id", chatId);
body.put("stream", true);
body.put("llm_id", llmId);
body.put("messages", messages);
body.put("pass_all_history_messages", false); // 已传完整messages,避免RAGFlow叠加自身会话历史导致每轮翻倍
if (StringUtils.isNotBlank(sessionId)) {
body.put("session_id", sessionId);
}
HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
// 使用RestTemplate获取原生Response,不自动消费body
ResponseEntity<org.springframework.core.io.Resource> respEntity = restTemplate.exchange(
url,
HttpMethod.POST,
entity,
org.springframework.core.io.Resource.class
);
HttpStatusCode statusCode = respEntity.getStatusCode();
if (!statusCode.is2xxSuccessful()) {
throw new RuntimeException("RAGFlow chatCompletionsV2Stream 调用异常,http状态码:" + statusCode.value());
}
org.springframework.core.io.Resource resource = respEntity.getBody();
if (resource == null) {
throw new RuntimeException("RAGFlow chatCompletionsV2Stream 返回body为空");
}
try {
// 包装输入流为BufferedReader,UTF-8
return new BufferedReader(new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8));
} catch (Exception e) {
throw new RuntimeException("读取RAGFlow流式响应输入流失败", e);
}
}
/**
* 创建会话 POST /api/v1/chats/{chat_id}/sessions
* @param chatId ragflow助手id
* @param name 会话名称
* @param userId 可选 用户自定义userId,可为null
* @return 返回ragflow会话data对象,id字段为ragflow sessionId
*/
public JSONObject createSession(String chatId, String name, String userId) {
if (StringUtils.isBlank(chatId)) {
throw new IllegalArgumentException("chatId不能为空");
}
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("会话name不能为空");
}
String url = baseUrl + "/api/v1/chats/" + chatId + "/sessions";
Map<String, Object> body = new HashMap<>();
body.put("name", name);
if (StringUtils.isNotBlank(userId)) {
body.put("user_id", userId);
}
HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if (!Objects.equals(code, 0)) {
String msg = json.getString("message");
throw new RuntimeException("RAGFlow创建会话失败:" + Objects.toString(msg, "未知错误"));
}
return json.getJSONObject("data");
}
/**
* 查询助手会话列表 GET /api/v1/chats/{chat_id}/sessions
* @param chatId 助手id
* @param page 页码 默认1
* @param pageSize 页大小 默认30
* @param orderby 排序字段 create_time / update_time
* @param desc 是否降序 默认true
* @param name 会话名称过滤
* @param sessionId 会话id精确匹配
* @param userId 用户自定义userId过滤
* @return JSONArray 会话数组
*/
public JSONArray listSession(String chatId,
Integer page,
Integer pageSize,
String orderby,
Boolean desc,
String name,
String sessionId,
String userId) {
if (StringUtils.isBlank(chatId)) {
throw new IllegalArgumentException("chatId不能为空");
}
StringBuilder sb = new StringBuilder(baseUrl + "/api/v1/chats/" + chatId + "/sessions?");
if (page != null) sb.append("page=").append(page).append("&");
if (pageSize != null) sb.append("page_size=").append(pageSize).append("&");
if (StringUtils.isNotBlank(orderby))
sb.append("orderby=").append(URLEncoder.encode(orderby, StandardCharsets.UTF_8)).append("&");
if (desc != null) sb.append("desc=").append(desc).append("&");
if (StringUtils.isNotBlank(name))
sb.append("name=").append(URLEncoder.encode(name, StandardCharsets.UTF_8)).append("&");
if (StringUtils.isNotBlank(sessionId))
sb.append("id=").append(URLEncoder.encode(sessionId, StandardCharsets.UTF_8)).append("&");
if (StringUtils.isNotBlank(userId))
sb.append("user_id=").append(URLEncoder.encode(userId, StandardCharsets.UTF_8)).append("&");
String url = sb.toString();
if (url.endsWith("&")) {
url = url.substring(0, url.length() - 1);
}
HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if (!Objects.equals(code, 0)) {
String msg = json.getString("message");
throw new RuntimeException("RAGFlow查询会话列表失败:" + Objects.toString(msg, "未知错误"));
}
return json.getJSONArray("data");
}
/**
* 获取单个会话详情 GET /api/v1/chats/{chat_id}/sessions/{sessionId}
* @param chatId 助手id
* @param sessionId ragflow会话id
* @return 会话data对象,包含messages历史消息
*/
public JSONObject getSessionDetail(String chatId, String sessionId) {
if (StringUtils.isAnyBlank(chatId, sessionId)) {
throw new IllegalArgumentException("chatId、sessionId不能为空");
}
String url = baseUrl + "/api/v1/chats/" + chatId + "/sessions/" + sessionId;
HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if (!Objects.equals(code, 0)) {
String msg = json.getString("message");
throw new RuntimeException("RAGFlow获取会话详情失败:" + Objects.toString(msg, "未知错误"));
}
return json.getJSONObject("data");
}
/**
* 更新消息点赞反馈 PUT /api/v1/chats/{chat_id}/sessions/{sessionId}/messages/{msgId}/feedback
* @param chatId 助手id
* @param sessionId 会话id
* @param msgId ragflow消息id
* @param thumbup true点赞 / false点踩
* @param feedback 反馈文本,可为null
* @return 更新后会话对象
*/
public JSONObject messageFeedback(String chatId, String sessionId, String msgId, Boolean thumbup, String feedback) {
if (StringUtils.isAnyBlank(chatId, sessionId, msgId) || thumbup == null) {
throw new IllegalArgumentException("参数不全");
}
String url = baseUrl + "/api/v1/chats/" + chatId + "/sessions/" + sessionId + "/messages/" + msgId + "/feedback";
Map<String, Object> body = new HashMap<>();
body.put("thumbup", thumbup);
if (StringUtils.isNotBlank(feedback)) {
body.put("feedback", feedback);
}
HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.PUT, entity, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if (!Objects.equals(code, 0)) {
String msg = json.getString("message");
throw new RuntimeException("RAGFlow设置消息反馈失败:" + Objects.toString(msg, "未知错误"));
}
return json.getJSONObject("data");
}
/**
* 批量删除会话 DELETE /api/v1/chats/{chat_id}/sessions
* @param chatId 助手id
* @param ids 需要删除的sessionId列表;可为null
* @param deleteAll 是否删除全部会话 为true时 ids写null
*/
public void batchDeleteSession(String chatId, List<String> ids, Boolean deleteAll) {
if (StringUtils.isBlank(chatId)) {
throw new IllegalArgumentException("chatId不能为空");
}
String url = baseUrl + "/api/v1/chats/" + chatId + "/sessions";
Map<String, Object> body = new HashMap<>();
if (ids != null && !ids.isEmpty()) {
body.put("ids", ids);
}
if (deleteAll != null) {
body.put("delete_all", deleteAll);
}
HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.DELETE, entity, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if (!Objects.equals(code, 0)) {
String msg = json.getString("message");
throw new RuntimeException("RAGFlow批量删除会话失败:" + Objects.toString(msg, "未知错误"));
}
}
/**
* 向量检索接口 POST /api/v1/retrieval
* @param question 用户问题
* @param datasetIds 知识库id列表,二选一 datasetIds / documentIds
* @param documentIds 文档id列表
* @param page 页码 默认1
* @param pageSize 每页数量 默认30
* @param similarityThreshold 相似度阈值
* @param vectorSimilarityWeight 向量权重
* @param topK topK
* @param rerankId 重排模型id
* @param keyword 是否开启关键词检索
* @param highlight 是否高亮
* @param crossLanguages 跨语言列表
* @param metadataCondition 元数据过滤条件,可为null
* @param useKg 是否启用知识图谱
* @param tocEnhance 是否目录增强
* @return 返回检索data对象,包含chunks、doc_aggs、total
*/
public JSONObject retrievalChunks(String question,
List<String> datasetIds,
List<String> documentIds,
Integer page,
Integer pageSize,
Float similarityThreshold,
Float vectorSimilarityWeight,
Integer topK,
String rerankId,
Boolean keyword,
Boolean highlight,
List<String> crossLanguages,
Map<String, Object> metadataCondition,
Boolean useKg,
Boolean tocEnhance) {
if (StringUtils.isBlank(question)) {
throw new IllegalArgumentException("question不能为空");
}
if ((datasetIds == null || datasetIds.isEmpty()) && (documentIds == null || documentIds.isEmpty())) {
throw new IllegalArgumentException("datasetIds 和 documentIds至少传一组");
}
String url = baseUrl + "/api/v1/retrieval";
Map<String, Object> body = new HashMap<>();
body.put("question", question);
if (datasetIds != null) body.put("dataset_ids", datasetIds);
if (documentIds != null) body.put("document_ids", documentIds);
if (page != null) body.put("page", page);
if (pageSize != null) body.put("page_size", pageSize);
if (similarityThreshold != null) body.put("similarity_threshold", similarityThreshold);
if (vectorSimilarityWeight != null) body.put("vector_similarity_weight", vectorSimilarityWeight);
if (topK != null) body.put("top_k", topK);
if (StringUtils.isNotBlank(rerankId)) body.put("rerank_id", rerankId);
if (keyword != null) body.put("keyword", keyword);
if (highlight != null) body.put("highlight", highlight);
if (crossLanguages != null) body.put("cross_languages", crossLanguages);
if (metadataCondition != null) body.put("metadata_condition", metadataCondition);
if (useKg != null) body.put("use_kg", useKg);
if (tocEnhance != null) body.put("toc_enhance", tocEnhance);
HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
Integer code = json.getInteger("code");
if (!Objects.equals(code, 0)) {
String msg = json.getString("message");
throw new RuntimeException("RAGFlow向量检索失败:" + Objects.toString(msg, "未知错误"));
}
return json.getJSONObject("data");
}
/**
* 生成相关推荐问题 POST /api/v1/chat/recommandation
* 注意:该接口需要login‑token,不是api‑key;本客户端暂不封装鉴权逻辑,业务层按需调用
* @param question 用户原始问题
* @param searchId 搜索配置id,可为null
* @return 推荐问题字符串数组
*/
public JSONArray generateRelatedQuestions(String question, String searchId) {
throw new UnsupportedOperationException("generateRelatedQuestions需要登录token,不是api‑key,业务层自行处理header鉴权");
}
/**
* 下载附件 GET /api/v1/agents/attachments/{attachmentId}/download
* @param attachmentId 附件id
* @param ext 输出格式 markdown/html/pdf/docx/xlsx/csv
* @return byte[] 文件二进制
*/
public byte[] downloadAttachment(String attachmentId, String ext) {
if (StringUtils.isBlank(attachmentId)) {
throw new IllegalArgumentException("attachmentId不能为空");
}
StringBuilder sb = new StringBuilder(baseUrl + "/api/v1/agents/attachments/" + attachmentId + "/download?");
if (StringUtils.isNotBlank(ext)) {
sb.append("ext=").append(URLEncoder.encode(ext, StandardCharsets.UTF_8));
}
String url = sb.toString();
HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
ResponseEntity<byte[]> resp = restTemplate.exchange(url, HttpMethod.GET, entity, byte[].class);
return resp.getBody();
}
/**
* 获取模型列表 GET /api/v1/models【DTO强类型版本】
* @param modelType 可选过滤:embedding / chat / rerank / tts / vision,传null返回全部
* @return RagFlowModelListResp
*/
public RagFlowModelListResp listModels(String modelType,String instanceName,String providerName,String name) {
StringBuilder sb = new StringBuilder(baseUrl + "/api/v1/models?t=1");
if (StringUtils.isNotBlank(modelType)) {
sb.append("&model_type=").append(URLEncoder.encode(modelType, StandardCharsets.UTF_8));
}
if (StringUtils.isNotBlank(instanceName)) {
sb.append("&instance_name=").append(URLEncoder.encode(instanceName, StandardCharsets.UTF_8));
}
if (StringUtils.isNotBlank(providerName)) {
sb.append("&provider_name=").append(URLEncoder.encode(providerName, StandardCharsets.UTF_8));
}
if (StringUtils.isNotBlank(name)) {
sb.append("&name=").append(URLEncoder.encode(name, StandardCharsets.UTF_8));
}
String url = sb.toString();
HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
RagFlowModelListResp result = JSON.parseObject(resp.getBody(), RagFlowModelListResp.class);
if (!Objects.equals(result.getCode(), 0)) {
String msg = result.getMessage();
throw new RuntimeException("RAGFlow查询模型列表失败:" + Objects.toString(msg, "未知错误"));
}
return result;
}
/**
* 查询ragFlow中用户配置的llm模型id
* @return
*/
public String getUserLLMId() {
RagFlowUserMeModels po = getUserMeModelsDto();
RagFlowUserMeModels.UserMeModelData data = po.getData();
return data.getTenantLlmId();
}
//==================== 引用展示:切片图片 / 文档缩略图 / 文档预览 / 原文件下载 ====================
/**
* 获取切片图片(也用于文档缩略图,二者都是 image_id 形态)
* GET /api/v1/documents/images/{imageId}
* 说明:imageId 形如 "{dataset_id}-{chunk_id}" 或 "{dataset_id}-thumbnail_{doc_id}.png"
* @param imageId ragflow image_id
* @return 图片字节
*/
public byte[] getChunkImage(String imageId) {
if (StringUtils.isBlank(imageId)) {
throw new IllegalArgumentException("imageId不能为空");
}
String url = baseUrl + "/api/v1/documents/images/" + imageId;
HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
ResponseEntity<byte[]> resp = restTemplate.exchange(url, HttpMethod.GET, entity, byte[].class);
return resp.getBody();
}
/**
* 批量查询文档缩略图 GET /api/v1/thumbnails?doc_ids=a&doc_ids=b
* RAGFlow返回:{"code":0,"data":{"docId":"/api/v1/documents/images/{dataset_id}-thumbnail_{doc_id}.png"或""}}
* 本方法把缩略图URL收敛成 image_id(最后一个路径段),空串表示该文档无缩略图(如doc/xlsx)
* @param docIds 文档id列表
* @return docId -> thumbnailImageId(可能为空串)
*/
public Map<String, String> getDocumentThumbnails(List<String> docIds) {
if (docIds == null || docIds.isEmpty()) {
return new HashMap<>();
}
StringBuilder sb = new StringBuilder(baseUrl + "/api/v1/thumbnails?");
for (String docId : docIds) {
if (StringUtils.isBlank(docId)) {
continue;
}
sb.append("doc_ids=").append(URLEncoder.encode(docId, StandardCharsets.UTF_8)).append("&");
}
String url = sb.substring(0, sb.length() - 1);
HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
JSONObject json = JSON.parseObject(resp.getBody());
if (!Objects.equals(json.getInteger("code"), 0)) {
String msg = json.getString("message");
throw new RuntimeException("RAGFlow查询文档缩略图失败:" + Objects.toString(msg, "未知错误"));
}
JSONObject dataObj = json.getJSONObject("data");
Map<String, String> result = new HashMap<>();
if (dataObj != null) {
for (String docId : dataObj.keySet()) {
String thumbUrl = dataObj.getString(docId);
if (StringUtils.isBlank(thumbUrl)) {
result.put(docId, "");
continue;
}
// 形如 /api/v1/documents/images/xxx-thumbnail_yyy.png → 取最后一段作为image_id
int idx = thumbUrl.lastIndexOf('/');
result.put(docId, idx >= 0 ? thumbUrl.substring(idx + 1) : thumbUrl);
}
}
return result;
}
/**
* 获取文档预览流(PDF返回application/pdf可直接渲染;doc/xlsx/ppt返回原始文件字节)
* GET /api/v1/documents/{documentId}/preview
* @param documentId ragflow文档id
* @return 原始响应(含Content-Type),业务层按类型决定 inline / attachment
*/
public ResponseEntity<byte[]> getDocumentPreviewResponse(String documentId) {
if (StringUtils.isBlank(documentId)) {
throw new IllegalArgumentException("documentId不能为空");
}
String url = baseUrl + "/api/v1/documents/" + documentId + "/preview";
HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
return restTemplate.exchange(url, HttpMethod.GET, entity, byte[].class);
}
/**
* GET /api/v1/users/me/models
* 获取当前API Key所属租户的模型配置信息【DTO强类型版本】
* @return RagFlowUserMeModelResp 完整返回实体
*/
public RagFlowUserMeModels getUserMeModelsDto() {
String url = baseUrl + "/api/v1/users/me/models";
HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
// fastjson2 直接转DTO
RagFlowUserMeModels result = JSON.parseObject(resp.getBody(), RagFlowUserMeModels.class);
if (!Objects.equals(result.getCode(), 0)) {
String msg = result.getMessage();
throw new RuntimeException("RAGFlow获取租户模型配置失败:" + Objects.toString(msg,"未知错误"));
}
return result;
}
/**
* 下载原文档 GET /api/v1/datasets/{datasetId}/documents/{documentId}
* @param datasetId 知识库id
* @param documentId 文档id
* @return 原始响应(含Content-Type),业务层按 attachment 处理
*/
public ResponseEntity<byte[]> downloadDocumentResponse(String datasetId, String documentId) {
if (StringUtils.isAnyBlank(datasetId, documentId)) {
throw new IllegalArgumentException("datasetId、documentId不能为空");
}
String url = baseUrl + "/api/v1/datasets/" + datasetId + "/documents/" + documentId;
HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
return restTemplate.exchange(url, HttpMethod.GET, entity, byte[].class);
}
}
3.4 Controller 层:权限校验(参考代码)
bash
/**
* 查询RAG知识库管理列表(本人创建 + 开放授权可见,非本人不可改删由 isOwner/canEdit 标记控制)
*/
@PreAuthorize("@ss.hasPermi('rag:knowledgeBase:list')")
@GetMapping("/list")
public TableDataInfo list(RagKnowledgeBase ragKnowledgeBase)
{
// 可见范围过滤(核心权限方法)
ragKnowledgeBase.setVisibleIds(ragKnowledgeBaseService.selectVisibleKbIds());
startPage();
List<RagKnowledgeBase> list = ragKnowledgeBaseService.selectRagKnowledgeBaseList(ragKnowledgeBase);
if(list != null){
RagFlowUserMeModels po = ragFlowApiClient.getUserMeModelsDto();
Map<String, String> modelItems = getModelItems(po);
String currentUser = SecurityUtils.getUsername();
for (RagKnowledgeBase base : list) {
base.setEmbeddingModelName(modelItems.get(base.getEmbeddingModel()));
// 权限标记:本人创建的可改删,授权可见的仅查看
boolean owner = currentUser.equals(base.getCreateBy());
base.setIsOwner(owner);
base.setCanEdit(owner);
}
}
return getDataTable(list);
}
关键设计 :浏览器不直接访问 RAGFlow,所有请求经若依后端代理------既解决跨域,又避免 RAGFlow 的 Token 暴露到前端,权限校验也统一在若依这一层完成。
四、总结
本在开发层面介绍了若依集成ragFlow的设计实现思路:
- RAGFlow 私有化部署:无 GPU 环境可跑,模型走 OpenAI 兼容 API 可插拔,生产可平滑替换本地模型;
- 8 个功能模块:模型、知识库、文档、提示词、助手、会话、问答,职责清晰、表结构完整;
- 五种开放范围权限:私有 / 公开 / 指定用户 / 指定部门 / 指定角色,配合若依数据权限,真正实精准的权限管控;
- Java 对接 RAGFlow:安全与权限统一收敛在若依层,我贴了一些有参考价值的代码,主要为了展示设计思路,具体实现那就见仁见智了。
------三类模型(重排序/向量化/大语言)各维护一条记录,全部启用,平台为"硅基流动",部署方式"公有云",每条记录支持验证/修改/删除。
------新增知识库表单:知识库名称、向量化模型、分片解析模板、开放范围(私有/公开/指定用户/指定部门/指定角色单选)、封面、描述。
------知识库 001 下 6 份文档,pdf/docx 混合,全部"解析成功",支持按文档名称/类型/解析状态/是否启用筛选,禁用的文档不参与rag检索。
------3 个助手(通用知识问答/对话助手002/我的私人对话助手),均绑定知识库001、使用 deepseek-ai/DeepSeek-V4-Flash。