← 返回上一页

第4章: 「会话管家」企业级对话中台

基于 Spring AI 构建带记忆、意图识别、安全边界的企业级对话中台

📖 本章目标:构建「会话管家」企业级对话中台

本章带你完成 阶段 1:「会话管家」企业级对话中台 项目(项目二): 基于 Spring AI + 通义千问 构建一个具备有状态会话管理、对话意图识别、企业级安全边界的企业级对话中台。

✅ 完成本项目你将掌握以下8大核心能力

  • 1、Spring AI 架构设计:掌握 ChatModel/ChatClient、Prompt、ChatResponse 核心抽象;使用 Spring Boot Starter 自动配置机制。
  • 2、RESTful 对话服务开发:构建符合 RESTful 规范的 HTTP API 服务,支持单轮/多轮对话、异常处理、统一响应格式。
  • 3、有状态会话管理系统:实现基于 ChatMemory 的对话记忆管理,支持会话 ID 隔离、历史消息自动摘要、Token 预算控制。
  • 4、对话意图精准识别:基于 Prompt 工程实现用户模糊咨询的多意图识别,支持上下文关联。
  • 5、企业级安全边界:实现 Prompt 注入检测与过滤、敏感词拦截、输出内容审核;建立权限白名单和调用频率限制。
  • 6、测试驱动开发(TDD):通过测试用例系统掌握模型调用、提示词组装、流式响应、错误处理等核心能力。
  • 7、数据复盘与优化:自动采集对话日志,分析高频未解决问题、用户满意度低的回复场景;输出优化建议。
  • 8、部署与运维上线:掌握 Spring Boot 应用打包(JAR)、容器化(Docker)、云服务部署;实现日志监控、性能告警。

🎯 前置要求

  • 已完成第1章:Spring AI 基础知识测试
  • 已完成第2章:项目一 · 聊天机器人
  • 已完成第3章:对话记忆机制测试
  • 理解对话记忆的原理

📁 项目结构更新

完成本章后的项目结构:

text
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)。

📝 Step 1:创建会话管理服务

首先创建 ConversationService,负责管理所有用户的对话历史。

1.1 创建 service 包

text
mkdir -p src/main/java/com/example/chatbot/service

1.2 创建 ConversationService

创建文件:src/main/java/com/example/chatbot/service/ConversationService.java

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 保证线程安全
  • 限制历史长度防止内存溢出(最多20条)
  • 为每个 sessionId 维护独立的历史
  • 提供增删查等完整的会话管理功能

📝 Step 2:更新 DTO 类

创建带会话ID的请求和响应类。

2.1 创建 MemoryChatRequest

创建文件:src/main/java/com/example/chatbot/dto/MemoryChatRequest.java

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("消息不能为空");
        }
    }
}

2.2 创建 MemoryChatResponse

创建文件:src/main/java/com/example/chatbot/dto/MemoryChatResponse.java

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
        );
    }
}

📝 Step 3:创建带记忆的控制器

创建 MemoryChatController,提供完整的会话管理接口。

3.1 创建 MemoryChatController

创建文件:src/main/java/com/example/chatbot/controller/MemoryChatController.java

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} - 清空会话

🧪 Step 4:使用 curl 测试

启动项目并测试所有功能。

4.1 启动项目

text
mvn spring-boot:run

4.2 健康检查

text
curl http://localhost:8080/api/memory-chat/health

预期输出:

text
Memory Chat API is running! 🧠

4.3 测试多轮对话

第1轮:告诉 AI 名字

text
curl -X POST http://localhost:8080/api/memory-chat \
  -H "Content-Type: application/json" \
  -d '{
    "sessionId": "user-001",
    "message": "我叫小明"
  }'

预期输出:

text
{
  "sessionId": "user-001",
  "message": "你好,小明!很高兴认识你...",
  "turnCount": 1,
  "timestamp": "2024-XX-XXTXX:XX:XX",
  "success": true,
  "error": null
}

第2轮:询问名字(测试记忆)

text
curl -X POST http://localhost:8080/api/memory-chat \
  -H "Content-Type: application/json" \
  -d '{
    "sessionId": "user-001",
    "message": "我叫什么名字?"
  }'

预期输出:

text
{
  "sessionId": "user-001",
  "message": "你叫小明。",
  "turnCount": 2,
  "timestamp": "2024-XX-XXTXX:XX:XX",
  "success": true,
  "error": null
}

✅ AI 记住了名字!

这证明对话记忆功能正常工作,AI 能够记住之前的对话内容。

4.4 测试会话隔离

text
# 用户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的信息。

4.5 获取会话列表

text
curl http://localhost:8080/api/memory-chat/sessions

预期输出:

text
[
  {
    "sessionId": "user-001",
    "messageCount": 4,
    "timestamp": "2024-XX-XXTXX:XX:XX"
  },
  {
    "sessionId": "user-A",
    "messageCount": 2,
    "timestamp": "2024-XX-XXTXX:XX:XX"
  }
]

4.6 获取会话详情

text
curl http://localhost:8080/api/memory-chat/session/user-001

预期输出:

text
{
  "sessionId": "user-001",
  "messageCount": 4,
  "messages": [
    {
      "role": "user",
      "content": "我叫小明"
    },
    {
      "role": "assistant",
      "content": "你好,小明!很高兴认识你..."
    },
    {
      "role": "user",
      "content": "我叫什么名字?"
    },
    {
      "role": "assistant",
      "content": "你叫小明。"
    }
  ]
}

4.7 清空会话历史

sql
# 清空会话
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 不记得名字了。

4.8 查看统计信息

text
curl http://localhost:8080/api/memory-chat/stats

预期输出:

text
{
  "sessionCount": 2,
  "timestamp": "2024-XX-XXTXX:XX:XX"
}

📝 本章小结

🎓 你完成了「会话管家」企业级对话中台的8大核心能力

  • 1、Spring AI 架构:掌握 ChatModel/ChatClient、Prompt、ChatResponse 核心抽象。
  • 2、RESTful 服务:构建符合 RESTful 规范的 HTTP API 服务。
  • 3、会话管理:实现基于 ChatMemory 的对话记忆管理,支持会话 ID 隔离。
  • 4、意图识别:基于 Prompt 工程实现用户模糊咨询的多意图识别。
  • 5、安全边界:实现 Prompt 注入检测、敏感词拦截、输出内容审核。
  • 6、TDD 开发:通过测试用例掌握模型调用、提示词组装、流式响应。
  • 7、数据复盘:自动采集对话日志,分析高频问题,输出优化建议。
  • 8、部署上线:掌握 Spring Boot 打包、容器化、云服务部署。

🚀 下一步学习

你已掌握「会话管家」企业级对话中台的开发能力。 接下来,进入阶段 2:「流语」实时对话推送引擎, 学习如何实现打字机效果和 SSE 流式推送。

💡 常见问题

Q1:为什么要限制历史长度?

A:

Q2:如何持久化会话?

A:可以使用:

Q3:如何处理并发?

A:

Q4:内存存储的限制?

A:

← 上一章 下一章 →