1. 项目概述:为什么是 RuoYi + RAGFlow 这个组合?
RuoYi 和 RAGFlow 的组合,不是随便拼凑的“技术网红CP”,而是国内中大型企业落地私有化知识库时,一个经过反复验证、踩过坑、调过参、最终跑通的务实路径。我带团队在三个不同行业的客户现场做过完整交付——制造业的设备维修手册问答系统、金融公司的合规政策检索助手、医疗集团的临床指南辅助查询平台——全部采用 RuoYi 做业务后台 + RAGFlow 做语义中枢的架构。它解决的不是“能不能跑起来”的问题,而是“能不能稳、能不能管、能不能扩、能不能审”的真实生产级诉求。
核心关键词RuoYi指的是那个开箱即用、权限体系成熟、数据库抽象层扎实、国产化适配度高的 Java 后端框架;而RAGFlow则是目前国内少有的、真正把文档解析、向量化、检索、重排、LLM 编排全链路做成可配置、可审计、可回溯的开源 RAG 引擎。它不像某些轻量级 RAG 工具只提供 API 调用,而是自带 Web 管理界面、支持多租户、能看懂 PDF 表格、能处理扫描件 OCR、能对接 MinIO/S3/本地存储、能导出检索日志——这些能力,恰恰是 RuoYi 作为业务系统最需要的“可集成底座”。
所谓私有化知识库,本质是把企业内部散落在 Confluence、SharePoint、NAS、邮件附件、扫描PDF里的非结构化信息,变成可被业务系统调用的“活数据”。它不追求大模型幻觉式回答,而强调答案可溯源、过程可审计、结果可解释。RuoYi 提供用户身份、组织架构、操作日志、审批流;RAGFlow 提供文档解析质量监控、chunk 切分策略配置、embedding 模型热切换、检索结果置信度阈值控制——二者分工明确,边界清晰,没有互相侵入,只有协议对接。
这个集成实践的难点从来不在“连上就行”,而在于:RuoYi 的登录态如何安全透传给 RAGFlow?RuoYi 的文件上传路径如何与 RAGFlow 的文档入库路径对齐?RuoYi 的部门树如何映射为 RAGFlow 的知识库权限?RuoYi 的操作审计日志里,如何记录一次“用户张三在报销模块点击了知识库问答按钮,命中了《差旅报销细则V2.3》第5条”?这些细节,才是决定项目能否从 PoC 走向正式上线的关键。网上很多教程只教你docker-compose up -d,但没告诉你,当 RAGFlow 解析一份 80 页带复杂表格的 PDF 时,内存溢出报错怎么定位;也没告诉你,RuoYi 的 Sa-Token 登录 token 怎么安全地转成 RAGFlow 的 API Key,又不暴露密钥轮换逻辑。这篇内容,就是补上这些“没人写但必须知道”的实操断点。
适合谁来读?如果你正在用 RuoYi 做内部管理系统,并且老板已经拍板要加“智能问答”模块;如果你的技术选型会议里,同事还在争论“要不要买商业 RAG 产品”;如果你已经试过 RAGFlow 单独部署,但卡在和现有业务系统打通这一步——那么你不是在找一篇教程,而是在找一份能直接抄作业、能预判雷区、能拿去和运维/安全/法务同事对齐口径的工程落地方案。接下来的内容,全部来自我们交付现场的真实配置、日志片段、参数对比表和调试截图(文字还原版),不讲原理,只讲怎么做、为什么这么选、哪里会崩、怎么修。
2. 整体架构设计与集成思路拆解
2.1 架构分层:为什么坚持“前后端分离+网关隔离”而非直连?
很多团队第一反应是让 RuoYi 后端直接调 RAGFlow 的 HTTP 接口,省事。但我们在线上环境强制要求走API 网关(我们用的是 Spring Cloud Gateway),中间加一层鉴权和路由。原因有三层,全是血泪教训:
第一层是安全审计硬性要求。某金融客户的安全规范明确要求:“所有跨系统调用必须经由统一网关,禁止后端服务直连”。RuoYi 直连 RAGFlow,意味着 RuoYi 的 JVM 进程里要存 RAGFlow 的 API Key,一旦 JVM 内存 dump 或日志泄露,整个知识库的读写权限就裸奔了。而网关层可以做 token 透传、请求签名、IP 白名单、QPS 限流,还能在日志里统一记录“RuoYi-报销模块 → RAGFlow-知识库查询”,审计时直接拉网关日志就行,不用翻两个系统的日志。
第二层是故障隔离。RAGFlow 因为解析大文件偶尔 OOM,重启期间如果 RuoYi 直连,就会导致报销页面整个白屏。而网关可以配置熔断降级:当 RAGFlow 健康检查失败时,自动返回预设的兜底响应(如“知识库服务暂不可用,请稍后再试”),RuoYi 前端只显示提示,不影响主流程提交。
第三层是协议适配。RuoYi 默认用 Sa-Token 的Authorization: Bearer <token>,而 RAGFlow 的/v1/rags/{rag_id}/chat接口要求Authorization: ApiKey <key>。网关层做 header 转换,RuoYi 不用改任何代码,只需配置网关路由规则。我们实测下来,这套方案比修改 RuoYi 的 FeignClient 或 Retrofit 配置更稳定,升级 RuoYi 版本时也不用同步改调用逻辑。
所以最终架构是:
RuoYi Vue 前端 → RuoYi Spring Boot 后端(含 Sa-Token) → Spring Cloud Gateway(JWT 解析 + Header 转换 + 熔断) → RAGFlow Docker 容器(仅开放/v1/API)
提示:网关不是必须用 Spring Cloud Gateway,Nginx + Lua 也能实现类似功能。但我们选 Spring Cloud Gateway 是因为 RuoYi 本身已集成 Spring Cloud 生态,复用同一套配置中心(Nacos)和注册中心,避免引入新组件增加运维复杂度。
2.2 权限模型对齐:Sa-Token 如何映射到 RAGFlow 的 Workspace 权限?
RuoYi 的权限体系基于角色(Role)和菜单(Menu),而 RAGFlow 的权限粒度在 Workspace(工作空间)级别,支持“只读”、“编辑”、“管理”三种角色。二者不能简单按用户名一一对应,因为 RuoYi 里一个用户可能属于多个部门(如“研发部+AI项目组”),而 RAGFlow 的 Workspace 是扁平的。我们的解法是:用 RuoYi 的部门编码(dept_id)作为 RAGFlow Workspace 的命名前缀,再通过网关动态注入权限上下文。
具体操作分三步:
- 在 RAGFlow 初始化时,预先创建 Workspace,命名规则为
dept_{dept_id},例如dept_102(财务部)、dept_205(合规部)。每个 Workspace 关联一个独立的向量库(ChromaDB Collection),物理隔离数据。 - RuoYi 用户登录后,Sa-Token 生成的 token 中已包含
deptId字段(RuoYi 默认存于StpUtil.getTokenSession().get("deptId"))。 - 网关拦截
/ragflow/chat请求,在转发前从 Sa-Token 解析出deptId,拼接成dept_{deptId},作为请求头X-RAG-WORKSPACE: dept_102透传给 RAGFlow。RAGFlow 的 API 层收到后,自动路由到对应 Workspace 执行检索。
这样做的好处是:
- 用户无感:前端完全不用感知 Workspace 概念,点击“知识库问答”按钮,后端自动根据当前登录人所属部门,查对应 Workspace。
- 权限收敛:RuoYi 的部门调整(如员工调岗)后,下次登录 token 自动更新
deptId,无需手动在 RAGFlow 后台改权限。 - 扩展性强:新增部门时,只需在 RAGFlow 创建同名 Workspace,无需改任何代码。
注意:RAGFlow 的 Workspace 创建不能用 API 自动化(官方未开放),必须首次手动创建。我们写了个 Python 脚本,读取 RuoYi 的
sys_dept表,生成 RAGFlow 的 Workspace 初始化 SQL,纳入部署流水线。脚本执行后,RAGFlow 的/api/v1/workspaces接口就能看到所有预置 Workspace。
2.3 文件协同机制:RuoYi 上传的文件,如何让 RAGFlow 自动解析?
这是集成中最容易被忽略的“隐性耦合点”。RuoYi 默认把文件存在profile/upload/目录下,而 RAGFlow 的文档入库有两种方式:API 上传(推荐)或挂载目录监听(不推荐)。我们放弃挂载目录,因为:
- RuoYi 的文件路径是
upload/2024/06/15/xxx.pdf,RAGFlow 的监听目录无法按日期动态创建子目录; - RuoYi 上传后会重命名文件(如
xxx_123456789.pdf),RAGFlow 监听不到原始文件名,无法关联业务单据号; - 权限问题:RAGFlow 容器以非 root 用户运行,挂载宿主机目录时容易因 SELinux 或文件权限报错。
最终方案是:RuoYi 上传成功后,触发异步任务,调用 RAGFlow 的/v1/rags/{rag_id}/documentsAPI 上传文件流。关键细节如下:
- RuoYi 的
FileController.upload()方法末尾,加一个@Async注解的方法,接收fileId和originalFilename; - 该方法构造 multipart/form-data 请求,body 包含
file(二进制流)、name(原始文件名)、workspace_id(即dept_{deptId}); - RAGFlow 收到后,自动解析并入库,返回
document_id; - RuoYi 将
document_id存入自定义表sys_knowledge_doc,关联业务单据 ID,方便后续溯源。
实测发现,RAGFlow 的/v1/rags/{rag_id}/documents接口对大文件(>50MB)支持不稳定,常因超时中断。解决方案是:RuoYi 端分片上传(用 axios 的onUploadProgress监控),RAGFlow 端启用--max-upload-size=200m启动参数(Docker run 时加-e MAX_UPLOAD_SIZE=200m),并在 Nginx 网关层调大client_max_body_size 200m。
3. 核心细节解析与实操要点
3.1 RAGFlow 本地化部署避坑指南(Win11 / Linux 双环境)
RAGFlow 官方文档说“支持 Windows”,但 Win11 下部署实际是“支持但不推荐”。我们团队在客户现场踩过的坑,按严重程度排序:
坑一:Windows 下 ChromaDB 的 SQLite 锁死问题
现象:上传 PDF 后,RAGFlow 日志卡在chromadb.api.models.Collection.add(),CPU 占用 100%,重启无效。
根因:ChromaDB 依赖的pysqlite3在 Windows 上对并发写入支持差,尤其当多个文档同时解析时。
解法:强制使用 PostgreSQL 替代 SQLite。步骤:
- 安装 PostgreSQL 15(官网下载,初始化时勾选
pgAdmin); - 创建数据库
ragflow,用户raguser,密码Rag@123; - 修改 RAGFlow 的
.env文件:
CHROMA_DB_IMPL=postgresql CHROMA_DB_HOST=localhost CHROMA_DB_PORT=5432 CHROMA_DB_NAME=ragflow CHROMA_DB_USER=raguser CHROMA_DB_PASSWORD=Rag@123- 重新
docker-compose up -d。实测后,10 并发上传 PDF 不再锁死。
坑二:Win11 WSL2 与 Docker Desktop 的 GPU 透传失效
现象:想用llama.cpp加速 embedding,但nvidia-smi在容器内不可见。
根因:WSL2 的 NVIDIA Container Toolkit 配置复杂,且 RAGFlow 的 Dockerfile 未声明--gpus all。
解法:放弃 WSL2,改用原生 Linux 服务器部署。若必须 Win11 开发,用 CPU 模式(EMBEDDING_MODEL_NAME=bge-m3),速度慢但稳定。我们测试过,bge-m3 在 i7-11800H 上单文档 embedding 耗时 8~12 秒,可接受。
坑三:Linux 下中文 PDF 解析乱码
现象:上传《采购管理办法.pdf》,RAGFlow 解析后文本全是方框或乱码。
根因:RAGFlow 默认用pymupdf解析,但未加载中文字体。
解法:在 RAGFlow 容器内挂载字体文件。步骤:
- 下载
NotoSansCJKsc-Regular.otf(Google 开源中文字体); - 修改
docker-compose.yml,添加卷映射:
volumes: - ./fonts:/app/fonts- 修改 RAGFlow 的
config.py,在PDF_PARSER_CONFIG中指定字体路径:
"pdf": { "font_path": "/app/fonts/NotoSansCJKsc-Regular.otf" }重启容器后,中文 PDF 解析准确率从 60% 提升至 98%。
实操心得:RAGFlow 的 Docker 部署,务必用
docker-compose.yml而非docker run单命令。我们整理了一份生产环境可用的docker-compose.yml,包含 PostgreSQL、Redis、MinIO、RAGFlow 四容器编排,网络互通,资源限制明确(CPU 4核,内存 8G),已通过等保三级测评。需要可留言,我贴核心配置。
3.2 RuoYi 侧关键改造点:Sa-Token 登录态安全透传
RuoYi 的 Sa-Token 默认 token 是 JWT,但 payload 里不包含deptId。很多教程教你在登录接口里手动塞deptId,但这违反了 Sa-Token 的设计哲学——token 应该只存必要字段,且由框架统一管理。我们的做法是:利用 Sa-Token 的TokenSigner扩展机制,在签发 token 时自动注入部门信息,且不破坏原有鉴权逻辑。
步骤:
- 创建
CustomTokenSigner类,继承JwtTokenSigner:
@Component public class CustomTokenSigner extends JwtTokenSigner { @Override public String sign(String tokenName, Object loginId, String loginType, long timeout) { // 获取登录用户部门ID SysUser user = sysUserService.selectUserByLoginName((String) loginId); String deptId = user.getDeptId() != null ? user.getDeptId().toString() : "0"; // 构造自定义 claims Map<String, Object> claims = new HashMap<>(); claims.put("deptId", deptId); claims.put("username", user.getUserName()); // 调用父类签发,注入 claims return super.sign(tokenName, loginId, loginType, timeout, claims); } }- 在
SaTokenConfig中注册该 signer:
@Bean public SaTokenConfigure saTokenConfigure() { return new SaTokenConfigure() { @Override public void setTokenSigner(TokenSigner tokenSigner) { // 替换为自定义 signer SaManager.getStpInterface().setTokenSigner(new CustomTokenSigner()); } }; }- 网关层解析 JWT 时,直接从
claims.get("deptId")取值,无需查数据库。
这样改造的好处:
- 无侵入:RuoYi 原有登录、续期、注销逻辑完全不变;
- 安全:
deptId存在 JWT 的 signature 中,无法篡改; - 高效:网关解析 JWT 即可获取部门信息,避免每次请求都查 DB。
注意:Sa-Token 的 JWT 默认有效期是 30 分钟,而 RAGFlow 的 API Key 有效期是永久的。所以网关不能把 JWT 当作 RAGFlow 的长期凭证,必须做“临时凭证转换”——即网关收到 RuoYi 请求后,用预置的 RAGFlow Master Key +
deptId动态生成一个 5 分钟有效期的临时 API Key,再透传给 RAGFlow。这部分逻辑写在网关的GlobalFilter里,代码约 20 行,核心是 HMAC-SHA256 签名。
3.3 MinIO 与 RAGFlow 的深度协同:不只是存文件
很多教程把 MinIO 当作 RAGFlow 的“文件柜”,只用来存原始 PDF。但我们把它用成了“元数据枢纽”。原因:RuoYi 上传文件时,会生成业务单据 ID(如PO20240615001),这个 ID 必须和 RAGFlow 的document_id关联,才能实现“从知识库答案反查业务单据”。
方案:MinIO 的 object metadata 中存业务上下文。
- RuoYi 上传文件到 MinIO 时,不仅传
file,还传自定义 metadata:
PutObjectArgs args = PutObjectArgs.builder() .bucket("rag-docs") .object("dept_102/PO20240615001.pdf") .stream(fileInputStream, fileLength, -1) .headers(Map.of( "X-Amz-Meta-Business-Id", "PO20240615001", "X-Amz-Meta-Dept-Id", "102", "X-Amz-Meta-Upload-Time", String.valueOf(System.currentTimeMillis()) )) .build(); minioClient.putObject(args);- RAGFlow 的文档解析完成后,会回调 RuoYi 的
POST /api/knowledge/callback接口,携带document_id和minio_object_name; - RuoYi 根据
minio_object_name查 MinIO metadata,拿到Business-Id,存入sys_knowledge_doc表。
这样,当用户在知识库问答中点击某条答案的“查看原文”,RuoYi 就能根据document_id查到Business-Id,跳转到对应的采购单详情页。整个链路闭环,无需 RAGFlow 存业务字段,也无需 RuoYi 维护冗余映射表。
4. 实操过程与核心环节实现
4.1 RAGFlow Docker 部署全流程(含 PostgreSQL + MinIO)
以下是我们线上环境使用的docker-compose.yml,已删减注释,保留核心配置。所有服务在同一rag-network网络下,IP 可互访。
version: '3.8' services: # PostgreSQL:RAGFlow 元数据存储 postgres: image: postgres:15-alpine container_name: rag-postgres environment: POSTGRES_DB: ragflow POSTGRES_USER: raguser POSTGRES_PASSWORD: Rag@123 volumes: - ./postgres/data:/var/lib/postgresql/data - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql ports: - "5432:5432" networks: - rag-network # Redis:RAGFlow 缓存 & 任务队列 redis: image: redis:7-alpine container_name: rag-redis command: redis-server --appendonly yes volumes: - ./redis/data:/data ports: - "6379:6379" networks: - rag-network # MinIO:对象存储,存原始文档 minio: image: minio/minio:latest container_name: rag-minio environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin volumes: - ./minio/data:/data ports: - "9000:9000" - "9001:9001" command: server /data --console-address ":9001" networks: - rag-network # RAGFlow 主服务 ragflow: image: langgenius/ragflow:1.12.0 container_name: ragflow environment: # 数据库 CHROMA_DB_IMPL: postgresql CHROMA_DB_HOST: postgres CHROMA_DB_PORT: 5432 CHROMA_DB_NAME: ragflow CHROMA_DB_USER: raguser CHROMA_DB_PASSWORD: Rag@123 # Redis REDIS_URL: redis://redis:6379/0 # MinIO S3_ENDPOINT: http://minio:9000 S3_BUCKET_NAME: rag-docs S3_ACCESS_KEY: minioadmin S3_SECRET_KEY: minioadmin S3_REGION: us-east-1 # 模型 EMBEDDING_MODEL_NAME: bge-m3 LLM_MODEL_NAME: qwen2-7b-chat-int4 # 其他 MAX_UPLOAD_SIZE: 200m LOG_LEVEL: INFO volumes: - ./ragflow/models:/app/models - ./ragflow/fonts:/app/fonts - ./ragflow/logs:/app/logs ports: - "3000:3000" depends_on: - postgres - redis - minio networks: - rag-network deploy: resources: limits: cpus: '4' memory: 8G关键参数说明:
CHROMA_DB_IMPL=postgresql:强制使用 PostgreSQL,规避 SQLite 锁问题;S3_ENDPOINT=http://minio:9000:注意是容器内网络地址,不是localhost;EMBEDDING_MODEL_NAME=bge-m3:中文场景下,bge-m3 的召回率比 text2vec-base-chinese 高 12%,且支持多粒度(sentence/paragraph/document);LLM_MODEL_NAME=qwen2-7b-chat-int4:Qwen2-7B-Chat 的 int4 量化版,显存占用 6GB,推理速度 18 tokens/s,足够应付企业知识库问答;MAX_UPLOAD_SIZE=200m:配合 Nginx 网关的client_max_body_size,确保大文件上传不超时。
部署后验证步骤:
docker-compose up -d启动;- 访问
http://localhost:3000,用默认账号admin/admin登录; - 创建 Workspace
dept_102,上传一份测试 PDF; - 查看
rag-postgres容器日志:docker logs rag-postgres | grep "INSERT",确认元数据写入 PostgreSQL; - 查看
rag-minio控制台(http://localhost:9001),确认 PDF 已存入rag-docsbucket。
实操心得:第一次启动 RAGFlow 时,会自动下载 embedding 模型和 LLM 模型,耗时较长(约 15 分钟)。建议提前下载好:
- bge-m3 模型:
https://huggingface.co/BAAI/bge-m3/resolve/main/pytorch_model.bin- qwen2-7b-chat-int4:
https://huggingface.co/Qwen/Qwen2-7B-Chat-AWQ/resolve/main/model-00001-of-00002.safetensors
下载后放./ragflow/models/对应目录,RAGFlow 启动时会跳过下载。
4.2 RuoYi 与 RAGFlow API 对接代码实录
以下是 RuoYi 后端KnowledgeService的核心代码,实现“用户提问 → 调用 RAGFlow → 返回答案”的全流程。代码已脱敏,保留关键逻辑。
@Service public class KnowledgeService { // 网关地址,非 RAGFlow 直连地址 private static final String RAGFLOW_GATEWAY_URL = "http://gateway:8080/ragflow"; @Autowired private RestTemplate restTemplate; /** * 处理用户提问 * @param question 用户输入的问题 * @param deptId 当前用户部门ID,由 Sa-Token 注入 * @return 答案列表 */ public List<KnowledgeAnswer> ask(String question, String deptId) { // 构造 RAGFlow 请求体 JSONObject requestBody = new JSONObject(); requestBody.put("question", question); requestBody.put("stream", false); // 同步返回,避免前端处理 SSE requestBody.put("history", new JSONArray()); // 无历史上下文 // 设置请求头:Workspace + 临时 API Key HttpHeaders headers = new HttpHeaders(); headers.set("X-RAG-WORKSPACE", "dept_" + deptId); headers.set("Authorization", "ApiKey " + generateTempApiKey(deptId)); HttpEntity<JSONObject> requestEntity = new HttpEntity<>(requestBody, headers); try { // 调用网关 ResponseEntity<String> response = restTemplate.postForEntity( RAGFLOW_GATEWAY_URL + "/v1/rags/dept_" + deptId + "/chat", requestEntity, String.class ); if (response.getStatusCode().is2xxSuccessful()) { return parseRagflowResponse(response.getBody()); } else { throw new RuntimeException("RAGFlow call failed: " + response.getStatusCode()); } } catch (Exception e) { log.error("RAGFlow ask error", e); throw new RuntimeException("知识库服务异常,请稍后再试"); } } /** * 生成 5 分钟有效期的临时 API Key * 使用 HMAC-SHA256,密钥为网关预置的 MASTER_KEY */ private String generateTempApiKey(String deptId) { long expireTime = System.currentTimeMillis() + 5 * 60 * 1000; // 5分钟 String message = deptId + "|" + expireTime; String signature = HmacUtils.hmacSha256Hex("GATEWAY_MASTER_KEY_2024", message); return deptId + "|" + expireTime + "|" + signature; } /** * 解析 RAGFlow 返回的 JSON * RAGFlow 返回格式:{"answer":"xxx","retrieved_docs":[{"document_id":"xxx","content":"yyy"}]} */ private List<KnowledgeAnswer> parseRagflowResponse(String responseBody) { JSONObject json = JSONObject.parseObject(responseBody); String answer = json.getString("answer"); JSONArray docs = json.getJSONArray("retrieved_docs"); List<KnowledgeAnswer> results = new ArrayList<>(); KnowledgeAnswer mainAnswer = new KnowledgeAnswer(); mainAnswer.setContent(answer); mainAnswer.setSourceType("RAGFlow"); results.add(mainAnswer); for (int i = 0; i < docs.size(); i++) { JSONObject doc = docs.getJSONObject(i); KnowledgeAnswer source = new KnowledgeAnswer(); source.setDocumentId(doc.getString("document_id")); source.setContent(doc.getString("content").substring(0, Math.min(200, doc.getString("content").length()))); source.setSourceUrl("/knowledge/doc/" + doc.getString("document_id")); // 前端跳转链接 results.add(source); } return results; } }关键点解析:
generateTempApiKey()方法生成的临时 Key,包含deptId|expireTime|signature,RAGFlow 网关层校验时,先检查expireTime是否过期,再用相同密钥计算signature,双重校验;parseRagflowResponse()中对content截取前 200 字,避免前端渲染超长文本卡顿;sourceUrl是前端路由,点击后调用 RuoYi 的GET /api/knowledge/doc/{document_id}接口,该接口根据document_id查 MinIO metadata,重定向到原始 PDF 地址。
前端 Vue 调用示例(Knowledge.vue):
async handleAsk() { try { const res = await this.$axios.post('/api/knowledge/ask', { question: this.question }); this.answers = res.data; } catch (err) { this.$message.error('知识库问答失败:' + err.response?.data?.message || '未知错误'); } }4.3 RAGFlow 文件解析质量调优实战
RAGFlow 的解析效果,直接决定知识库的可用性。我们针对不同文档类型,总结出一套“解析前预处理 + 解析后校验”的 SOP。
PDF 类型分级处理:
| 类型 | 特征 | RAGFlow 配置 | 效果提升点 |
|---|---|---|---|
| 扫描件 PDF | 图片为主,无文字层 | parser_type: ocr+ocr_language: ch | OCR 准确率从 40% → 85% |
| 文字 PDF | 可复制文字,但含复杂表格 | parser_type: pdf+enable_table: true | 表格结构保留,非纯文本 |
| 混合 PDF | 文字+图片+表格混合 | parser_type: auto+auto_parser_threshold: 0.3 | 自动选择最优解析器 |
关键配置项实测对比:
chunk_size: 默认 500,但法律条款类文档需设为 200(保证条款完整性),设备手册设为 1000(保持上下文连贯);chunk_overlap: 设为chunk_size * 0.2,实测重叠率 20% 时,跨 chunk 检索召回率最高;embedding_batch_size: 默认 32,调至 64 后,embedding 速度提升 35%,但内存占用增加 1.2G。
解析质量校验脚本:
我们写了一个 Python 脚本,定时扫描 RAGFlow 的retrieved_docs,抽样 100 份文档,人工标注“是否包含问题答案”。统计准确率,低于 85% 时自动告警。脚本核心逻辑:
# 从 RAGFlow API 获取最近 100 次问答的 retrieved_docs docs = get_recent_docs(limit=100) # 对每份 doc,提取 content 前 500 字,人工打标 for doc in docs: sample_text = doc['content'][:500] # 生成打标问卷链接,发给 QA 团队 send_qa_survey(sample_text, doc['document_id']) # 汇总打标结果,计算准确率 accuracy = calculate_accuracy() if accuracy < 0.85: send_alert("RAGFlow 解析准确率低于阈值,请检查 PDF 质量或 parser 配置")实操心得:不要迷信“一键解析”。我们给客户做交付时,会花 2 天时间,用他们的真实文档(采购合同、设备说明书、合规制度)做解析测试,调整
chunk_size、parser_type、embedding_model,直到准确率达到 90% 以上才进入开发阶段。这个阶段省下的时间,远大于后期反复调参的成本。
5. 常见问题与排查技巧实录
5.1 RAGFlow 解析 PDF 后内容缺失?90% 是字体问题
现象:上传 PDF 后,RAGFlow Web 界面显示“解析完成”,但检索时返回空结果,或内容只有标题没有正文。
排查路径:
- 进入 RAGFlow 容器:
docker exec -it ragflow bash; - 查看解析日志:
tail -f /app/logs/parser.log; - 搜索关键词
font not found或glyph missing; - 如果出现,说明 PDF 内嵌字体未被
pymupdf识别。
终极解法:
- 步骤一:用
pdfinfo your_file.pdf查 PDF 字体信息,确认是否含CIDFont或Type3字体; - 步骤二:安装
poppler-utils,用pdffonts your_file.pdf列出所有字体; - 步骤三:若字体为
AdobeSongStd-Light等 Adobe 字体,需在 RAGFlow 容器内挂载对应字体文件(如AdobeSongStd-Light.otf),并在config.py中指定font_path; - 步骤四:若字体为 Type3(位图字体),则必须用 OCR 模式解析,强制设置
parser_type: ocr。
我们曾遇到一份《供应商管理规范.pdf》,解析后正文全为空,pdffonts显示Type3字体。改用 OCR 后,准确率 92%,但解析耗时从 8 秒增至 42 秒。权衡后,我们为客户定制了一个“OCR 开关”:在 RAGFlow Web 界面上传时,勾选“启用 OCR”,后台自动切换解析器。
5.2 RuoYi 调用 RAGFlow 返回 401?检查网关的 Authorization 转换
现象:RuoYi 日志显示401 Unauthorized,但 RAGFlow 日志无请求记录。
根因:网关未正确