恐怖黎明:终极版官方中文版
9.86G · 2025-11-13
兄弟们,搞大模型应用的,是不是都遇到过这个坎儿:你满怀激情地用 Spring AI 撸了个对话机器人,开始聊得还行,但多聊几句,它就把前面说过的话忘得一干二净,跟个“七秒记忆的金鱼”一样。用户体验直接拉胯。
这背后其实就是上下文管理的锅。大模型本身是无状态的 (Stateless),它不会自动记住你之前的对话。你每发一次请求,对它来说都是一次“全新的邂逅”。要想让它变得智能,能联系上下文,我们就必须手动把历史对话“喂”给它。
今天,我们就来聊聊如何用 Spring AI,结合一种我称之为 MCP (Model Context Protocol,大模型上下文协议) 的模式,构建一个能真正“记住”对话历史的智能应用。这篇文章不扯虚的,直接上代码,从零到一,保证你读完就能上手。
别被这个名词吓到,这不是什么官方标准,而是我从实践中总结出来的一套简单有效的设计思路。它的核心思想很简单:
这个协议层专门负责:
User: ..., AI: ...)拼接成一个完整的上下文提示 (Prompt)。听起来是不是很简单?说白了,就是把“聊天记录”的管理工作,从你的业务代码里解耦出来,让代码更清晰,也更容易扩展。
Maven 依赖 (pom.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) :
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY} # 强烈建议使用环境变量
options:
model: gpt-3.5-turbo
咱们先不管 Spring AI,先来实现 MCP 的核心——上下文管理器。这里我们用最简单的方式,直接存在内存里。用一个 Map 来存,key 是会话 ID (比如用户 ID),value 是这个会話的聊天记录 List<Message>。
Spring AI 提供了 Message 这个抽象,正好拿来用。它有好几种实现,比如 UserMessage 和 AssistantMessage,非常适合用来区分用户和 AI 的发言。
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 来接收用户的请求。关键点在于,我们需要一个 sessionId 来区分不同的用户对话。
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,你是什么模型?
第二次请求(注意,还是同一个 sessionId):
http://localhost:8080/chat?sessionId=user123&message=你还记得我叫什么名字吗?
成功了!AI “记住”了我们的对话。因为它在第二次回答时,看到的 Prompt 大概是这样的:
User: 你好,我叫Lander,你是什么模型?
AI: 你好 Lander!我是一个由 OpenAI 训练的大型语言模型。有什么可以帮助你的吗?
User: 你还记得我叫什么名字吗?
有了前面的对话作为“记忆”,它自然就能正确回答了。
我们上面实现的只是最基础的内存版上下文管理,但 MCP 模式的优势在于它的可扩展性。
更换存储介质:不想存在内存里?应用一重启就丢了。很简单,把 ConversationContextManager 的实现换成基于 Redis 或者数据库的。比如用 Redis 的 List 数据结构,每个 sessionId 对应一个 List,简直完美。
优化修剪策略:简单的保留最近N条,有时候会丢失重要的初始信息(比如用户最开始设定的角色)。你可以实现更智能的策略:
系统级指令 (System Prompt) :你可以在 getHistory 的时候,总是在列表的最前面插入一条 SystemMessage,比如 new SystemMessage("你是一个专业的 Java 开发助手")。这样可以给你的 AI 机器人设定一个全局的角色,让它的回答更专业。
今天我们通过一个简单的实战,掌握了用 Spring AI 构建多轮对话应用的核心技术。关键在于理解并实现 MCP(大模型上下文协议) 这个思路,将上下文管理从业务逻辑中解耦出来。
记住这三个核心步骤:存储、构建、修剪。掌握了它,你就能告别“金鱼记忆”,打造出真正智能、连贯的 AI 应用。
代码已经很简单了,但背后的思想才是最重要的。
9.86G · 2025-11-13
163M · 2025-11-13
209.37 MB · 2025-11-13