1. Spring AI ChatModel 入门实践:从零构建智能对话Demo
最近在探索Spring AI框架时,我基于spring-ai 1.1.0版本开发了一个最小可用的Chat Demo。这个项目虽然基础,但完整实现了对话调用和上下文缓存功能,特别适合想要快速上手Spring AI的Java开发者。下面我将详细分享整个实现过程,包括版本选型、配置细节、上下文设计等关键环节,以及实际开发中遇到的典型问题和解决方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与依赖配置
2.1 版本兼容性要点
Spring AI作为新兴框架,版本兼容性是需要特别注意的问题。经过多次测试验证,我最终确定了以下稳定组合:
- Spring Boot 3.3.5
- Spring AI 1.1.0
- JDK 17
特别提醒:Spring AI对Spring Boot版本有严格要求,如果版本不匹配,常见的报错包括ClassNotFoundException或Bean注入失败等问题。例如尝试使用Spring Boot 3.2.x搭配Spring AI 1.1.0时,就会出现自动配置类加载失败的情况。
2.2 Maven依赖配置详解
完整的pom.xml配置如下,有几个关键点需要注意:
xml复制<?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
https://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.3.5</version>
<relativePath/>
</parent>
<groupId>com.ljx.ai</groupId>
<artifactId>demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<!-- Spring AI 版本 -->
<spring-ai.version>1.1.0</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>group.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>group.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
关键配置说明:
- 必须通过dependencyManagement引入spring-ai-bom来管理所有Spring AI相关依赖的版本
- spring-ai-openai-spring-boot-starter是核心依赖,提供了ChatModel等关键接口
- 建议锁定所有相关组件的版本号,避免自动升级带来的兼容性问题
3. 服务配置与模型接入
3.1 多模型支持架构
Spring AI的一个强大之处在于它对多种AI模型的统一抽象。本Demo虽然使用的是硅基流动的OpenAI兼容接口,但架构设计上可以轻松切换不同模型提供商。这是通过Spring的配置体系实现的:
yaml复制spring:
application:
name: ai
ai:
openai:
# OpenAI 兼容接口(硅基流动)
base-url: https://api.siliconflow.cn
api-key: ${SILICONFLOW_API_KEY:自己的密钥}
chat:
options:
model: deepseek-ai/DeepSeek-V3.2
temperature: 0.7
max-tokens: 1024
参数解析:
base-url: 可替换为任何OpenAI兼容API的端点,包括本地部署的Ollama等model: 指定实际使用的模型名称temperature: 控制生成结果的随机性(0-1),值越大结果越多样max-tokens: 限制单次响应的最大长度
3.2 模型参数调优经验
在实际测试中,我发现temperature设置为0.7能在一致性和创造性之间取得较好平衡。对于专业问答场景,可以降到0.3-0.5;而对于创意生成,可以提高到0.8-1.0。
max-tokens需要根据模型的实际能力设置。像DeepSeek-V3这样的模型,1024是个安全的数值,既能保证回答完整性,又不会因响应过长导致等待时间过久。
4. 上下文管理实现
4.1 多轮对话原理剖析
Spring AI的多轮对话本质是通过在Prompt中携带历史Message列表实现的。系统主要处理三种消息类型:
- SystemMessage - 设定AI角色和对话规则
- UserMessage - 用户输入内容
- AssistantMessage - AI之前的回复
4.2 内存上下文实现方案
以下是基于内存的上下文管理实现,重点解决了历史消息裁剪问题:
java复制import groovy.util.logging.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class ChatStore {
/**
* 历史上下文
*/
private static final List<Message> history = new ArrayList<>();
/**
* 最大保留轮数(不含 system)
*/
private static final int MAX_HISTORY = 10;
private static final Logger log = LoggerFactory.getLogger(ChatStore.class);
/**
* 将当前用户消息加入上下文,并返回裁剪后的历史
*/
public static List<Message> trimHistory(String message) {
if (ObjectUtils.isEmpty(history)) {
history.add(new SystemMessage("你是一个金发兽耳小萝莉,请讨好你的主人"));
}
// system message 永远保留
Message system = history.get(0);
// 先加入用户消息
history.add(new UserMessage(message));
// 超出最大上下文长度则裁剪
if (history.size() > MAX_HISTORY + 1) {
List<Message> recent = history.subList(
history.size() - MAX_HISTORY,
history.size()
);
history.clear();
history.add(system);
history.addAll(recent);
}
return history;
}
/**
* 记录模型回复
*/
public static void addHistory(Message message) {
history.add(message);
}
/**
* 查看当前上下文内容
*/
public static String showHistory() {
StringBuilder sb = new StringBuilder();
if (ObjectUtils.isNotEmpty(history)) {
for (Message message : history) {
if (message instanceof SystemMessage) {
continue;
} else if (message instanceof UserMessage) {
sb.append("You say: ");
} else if (message instanceof AssistantMessage) {
sb.append("She say: ");
}
sb.append(message.getContent()).append("\n");
}
}
return sb.toString();
}
/**
* 清空上下文
*/
public static void clear() {
history.clear();
}
}
设计要点:
- 使用静态List保存对话历史,SystemMessage始终保留在首位
- 采用滑动窗口机制,保留最近的MAX_HISTORY轮对话
- 提供历史查看和清空功能,方便调试和管理
生产环境注意事项:当前实现是全局静态的,不支持多用户隔离。实际项目中应该按session或userId进行上下文隔离,可以考虑使用Redis等分布式缓存替代内存存储。
5. 核心聊天服务实现
5.1 ChatModel调用流程
Spring AI提供了简洁的API进行AI交互,主要流程如下:
java复制import groovy.util.logging.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@Slf4j
public class ChatServiceImpl extends ChatService {
private static final Logger log = LoggerFactory.getLogger(ChatServiceImpl.class);
@Autowired
private ChatModel chatModel;
@Override
public String sendMessage(String message) {
// 生成上下文
List<Message> messages = ChatStore.trimHistory(message);
// 构造 Prompt
Prompt prompt = new Prompt(messages);
// 调用模型
String result = chatModel
.call(prompt)
.getResult()
.getOutput()
.getContent();
// 保存回复
ChatStore.addHistory(new AssistantMessage(result));
return result;
}
@Override
public String show() {
return ChatStore.showHistory();
}
@Override
public void clear() {
ChatStore.clear();
}
}
关键步骤解析:
trimHistory()准备带上下文的Message列表- 构造Prompt对象封装输入
- 调用chatModel.call()获取AI响应
- 将响应保存到历史记录
5.2 异常处理建议
在实际使用中,我发现以下几个常见异常需要特别处理:
-
API调用超时:网络不稳定或模型响应慢时发生
- 解决方案:配置合理的超时时间,添加重试机制
-
额度不足/鉴权失败:API密钥无效或配额用完
- 解决方案:检查密钥配置,监控额度使用情况
-
上下文过长:历史消息超出模型限制
- 解决方案:优化上下文裁剪逻辑,控制MAX_HISTORY大小
一个健壮的生产级实现应该包含这些异常处理逻辑,这里给出一个改进版本:
java复制public String sendMessageWithRetry(String message) {
int retryCount = 0;
while (retryCount < MAX_RETRY) {
try {
List<Message> messages = ChatStore.trimHistory(message);
Prompt prompt = new Prompt(messages);
ChatResponse response = chatModel.call(prompt);
String result = response.getResult().getOutput().getContent();
ChatStore.addHistory(new AssistantMessage(result));
return result;
} catch (ResourceAccessException e) {
log.warn("API调用超时,重试中...");
retryCount++;
if (retryCount >= MAX_RETRY) {
throw new RuntimeException("API调用失败,请检查网络连接");
}
try {
Thread.sleep(1000 * retryCount);
} catch (InterruptedException ignored) {}
} catch (HttpClientErrorException e) {
if (e.getStatusCode() == HttpStatus.UNAUTHORIZED) {
throw new RuntimeException("API密钥无效,请检查配置");
} else if (e.getStatusCode() == HttpStatus.TOO_MANY_REQUESTS) {
throw new RuntimeException("API额度不足,请检查使用情况");
}
throw e;
}
}
throw new RuntimeException("未知错误");
}
6. 进阶优化方向
虽然当前Demo已经实现了基本功能,但在生产环境中还需要考虑以下优化:
6.1 多用户隔离方案
- 基于Session的隔离:
java复制@RestController
public class ChatController {
private final Map<String, List<Message>> sessionContexts = new ConcurrentHashMap<>();
@PostMapping("/chat")
public String chat(@RequestParam String message, HttpSession session) {
List<Message> history = sessionContexts.computeIfAbsent(
session.getId(),
k -> new ArrayList<>(List.of(new SystemMessage("默认系统提示")))
);
// ...其余逻辑
}
}
- 基于用户ID的隔离:
java复制@Service
public class UserChatService {
private final Map<Long, List<Message>> userContexts = new ConcurrentHashMap<>();
public String chat(Long userId, String message) {
List<Message> history = userContexts.computeIfAbsent(
userId,
k -> new ArrayList<>(List.of(new SystemMessage("用户专属提示")))
);
// ...其余逻辑
}
}
6.2 持久化存储方案
对于需要长期保存对话历史的场景,可以考虑:
- 关系型数据库存储:
java复制@Entity
public class ChatHistory {
@Id @GeneratedValue
private Long id;
private Long userId;
private String role; // "system", "user", "assistant"
private String content;
private LocalDateTime createdAt;
}
- Redis缓存方案:
java复制public class RedisChatStore {
private final RedisTemplate<String, Object> redisTemplate;
public void saveMessage(String sessionId, Message message) {
redisTemplate.opsForList().rightPush(
"chat:" + sessionId,
new RedisMessage(message)
);
redisTemplate.expire(
"chat:" + sessionId,
30, TimeUnit.MINUTES
);
}
}
6.3 性能优化技巧
- 异步非阻塞调用:
java复制public CompletableFuture<String> sendMessageAsync(String message) {
return CompletableFuture.supplyAsync(() -> {
List<Message> messages = ChatStore.trimHistory(message);
Prompt prompt = new Prompt(messages);
return chatModel.call(prompt)
.getResult()
.getOutput()
.getContent();
});
}
- 流式响应处理:
java复制public Flux<String> streamChat(String message) {
List<Message> messages = ChatStore.trimHistory(message);
Prompt prompt = new Prompt(messages);
return chatModel.stream(prompt)
.map(ChatResponse::getResults)
.flatMapIterable(list -> list)
.map(result -> result.getOutput().getContent());
}
7. 常见问题排查
在实际开发过程中,我遇到了以下几个典型问题及解决方案:
-
Bean注入失败:
- 现象:启动时报
No qualifying bean of type 'ChatModel' available - 原因:Spring AI自动配置未生效
- 解决:检查依赖是否正确引入,特别是
spring-ai-openai-spring-boot-starter
- 现象:启动时报
-
上下文丢失:
- 现象:多轮对话中AI"忘记"之前的内容
- 原因:历史消息未正确传递或裁剪过多
- 解决:检查
trimHistory逻辑,确保SystemMessage始终保留
-
响应速度慢:
- 现象:每次调用等待时间过长
- 原因:模型参数设置不当或网络延迟
- 解决:调整
max-tokens,考虑使用异步或流式响应
-
中文乱码问题:
- 现象:返回内容出现乱码
- 原因:字符编码配置不正确
- 解决:确保应用使用UTF-8编码,可在application.yml中添加:
yaml复制spring: servlet: encoding: charset: UTF-8 force: true
8. 测试验证方案
为确保功能可靠性,我设计了以下测试用例:
- 基础对话测试:
java复制@Test
void testSingleTurnChat() {
String response = chatService.sendMessage("你好");
assertNotNull(response);
assertFalse(response.isEmpty());
}
- 多轮对话一致性测试:
java复制@Test
void testMultiTurnChat() {
chatService.sendMessage("我叫张三");
String response = chatService.sendMessage("我是谁?");
assertTrue(response.contains("张三"));
}
- 上下文裁剪测试:
java复制@Test
void testContextTrimming() {
// 填充超过MAX_HISTORY的消息
for (int i = 0; i < 15; i++) {
chatService.sendMessage("消息" + i);
}
// 验证上下文长度不超过MAX_HISTORY + 1
String history = chatService.show();
assertTrue(history.split("\n").length <= 10 * 2); // 10轮对话(用户+AI各一条)
}
- 异常场景测试:
java复制@Test
void testInvalidApiKey() {
// 模拟无效API密钥
environment.setProperty("spring.ai.openai.api-key", "invalid");
assertThrows(RuntimeException.class, () -> {
chatService.sendMessage("测试");
});
}
9. 部署与监控建议
对于生产环境部署,我有以下建议:
- 健康检查端点:
java复制@RestController
public class HealthController {
@Autowired
private ChatModel chatModel;
@GetMapping("/health")
public ResponseEntity<String> healthCheck() {
try {
chatModel.call(new Prompt("ping"));
return ResponseEntity.ok("OK");
} catch (Exception e) {
return ResponseEntity.status(503).body("Service Unavailable");
}
}
}
- 性能监控指标:
java复制@RestController
public class MetricsController {
private final MeterRegistry meterRegistry;
@PostMapping("/chat")
public String chatWithMetrics(@RequestParam String message) {
Timer.Sample sample = Timer.start(meterRegistry);
try {
String response = chatService.sendMessage(message);
sample.stop(meterRegistry.timer("chat.response.time"));
return response;
} catch (Exception e) {
sample.stop(meterRegistry.timer("chat.error.time"));
throw e;
}
}
}
- 日志记录规范:
java复制@Slf4j
@Service
public class ChatServiceImpl implements ChatService {
public String sendMessage(String message) {
log.info("收到用户消息: {}", message);
try {
String response = // ...调用逻辑
log.info("AI响应: {}", response);
return response;
} catch (Exception e) {
log.error("聊天服务异常", e);
throw e;
}
}
}
10. 项目扩展思路
基于当前Demo,可以进一步扩展以下功能:
- 多模态支持:
java复制public String analyzeImage(MultipartFile image, String question) {
ImagePrompt prompt = new ImagePrompt(
new UserMessage(question),
new ImageMessage(image.getBytes())
);
return chatModel.call(prompt).getResult().getOutput().getContent();
}
- 函数调用集成:
java复制@Bean
public FunctionCallback weatherFunction() {
return new FunctionCallback("getWeather", "获取天气信息") {
@Override
public Object apply(Object... args) {
// 调用天气API
return weatherService.getCurrentWeather(args[0].toString());
}
};
}
// 在Prompt中指定可用函数
Prompt prompt = new Prompt(
messages,
OpenAiChatOptions.builder()
.withFunctionCallbacks(List.of(weatherFunction()))
.build()
);
- RAG(检索增强生成)实现:
java复制public String searchAndAnswer(String question) {
// 1. 从知识库检索相关文档
List<Document> docs = vectorStore.similaritySearch(question);
// 2. 将检索结果作为上下文
String context = docs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n"));
// 3. 构造增强后的Prompt
Prompt prompt = new Prompt(
List.of(
new SystemMessage("基于以下信息回答问题:" + context),
new UserMessage(question)
)
);
return chatModel.call(prompt).getResult().getOutput().getContent();
}
这个Spring AI ChatModel Demo虽然简单,但涵盖了从环境搭建到核心功能实现的完整流程。在实际开发中,根据具体业务需求,可以在此基础上不断扩展和完善。我在实现过程中最大的体会是,Spring AI通过统一的API抽象,极大简化了不同AI模型的集成工作,让开发者可以更专注于业务逻辑的实现。
