commit 0ee73fd3debac255e07f5086eaa469c7a05efc4c Author: liangjinglin Date: Sat Jul 19 00:52:25 2025 +0800 20250719 spring-ai搭建 diff --git a/README.md b/README.md new file mode 100644 index 0000000..dc9e69c --- /dev/null +++ b/README.md @@ -0,0 +1,190 @@ +# Spring AI 聊天演示项目 + +这是一个基于 Spring AI 的简单聊天应用演示项目,展示了如何与大语言模型进行对话。 + +## 功能特性 + +- 🤖 支持多种大语言模型(OpenAI、Ollama) +- 💬 简洁美观的聊天界面 +- 🔧 简单的配置和部署 +- 📱 响应式设计,支持移动端 + +## 技术栈 + +- **Spring Boot 3.2.0** - 应用框架 +- **Spring AI 0.8.1** - AI 集成框架 +- **Thymeleaf** - 模板引擎 +- **HTML/CSS/JavaScript** - 前端界面 + +## 快速开始 + +### 1. 环境要求 + +- Java 17 或更高版本 +- Maven 3.6 或更高版本 + +### 2. 配置大语言模型 + +#### 使用 OpenAI(推荐) + +在 `src/main/resources/application.yml` 中配置: + +```yaml +spring: + ai: + openai: + api-key: your-openai-api-key-here + base-url: https://api.openai.com # 可选,默认值 +``` + +或者通过环境变量设置: + +```bash +export OPENAI_API_KEY=your-openai-api-key-here +``` + +#### 使用 Ollama(本地部署) + +1. 首先安装并启动 Ollama: + ```bash + # 安装 Ollama + curl -fsSL https://ollama.ai/install.sh | sh + + # 拉取模型(例如 llama2) + ollama pull llama2 + + # 启动 Ollama 服务 + ollama serve + ``` + +2. 在配置文件中启用 Ollama: + ```yaml + spring: + ai: + ollama: + base-url: http://localhost:11434 + chat: + options: + model: llama2 + ``` + +### 3. 运行应用 + +```bash +# 编译项目 +mvn clean compile + +# 运行应用 +mvn spring-boot:run +``` + +应用启动后,访问 http://localhost:8080 即可使用聊天界面。 + +### 4. API 接口 + +除了网页界面,还提供了简单的 API 接口: + +```bash +# 简单对话接口 +curl "http://localhost:8080/simple?q=你好" + +# POST 接口 +curl -X POST http://localhost:8080/chat \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "message=介绍一下 Spring AI" +``` + +## 项目结构 + +``` +src/ +├── main/ +│ ├── java/com/example/springaidemo/ +│ │ ├── SpringAiDemoApplication.java # 应用主类 +│ │ ├── config/ +│ │ │ └── AiConfig.java # AI 配置类 +│ │ └── controller/ +│ │ └── ChatController.java # 聊天控制器 +│ └── resources/ +│ ├── application.yml # 应用配置 +│ └── templates/ +│ └── chat.html # 聊天界面模板 +``` + +## 配置说明 + +### OpenAI 配置 + +```yaml +spring: + ai: + openai: + api-key: ${OPENAI_API_KEY:your-openai-api-key} # OpenAI API Key + base-url: ${OPENAI_BASE_URL:https://api.openai.com} # API 基础 URL + chat: + options: + model: gpt-3.5-turbo # 使用的模型 + temperature: 0.7 # 温度参数(0-1) +``` + +### Ollama 配置 + +```yaml +spring: + ai: + ollama: + base-url: ${OLLAMA_BASE_URL:http://localhost:11434} # Ollama 服务地址 + chat: + options: + model: ${OLLAMA_MODEL:llama2} # 使用的模型 +``` + +## 自定义和扩展 + +### 添加新的对话端点 + +在 `ChatController` 中添加新的方法: + +```java +@GetMapping("/custom") +@ResponseBody +public String customChat(@RequestParam("message") String message) { + // 自定义处理逻辑 + String prompt = "请用专业的语言回答:" + message; + return chatClient.call(prompt); +} +``` + +### 修改聊天界面 + +编辑 `src/main/resources/templates/chat.html` 文件来自定义界面样式和功能。 + +### 添加更多 AI 提供商 + +Spring AI 支持多种 AI 提供商,包括: +- OpenAI +- Azure OpenAI +- Hugging Face +- Ollama +- Anthropic Claude + +## 常见问题 + +### 1. OpenAI API Key 错误 +- 确保 API Key 正确设置 +- 检查 API Key 是否有足够的配额 +- 验证网络连接是否正常 + +### 2. Ollama 连接失败 +- 确保 Ollama 服务正在运行 +- 检查端口 11434 是否可访问 +- 确认模型已正确下载 + +### 3. 应用启动失败 +- 检查 Java 版本是否为 17 或更高 +- 确保网络连接正常 +- 查看控制台错误日志 + +## 许可证 + +MIT License \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..9a18b21 --- /dev/null +++ b/pom.xml @@ -0,0 +1,140 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.example + spring-ai-demo + 1.0.0 + spring-ai-demo + Spring AI Demo Project + + + 17 + 0.8.1 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.ai + spring-ai-starter-model-openai + + + + org.springframework.ai + spring-ai-starter-model-deepseek + + + + org.springframework.ai + spring-ai-starter-vector-store-elasticsearch + + + + co.elastic.clients + elasticsearch-java + 8.13.3 + + + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.projectlombok + lombok + true + + + cn.hutool + hutool-all + 5.8.25 + + + + + + + org.springframework.ai + spring-ai-bom + 1.0.0 + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 17 + 17 + UTF-8 + + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + Central Portal Snapshots + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + + \ No newline at end of file diff --git a/src/main/java/com/example/springaidemo/SpringAiDemoApplication.java b/src/main/java/com/example/springaidemo/SpringAiDemoApplication.java new file mode 100644 index 0000000..0f6267e --- /dev/null +++ b/src/main/java/com/example/springaidemo/SpringAiDemoApplication.java @@ -0,0 +1,18 @@ +package com.example.springaidemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringAiDemoApplication { + + public static void main(String[] args) { + String proxy = "127.0.0.1"; + int port = 7897; + System.setProperty("proxyType", "4"); + System.setProperty("proxyPort", String.valueOf(port)); + System.setProperty("proxyHost", proxy); + System.setProperty("proxySet", "true"); + SpringApplication.run(SpringAiDemoApplication.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/springaidemo/bean/ChatRequest.java b/src/main/java/com/example/springaidemo/bean/ChatRequest.java new file mode 100644 index 0000000..ceaa35e --- /dev/null +++ b/src/main/java/com/example/springaidemo/bean/ChatRequest.java @@ -0,0 +1,11 @@ +package com.example.springaidemo.bean; + +import lombok.Data; + +@Data +public class ChatRequest { + + private String message; + + private String sessionId; +} diff --git a/src/main/java/com/example/springaidemo/controller/ChatController.java b/src/main/java/com/example/springaidemo/controller/ChatController.java new file mode 100644 index 0000000..98f12af --- /dev/null +++ b/src/main/java/com/example/springaidemo/controller/ChatController.java @@ -0,0 +1,44 @@ +package com.example.springaidemo.controller; + + +import com.example.springaidemo.bean.ChatRequest; +import com.example.springaidemo.service.DeepseekChatService; +import org.springframework.ai.chat.client.ChatClientRequest; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import reactor.core.publisher.Flux; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +@RestController +@RequestMapping("/chatController") +public class ChatController { + + @Autowired + private DeepseekChatService deepseekChatService; + + @GetMapping("/ai/chat") + public Map generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) { + return deepseekChatService.chat(message); + } + + @GetMapping("/ai/generateStream") + public Flux generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) { + return deepseekChatService.streamChat(message); + } + + @PostMapping(value = "/ai/sseChat", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter streamChat(@RequestBody ChatRequest chatRequest) { + return deepseekChatService.sseChat(chatRequest.getMessage()); + } + + @PostMapping(value = "/ai/thinkChat", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter thinkChat(@RequestBody ChatRequest chatRequest) { + return deepseekChatService.thinkChat(chatRequest.getMessage()); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/springaidemo/service/DeepseekChatService.java b/src/main/java/com/example/springaidemo/service/DeepseekChatService.java new file mode 100644 index 0000000..e3c5221 --- /dev/null +++ b/src/main/java/com/example/springaidemo/service/DeepseekChatService.java @@ -0,0 +1,123 @@ +package com.example.springaidemo.service; + +import io.micrometer.common.util.StringUtils; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekAssistantMessage; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import reactor.core.publisher.Flux; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +@Service +public class DeepseekChatService { + + private final DeepSeekChatModel chatModel; + + @Autowired + public DeepseekChatService(DeepSeekChatModel chatModel) { + this.chatModel = chatModel; + } + + public Map chat(String message) { + return Map.of("generation", chatModel.call(message)); + } + + public Flux streamChat(String message) { + var prompt = new Prompt(new UserMessage(message)); + return chatModel.stream(prompt); + } + + public SseEmitter sseChat(String message) { + SseEmitter emitter = new SseEmitter(30000L); // 30秒超时 + + // 异步处理 + CompletableFuture.runAsync(() -> { + try { + // 使用 Spring AI 的流式调用 + var prompt = new Prompt(new UserMessage(message)); + Flux responseFlux = chatModel.stream(prompt); + + // 处理流式响应 + responseFlux.subscribe( + response -> { + try { + // 发送数据到客户端 + String content = response.getResult().getOutput().getText(); + emitter.send(SseEmitter.event() + .name("message") + .data(content)); + } catch (IOException e) { + emitter.completeWithError(e); + } + }, + error -> { + emitter.completeWithError(error); + }, + () -> { + emitter.complete(); + } + ); + } catch (Exception e) { + emitter.completeWithError(e); + } + }); + + return emitter; + } + + public SseEmitter thinkChat(String message) { + SseEmitter emitter = new SseEmitter(30000L); + CompletableFuture.runAsync(() -> { + try { + DeepSeekChatOptions promptOptions = DeepSeekChatOptions.builder() + .model(DeepSeekApi.ChatModel.DEEPSEEK_REASONER.getValue()) + .build(); + Prompt prompt = new Prompt(message, promptOptions); + Flux responseFlux = chatModel.stream(prompt); + + responseFlux.subscribe( + response -> { + try { + // 发送数据到客户端 + DeepSeekAssistantMessage deepSeekAssistantMessage = (DeepSeekAssistantMessage) response.getResult().getOutput(); + String reasoningContent = deepSeekAssistantMessage.getReasoningContent(); + String text = deepSeekAssistantMessage.getText(); + if (StringUtils.isNotEmpty(reasoningContent)) { + emitter.send(SseEmitter.event() + .name("message") + .data(reasoningContent)); + } + if (StringUtils.isNotEmpty(text)) { + emitter.send(SseEmitter.event() + .name("message") + .data(text)); + } + } catch (IOException e) { + emitter.completeWithError(e); + } + }, + error -> { + emitter.completeWithError(error); + }, + () -> { + emitter.complete(); + } + ); + + } catch (Exception e) { + emitter.completeWithError(e); + } + }); + + return emitter; + } +} diff --git a/src/main/java/com/example/springaidemo/service/VectorService.java b/src/main/java/com/example/springaidemo/service/VectorService.java new file mode 100644 index 0000000..764cd1b --- /dev/null +++ b/src/main/java/com/example/springaidemo/service/VectorService.java @@ -0,0 +1,12 @@ +package com.example.springaidemo.service; + +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.beans.factory.annotation.Autowired; + +public class VectorService { + + @Autowired + private VectorStore vectorStore; + + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..5bd8389 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,41 @@ +spring: + application: + name: spring-ai-demo + elasticsearch: + uris: http://154.12.80.119:9200 + username: elastic + password: 123456 + ai: + deepseek: + api-key: sk-3043bb4777404970a22c7544dd30aaa2 + openai: + api-key: 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 + # 如果您有代理服务,可以修改为代理地址 + # base-url: https://your-proxy-service.com/v1 + base-url: https://api.openai.com + chat: + options: + model: gpt-3.5-turbo + temperature: 0.7 + # 增加超时配置 + client: + connect-timeout: 30000 # 30秒连接超时 + read-timeout: 60000 # 60秒读取超时 + vectorstore: + elasticsearch: + initialize-schema: true + index-name: custom-index + dimensions: 1536 + similarity: cosine +# ollama: +# base-url: ${OLLAMA_BASE_URL:http://localhost:11434} +# chat: +# options: +# model: ${OLLAMA_MODEL:llama2} + +server: + port: 8080 + +logging: + level: + org.springframework.ai: DEBUG \ No newline at end of file diff --git a/src/main/resources/templates/chat.html b/src/main/resources/templates/chat.html new file mode 100644 index 0000000..e0d2119 --- /dev/null +++ b/src/main/resources/templates/chat.html @@ -0,0 +1,174 @@ + + + + + + Spring AI 聊天演示 + + + +
+

🤖 Spring AI 聊天演示

+
+
+ AI助手: 你好!我是基于 Spring AI 的聊天助手,有什么可以帮助你的吗? +
+
+
+ + +
+
+ + + + \ No newline at end of file