基于 Spring AI 构建带记忆、意图识别、安全边界的企业级对话中台
本章带你完成 阶段 1:「会话管家」企业级对话中台 项目(项目二): 基于 Spring AI + 通义千问 构建一个具备有状态会话管理、对话意图识别、企业级安全边界的企业级对话中台。
完成本章后的项目结构:
spring-ai-tutorial-root/ # 本课程示例根目录
└── windsurf-project/ # Spring AI 后端示例工程
├── pom.xml
└── src/
├── main/
│ ├── java/com/example/chatbot/
│ │ ├── AiChatbotApplication.java
│ │ ├── controller/
│ │ │ ├── ChatController.java # 基础聊天(第2章)
│ │ │ └── MemoryChatController.java # 带记忆聊天 ⭐(本章新增)
│ │ ├── dto/
│ │ │ ├── ChatRequest.java # 第2章
│ │ │ ├── ChatResponse.java # 第2章
│ │ │ ├── MemoryChatRequest.java # ⭐ 本章新增
│ │ │ └── MemoryChatResponse.java # ⭐ 本章新增
│ │ └── service/
│ │ └── ConversationService.java # ⭐ 本章新增
│ └── resources/
│ ├── application.yml
│ └── static/
│ ├── index.html
│ ├── app.js
│ ├── style.css
│ ├── demo.html
│ ├── demo-style.css
│ └── demo-script.js
└── test/
└── java/com/example/chatbot/knowledge/
├── Test01_SpringAIBasics.java
└── Test03_ChatMemory.java
前端页面建议继续放在 src/main/resources/static/ 下,后端启动后可直接访问 http://localhost:8080/index.html。
如果你的接口路径不同,请在 app.js 中把 API 地址改成你的实际接口(例如 /api/memory-chat)。
首先创建 ConversationService,负责管理所有用户的对话历史。
mkdir -p src/main/java/com/example/chatbot/service创建文件:src/main/java/com/example/chatbot/service/ConversationService.java
package com.example.chatbot.service;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 对话管理服务
*
* 负责管理所有用户的对话历史
*/
@Service
public class ConversationService {
/**
* 存储所有会话
* Key: sessionId
* Value: 对话历史
*/
private final Map<String, List<Message>> conversations = new ConcurrentHashMap<>();
/**
* 最大历史消息数(防止内存溢出)
*/
private static final int MAX_HISTORY_SIZE = 20;
/**
* 获取会话历史
*
* @param sessionId 会话ID
* @return 对话历史
*/
public List<Message> getHistory(String sessionId) {
return conversations.computeIfAbsent(sessionId, k -> new ArrayList<>());
}
/**
* 添加用户消息
*
* @param sessionId 会话ID
* @param message 用户消息
*/
public void addUserMessage(String sessionId, String message) {
List<Message> history = getHistory(sessionId);
history.add(new UserMessage(message));
// 限制历史长度
trimHistory(history);
}
/**
* 添加 AI 回复
*
* @param sessionId 会话ID
* @param message AI 回复
*/
public void addAssistantMessage(String sessionId, String message) {
List<Message> history = getHistory(sessionId);
history.add(new AssistantMessage(message));
// 限制历史长度
trimHistory(history);
}
/**
* 清空会话历史
*
* @param sessionId 会话ID
*/
public void clearHistory(String sessionId) {
conversations.remove(sessionId);
}
/**
* 获取会话数量
*
* @return 会话数量
*/
public int getSessionCount() {
return conversations.size();
}
/**
* 获取所有会话ID列表
*
* @return 会话ID列表
*/
public List<String> getAllSessionIds() {
return new ArrayList<>(conversations.keySet());
}
/**
* 获取会话的消息数量
*
* @param sessionId 会话ID
* @return 消息数量
*/
public int getMessageCount(String sessionId) {
List<Message> history = conversations.get(sessionId);
return history != null ? history.size() : 0;
}
/**
* 限制历史长度
*
* @param history 对话历史
*/
private void trimHistory(List<Message> history) {
if (history.size() > MAX_HISTORY_SIZE) {
// 保留最近的消息
int removeCount = history.size() - MAX_HISTORY_SIZE;
history.subList(0, removeCount).clear();
}
}
}ConcurrentHashMap 保证线程安全创建带会话ID的请求和响应类。
创建文件:src/main/java/com/example/chatbot/dto/MemoryChatRequest.java
package com.example.chatbot.dto;
/**
* 带记忆的聊天请求
*/
public record MemoryChatRequest(
/**
* 会话ID(用于区分不同用户)
*/
String sessionId,
/**
* 用户消息
*/
String message
) {
/**
* 验证方法
*/
public void validate() {
if (sessionId == null || sessionId.trim().isEmpty()) {
throw new IllegalArgumentException("会话ID不能为空");
}
if (message == null || message.trim().isEmpty()) {
throw new IllegalArgumentException("消息不能为空");
}
}
}创建文件:src/main/java/com/example/chatbot/dto/MemoryChatResponse.java
package com.example.chatbot.dto;
import java.time.LocalDateTime;
/**
* 带记忆的聊天响应
*/
public record MemoryChatResponse(
/**
* 会话ID
*/
String sessionId,
/**
* AI 回复内容
*/
String message,
/**
* 当前对话轮次
*/
int turnCount,
/**
* 响应时间戳
*/
LocalDateTime timestamp,
/**
* 是否成功
*/
boolean success,
/**
* 错误信息
*/
String error
) {
/**
* 成功响应
*/
public static MemoryChatResponse success(
String sessionId,
String message,
int turnCount
) {
return new MemoryChatResponse(
sessionId,
message,
turnCount,
LocalDateTime.now(),
true,
null
);
}
/**
* 失败响应
*/
public static MemoryChatResponse error(String errorMessage) {
return new MemoryChatResponse(
null,
null,
0,
LocalDateTime.now(),
false,
errorMessage
);
}
}
创建 MemoryChatController,提供完整的会话管理接口。
创建文件:src/main/java/com/example/chatbot/controller/MemoryChatController.java
package com.example.chatbot.controller;
import com.example.chatbot.dto.MemoryChatRequest;
import com.example.chatbot.dto.MemoryChatResponse;
import com.example.chatbot.service.ConversationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 带记忆的聊天控制器
*/
@RestController
@RequestMapping("/api/memory-chat")
public class MemoryChatController {
private static final Logger log = LoggerFactory.getLogger(MemoryChatController.class);
private final ChatModel chatModel;
private final ConversationService conversationService;
public MemoryChatController(
ChatModel chatModel,
ConversationService conversationService
) {
this.chatModel = chatModel;
this.conversationService = conversationService;
}
/**
* 健康检查
*
* GET /api/memory-chat/health
*/
@GetMapping("/health")
public String health() {
return "Memory Chat API is running! 🧠";
}
/**
* 带记忆的聊天接口
*
* POST /api/memory-chat
*/
@PostMapping
public MemoryChatResponse chat(@RequestBody MemoryChatRequest request) {
try {
// 验证请求
request.validate();
log.info("收到带记忆的聊天请求 [会话: {}]: {}",
request.sessionId(), request.message());
// 添加用户消息到历史
conversationService.addUserMessage(
request.sessionId(),
request.message()
);
// 获取完整历史
List<Message> history = conversationService.getHistory(request.sessionId());
// 调用 AI
Prompt prompt = new Prompt(history);
ChatResponse response = chatModel.call(prompt);
String aiMessage = response.getResult().getOutput().getText();
// 保存 AI 回复到历史
conversationService.addAssistantMessage(
request.sessionId(),
aiMessage
);
// 计算对话轮次
int turnCount = history.size() / 2;
log.info("AI 回复 [会话: {}, 轮次: {}]: {} 字符",
request.sessionId(), turnCount, aiMessage.length());
return MemoryChatResponse.success(
request.sessionId(),
aiMessage,
turnCount
);
} catch (IllegalArgumentException e) {
log.warn("参数验证失败: {}", e.getMessage());
return MemoryChatResponse.error("参数错误: " + e.getMessage());
} catch (Exception e) {
log.error("处理聊天请求时发生错误", e);
return MemoryChatResponse.error("服务器错误: " + e.getMessage());
}
}
/**
* 清空会话历史
*
* DELETE /api/memory-chat/{sessionId}
*/
@DeleteMapping("/{sessionId}")
public String clearHistory(@PathVariable String sessionId) {
conversationService.clearHistory(sessionId);
log.info("清空会话历史: {}", sessionId);
return "会话历史已清空";
}
/**
* 获取会话统计
*
* GET /api/memory-chat/stats
*/
@GetMapping("/stats")
public Map<String, Object> getStats() {
return Map.of(
"sessionCount", conversationService.getSessionCount(),
"timestamp", java.time.LocalDateTime.now()
);
}
/**
* 获取所有会话列表
*
* GET /api/memory-chat/sessions
*/
@GetMapping("/sessions")
public List<Map<String, Object>> getSessions() {
List<String> sessionIds = conversationService.getAllSessionIds();
return sessionIds.stream()
.map(sessionId -> Map.of(
"sessionId", (Object) sessionId,
"messageCount", conversationService.getMessageCount(sessionId),
"timestamp", java.time.LocalDateTime.now()
))
.collect(Collectors.toList());
}
/**
* 获取会话详情(历史消息)
*
* GET /api/memory-chat/session/{sessionId}
*/
@GetMapping("/session/{sessionId}")
public Map<String, Object> getSessionDetail(@PathVariable String sessionId) {
List<Message> history = conversationService.getHistory(sessionId);
// 转换消息格式
List<Map<String, Object>> messages = history.stream()
.map(msg -> {
String role = msg instanceof UserMessage ? "user" : "assistant";
String content = msg.getText();
return Map.<String, Object>of("role", role, "content", content);
})
.collect(Collectors.toList());
return Map.of(
"sessionId", sessionId,
"messages", messages,
"messageCount", history.size()
);
}
}POST /api/memory-chat - 发送消息(带记忆)GET /api/memory-chat/health - 健康检查GET /api/memory-chat/sessions - 获取所有会话列表GET /api/memory-chat/session/{sessionId} - 获取会话详情GET /api/memory-chat/stats - 获取统计信息DELETE /api/memory-chat/{sessionId} - 清空会话启动项目并测试所有功能。
mvn spring-boot:runcurl http://localhost:8080/api/memory-chat/health预期输出:
Memory Chat API is running! 🧠第1轮:告诉 AI 名字
curl -X POST http://localhost:8080/api/memory-chat \
-H "Content-Type: application/json" \
-d '{
"sessionId": "user-001",
"message": "我叫小明"
}'预期输出:
{
"sessionId": "user-001",
"message": "你好,小明!很高兴认识你...",
"turnCount": 1,
"timestamp": "2024-XX-XXTXX:XX:XX",
"success": true,
"error": null
}第2轮:询问名字(测试记忆)
curl -X POST http://localhost:8080/api/memory-chat \
-H "Content-Type: application/json" \
-d '{
"sessionId": "user-001",
"message": "我叫什么名字?"
}'预期输出:
{
"sessionId": "user-001",
"message": "你叫小明。",
"turnCount": 2,
"timestamp": "2024-XX-XXTXX:XX:XX",
"success": true,
"error": null
}这证明对话记忆功能正常工作,AI 能够记住之前的对话内容。
# 用户A的会话
curl -X POST http://localhost:8080/api/memory-chat \
-H "Content-Type: application/json" \
-d '{
"sessionId": "user-A",
"message": "我喜欢编程"
}'
# 用户B的会话
curl -X POST http://localhost:8080/api/memory-chat \
-H "Content-Type: application/json" \
-d '{
"sessionId": "user-B",
"message": "我喜欢音乐"
}'
# 用户A询问(应该记得编程)
curl -X POST http://localhost:8080/api/memory-chat \
-H "Content-Type: application/json" \
-d '{
"sessionId": "user-A",
"message": "我喜欢什么?"
}'预期:AI 回答"编程",不会混淆用户B的信息。
curl http://localhost:8080/api/memory-chat/sessions预期输出:
[
{
"sessionId": "user-001",
"messageCount": 4,
"timestamp": "2024-XX-XXTXX:XX:XX"
},
{
"sessionId": "user-A",
"messageCount": 2,
"timestamp": "2024-XX-XXTXX:XX:XX"
}
]curl http://localhost:8080/api/memory-chat/session/user-001预期输出:
{
"sessionId": "user-001",
"messageCount": 4,
"messages": [
{
"role": "user",
"content": "我叫小明"
},
{
"role": "assistant",
"content": "你好,小明!很高兴认识你..."
},
{
"role": "user",
"content": "我叫什么名字?"
},
{
"role": "assistant",
"content": "你叫小明。"
}
]
}# 清空会话
curl -X DELETE http://localhost:8080/api/memory-chat/user-001
# 再次询问名字(应该不记得了)
curl -X POST http://localhost:8080/api/memory-chat \
-H "Content-Type: application/json" \
-d '{
"sessionId": "user-001",
"message": "我叫什么名字?"
}'预期:AI 不记得名字了。
curl http://localhost:8080/api/memory-chat/stats预期输出:
{
"sessionCount": 2,
"timestamp": "2024-XX-XXTXX:XX:XX"
}你已掌握「会话管家」企业级对话中台的开发能力。 接下来,进入阶段 2:「流语」实时对话推送引擎, 学习如何实现打字机效果和 SSE 流式推送。
A:
A:可以使用:
A:
A: