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 应用。

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

相关推荐
海兰15 分钟前
使用 Spring AI 打造企业级 RAG 知识库第二部分:AI 实战
java·人工智能·spring
历程里程碑33 分钟前
二叉树---二叉树的中序遍历
java·大数据·开发语言·elasticsearch·链表·搜索引擎·lua
小信丶1 小时前
Spring Cloud Stream EnableBinding注解详解:定义、应用场景与示例代码
java·spring boot·后端·spring
无限进步_1 小时前
【C++】验证回文字符串:高效算法详解与优化
java·开发语言·c++·git·算法·github·visual studio
亚历克斯神1 小时前
Spring Cloud 2026 架构演进
java·spring·微服务
七夜zippoe1 小时前
Spring Cloud与Dubbo架构哲学对决
java·spring cloud·架构·dubbo·配置中心
海派程序猿1 小时前
Spring Cloud Config拉取配置过慢导致服务启动延迟的优化技巧
java
阿维的博客日记1 小时前
为什么不逃逸代表不需要锁,JIT会直接删掉锁
java
William Dawson1 小时前
CAS的底层实现
java
九英里路1 小时前
cpp容器——string模拟实现
java·前端·数据结构·c++·算法·容器·字符串