Spring AI 实战:构建一个“懂上下文”的智能对话机器人 (MCP 模式)

前言:为什么你的 AI 应用总是"金鱼记忆"?

兄弟们,搞大模型应用的,是不是都遇到过这个坎儿:你满怀激情地用 Spring AI 撸了个对话机器人,开始聊得还行,但多聊几句,它就把前面说过的话忘得一干二净,跟个"七秒记忆的金鱼"一样。用户体验直接拉胯。

这背后其实就是上下文管理的锅。大模型本身是无状态的 (Stateless),它不会自动记住你之前的对话。你每发一次请求,对它来说都是一次"全新的邂逅"。要想让它变得智能,能联系上下文,我们就必须手动把历史对话"喂"给它。

今天,我们就来聊聊如何用 Spring AI,结合一种我称之为 MCP (Model Context Protocol,大模型上下文协议) 的模式,构建一个能真正"记住"对话历史的智能应用。这篇文章不扯虚的,直接上代码,从零到一,保证你读完就能上手。

什么是 MCP (大模型上下文协议)?

别被这个名词吓到,这不是什么官方标准,而是我从实践中总结出来的一套简单有效的设计思路。它的核心思想很简单:

将与大模型交互的上下文管理,抽象成一个独立的、可插拔的协议层。

这个协议层专门负责:

  1. 存储 (Storage) :把每一轮的对话(用户提问、AI 回答)都存起来。存哪儿?内存、Redis、数据库... 随便你,只要方便取就行。
  2. 构建 (Construction) :在下一次向大模型提问时,从存储中捞出相关的历史对话,按照特定的格式(比如 User: ..., AI: ...)拼接成一个完整的上下文提示 (Prompt)。
  3. 修剪 (Pruning) :上下文不能无限长,不然 token 数量爆炸,钱包就得爆炸。所以得有策略地"修剪"历史记录,比如只保留最近的 N 轮对话,或者更复杂的总结式记忆。

听起来是不是很简单?说白了,就是把"聊天记录"的管理工作,从你的业务代码里解耦出来,让代码更清晰,也更容易扩展。

开干!环境准备

  • JDK 17+ :Spring Boot 3.x 的标配,不多说。
  • Spring Boot 3.2.x
  • Spring AI 0.8.0 (注意,Spring AI 版本迭代很快,核心思想不变,但具体 API 可能有微调)
  • 一个能用的大模型 API Key:比如 OpenAI, 或者国内都行。这里我们用 OpenAI 做演示。

Maven 依赖 (pom.xml) :

XML 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
        <version>0.8.0</version> 
    </dependency>
</dependencies>

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

配置文件 (application.yml) :

YAML 复制代码
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY} # 强烈建议使用环境变量
      options:
        model: gpt-3.5-turbo

代码实战:三步走

第一步:定义我们的"上下文管理器" (ContextManager)

咱们先不管 Spring AI,先来实现 MCP 的核心------上下文管理器。这里我们用最简单的方式,直接存在内存里。用一个 Map 来存,key 是会话 ID (比如用户 ID),value 是这个会話的聊天记录 List<Message>

Spring AI 提供了 Message 这个抽象,正好拿来用。它有好几种实现,比如 UserMessageAssistantMessage,非常适合用来区分用户和 AI 的发言。

Java 复制代码
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.messages.AssistantMessage;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

// 为了简单,我们把它做成一个 Bean
@Component
public class ConversationContextManager {

    // 使用线程安全的 Map 来存储多用户的会话
    private final Map<String, List<Message>> conversationHistory = new ConcurrentHashMap<>();

    // 定义一个最大历史记录数,防止上下文无限膨胀
    private static final int MAX_HISTORY_SIZE = 10;

    /**
     * 添加一条消息到指定会话
     * @param sessionId 会话ID
     * @param message 消息
     */
    public void addMessage(String sessionId, Message message) {
        // 如果会话不存在,就创建一个新的
        conversationHistory.computeIfAbsent(sessionId, k -> new CopyOnWriteArrayList<>());
        
        List<Message> messages = conversationHistory.get(sessionId);
        messages.add(message);

        // 实现上下文修剪 (Pruning) 策略
        if (messages.size() > MAX_HISTORY_SIZE) {
            // 简单粗暴:移除最早的一条记录
            messages.remove(0); 
        }
    }

    /**
     * 获取指定会话的完整历史记录
     * @param sessionId 会话ID
     * @return 消息列表
     */
    public List<Message> getHistory(String sessionId) {
        return conversationHistory.getOrDefault(sessionId, new CopyOnWriteArrayList<>());
    }

    /**
     * 清除会话历史
     * @param sessionId 会话ID
     */
    public void clearHistory(String sessionId) {
        conversationHistory.remove(sessionId);
    }
}

代码解读

  1. ConcurrentHashMapCopyOnWriteArrayList:为了应付多线程环境,直接用 JUC 包里的线程安全集合,省心。
  2. addMessage: 核心方法。每次有新的对话,不管是用户的还是 AI 的,都往里塞。同时,这里实现了最简单的"修剪"策略------超过10条就扔掉最旧的。在实际项目中,你可以换成更复杂的策略,比如基于 token 数计算,或者做一些总结性的压缩。
  3. getHistory: 从存储里把历史记录捞出来,准备"喂"给大模型。

第二步:改造我们的 Controller

接下来,写一个 Controller 来接收用户的请求。关键点在于,我们需要一个 sessionId 来区分不同的用户对话。

Java 复制代码
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@RestController
public class ChatController {

    private final ChatClient chatClient;
    private final ConversationContextManager contextManager;

    @Autowired
    public ChatController(ChatClient chatClient, ConversationContextManager contextManager) {
        this.chatClient = chatClient;
        this.contextManager = contextManager;
    }

    @GetMapping("/chat")
    public String chat(@RequestParam String sessionId, @RequestParam String message) {
        // 1. 构建 Prompt:将新消息和历史消息组合起来
        
        // 先把用户这次的提问存起来
        contextManager.addMessage(sessionId, new UserMessage(message));
        
        // 从 ContextManager 获取当前会话的完整历史
        List<Message> history = contextManager.getHistory(sessionId);

        // 创建 Prompt 对象
        Prompt prompt = new Prompt(history);

        // 2. 调用大模型
        String aiResponse = chatClient.call(prompt).getResult().getOutput().getContent();

        // 3. 将 AI 的回答也存入上下文,为下一次对话做准备
        contextManager.addMessage(sessionId, new AssistantMessage(aiResponse));

        return aiResponse;
    }
}

代码解读,这是整个流程的核心

  1. 注入 ChatClientConversationContextManagerChatClient 是 Spring AI 的核心,用来和 AI 模型交互。ConversationContextManager 就是我们上一步写的上下文管理器。

  2. 获取 sessionId:这里我们简单地通过 URL 参数传来。实际项目中,可以从用户登录的 Session、JWT Token 或者其他地方获取,保证每个用户的对话隔离。

  3. MCP 模式的体现

    • 存储contextManager.addMessage(...) 负责把用户的提问和 AI 的回答都记录下来。
    • 构建contextManager.getHistory(sessionId) 获取完整的历史记录,然后 new Prompt(history) 将其构建成一个可以发送给大模型的请求。这一步完美地将上下文"注入"了请求。
  4. 循环:用户提问 -> 存入上下文 -> 连同历史记录一起发给 AI -> AI 回答 -> 把 AI 回答也存入上下文 -> 等待下一次提问。一个完美的闭环形成了!

第三步:测试一下!

启动你的 Spring Boot 应用。然后打开浏览器或者 Postman,我们来模拟一次对话。

第一次请求

http://localhost:8080/chat?sessionId=user123&message=你好,我叫Lander,你是什么模型?

AI 可能返回:你好 Lander!我是一个由 OpenAI 训练的大型语言模型。有什么可以帮助你的吗?

第二次请求 (注意,还是同一个 sessionId):

http://localhost:8080/chat?sessionId=user123&message=你还记得我叫什么名字吗?

AI 应该会返回:当然记得,你叫 Lander。

成功了!AI "记住"了我们的对话。因为它在第二次回答时,看到的 Prompt 大概是这样的:

makefile 复制代码
User: 你好,我叫Lander,你是什么模型?
AI: 你好 Lander!我是一个由 OpenAI 训练的大型语言模型。有什么可以帮助你的吗?
User: 你还记得我叫什么名字吗?

有了前面的对话作为"记忆",它自然就能正确回答了。

还能怎么玩?进阶思考

我们上面实现的只是最基础的内存版上下文管理,但 MCP 模式的优势在于它的可扩展性

  1. 更换存储介质 :不想存在内存里?应用一重启就丢了。很简单,把 ConversationContextManager 的实现换成基于 Redis 或者数据库的。比如用 Redis 的 List 数据结构,每个 sessionId 对应一个 List,简直完美。

  2. 优化修剪策略:简单的保留最近N条,有时候会丢失重要的初始信息(比如用户最开始设定的角色)。你可以实现更智能的策略:

    • Token-based Pruning:计算历史对话的总 token 数,超过阈值就从最旧的开始删,直到满足要求。
    • Summarization Pruning:当历史记录很长时,可以调用一次 AI,让它把前面的对话"总结"一下,用一个简短的摘要替换掉多轮对话。这是一种"记忆压缩",成本更高,但效果更好。
  3. 系统级指令 (System Prompt) :你可以在 getHistory 的时候,总是在列表的最前面插入一条 SystemMessage,比如 new SystemMessage("你是一个专业的 Java 开发助手")。这样可以给你的 AI 机器人设定一个全局的角色,让它的回答更专业。

总结

今天我们通过一个简单的实战,掌握了用 Spring AI 构建多轮对话应用的核心技术。关键在于理解并实现 MCP(大模型上下文协议) 这个思路,将上下文管理从业务逻辑中解耦出来。

记住这三个核心步骤:存储、构建、修剪。掌握了它,你就能告别"金鱼记忆",打造出真正智能、连贯的 AI 应用。

代码已经很简单了,但背后的思想才是最重要的。

相关推荐
李贺梖梖3 小时前
Maven 设置项目编码,防止编译打包出现编码错误
java·maven
SimonKing3 小时前
继老乡鸡菜谱之后,真正的AI菜谱来了,告别今天吃什么的烦恼...
java·后端·程序员
假客套3 小时前
2025 FastExcel在Java的Maven项目的导出和导入,简单易上手,以下为完整示例
java·maven·fastexcel
有梦想的攻城狮3 小时前
Maven中的settings.xml文件配置详解
xml·java·maven·settings.xml
wearegogog1233 小时前
液压位置控制源代码实现与解析(C语言+MATLAB联合方案)
java·c语言·matlab
游坦之3 小时前
基于Java Swing的智能数据结构可视化系统 | 支持自然语言交互的AI算法助手
java·数据结构·交互
王嘉俊9254 小时前
设计模式--装饰器模式:动态扩展对象功能的优雅设计
java·设计模式·装饰器模式
循着风4 小时前
多种二分查找
java
努力也学不会java4 小时前
【Java并发】深入理解synchronized
java·开发语言·人工智能·juc