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.ymlapplication.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里实现多轮对话的几个核心概念。这就像组装乐高,你得知道每块积木是干什么的,才能拼出想要的东西。这套机制的核心是 ChatClientChatMemoryAdvisor

ChatClient 是你的主要操作界面。相比直接调用底层的ChatModelChatClient提供了更高级、更流畅的链式调用(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。它的工作流程非常清晰:

  1. 当用户发送一条新消息时,ChatMemoryAdvisor会先根据会话ID(比如用户ID或房间号)从ChatMemory中加载出之前的对话历史。
  2. 它将这段历史记录和用户的新消息组合成一个新的、包含上下文的提示(Prompt),然后发给大模型。
  3. 大模型回复后,ChatMemoryAdvisor再将这一轮新的问答对(用户消息和AI回复)保存回ChatMemory,供下次对话使用。

为了更直观地展示这几个组件的关系,我们可以看下面这个简化的对比表格:

组件角色类比核心职责常用实现/配置
ChatClient总调度员/客户端提供统一的API进行链式调用,集成各种功能组件。通过Builder构造,或注入默认Bean。
ChatMemory记忆仓库存储和检索指定会话的历史消息。InMemoryChatMemory (开发)、RedisChatMemory (生产)。
ChatMemoryAdvisor记忆管家自动在对话前后管理ChatMemory的读取和写入。构造时需传入一个ChatMemory实例。
Model (Dashscope)大脑接收包含历史的Prompt,生成智能回复。application.yml中配置api-keymodel

理解了这套机制,我们就能明白,所谓的“记忆”,本质上就是在每次对话时,动态地为大模型提供它“应该知道”的过往信息。而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;
    }
}

让我拆解一下这段代码的要点:

  1. 构造注入:在构造函数中,我们注入了ChatModel(由Spring AI Alibaba Starter自动配置好的灵积模型)和我们自己定义的ChatMemory Bean。
  2. 装备Advisor:我们创建了一个AbstractChatMemoryAdvisor实例,它需要ChatMemory来工作。然后通过ChatClient.builder()创建客户端时,使用.defaultAdvisors()方法将这个记忆顾问装配上去。这样,所有通过这个ChatClient发起的对话都会自动享有记忆功能。
  3. 设置系统指令.defaultSystem()方法用于设置系统级别的提示词。这个指令会在每次对话中潜移默化地影响AI的行为模式。比如这里设定为“乐于助人且知识渊博的助手”,那么AI的回复风格就会更倾向于服务型。
  4. 会话ID是关键:在chat方法中,最精妙的一步是.advisors(a -> a.param("chatMemory.overrideChatId", sessionId))。这行代码告诉ChatMemoryAdvisor:“请使用我提供的这个sessionId来存取记忆”。这个sessionId可以是用户的唯一标识符(如UserID),也可以是一个聊天室的房间号。正是通过不同的sessionId,我们实现了不同用户或不同聊天室之间的记忆隔离。 用户A和用户B的对话历史完全独立,互不干扰。
  5. 执行与记录.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进行测试,在请求体中传递不同的sessionIdmessage,观察AI是否能根据同一sessionId下的历史进行连贯对话。

4. 功能增强与生产级考量

上面的代码是一个可运行的MVP(最小可行产品)。但要投入实际使用,无论是作为内部工具还是面向用户的服务,我们还需要考虑更多。

4.1 记忆存储的持久化与扩展 内存存储InMemoryChatMemory只是个开始。生产环境需要持久化,并且可能面临多实例部署。Spring AI的设计允许我们轻松替换ChatMemory的实现。例如,集成Redis:

  1. 添加Redis依赖:spring-boot-starter-data-redis
  2. 创建一个RedisChatMemory类(可能需要自己实现ChatMemory接口,或寻找社区starter),利用Redis的ListSorted Set结构,以sessionId为key存储消息列表。
  3. 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)实现打字机效果的流式回复,可以通过定制Advisorafter阶段处理流式响应。

装配多个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的全局唯一性和稳定性非常重要,尤其是在分布式环境下。

更多推荐