面向 PDF 文档的 RAG 知识库问答平台后端(项目名 Medrec):把文档变成可检索的知识,让 AI 的回答每句话都能追溯到原文的具体页码与位置。
前端仓库:medrec-sys/front
Medrec 是一个完整的 RAG(Retrieval-Augmented Generation)应用后端。用户可以:
- 创建知识库(一个知识库对应 Qdrant 中一个 collection,可自定义向量维度);
- 上传 PDF 文档,系统异步完成 解析 → 结构化 → 父子分块 → 向量化入库;
- 创建AI 智能体,为它配置提示词、温度、
topK、相似度阈值、最大记忆轮数,并挂载任意多个知识库; - 与智能体流式对话,回答中带有
<ID>文档ID</ID>引用标记,点击即可在后端返回的page+bbox指引下跳转到 PDF 原文位置。
与"拿现成框架拼一个 demo"不同,本项目的重点在于 RAG 全链路的工程化实现:文档结构化解析、父子分块检索、多向量库并发检索、查询扩展、引用溯源、异步入库与失败补偿,都是在 Spring AI 之上自行实现的。
| 模块 | 能力 |
|---|---|
| 认证 | 注册 / 登录,JWT 鉴权 + BCrypt 密码加密,拦截器统一校验并注入用户上下文 |
| AI 智能体 | 增删改查;配置提示词、温度(0~1)、max_message、top_k、相似度阈值;挂载 / 解绑多个知识库 |
| 知识库(向量库) | 增删改查;自定义向量维度;直接发起向量相似度检索并查看命中片段 |
| 文档管理 | PDF 上传(异步任务,返回 taskId);大文件自动分片;分页查询;删除(同时清理 DB、MinIO、向量) |
| 对话 | SSE 流式输出;会话记忆持久化到 MySQL;历史记录查询 / 单条删除 / 清空;把本轮命中文档回传前端 |
| 异步任务 | 上传解析全流程进度可查(等待许可 → 文件转 md → 保存元数据 → 文本转换 → 写向量库 → 完成 / 失败) |
| 分类 | 技术 | 版本 / 说明 |
|---|---|---|
| 语言 | Java | 21 |
| 框架 | Spring Boot | 4.1.1(spring-boot-starter-webmvc + webflux:前者提供 MVC,后者提供 Flux 流式响应) |
| AI 框架 | Spring AI | 2.0.1(spring-ai-rag、spring-ai-vector-store-advisor、spring-ai-starter-model-openai) |
| 大模型 | 阿里云百炼 DashScope(OpenAI 兼容模式) | 对话 qwen-flash、多模态 qwen3.5-flash、向量 text-embedding-v4 |
| 向量数据库 | Qdrant | 通过 gRPC(默认 6334)访问,每个知识库独立 collection |
| 关系数据库 | MySQL 8 + MyBatis-Plus | MyBatis-Plus 3.5.16,含 mybatis-plus-jsqlparser |
| 对象存储 | MinIO | 8.5.13,保存上传的原始 PDF |
| 文档解析 | MinerU 云端 API | mineru.net/api/v4,PDF → Markdown / content_list.json(含页码与 bbox) |
| PDF 处理 | Apache PDFBox | 3.0.8,用于页数统计与超大 PDF 分片 |
| 缓存 | Caffeine | 3.2.4,缓存向量库实例、智能体实例、异步任务状态 |
| 认证 | JJWT 0.13.0 + jBCrypt 0.4 | |
| 其他 | commons-compress 1.28.0、simplemagic 1.17、fastjson 2.0.64、Lombok 1.18.42 | 解压解析结果、MIME 类型识别等 |
┌───────────────────────────────────────────────────────────────────────┐
│ 前端 Vue 3(medrec-sys/front) │
│ 对话窗口 · 智能体管理 · 知识库管理 · 检索 · PDF 原文定位 │
└──────────────────────────┬────────────────────────────────────────────┘
│ REST + SSE(Flux 流式)
┌──────────────────────────▼────────────────────────────────────────────┐
│ Spring Boot 4 + Spring AI │
│ │
│ Controller ──▶ LoginInterceptor(JWT) ──▶ Service ──▶ Store / Client │
│ │
│ ┌── RAG 问答链路 ────────────────────────────────────────────────┐ │
│ │ MyQueryExpander 查询扩展(消解指代 → 生成 N 个检索句式) │ │
│ │ ↓ │ │
│ │ MultiVectorStoreDocumentRetriever 多知识库并发检索 │ │
│ │ ↓ │ │
│ │ MyDocumentPostProcessor 按相似度重排 + 截断 topK │ │
│ │ ↓ │ │
│ │ MyContextualQueryAugmenter 拼装上下文(带 <ID> 标记) │ │
│ │ ↓ │ │
│ │ OpenAiChatModel → SSE 流式输出 │ │
│ └────────────────────────────────────────────────────────────────┘ │
└───┬──────────────┬───────────────┬────────────────┬───────────────────┘
│ │ │ │
┌───▼────┐ ┌──────▼─────┐ ┌──────▼─────┐ ┌───────▼───────┐
│ Qdrant │ │ MySQL 8 │ │ MinIO │ │ MinerU │
│ 向量检索│ │ 业务+会话记忆│ │ 原始 PDF │ │ PDF → 结构化 │
└────────┘ └────────────┘ └────────────┘ └───────┬───────┘
│
┌───────▼────────┐
│ DashScope 模型 │
│ 对话 / VLM / 向量│
└────────────────┘
| 包 | 职责 |
|---|---|
controller |
REST 接口层,统一返回 Result<T> / PageVO<T> |
service / service.impl |
业务逻辑,含文档异步入库主流程 |
Ai |
RAG 核心:AiAgent、VectorStore 及自研 Advisor / Retriever / Expander |
store |
有状态组件管理器:VectorStoreManager、AgentManager、AsyncTaskStore(均基于 Caffeine 缓存) |
client |
外部服务客户端:ModelClient(模型)、MinerClient(MinerU)、MinioClient(对象存储) |
mapper |
MyBatis-Plus Mapper |
domain |
entity / dto / bo / common(Result、PageDTO、PageVO) |
interceptor / utils |
JWT 拦截器与 UserContext、文本处理与工具类 |
exception |
BusinessException + GlobalExceptionHandler 全局异常收敛 |
KnowledgeServiceImpl#saveAsync 是整个项目的入口级流程:
| 步骤 | 说明 |
|---|---|
| 1. 参数校验 | 仅接受 PDF(simplemagic 识别 MIME),校验知识库存在 |
| 2. 并发控制 | 使用 Semaphore(1) 全局串行执行解析任务,避免多个大文件同时解析压垮服务 |
| 3. 文档解析 | 上传 MinerU 转 Markdown,轮询解析状态,并把实时进度(含分片解析的页数进度)写入任务 |
| 4. 元数据落库 | 用 @Transactional(REQUIRES_NEW) 独立事务插入 t_knowledge 拿到 ID,不受异步线程事务影响 |
| 5. 结构化分块 | TextUtil 把解析结果转为 TextSegment,再做父子分块 |
| 6. 回填分块数 | 更新 t_knowledge.chunk |
| 7. 存原始文件 | 原始 PDF 写入 MinIO |
| 8. 写入向量库 | 批量并发写入 Qdrant |
| 9. 失败补偿 | 任一步失败即回滚三处数据:删除 MySQL 记录、删除 MinIO 文件、删除已写入的向量 |
MinerClient 针对 MinerU 返回的元素类型分别处理,尽量保留文档语义:
- 过滤噪声:丢弃页眉(
HEADER)与页码(PAGE_NUMBER); - 长文本:超过 2000 字再二次切分;
- 表格:转为"表格标题 + 表格内容 + 表格脚注"的自然语言文本,便于向量检索命中;
- 列表:相邻列表项合并到 2000 字以内,避免碎片化;
- 图片:交给多模态模型
qwen3.5-flash生成结构化描述(图片类型 / 核心内容 / 详细描述 / 关键词 / 适用场景),并发处理、10 分钟超时,单张失败降级为占位文本而不影响整体入库; - 小文本块合并:同页且不足 500 字的相邻块合并,并合并两者的 bbox 范围;
- 超大 PDF 分片:超过 200 页自动用 PDFBox 切分后分段解析,并在回收结果时修正分片导致的页码偏移(
pageIdx + 1 + 分片序号 × 200)。
TextUtil 构建两级结构,解决"检索要准、喂给模型要全"的矛盾:
- 父块 = MinerU 的段落级内容,
parentId = "0",携带page与bbox; - 子块 = 用
BreakIterator按中文句子切分父块(丢弃 ≤10 字的碎句),parentId = 知识ID_父块ID。
检索在 VectorStore#similaritySearchWithSplitSentence 中完成:
- 以
topK × 2只在子块中检索(过滤parentId != "0"); - 按
parentId聚合,取每个父块下最高的子块相似度; - 按父块 ID 批量取回父块完整原文;
- 把子块分数回填到父块(同时修正
distance = 1 - score)并降序排序。
即:用细粒度子块决定相关性排序,用完整父块喂给模型生成,兼顾准确率与上下文完整性。
Spring AI 原生的 DocumentRetriever 只支持单个向量库,而本项目一个智能体可挂载多个知识库。MultiVectorStoreDocumentRetriever 实现了 DocumentRetriever 接口,用 CompletableFuture 并发检索所有挂载的知识库并合并结果。
AiAgent 在运行时按智能体配置动态构建 ChatClient,装配以下组件:
| 组件 | 解决的问题 |
|---|---|
MessageChatMemoryAdvisor + JDBC 仓库 |
会话记忆持久化到 SPRING_AI_CHAT_MEMORY,按智能体的 max_message 做窗口截断 |
MyQueryExpander(查询扩展) |
用 LLM 结合对话历史消解代词指代,生成 4 个适合向量检索的完整问句,与原问题一起检索;历史仅保留最近 10 条;扩展失败自动退回原查询 |
MultiVectorStoreDocumentRetriever |
多知识库并发检索 |
MyDocumentPostProcessor(后处理) |
多库 × 多查询会召回远超 topK 的文档,此处按相似度降序截断到 topK |
MyContextualQueryAugmenter(查询增强) |
改写自 Spring AI 源码:原生实现会 new Query(...) 从而丢失历史消息,此处改为 query.mutate() 保留历史;同时自定义文档格式化,把每段上下文渲染为 <ID>文档ID</ID> + 正文 |
DocumentAdvisor |
从 Advisor 上下文中取出本轮检索到的文档并挂载回 AiAgent,供接口返回给前端展示 |
| 自定义提示词模板 | 约束模型"只依据上下文回答"、每句话尾标注 <ID>id</ID>、无依据时明确说明未提供相关信息 |
- 后端在拼装上下文时把文档渲染为
<ID>文档ID</ID>正文,并在提示词中要求模型逐句标注来源 ID; - 前端把
<ID>x</ID>渲染为可点击元素,点击后按 ID 调用/api/knowledge/docs拉取该文档; - 文档 metadata 中的
page与bbox直接驱动右侧 PDF 预览组件滚动到指定页并高亮坐标区域。
这样"AI 的每一句话"都可被验证,而不是黑盒回答。
- 批量并发写入向量库:每批 10 条,线程池大小取
min(CPU 核数, 批次数),CompletableFuture.allOf汇合,并做优雅关闭(超时shutdownNow); - 实例缓存:
VectorStoreManager与AgentManager用 Caffeine 缓存向量库 / 智能体实例(上限 1000,访问后 30 分钟过期),避免每次对话重复构建 embedding 模型与 Qdrant 连接; - 并发检索 + 历史窗口限制:查询扩展并发化、会话历史限制条数后,端到端响应速度明显提升(见提交记录
8676e55、6ecca24); - 大文件处理:200 页 / 200MB 上限 + 自动分片,前端上传接口改为异步任务,避免长连接超时。
- JDK 21+
- Maven 3.9+
- Docker / Docker Compose(用于中间件)
- 一个可用的 DashScope API Key 与 MinerU Token
仓库提供了 md-com.yml,一键拉起所需依赖:
docker compose -f md-com.yml up -d| 服务 | 容器名 | 端口映射 | 用途 |
|---|---|---|---|
| MySQL 8.0 | md-mysql |
9006:3306 |
业务数据 + 会话记忆 |
| MinIO | md-minio |
9090:9000(S3 API)、9000:9001(控制台) |
原始 PDF 存储 |
| Qdrant | md-qdrant |
16333:6333(REST)、16334:6334(gRPC) |
向量检索 |
| Nginx | md-nginx |
9050:80 |
前端静态资源(使用预构建镜像 medrec-web:latest) |
| 后端 | md-tomcat |
9016:8080 |
后端服务(使用预构建镜像 medrec-server:latest) |
注意:
md-com.yml中的md-nginx/md-tomcat使用的是预构建镜像,需要先自行执行两端的docker build;另外 volume 路径(/volume/Medrec/...)是 Linux 部署写法,本地运行请改为自己的路径。
data/table.sql 会重建 medrec 库并创建全部表:
mysql -h127.0.0.1 -P9006 -uroot -p < data/table.sql| 文件 | 用途 |
|---|---|
src/main/resources/args-dev.yaml |
本地开发配置(默认被 application.yaml 通过 spring.config.import 导入):MySQL、MinIO、Qdrant 地址与端口 |
src/main/resources/args.yaml |
容器 / 生产配置模板:地址使用 compose 中的服务名(md-mysql、md-minio、md-qdrant) |
src/main/resources/application.yaml |
模型接入(base-url、api-key、对话 / 向量模型名)、MinerU Token、文件上传大小上限、MyBatis-Plus 配置 |
请按自己的环境填写 DashScope API Key 与 MinerU Token;若使用其他 OpenAI 兼容服务,只需替换 base-url、api-key 与模型名。
# 开发模式
mvn spring-boot:run
# 打包运行(产物为 target/ROOT.jar)
mvn clean package -DskipTests
java -jar target/ROOT.jar默认端口 8080(由 args-dev.yaml 中的 internet.PORT 决定)。前端默认请求 http://localhost:9016,对应 compose 中的 9016:8080 映射。
docker build -t medrec-server:latest .Dockerfile 为多阶段构建:maven:3.9-eclipse-temurin-21 中借助 .mvn/settings.xml(阿里云镜像)下载依赖并打包 → eclipse-temurin:21-jre 运行 target/ROOT.jar。注意构建时会 COPY .mvn/settings.xml,该文件不可缺失。
所有接口统一返回 Result<T>:code = 1 表示成功,message = "NOT_LOGIN" 表示未登录。除 /api/login/** 外的请求都需要在 Authorization 头中携带 JWT。
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/login/login |
登录,返回 JWT |
| POST | /api/login/register |
注册 |
| POST | /api/user/list |
用户分页查询 |
| GET | /api/user/{id} |
用户详情 |
| POST / PUT / DELETE | /api/user、/api/user、/api/user/{id} |
新增 / 修改 / 删除 |
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/agent/list |
分页查询 |
| GET | /api/agent/{id} |
详情 |
| POST / PUT | /api/agent |
新增 / 修改 |
| DELETE | /api/agent/{id} |
删除 |
| POST | /api/agent/knowledge |
为智能体挂载知识库(agentId、vectorId) |
| DELETE | /api/agent/knowledge |
解绑知识库 |
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/vector/list |
分页查询 |
| GET | /api/vector/{id} |
详情 |
| POST / PUT | /api/vector |
新增 / 修改 |
| DELETE | /api/vector/{id} |
删除 |
| POST | /api/vector/search/{id} |
在指定知识库中做向量相似度检索 |
| GET | /api/vector/agent/{id} |
查询某智能体已挂载的知识库 |
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/knowledge/list |
分页查询 |
| GET | /api/knowledge/{id} |
详情 |
| POST | /api/knowledge/{vectorId} |
上传 PDF,异步入库,返回 taskId |
| PUT | /api/knowledge |
修改 |
| DELETE | /api/knowledge/{id} |
删除(同步清理向量与 MinIO 文件) |
| GET | /api/knowledge/vector/{id} |
查询某知识库下的全部文档 |
| POST | /api/knowledge/docs |
按文档 ID 列表批量取回文档(引用溯源用) |
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/ai/chat/{id} |
与指定智能体对话,SSE 流式返回 |
| POST | /api/ai/query |
查询扩展预览:传入原问题、生成数量 n 与历史消息,流式返回 LLM 生成的检索问句 |
| GET | /api/ai/history/{id} |
查询会话历史 |
| DELETE | /api/ai/{agentId}/{id} |
删除单条会话记录 |
| DELETE | /api/ai/all/{id} |
清空某智能体会话 |
| GET | /api/ai/vectors/{id} |
获取本轮检索命中的文档 |
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/task?taskId=xxx |
查询任务状态与进度描述 |
data/table.sql
| 表 | 说明 |
|---|---|
t_user |
用户(账号唯一、BCrypt 密码、注册 / 登录时间) |
t_agent |
AI 智能体(提示词、温度、max_message、top_k、相似度,温度有 0~1 的 CHECK 约束) |
t_vector |
知识库 / 向量库(名称、描述、向量维度 dim) |
t_knowledge |
知识文档(源文件名、MinIO 路径、所属知识库、分块数) |
t_agent_vector |
智能体 ↔ 知识库 的多对多关联 |
SPRING_AI_CHAT_MEMORY |
Spring AI 会话记忆(会话 ID + 序号,含联合索引) |
Spring/
├── data/table.sql # 建库建表脚本
├── md-com.yml # Docker Compose:MySQL / MinIO / Qdrant / Nginx / 后端
├── Dockerfile # 后端多阶段构建
├── pom.xml
└── src/main/
├── java/fun/medrec/spring/
│ ├── Application.java
│ ├── Ai/ # RAG 核心:AiAgent、VectorStore、自研 Advisor/Retriever/Expander
│ ├── client/ # ModelClient、MinerClient、MinioClient
│ ├── config/ # MyBatis-Plus、异步线程池、MVC 配置
│ ├── controller/ # 7 个 REST Controller
│ ├── domain/ # entity / dto / bo / common
│ ├── exception/ # 业务异常与全局异常处理
│ ├── interceptor/ # JWT 登录拦截器、UserContext
│ ├── mapper/ # MyBatis-Plus Mapper
│ ├── service/ # 业务接口与实现
│ ├── store/ # VectorStoreManager、AgentManager、AsyncTaskStore
│ └── utils/ # JWT、BCrypt、文本分块、时间工具
└── resources/
├── application.yaml # 模型接入与全局配置
├── args.yaml # 容器环境配置模板
├── args-dev.yaml # 本地开发配置
└── logback-spring.xml # 日志配置