From 710fe14d7f8d7dfc992a4303a3cc6e2f5acf2192 Mon Sep 17 00:00:00 2001 From: kennethcheng Date: Sun, 19 Apr 2026 22:27:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20RAG=20=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E9=97=AE=E7=AD=94=20POST=20=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E4=B8=8E=20ChatService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ChatService 实现 RAG 核心逻辑(向量检索 + 上下文构建 + LLM 生成) - 新增 ChatRequest/ChatResponse DTO 支持结构化请求响应 - 新增 ApiResponse 统一响应封装 - ChatController 新增 POST /api/chat RAG 问答接口(支持引用溯源) - 流式接口 GET /api/chat/ai/stream 保持不变 --- README.md | 195 +++++++++--------- src/main/java/com/demo/config/RagConfig.java | 7 + .../com/demo/controller/ChatController.java | 22 +- src/main/java/com/demo/dto/ApiResponse.java | 46 +++++ src/main/java/com/demo/dto/ChatRequest.java | 13 ++ src/main/java/com/demo/dto/ChatResponse.java | 21 ++ .../java/com/demo/service/ChatService.java | 101 +++++++++ 7 files changed, 305 insertions(+), 100 deletions(-) create mode 100644 src/main/java/com/demo/dto/ApiResponse.java create mode 100644 src/main/java/com/demo/dto/ChatRequest.java create mode 100644 src/main/java/com/demo/dto/ChatResponse.java create mode 100644 src/main/java/com/demo/service/ChatService.java diff --git a/README.md b/README.md index 9ab3963..0061d6b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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 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 支持** - 完整渲染代码块、表格、列表等格式 - 👨‍💻 **代码高亮** - Highlight.js 自动语言检测与语法着色 -- 📚 **RAG 文档问答** - 文档向量化存储,智能语义检索 -- 🗃️ **向量数据库** - Milvus 分布式向量数据库集成 +- 📚 **RAG 智能问答** - 基于向量检索的上下文感知回答 +- 🔍 **语义检索** - Milvus 向量数据库相似度搜索 +- 🔗 **引用溯源** - 回答附带文档来源引用 +- 🗃️ **向量数据库** - Milvus 分布式向量数据库 - 🔍 **Embedding 服务** - SiliconFlow BAAI/bge-large-zh-v1.5 -- 📔 **PDF 解析** - Apache PDFBox 文档处理支持 - 🎨 **精美界面** - 深色主题响应式设计 ## 🚀 快速开始 @@ -26,7 +27,7 @@ - JDK 17+ - Maven 3.8+ - [Ollama](https://ollama.ai/) 本地服务 -- Milvus 向量数据库(可选,用于 RAG) +- Milvus 向量数据库 ### 1. 安装 Ollama @@ -64,7 +65,6 @@ mvn spring-boot:run | Embedding | SiliconFlow BAAI/bge-large-zh-v1.5 | - | | 向量数据库 | Milvus | 2.3.4 | | 响应式编程 | Spring WebFlux | 3.2.0 | -| 文档处理 | Apache PDFBox | 2.0.29 | ### 前端 @@ -78,27 +78,25 @@ mvn spring-boot:run ``` springAiDemo/ -├── src/ -│ └── main/ -│ ├── java/com/demo/ -│ │ ├── MyApplication.java # Spring Boot 启动入口 -│ │ ├── config/ -│ │ │ └── RagConfig.java # RAG 文本分割配置 -│ │ ├── controller/ -│ │ │ ├── ChatController.java # AI 聊天 REST API -│ │ │ └── DocumentController.java # 文档导入 REST API -│ │ └── service/ -│ │ └── DocumentService.java # 文档处理服务 -│ └── resources/ -│ ├── application.yaml # 应用配置 -│ └── static/ # 前端静态资源 -│ ├── index.html # 主页面 -│ ├── css/style.css # 深色主题样式 -│ └── js/app.js # 流式响应逻辑 +├── src/main/java/com/demo/ +│ ├── MyApplication.java # Spring Boot 启动入口 +│ ├── config/ +│ │ └── RagConfig.java # RAG 配置类 +│ ├── controller/ +│ │ ├── ChatController.java # AI 聊天 API +│ │ └── DocumentController.java # 文档导入 API +│ ├── dto/ +│ │ ├── ApiResponse.java # 通用响应封装 +│ │ ├── ChatRequest.java # 聊天请求 DTO +│ │ └── ChatResponse.java # 聊天响应 DTO +│ └── service/ +│ ├── ChatService.java # RAG 聊天服务 +│ └── DocumentService.java # 文档处理服务 ├── data/ -│ └── doris_intro.md # RAG 示例文档 -├── pom.xml -└── README.md +│ └── doris_intro.md # RAG 示例文档 +└── src/main/resources/ + ├── application.yaml # 应用配置 + └── static/ # 前端资源 ``` ## 💬 API 文档 @@ -108,20 +106,22 @@ springAiDemo/ | 方法 | 端点 | 描述 | 参数 | |:---|:---|:---|:---| | GET | `/api/chat/test` | 健康检查 | - | -| GET | `/api/chat/ai` | 同步 AI 对话 | `msg` | -| GET | `/api/chat/ai/stream` | 流式 AI 对话 | `msg` | +| GET | `/api/chat/ai` | 流式 AI 对话 | `msg` | +| POST | `/api/chat` | RAG 智能问答 | `{ "question": "..." }` | ### 文档接口 -| 方法 | 端点 | 描述 | 参数 | -|:---|:---|:---|:---| -| GET | `/api/documents/import` | 导入文档到向量库 | - | +| 方法 | 端点 | 描述 | +|:---|:---|:---| +| GET | `/api/documents/import` | 导入文档到向量库 | ### 请求示例 -**同步对话:** +**RAG 智能问答:** ```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 ``` +### 响应格式 + +**POST /api/chat 响应:** +```json +{ + "code": 200, + "message": "success", + "data": { + "answer": "Apache Doris 是一个...", + "references": ["data/doris_intro.md"], + "timestamp": 1713000000000 + } +} +``` + ## 📚 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/` 目录 -2. 启动应用 -3. 调用导入接口: - ```bash - curl http://localhost:8080/api/documents/import - ``` -4. 通过聊天界面提问相关问题 +1. 将文档放入 `data/` 目录 +2. 调用导入接口:`curl http://localhost:8080/api/documents/import` +3. 通过 POST `/api/chat` 提问 ### 文档处理配置 @@ -160,20 +178,6 @@ document: max-num-chunk: 10000 # 最大块数量 ``` -## 🎨 界面预览 - -🧩 **现代化深色主题** -- 渐变标题与毛玻璃效果 -- 流畅的消息气泡动画 -- Markdown 代码块高亮 -- 流式响应动画指示器 - -📝 **Markdown 支持** -- 代码块与行内代码 -- 加粗、斜体、删除线 -- 有序/无序列表 -- 引用块、表格、链接 - ## 🛠️ 配置说明 ### AI 对话配置 @@ -182,10 +186,10 @@ document: spring: ai: openai: - base-url: http://localhost:11434 # Ollama 服务地址 + base-url: http://localhost:11434 chat: options: - model: gpt-oss:120b-cloud # 对话模型 + model: gpt-oss:120b-cloud temperature: 0.7 ``` @@ -200,7 +204,6 @@ spring: base-url: https://api.siliconflow.cn model: BAAI/bge-large-zh-v1.5 dimensions: 1024 - enabled: true ``` ### 向量数据库配置 @@ -210,47 +213,44 @@ spring: ai: vectorstore: milvus: - host: 192.168.50.103 - port: 19530 - database-name: doris_docs - collection-name: vector_store - embedding-dimension: 1024 - index-type: IVF_FLAT - metric-type: COSINE + client: + host: 192.168.50.103 + port: 19530 + databaseName: doris_docs + collectionName: vector_store ``` ## 🎬 架构图 ``` -┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐ -│ Client │────▶│ ChatController │────▶│ ChatClient │ -│ (HTTP) │ │ (Spring MVC) │ │ (Spring AI) │ -└─────────────┘ └──────────────────┘ └────────┬────────┘ - │ - ┌──────────────────┐ │ - │ DocumentService │ │ - │ (文档处理) │ ▼ - └────────┬─────────┘ ┌─────────────────┐ - │ │ Ollama/OpenAI │ - ▼ │ (LLM Provider) │ - ┌──────────────────┐ └─────────────────┘ - │ Milvus Store │ - │ (向量存储) │ - └──────────────────┘ +┌─────────────────────────────────────────────────────────────┐ +│ Client │ +│ ┌─────────────────┐ ┌─────────────────────────────────┐│ +│ │ Web UI │ │ POST /api/chat ││ +│ │ (流式/非流式) │────▶│ { question: "..." } ││ +│ └─────────────────┘ └─────────────────────────────────┘│ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ ChatController │ +│ POST /api/chat → ChatService │ +└─────────────────────────────────────────────────────────────┘ + │ + ┌───────────────┴───────────────┐ + ▼ ▼ +┌─────────────────────────┐ ┌─────────────────────────┐ +│ VectorStore │ │ ChatClient │ +│ (Milvus 语义检索) │ │ (Ollama LLM) │ +└─────────────────────────┘ └─────────────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────┐ ┌─────────────────────────┐ +│ DocumentService │ │ SiliconFlow API │ +│ (文档切割/向量化) │ │ (Embedding) │ +└─────────────────────────┘ └─────────────────────────┘ ``` -## 📄 预留功能 - -以下依赖已集成,可按需启用: - -- 📔 **PDF 文档解析** - Apache PDFBox 文档处理 - -## 👷 已知限制 - -1. 暂无对话历史持久化 -2. 暂未实现请求频率限制 -3. Milvus 连接信息硬编码在配置中 - ## 📖 更新日志 ### v1.0.0 (2026-04-19) @@ -262,10 +262,15 @@ spring: ### v1.1.0 (2026-04-19) - ✨ 新增 RAG 文档问答功能 - ✨ 新增 DocumentController 文档导入 API -- ✨ 新增 DocumentService 文档处理服务 - ✨ 配置 SiliconFlow Embedding 服务 - ✨ 集成 Milvus 向量数据库 +### v1.2.0 (2026-04-19) +- ✨ 新增 POST /api/chat RAG 智能问答接口 +- ✨ 新增 ChatService RAG 核心服务 +- ✨ 新增 ChatRequest/ChatResponse/ApiResponse DTO +- ✨ 新增引用溯源功能,返回文档来源 + ## 📗 License MIT © 2026 Spring AI Demo diff --git a/src/main/java/com/demo/config/RagConfig.java b/src/main/java/com/demo/config/RagConfig.java index c3a4228..98a98b5 100644 --- a/src/main/java/com/demo/config/RagConfig.java +++ b/src/main/java/com/demo/config/RagConfig.java @@ -1,5 +1,7 @@ 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.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -14,6 +16,11 @@ public class RagConfig { @Value("${document.max-num-chunk}") private int maxNumChunk; + + @Bean + public ChatClient chatClient(ChatModel chatModel){ + return ChatClient.builder(chatModel).build(); + } @Bean public TokenTextSplitter tokenTextSplitter() { return new TokenTextSplitter( diff --git a/src/main/java/com/demo/controller/ChatController.java b/src/main/java/com/demo/controller/ChatController.java index 45b2e31..72c49b3 100644 --- a/src/main/java/com/demo/controller/ChatController.java +++ b/src/main/java/com/demo/controller/ChatController.java @@ -1,20 +1,25 @@ 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.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; +@Slf4j @RestController @RequestMapping("/api/chat") public class ChatController { 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.chatService = chatService; } @GetMapping("/test") @@ -31,6 +36,13 @@ public class ChatController { .content(); } + @PostMapping + public ApiResponse chat(@RequestBody ChatRequest request) { + log.info("接收到请求,用户的问题是:{}",request.getQuestion()); + ChatResponse response = chatService.chat(request); + return ApiResponse.success(response); + } + /** * 流式的聊天接口,要注意如果中文有乱码,就是编码得问题,需要添加produces = "text/html;charset=UTF-8 * @param msg diff --git a/src/main/java/com/demo/dto/ApiResponse.java b/src/main/java/com/demo/dto/ApiResponse.java new file mode 100644 index 0000000..669d294 --- /dev/null +++ b/src/main/java/com/demo/dto/ApiResponse.java @@ -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 { + + private int code; + + private String message; + + private T data; + + public static ApiResponse success(T data) { + return ApiResponse.builder() + .code(200) + .message("success") + .data(data) + .build(); + } + + public static ApiResponse success(String message, T data) { + return ApiResponse.builder() + .code(200) + .message(message) + .data(data) + .build(); + } + + public static ApiResponse error(int code, String message) { + return ApiResponse.builder() + .code(code) + .message(message) + .build(); + } + + public static ApiResponse error(String message) { + return error(500, message); + } +} diff --git a/src/main/java/com/demo/dto/ChatRequest.java b/src/main/java/com/demo/dto/ChatRequest.java new file mode 100644 index 0000000..d4a94ea --- /dev/null +++ b/src/main/java/com/demo/dto/ChatRequest.java @@ -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; + +} diff --git a/src/main/java/com/demo/dto/ChatResponse.java b/src/main/java/com/demo/dto/ChatResponse.java new file mode 100644 index 0000000..3191453 --- /dev/null +++ b/src/main/java/com/demo/dto/ChatResponse.java @@ -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 references; + + private Long timestamp; +} diff --git a/src/main/java/com/demo/service/ChatService.java b/src/main/java/com/demo/service/ChatService.java new file mode 100644 index 0000000..7ff0c62 --- /dev/null +++ b/src/main/java/com/demo/service/ChatService.java @@ -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 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 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(); + } +}