前言:为什么你的 AI 应用总是"金鱼记忆"?
兄弟们,搞大模型应用的,是不是都遇到过这个坎儿:你满怀激情地用 Spring AI 撸了个对话机器人,开始聊得还行,但多聊几句,它就把前面说过的话忘得一干二净,跟个"七秒记忆的金鱼"一样。用户体验直接拉胯。
这背后其实就是上下文管理的锅。大模型本身是无状态的 (Stateless),它不会自动记住你之前的对话。你每发一次请求,对它来说都是一次"全新的邂逅"。要想让它变得智能,能联系上下文,我们就必须手动把历史对话"喂"给它。
今天,我们就来聊聊如何用 Spring AI,结合一种我称之为 MCP (Model Context Protocol,大模型上下文协议) 的模式,构建一个能真正"记住"对话历史的智能应用。这篇文章不扯虚的,直接上代码,从零到一,保证你读完就能上手。
什么是 MCP (大模型上下文协议)?
别被这个名词吓到,这不是什么官方标准,而是我从实践中总结出来的一套简单有效的设计思路。它的核心思想很简单:
将与大模型交互的上下文管理,抽象成一个独立的、可插拔的协议层。
这个协议层专门负责:
- 存储 (Storage) :把每一轮的对话(用户提问、AI 回答)都存起来。存哪儿?内存、Redis、数据库... 随便你,只要方便取就行。
- 构建 (Construction) :在下一次向大模型提问时,从存储中捞出相关的历史对话,按照特定的格式(比如
User: ...
,AI: ...
)拼接成一个完整的上下文提示 (Prompt)。 - 修剪 (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
这个抽象,正好拿来用。它有好几种实现,比如 UserMessage
和 AssistantMessage
,非常适合用来区分用户和 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);
}
}
代码解读:
ConcurrentHashMap
和CopyOnWriteArrayList
:为了应付多线程环境,直接用 JUC 包里的线程安全集合,省心。addMessage
: 核心方法。每次有新的对话,不管是用户的还是 AI 的,都往里塞。同时,这里实现了最简单的"修剪"策略------超过10条就扔掉最旧的。在实际项目中,你可以换成更复杂的策略,比如基于 token 数计算,或者做一些总结性的压缩。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;
}
}
代码解读,这是整个流程的核心:
-
注入
ChatClient
和ConversationContextManager
:ChatClient
是 Spring AI 的核心,用来和 AI 模型交互。ConversationContextManager
就是我们上一步写的上下文管理器。 -
获取
sessionId
:这里我们简单地通过 URL 参数传来。实际项目中,可以从用户登录的 Session、JWT Token 或者其他地方获取,保证每个用户的对话隔离。 -
MCP 模式的体现:
- 存储 :
contextManager.addMessage(...)
负责把用户的提问和 AI 的回答都记录下来。 - 构建 :
contextManager.getHistory(sessionId)
获取完整的历史记录,然后new Prompt(history)
将其构建成一个可以发送给大模型的请求。这一步完美地将上下文"注入"了请求。
- 存储 :
-
循环:用户提问 -> 存入上下文 -> 连同历史记录一起发给 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 模式的优势在于它的可扩展性。
-
更换存储介质 :不想存在内存里?应用一重启就丢了。很简单,把
ConversationContextManager
的实现换成基于 Redis 或者数据库的。比如用 Redis 的 List 数据结构,每个sessionId
对应一个 List,简直完美。 -
优化修剪策略:简单的保留最近N条,有时候会丢失重要的初始信息(比如用户最开始设定的角色)。你可以实现更智能的策略:
- Token-based Pruning:计算历史对话的总 token 数,超过阈值就从最旧的开始删,直到满足要求。
- Summarization Pruning:当历史记录很长时,可以调用一次 AI,让它把前面的对话"总结"一下,用一个简短的摘要替换掉多轮对话。这是一种"记忆压缩",成本更高,但效果更好。
-
系统级指令 (System Prompt) :你可以在
getHistory
的时候,总是在列表的最前面插入一条SystemMessage
,比如new SystemMessage("你是一个专业的 Java 开发助手")
。这样可以给你的 AI 机器人设定一个全局的角色,让它的回答更专业。
总结
今天我们通过一个简单的实战,掌握了用 Spring AI 构建多轮对话应用的核心技术。关键在于理解并实现 MCP(大模型上下文协议) 这个思路,将上下文管理从业务逻辑中解耦出来。
记住这三个核心步骤:存储、构建、修剪。掌握了它,你就能告别"金鱼记忆",打造出真正智能、连贯的 AI 应用。
代码已经很简单了,但背后的思想才是最重要的。