SpringBoot实战:用阿里灵积大模型打造带记忆的AI聊天室(附完整代码)
SpringBoot实战:用阿里灵积大模型打造带记忆的AI聊天室(附完整代码)
最近在捣鼓一些AI应用,发现很多开发者对“让AI记住对话”这个功能特别感兴趣。确实,一个只会“一问一答”的聊天机器人,用起来总感觉少了点什么,就像跟一个健忘的朋友聊天,每次都得从头说起。而一个能记住上下文、能进行多轮连贯对话的AI,才真正具备了“智能体”的雏形,能处理更复杂的任务,比如辅导学习、规划行程或者进行深度技术讨论。今天,我就结合自己最近在SpringBoot项目中集成阿里云灵积大模型的经验,跟大家聊聊如何从零开始,构建一个具备记忆能力的AI聊天室。整个过程会聚焦于Java开发者的视角,我会把代码实现、配置细节以及我踩过的坑都分享出来,目标是让你看完就能动手实现一个。
1. 项目环境搭建与核心依赖引入
在开始编码之前,我们需要先把“舞台”搭好。一个SpringBoot项目是基础,但关键在于引入正确的“演员”——那些能让AI拥有记忆和对话能力的依赖库。
首先,通过Spring Initializr创建一个标准的SpringBoot项目,我习惯用Java 17或21,构建工具选Maven或Gradle都可以。核心的依赖除了SpringBoot Web Starter(用于提供HTTP接口),最重要的就是阿里云灵积大模型的Java SDK,以及Spring AI的相关组件。Spring AI是Spring官方推出的AI应用开发框架,它抽象了不同大模型供应商的接口,让我们能用一套统一的API去调用,极大地简化了集成工作。
下面是我的pom.xml中关键依赖部分:
<dependencies>
<!-- SpringBoot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI: 核心抽象层 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
<version>0.8.1</version> <!-- 请使用最新稳定版 -->
</dependency>
<!-- Spring AI 阿里云灵积连接器 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-alibaba-dashscope-spring-boot-starter</artifactId>
<version>0.8.1</version>
</dependency>
<!-- 数据存储(用于持久化对话记忆,这里用内存H2演示) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
注意:Spring AI及其各厂商的Starter版本迭代较快,建议在开发时查看官方文档或Maven中央仓库,使用最新的稳定版本。版本不匹配可能会导致一些类或方法找不到。
引入依赖后,下一步是配置。在application.yml或application.properties中,我们需要配置阿里云灵积的访问密钥和模型参数。这些信息需要你在阿里云官网开通灵积服务后获取。
spring:
ai:
alibaba:
dashscope:
api-key: ${ALIBABA_API_KEY:你的API_KEY} # 强烈建议通过环境变量注入,避免硬编码
chat:
options:
model: qwen-max # 指定使用的模型,例如qwen-max, qwen-plus等
temperature: 0.7 # 控制输出的随机性
max-tokens: 2000 # 控制单次回复的最大长度
这里有个小技巧:api-key不要直接写在配置文件里提交到代码仓库。我通常使用环境变量ALIBABA_API_KEY,或者在本地开发时使用Spring Boot的profile-specific配置文件。模型选择上,qwen-max在通用能力和长上下文上表现不错,对于聊天室场景很合适。temperature设为0.7能在创造性和稳定性间取得较好平衡。
2. 理解多轮对话的核心机制:从ChatClient到Memory
代码跑起来之前,我们得先搞清楚Spring AI里实现多轮对话的几个核心概念。这就像组装乐高,你得知道每块积木是干什么的,才能拼出想要的东西。这套机制的核心是 ChatClient、ChatMemory 和 Advisor。
ChatClient 是你的主要操作界面。相比直接调用底层的ChatModel,ChatClient提供了更高级、更流畅的链式调用(Fluent API)能力。你可以把它想象成一个功能丰富的“聊天客户端”,它不仅负责发送消息给大模型,还能让你方便地附加各种“插件”或“中间件”,比如内容过滤器、记忆管理器、输出格式化器等。创建ChatClient很简单,Spring AI支持通过@Bean注入,或者直接用ChatClient.builder(model).build()来构造。
ChatMemory 是记忆存储的核心。它的职责就是保存和检索与特定会话相关的历史消息。Spring AI内置了几种ChatMemory实现:
InMemoryChatMemory:将对话历史存储在应用内存中。优点是快,缺点是应用重启后记忆就消失了,且不适合分布式部署。VectorStoreChatMemory:结合向量数据库,能进行基于语义的相似度搜索来回忆历史,适合更复杂的记忆检索场景。- 你也可以通过实现
ChatMemory接口,将其连接到Redis、MySQL或MongoDB,实现持久化和跨实例共享。
Advisor 可以理解为“顾问”或“拦截器”。它在消息流经ChatClient的路径上起作用,可以在消息发送给模型前(before)或模型返回结果后(after)执行一些逻辑。实现多轮对话记忆的关键是一个特殊的Advisor——ChatMemoryAdvisor。它的工作流程非常清晰:
- 当用户发送一条新消息时,
ChatMemoryAdvisor会先根据会话ID(比如用户ID或房间号)从ChatMemory中加载出之前的对话历史。 - 它将这段历史记录和用户的新消息组合成一个新的、包含上下文的提示(Prompt),然后发给大模型。
- 大模型回复后,
ChatMemoryAdvisor再将这一轮新的问答对(用户消息和AI回复)保存回ChatMemory,供下次对话使用。
为了更直观地展示这几个组件的关系,我们可以看下面这个简化的对比表格:
| 组件 | 角色类比 | 核心职责 | 常用实现/配置 |
|---|---|---|---|
| ChatClient | 总调度员/客户端 | 提供统一的API进行链式调用,集成各种功能组件。 | 通过Builder构造,或注入默认Bean。 |
| ChatMemory | 记忆仓库 | 存储和检索指定会话的历史消息。 | InMemoryChatMemory (开发)、RedisChatMemory (生产)。 |
| ChatMemoryAdvisor | 记忆管家 | 自动在对话前后管理ChatMemory的读取和写入。 | 构造时需传入一个ChatMemory实例。 |
| Model (Dashscope) | 大脑 | 接收包含历史的Prompt,生成智能回复。 | 在application.yml中配置api-key和model。 |
理解了这套机制,我们就能明白,所谓的“记忆”,本质上就是在每次对话时,动态地为大模型提供它“应该知道”的过往信息。而ChatMemoryAdvisor就是这个过程的自动化管家。
3. 代码实战:构建带记忆的聊天服务
理论说得差不多了,现在动手写代码。我们会创建一个ChatService,它对外提供一个简单的聊天接口,内部则完整实现带记忆的多轮对话逻辑。
首先,我们需要配置一个ChatMemory的Bean。为了演示的简单和可重现性,这里先使用内存存储。在生产环境中,你肯定会替换成Redis之类的持久化方案。
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatConfig {
@Bean
public ChatMemory chatMemory() {
// InMemoryChatMemory 适合开发和测试
// 参数 10 表示每个会话最多保留多少轮对话记录(防止上下文过长)
return new InMemoryChatMemory(10);
}
}
接下来是重头戏——ChatService。我将分步解释关键代码。
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@Service
@Slf4j
public class ChatRoomService {
private final ChatClient chatClient;
@Autowired
public ChatRoomService(ChatModel chatModel, ChatMemory chatMemory) {
// 1. 构建ChatClient,并为其装备上“记忆顾问”
AbstractChatMemoryAdvisor memoryAdvisor = new AbstractChatMemoryAdvisor(chatMemory) {};
this.chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(memoryAdvisor) // 将记忆顾问设置为默认
.defaultSystem("你是一个乐于助人且知识渊博的AI助手。") // 设置系统指令,定义AI角色
.build();
}
public String chat(String userMessage, String sessionId) {
// 2. 执行链式调用,关键是指定本次对话的会话ID
String aiResponse = chatClient.prompt()
.user(userMessage)
.advisors(a -> a.param("chatMemory.overrideChatId", sessionId)) // 绑定会话ID
.call()
.content();
log.info("会话 [{}] 的对话记录已更新。用户说:{},AI回复:{}", sessionId, userMessage, aiResponse);
return aiResponse;
}
}
让我拆解一下这段代码的要点:
- 构造注入:在构造函数中,我们注入了
ChatModel(由Spring AI Alibaba Starter自动配置好的灵积模型)和我们自己定义的ChatMemoryBean。 - 装备Advisor:我们创建了一个
AbstractChatMemoryAdvisor实例,它需要ChatMemory来工作。然后通过ChatClient.builder()创建客户端时,使用.defaultAdvisors()方法将这个记忆顾问装配上去。这样,所有通过这个ChatClient发起的对话都会自动享有记忆功能。 - 设置系统指令:
.defaultSystem()方法用于设置系统级别的提示词。这个指令会在每次对话中潜移默化地影响AI的行为模式。比如这里设定为“乐于助人且知识渊博的助手”,那么AI的回复风格就会更倾向于服务型。 - 会话ID是关键:在
chat方法中,最精妙的一步是.advisors(a -> a.param("chatMemory.overrideChatId", sessionId))。这行代码告诉ChatMemoryAdvisor:“请使用我提供的这个sessionId来存取记忆”。这个sessionId可以是用户的唯一标识符(如UserID),也可以是一个聊天室的房间号。正是通过不同的sessionId,我们实现了不同用户或不同聊天室之间的记忆隔离。 用户A和用户B的对话历史完全独立,互不干扰。 - 执行与记录:
.call().content()触发真正的模型调用并获取文本回复。之后,我们通过日志记录下这次交互,方便调试和监控。
最后,我们需要一个控制器(Controller)来暴露HTTP API。
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatRoomService chatRoomService;
public ChatController(ChatRoomService chatRoomService) {
this.chatRoomService = chatRoomService;
}
@PostMapping
public ChatResponse chat(@RequestBody ChatRequest request) {
String response = chatRoomService.chat(request.getMessage(), request.getSessionId());
return new ChatResponse(response);
}
// 简单的请求/响应DTO
public static class ChatRequest {
private String message;
private String sessionId;
// getters and setters ...
}
public static class ChatResponse {
private String reply;
// constructor, getters ...
}
}
现在,一个具备基础记忆功能的AI聊天后端就完成了。你可以用Postman或curl进行测试,在请求体中传递不同的sessionId和message,观察AI是否能根据同一sessionId下的历史进行连贯对话。
4. 功能增强与生产级考量
上面的代码是一个可运行的MVP(最小可行产品)。但要投入实际使用,无论是作为内部工具还是面向用户的服务,我们还需要考虑更多。
4.1 记忆存储的持久化与扩展
内存存储InMemoryChatMemory只是个开始。生产环境需要持久化,并且可能面临多实例部署。Spring AI的设计允许我们轻松替换ChatMemory的实现。例如,集成Redis:
- 添加Redis依赖:
spring-boot-starter-data-redis。 - 创建一个
RedisChatMemory类(可能需要自己实现ChatMemory接口,或寻找社区starter),利用Redis的List或Sorted Set结构,以sessionId为key存储消息列表。 - 将
ChatConfig中的Bean定义改为返回你的RedisChatMemory实例。
这样,即使应用重启或扩容,用户的对话记忆也不会丢失,并且所有服务实例都能访问到同一份记忆。
4.2 上下文长度管理与记忆窗口
大模型有上下文窗口限制(例如,8K、32K、128K tokens)。我们不能无限制地保存所有历史记录。InMemoryChatMemory(10)中的参数10就是一个简单的“记忆窗口”大小,它只保留最近10轮对话。
更高级的策略可以是:
- 摘要式记忆:当对话轮数超过一定阈值,用一个单独的提示词让AI对之前的漫长对话进行总结,然后用这个总结摘要替代旧的历史记录,再继续新对话。
- 重要性筛选:尝试只保留那些可能对未来对话至关重要的历史消息(这需要更复杂的逻辑或另一个AI调用来判断)。
在ChatMemoryAdvisor中,我们可以通过重写相关方法来实现这些自定义的记忆修剪策略。
4.3 为聊天室引入更多Advisor
Advisor的威力不止于记忆。我们可以创建多个Advisor来完成不同任务,形成一个处理管道(Pipeline)。例如:
- 内容安全顾问(SafetyAdvisor):在消息发送给模型前,检查用户输入是否包含违规、敏感或恶意内容。这既是对用户的保护,也是对模型提供商的合规要求。阿里灵积本身可能有内容安全API,我们可以在
Advisor中先行调用。 - 日志与监控顾问(LoggingAdvisor):详细记录每一条请求和响应的元数据(耗时、token使用量、sessionId等),方便后续进行用量分析、成本核算和问题排查。
- 流式输出顾问:如果需要支持SSE(Server-Sent Events)实现打字机效果的流式回复,可以通过定制
Advisor在after阶段处理流式响应。
装配多个Advisor很简单:
this.chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(memoryAdvisor, safetyAdvisor, loggingAdvisor) // 按顺序执行
.build();
4.4 处理异常与优化性能 网络调用、模型服务都可能出错。我们需要健壮的异常处理。
public String chat(String userMessage, String sessionId) {
try {
return chatClient.prompt()
.user(userMessage)
.advisors(a -> a.param("chatMemory.overrideChatId", sessionId))
.call()
.content();
} catch (Exception e) {
log.error("调用AI模型失败,会话ID: {}, 用户消息: {}", sessionId, userMessage, e);
// 返回友好的错误信息,避免暴露内部细节
return "抱歉,AI助手暂时无法响应,请稍后再试。";
}
}
性能方面,对于高频使用的聊天室,可以考虑:
- 连接池:确保HTTP客户端(如底层使用的RestTemplate或WebClient)配置了合理的连接池,以复用连接。
- 超时设置:在
application.yml中为AI调用配置明确的连接超时和读取超时。 - 异步处理:如果业务允许,可以将
chat方法改为返回CompletableFuture<String>或使用Spring的@Async,避免长时间的网络IO阻塞Web容器线程。
我在实际项目中遇到过记忆混乱的问题,后来发现是sessionId生成逻辑在并发场景下有极小概率冲突。所以,确保sessionId的全局唯一性和稳定性非常重要,尤其是在分布式环境下。
更多推荐
所有评论(0)