feat: 新增 RAG 智能问答 POST 接口与 ChatService

- 新增 ChatService 实现 RAG 核心逻辑(向量检索 + 上下文构建 + LLM 生成)
- 新增 ChatRequest/ChatResponse DTO 支持结构化请求响应
- 新增 ApiResponse 统一响应封装
- ChatController 新增 POST /api/chat RAG 问答接口(支持引用溯源)
- 流式接口 GET /api/chat/ai/stream 保持不变
This commit is contained in:
kennethcheng 2026-04-19 22:27:44 +08:00
parent 43891f876e
commit 710fe14d7f
7 changed files with 305 additions and 100 deletions

195
README.md
View File

@ -1,6 +1,6 @@
# Spring AI Demo # Spring AI Demo
> 🤖 一个简洁优雅的 Spring AI 对话演示项目,基于 Spring Boot 3.2.0 与 Spring AI 1.0.0-M3 构建支持流式响应、Markdown 渲染与 RAG 文档问答。 > 🤖 一个简洁优雅的 Spring AI 对话演示项目,基于 Spring Boot 3.2.0 与 Spring AI 1.0.0-M3 构建支持流式响应、Markdown 渲染与 RAG 智能问答。
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.2.0-brightgreen)](https://spring.io/projects/spring-boot) [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.2.0-brightgreen)](https://spring.io/projects/spring-boot)
[![Spring AI](https://img.shields.io/badge/Spring%20AI-1.0.0--M3-blue)](https://spring.io/projects/spring-ai) [![Spring AI](https://img.shields.io/badge/Spring%20AI-1.0.0--M3-blue)](https://spring.io/projects/spring-ai)
@ -9,14 +9,15 @@
## ✨ 特性 ## ✨ 特性
- 🌐 **智能对话** - 基于 OpenAI/Ollama 的自然语言交互 - 🌐 **AI 智能对话** - 基于 Ollama 的自然语言交互
- 📡 **流式响应** - 实时逐字输出,体验流畅 - 📡 **流式响应** - 实时逐字输出,体验流畅
- 📝 **Markdown 支持** - 完整渲染代码块、表格、列表等格式 - 📝 **Markdown 支持** - 完整渲染代码块、表格、列表等格式
- 👨‍💻 **代码高亮** - Highlight.js 自动语言检测与语法着色 - 👨‍💻 **代码高亮** - Highlight.js 自动语言检测与语法着色
- 📚 **RAG 文档问答** - 文档向量化存储,智能语义检索 - 📚 **RAG 智能问答** - 基于向量检索的上下文感知回答
- 🗃️ **向量数据库** - Milvus 分布式向量数据库集成 - 🔍 **语义检索** - Milvus 向量数据库相似度搜索
- 🔗 **引用溯源** - 回答附带文档来源引用
- 🗃️ **向量数据库** - Milvus 分布式向量数据库
- 🔍 **Embedding 服务** - SiliconFlow BAAI/bge-large-zh-v1.5 - 🔍 **Embedding 服务** - SiliconFlow BAAI/bge-large-zh-v1.5
- 📔 **PDF 解析** - Apache PDFBox 文档处理支持
- 🎨 **精美界面** - 深色主题响应式设计 - 🎨 **精美界面** - 深色主题响应式设计
## 🚀 快速开始 ## 🚀 快速开始
@ -26,7 +27,7 @@
- JDK 17+ - JDK 17+
- Maven 3.8+ - Maven 3.8+
- [Ollama](https://ollama.ai/) 本地服务 - [Ollama](https://ollama.ai/) 本地服务
- Milvus 向量数据库(可选,用于 RAG - Milvus 向量数据库
### 1. 安装 Ollama ### 1. 安装 Ollama
@ -64,7 +65,6 @@ mvn spring-boot:run
| Embedding | SiliconFlow BAAI/bge-large-zh-v1.5 | - | | Embedding | SiliconFlow BAAI/bge-large-zh-v1.5 | - |
| 向量数据库 | Milvus | 2.3.4 | | 向量数据库 | Milvus | 2.3.4 |
| 响应式编程 | Spring WebFlux | 3.2.0 | | 响应式编程 | Spring WebFlux | 3.2.0 |
| 文档处理 | Apache PDFBox | 2.0.29 |
### 前端 ### 前端
@ -78,27 +78,25 @@ mvn spring-boot:run
``` ```
springAiDemo/ springAiDemo/
├── src/ ├── src/main/java/com/demo/
│ └── main/ │ ├── MyApplication.java # Spring Boot 启动入口
│ ├── java/com/demo/ │ ├── config/
│ │ ├── MyApplication.java # Spring Boot 启动入口 │ │ └── RagConfig.java # RAG 配置类
│ │ ├── config/ │ ├── controller/
│ │ │ └── RagConfig.java # RAG 文本分割配置 │ │ ├── ChatController.java # AI 聊天 API
│ │ ├── controller/ │ │ └── DocumentController.java # 文档导入 API
│ │ │ ├── ChatController.java # AI 聊天 REST API │ ├── dto/
│ │ │ └── DocumentController.java # 文档导入 REST API │ │ ├── ApiResponse.java # 通用响应封装
│ │ └── service/ │ │ ├── ChatRequest.java # 聊天请求 DTO
│ │ └── DocumentService.java # 文档处理服务 │ │ └── ChatResponse.java # 聊天响应 DTO
│ └── resources/ │ └── service/
│ ├── application.yaml # 应用配置 │ ├── ChatService.java # RAG 聊天服务
│ └── static/ # 前端静态资源 │ └── DocumentService.java # 文档处理服务
│ ├── index.html # 主页面
│ ├── css/style.css # 深色主题样式
│ └── js/app.js # 流式响应逻辑
├── data/ ├── data/
│ └── doris_intro.md # RAG 示例文档 │ └── doris_intro.md # RAG 示例文档
├── pom.xml └── src/main/resources/
└── README.md ├── application.yaml # 应用配置
└── static/ # 前端资源
``` ```
## 💬 API 文档 ## 💬 API 文档
@ -108,20 +106,22 @@ springAiDemo/
| 方法 | 端点 | 描述 | 参数 | | 方法 | 端点 | 描述 | 参数 |
|:---|:---|:---|:---| |:---|:---|:---|:---|
| GET | `/api/chat/test` | 健康检查 | - | | GET | `/api/chat/test` | 健康检查 | - |
| GET | `/api/chat/ai` | 同步 AI 对话 | `msg` | | GET | `/api/chat/ai` | 流式 AI 对话 | `msg` |
| GET | `/api/chat/ai/stream` | 流式 AI 对话 | `msg` | | POST | `/api/chat` | RAG 智能问答 | `{ "question": "..." }` |
### 文档接口 ### 文档接口
| 方法 | 端点 | 描述 | 参数 | | 方法 | 端点 | 描述 |
|:---|:---|:---|:---| |:---|:---|:---|
| GET | `/api/documents/import` | 导入文档到向量库 | - | | GET | `/api/documents/import` | 导入文档到向量库 |
### 请求示例 ### 请求示例
**同步对话** **RAG 智能问答**
```bash ```bash
curl "http://localhost:8080/api/chat/ai?msg=什么是Spring AI" curl -X POST http://localhost:8080/api/chat \
-H "Content-Type: application/json" \
-d '{"question": "Apache Doris 是什么?"}'
``` ```
**流式对话:** **流式对话:**
@ -134,21 +134,39 @@ curl "http://localhost:8080/api/chat/ai/stream?msg=讲一个故事"
curl http://localhost:8080/api/documents/import curl http://localhost:8080/api/documents/import
``` ```
### 响应格式
**POST /api/chat 响应:**
```json
{
"code": 200,
"message": "success",
"data": {
"answer": "Apache Doris 是一个...",
"references": ["data/doris_intro.md"],
"timestamp": 1713000000000
}
}
```
## 📚 RAG 文档问答 ## 📚 RAG 文档问答
### 功能说明 ### 工作原理
将本地文档导入 Milvus 向量数据库,实现基于语义理解的智能问答。 ```
用户问题 → Embedding → 向量检索 → 构建上下文 → LLM 生成回答 → 返回答案+引用
```
1. **文档导入** - 将 `.md` / `.txt` 文档读取并切割成 chunks
2. **向量化** - 使用 BAAI/bge-large-zh-v1.5 生成 1024 维向量
3. **存储检索** -存入 Milvus向量相似度搜索 top-K 结果
4. **生成回答** - 将检索结果作为上下文LLM 生成答案
### 使用步骤 ### 使用步骤
1. 将 `.md``.txt` 文档放入 `data/` 目录 1. 将文档放入 `data/` 目录
2. 启动应用 2. 调用导入接口:`curl http://localhost:8080/api/documents/import`
3. 调用导入接口: 3. 通过 POST `/api/chat` 提问
```bash
curl http://localhost:8080/api/documents/import
```
4. 通过聊天界面提问相关问题
### 文档处理配置 ### 文档处理配置
@ -160,20 +178,6 @@ document:
max-num-chunk: 10000 # 最大块数量 max-num-chunk: 10000 # 最大块数量
``` ```
## 🎨 界面预览
🧩 **现代化深色主题**
- 渐变标题与毛玻璃效果
- 流畅的消息气泡动画
- Markdown 代码块高亮
- 流式响应动画指示器
📝 **Markdown 支持**
- 代码块与行内代码
- 加粗、斜体、删除线
- 有序/无序列表
- 引用块、表格、链接
## 🛠️ 配置说明 ## 🛠️ 配置说明
### AI 对话配置 ### AI 对话配置
@ -182,10 +186,10 @@ document:
spring: spring:
ai: ai:
openai: openai:
base-url: http://localhost:11434 # Ollama 服务地址 base-url: http://localhost:11434
chat: chat:
options: options:
model: gpt-oss:120b-cloud # 对话模型 model: gpt-oss:120b-cloud
temperature: 0.7 temperature: 0.7
``` ```
@ -200,7 +204,6 @@ spring:
base-url: https://api.siliconflow.cn base-url: https://api.siliconflow.cn
model: BAAI/bge-large-zh-v1.5 model: BAAI/bge-large-zh-v1.5
dimensions: 1024 dimensions: 1024
enabled: true
``` ```
### 向量数据库配置 ### 向量数据库配置
@ -210,47 +213,44 @@ spring:
ai: ai:
vectorstore: vectorstore:
milvus: milvus:
host: 192.168.50.103 client:
port: 19530 host: 192.168.50.103
database-name: doris_docs port: 19530
collection-name: vector_store databaseName: doris_docs
embedding-dimension: 1024 collectionName: vector_store
index-type: IVF_FLAT
metric-type: COSINE
``` ```
## 🎬 架构图 ## 🎬 架构图
``` ```
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐ ┌─────────────────────────────────────────────────────────────┐
│ Client │────▶│ ChatController │────▶│ ChatClient │ │ Client │
│ (HTTP) │ │ (Spring MVC) │ │ (Spring AI) │ │ ┌─────────────────┐ ┌─────────────────────────────────┐│
└─────────────┘ └──────────────────┘ └────────┬────────┘ │ │ Web UI │ │ POST /api/chat ││
│ │ (流式/非流式) │────▶│ { question: "..." } ││
┌──────────────────┐ │ │ └─────────────────┘ └─────────────────────────────────┘│
│ DocumentService │ │ └─────────────────────────────────────────────────────────────┘
│ (文档处理) │ ▼
└────────┬─────────┘ ┌─────────────────┐
│ │ Ollama/OpenAI │ ┌─────────────────────────────────────────────────────────────┐
▼ │ (LLM Provider) │ │ ChatController │
┌──────────────────┐ └─────────────────┘ │ POST /api/chat → ChatService │
│ Milvus Store │ └─────────────────────────────────────────────────────────────┘
│ (向量存储) │
└──────────────────┘ ┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ VectorStore │ │ ChatClient │
│ (Milvus 语义检索) │ │ (Ollama LLM) │
└─────────────────────────┘ └─────────────────────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ DocumentService │ │ SiliconFlow API │
│ (文档切割/向量化) │ │ (Embedding) │
└─────────────────────────┘ └─────────────────────────┘
``` ```
## 📄 预留功能
以下依赖已集成,可按需启用:
- 📔 **PDF 文档解析** - Apache PDFBox 文档处理
## 👷 已知限制
1. 暂无对话历史持久化
2. 暂未实现请求频率限制
3. Milvus 连接信息硬编码在配置中
## 📖 更新日志 ## 📖 更新日志
### v1.0.0 (2026-04-19) ### v1.0.0 (2026-04-19)
@ -262,10 +262,15 @@ spring:
### v1.1.0 (2026-04-19) ### v1.1.0 (2026-04-19)
- ✨ 新增 RAG 文档问答功能 - ✨ 新增 RAG 文档问答功能
- ✨ 新增 DocumentController 文档导入 API - ✨ 新增 DocumentController 文档导入 API
- ✨ 新增 DocumentService 文档处理服务
- ✨ 配置 SiliconFlow Embedding 服务 - ✨ 配置 SiliconFlow Embedding 服务
- ✨ 集成 Milvus 向量数据库 - ✨ 集成 Milvus 向量数据库
### v1.2.0 (2026-04-19)
- ✨ 新增 POST /api/chat RAG 智能问答接口
- ✨ 新增 ChatService RAG 核心服务
- ✨ 新增 ChatRequest/ChatResponse/ApiResponse DTO
- ✨ 新增引用溯源功能,返回文档来源
## 📗 License ## 📗 License
MIT © 2026 Spring AI Demo MIT © 2026 Spring AI Demo

View File

@ -1,5 +1,7 @@
package com.demo.config; package com.demo.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -14,6 +16,11 @@ public class RagConfig {
@Value("${document.max-num-chunk}") @Value("${document.max-num-chunk}")
private int maxNumChunk; private int maxNumChunk;
@Bean
public ChatClient chatClient(ChatModel chatModel){
return ChatClient.builder(chatModel).build();
}
@Bean @Bean
public TokenTextSplitter tokenTextSplitter() { public TokenTextSplitter tokenTextSplitter() {
return new TokenTextSplitter( return new TokenTextSplitter(

View File

@ -1,20 +1,25 @@
package com.demo.controller; package com.demo.controller;
import com.demo.dto.ApiResponse;
import com.demo.dto.ChatRequest;
import com.demo.dto.ChatResponse;
import com.demo.service.ChatService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
@Slf4j
@RestController @RestController
@RequestMapping("/api/chat") @RequestMapping("/api/chat")
public class ChatController { public class ChatController {
private final ChatClient chatClient; private final ChatClient chatClient;
private final ChatService chatService;
public ChatController(ChatClient.Builder chatClientBuilder) { public ChatController(ChatClient.Builder chatClientBuilder, ChatService chatService) {
this.chatClient = chatClientBuilder.build(); this.chatClient = chatClientBuilder.build();
this.chatService = chatService;
} }
@GetMapping("/test") @GetMapping("/test")
@ -31,6 +36,13 @@ public class ChatController {
.content(); .content();
} }
@PostMapping
public ApiResponse<ChatResponse> chat(@RequestBody ChatRequest request) {
log.info("接收到请求,用户的问题是:{}",request.getQuestion());
ChatResponse response = chatService.chat(request);
return ApiResponse.success(response);
}
/** /**
* 流式的聊天接口要注意如果中文有乱码就是编码得问题需要添加produces = "text/html;charset=UTF-8 * 流式的聊天接口要注意如果中文有乱码就是编码得问题需要添加produces = "text/html;charset=UTF-8
* @param msg * @param msg

View File

@ -0,0 +1,46 @@
package com.demo.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
private int code;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.code(200)
.message("success")
.data(data)
.build();
}
public static <T> ApiResponse<T> success(String message, T data) {
return ApiResponse.<T>builder()
.code(200)
.message(message)
.data(data)
.build();
}
public static <T> ApiResponse<T> error(int code, String message) {
return ApiResponse.<T>builder()
.code(code)
.message(message)
.build();
}
public static <T> ApiResponse<T> error(String message) {
return error(500, message);
}
}

View File

@ -0,0 +1,13 @@
package com.demo.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class ChatRequest {
@NotBlank(message = "请输入你的问题")
private String question;
private Integer topK = 3;
}

View File

@ -0,0 +1,21 @@
package com.demo.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatResponse {
private String answer;
private List<String> references;
private Long timestamp;
}

View File

@ -0,0 +1,101 @@
package com.demo.service;
import com.demo.dto.ChatRequest;
import com.demo.dto.ChatResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Service
public class ChatService {
@Autowired
private final VectorStore vectorStore;
@Autowired
private final ChatClient chatClient;
private static final String SYSTEM_PROMPT = """
你是一个专业的 Apache Doris 技术支持助手
请基于以下提供的文档内容回答用户的问题
\s
文档内容
{context}
\s
回答要求
1. 如果文档中有相关信息请准确引用并详细回答
2. 如果文档中没有相关信息请明确告知用户
3. 保持专业准确友好的语气
4. 使用中文回答
""";
public ChatService(VectorStore vectorStore, ChatClient.Builder chatClientBuilder) {
this.vectorStore = vectorStore;
this.chatClient = chatClientBuilder.build();
}
/**
* 根据用户的请求查询知识库并通过大模型回答问题
* @param request
* @return
*/
public ChatResponse chat(ChatRequest request) {
// 1. 拿到用户问题首先需要检错向量数据库查看是否有相关的文档
// 这里是向量检索我们拿到的用户的问题是文字通过embedding模型进行向量化处理后再进进行检索
List<Document> relevantDocs = vectorStore.similaritySearch(
SearchRequest.query(request.getQuestion())
.withTopK(request.getTopK())
.withSimilarityThreshold(0.5) // 关键核心相似度必须大于 0.75满分通常是1.0
);
log.info("检索到 {} 个相关文档", relevantDocs.size());
// // debug:遍历打印它们的真实得分
// for (int i = 0; i < relevantDocs.size(); i++) {
// Document doc = relevantDocs.get(i);
// // 打印每一条文档的 metadata里面通常会有一个叫 "distance" "similarity" 的字段
// log.info("第 {} 个文档的 Metadata: {}", i + 1, doc.getMetadata());
// // log.info("文档片段内容: {}", doc.getContent().substring(0, Math.min(50, doc.getContent().length())) + "...");
// }
// 2. 根据检索结果构建上下文(请求llm的上下文)
String content = relevantDocs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n --- \n\n"));
// 3. 创建提示词
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(SYSTEM_PROMPT);
Message systemPrompt = systemPromptTemplate.createMessage(Map.of("context", content));
UserMessage userMessage = new UserMessage(request.getQuestion());
Prompt prompt = new Prompt(List.of(systemPrompt, userMessage));
// 4. 调用 LLM 生成答案
String answer = chatClient.prompt(prompt)
.call()
.content();
// 5. 封装返回的内容
List<String> references = relevantDocs.stream()
.map(doc -> (String) doc.getMetadata().get("source"))
.distinct()
.collect(Collectors.toList());
return ChatResponse.builder()
.answer(answer)
.references(references)
.timestamp(System.currentTimeMillis())
.build();
}
}