20250719 spring-ai搭建

This commit is contained in:
liangjinglin 2025-07-19 00:52:25 +08:00
commit 0ee73fd3de
9 changed files with 753 additions and 0 deletions

190
README.md Normal file
View File

@ -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

140
pom.xml Normal file
View File

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>spring-ai-demo</artifactId>
<version>1.0.0</version>
<name>spring-ai-demo</name>
<description>Spring AI Demo Project</description>
<properties>
<java.version>17</java.version>
<spring-ai.version>0.8.1</spring-ai.version>
</properties>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Thymeleaf (用于前端模板) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.13.3</version>
</dependency>
<!-- Spring Boot DevTools (开发工具) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<name>Central Portal Snapshots</name>
<id>central-portal-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</project>

View File

@ -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);
}
}

View File

@ -0,0 +1,11 @@
package com.example.springaidemo.bean;
import lombok.Data;
@Data
public class ChatRequest {
private String message;
private String sessionId;
}

View File

@ -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<ChatResponse> 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());
}
}

View File

@ -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<ChatResponse> 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<ChatResponse> 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<ChatResponse> 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;
}
}

View File

@ -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;
}

View File

@ -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

View File

@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spring AI 聊天演示</title>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.chat-container {
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 20px;
}
.header {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.chat-box {
height: 400px;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
overflow-y: auto;
background-color: #fafafa;
margin-bottom: 20px;
}
.message {
margin-bottom: 15px;
padding: 10px;
border-radius: 5px;
}
.user-message {
background-color: #007bff;
color: white;
text-align: right;
margin-left: 50px;
}
.ai-message {
background-color: #e9ecef;
color: #333;
margin-right: 50px;
}
.input-group {
display: flex;
gap: 10px;
}
#messageInput {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
#sendButton {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
#sendButton:hover {
background-color: #0056b3;
}
#sendButton:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
.loading {
color: #666;
font-style: italic;
}
</style>
</head>
<body>
<div class="chat-container">
<h1 class="header">🤖 Spring AI 聊天演示</h1>
<div id="chatBox" class="chat-box">
<div class="message ai-message">
<strong>AI助手:</strong> 你好!我是基于 Spring AI 的聊天助手,有什么可以帮助你的吗?
</div>
</div>
<div class="input-group">
<input type="text" id="messageInput" placeholder="输入你的消息..."
onkeypress="if(event.key==='Enter') sendMessage()">
<button id="sendButton" onclick="sendMessage()">发送</button>
</div>
</div>
<script>
const chatBox = document.getElementById('chatBox');
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
function addMessage(content, isUser = false) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${isUser ? 'user-message' : 'ai-message'}`;
messageDiv.innerHTML = `<strong>${isUser ? '你' : 'AI助手'}:</strong> ${content}`;
chatBox.appendChild(messageDiv);
chatBox.scrollTop = chatBox.scrollHeight;
}
function addLoadingMessage() {
const messageDiv = document.createElement('div');
messageDiv.className = 'message ai-message loading';
messageDiv.id = 'loadingMessage';
messageDiv.innerHTML = '<strong>AI助手:</strong> 正在思考中...';
chatBox.appendChild(messageDiv);
chatBox.scrollTop = chatBox.scrollHeight;
return messageDiv;
}
function removeLoadingMessage() {
const loadingMessage = document.getElementById('loadingMessage');
if (loadingMessage) {
loadingMessage.remove();
}
}
async function sendMessage() {
const message = messageInput.value.trim();
if (!message) return;
// 添加用户消息
addMessage(message, true);
messageInput.value = '';
// 禁用发送按钮
sendButton.disabled = true;
// 显示加载消息
const loadingMessage = addLoadingMessage();
try {
const response = await fetch('/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `message=${encodeURIComponent(message)}`
});
const aiResponse = await response.text();
// 移除加载消息
removeLoadingMessage();
// 添加AI响应
addMessage(aiResponse);
} catch (error) {
removeLoadingMessage();
addMessage('抱歉,发送消息时出现错误: ' + error.message);
} finally {
sendButton.disabled = false;
messageInput.focus();
}
}
// 页面加载完成后聚焦输入框
window.onload = function() {
messageInput.focus();
};
</script>
</body>
</html>