最近在做一个智能客服项目客户反馈最集中的问题就是“机器人聊着聊着就忘了前面说过什么”。比如用户想订机票先问了“明天北京到上海的航班”接着问“下午的呢”机器人很可能就懵了因为它丢失了“北京到上海”这个关键上下文。这种多轮对话中的上下文断裂、意图漂移问题严重影响了用户体验。为了解决这个问题我们基于 Spring AI 框架进行了一次深度实战目标是构建一个能“记住”对话历史的智能客服系统。下面就把从架构设计到上线的完整过程和踩过的坑分享给大家。1. 为什么选择 Spring AI技术选型的思考在项目初期我们对比了几个主流方案Rasa功能强大NLU和对话管理一体但需要学习其特定的领域语言DSL对纯 Java 技术栈的团队来说集成和定制成本较高。Dialogflow (Google)云服务开箱即用但存在数据隐私、网络延迟问题且定制能力和成本控制较弱。Spring AI作为 Spring 生态的新成员它的最大优势是“原生”。对于已经深度使用 Spring Boot 的团队可以无缝集成用熟悉的Bean、Configuration来管理 AI 组件。它抽象了底层模型如 OpenAI、Azure OpenAI让我们能更专注于业务逻辑对话状态管理而非模型接口的适配。最终我们选择了 Spring AI核心考量是降低集成复杂度充分利用现有 Java 技术资产如 Redis、Spring Security让团队能快速上手和迭代。2. 核心架构三招解决上下文丢失我们的方案围绕三个核心展开对话状态管理、意图识别优化和上下文缓存。对话状态管理 (Dialogue State Management) 这是多轮对话的“大脑”。我们定义了一个ConversationSession对象它不仅保存原始的对话消息列表还额外维护了几个关键状态currentIntent当前识别出的用户意图如BOOK_FLIGHT,QUERY_REFUND。slots一个 Map用于填充意图所需的参数。例如BOOK_FLIGHT意图的 slots 可能包含{departureCity: 北京, arrivalCity: 上海, date: 2023-10-27}。step记录在多轮对话填充 slots 过程中的当前步骤。每次用户输入系统先更新slots和step再根据最新的完整状态生成给 AI 模型的 Prompt。这样AI 每次回复都基于完整的对话上下文和明确的任务状态。意图识别优化 单纯依赖大语言模型LLM做意图分类在特定业务场景下可能不够精确且成本高。我们的策略是“规则 模型”混合规则匹配对于“你好”、“谢谢”等简单意图或“查订单号XXX”这种有固定模式的需求用正则表达式或关键词快速匹配直接返回速度快且准。模型分类对于复杂、模糊的表达才调用 Spring AI 的ChatClient通过精心设计的PromptTemplate让其进行意图分类。Prompt 里会提供例子Few-Shot Learning显著提升准确率。这套混合方案将意图识别准确率提升到了 98% 以上。上下文缓存与持久化 这是保证“记忆”不丢失的基础。我们使用Redis作为会话存储。每个会话有一个唯一sessionId通常由前端生成或基于用户ID创建。ConversationSession对象被序列化后以sessionId为 Key 存入 Redis。设置合理的 TTL如 30 分钟实现会话自动超时清理避免内存泄漏。使用 Redis 的分布式特性天然支持集群部署用户请求打到任何服务实例都能获取到正确的上下文。3. 手把手代码实现下面是一些关键代码片段基于 Spring Boot 3.x 和 Spring AI。1. 应用配置 (application.yml)spring: ai: openai: api-key: ${OPENAI_API_KEY} chat: options: model: gpt-3.5-turbo # 可根据精度和成本选择模型 temperature: 0.2 # 较低的温度值使输出更确定、更专注于业务 max-tokens: 500 # 限制单次响应长度控制成本 redis: host: localhost port: 6379 timeout: 2000ms # 会话缓存配置 conversation: ttl: 30m # 会话存活时间根据业务调整2. 自定义对话历史存储 (RedisConversationStore)这是实现持久化的核心。import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.metadata.ChatResponseMetadata; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.time.Duration; import java.util.ArrayList; import java.util.List; Component public class RedisConversationStore { private final RedisTemplateString, ConversationSession redisTemplate; private final Duration ttl; public RedisConversationStore(RedisTemplateString, ConversationSession redisTemplate, Value(${spring.redis.conversation.ttl}) Duration ttl) { this.redisTemplate redisTemplate; this.ttl ttl; } // 根据sessionId获取完整的会话状态 public ConversationSession getSession(String sessionId) { return redisTemplate.opsForValue().get(buildKey(sessionId)); } // 保存或更新整个会话状态 public void saveSession(String sessionId, ConversationSession session) { String key buildKey(sessionId); redisTemplate.opsForValue().set(key, session, ttl); // 设置TTL } // 向指定会话中添加一条消息并更新状态 public void addMessage(String sessionId, Message message, String recognizedIntent) { ConversationSession session getSession(sessionId); if (session null) { session new ConversationSession(sessionId); } session.addMessage(message); session.setCurrentIntent(recognizedIntent); // 此处可添加更复杂的slot填充逻辑 saveSession(sessionId, session); } private String buildKey(String sessionId) { return conversation: sessionId; } }3. 对话控制器 (ChatController)import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.PromptTemplate; import org.springframework.web.bind.annotation.*; import java.util.Map; RestController RequestMapping(/api/chat) public class ChatController { private final ChatClient chatClient; private final RedisConversationStore conversationStore; private final IntentRecognizer intentRecognizer; // 自定义的意图识别器混合策略 PostMapping(/{sessionId}) public ChatResponse chat(PathVariable String sessionId, RequestBody UserInput userInput) { // 1. 识别用户意图 String intent intentRecognizer.recognize(userInput.getContent()); // 2. 从Redis恢复或创建会话状态 ConversationSession session conversationStore.getSession(sessionId); if (session null) { session new ConversationSession(sessionId); } // 3. 更新会话状态填充slots等 session.updateWithUserInput(userInput.getContent(), intent); // 4. 构建包含上下文的Prompt // 这里从session中提取最近的N条消息作为上下文避免Prompt过长 ListMessage recentMessages session.getRecentMessages(10); String context convertMessagesToContext(recentMessages); PromptTemplate promptTemplate new PromptTemplate( 你是一个专业的客服助手。以下是之前的对话历史 {context} 当前用户的最新问题是{latestQuestion} 已知用户的意图是{intent} 请根据以上信息进行回复。 ); MapString, Object model Map.of( context, context, latestQuestion, userInput.getContent(), intent, intent ); Prompt prompt promptTemplate.create(model); // 5. 调用AI模型 ChatResponse response chatClient.call(prompt); // 6. 将AI的回复也存入会话历史 conversationStore.addMessage(sessionId, new AssistantMessage(response.getResult().getOutput().getContent()), intent); // 7. 返回响应 return response; } }4. 生产环境部署与考量系统上线前我们重点解决了以下几个问题性能压测 我们使用 JMeter 模拟了从 50 到 500 的并发用户。结果发现在并发 200 以下时平均响应时间稳定在 800ms 左右主要耗时在 LLM API 调用。超过 300 并发后由于 Redis 读写竞争和线程池排队响应时间曲线开始陡增。通过优化 Redis 连接池、调整 Web 服务器线程数并将部分非实时的意图识别改为异步处理最终将 300 并发的平均响应时间控制在 1.5 秒内。安全与合规日志脱敏所有对话日志在落盘前会通过正则规则过滤手机号、身份证号、邮箱等敏感信息替换为***。内容审核在将用户输入拼接到 Prompt 前会调用一个简单的关键词过滤服务拦截明显的不当内容。AI 的回复也会经过一次审核后再返回给用户。Token 成本控制监控每次对话的 Token 消耗对上下文长度进行裁剪只保留最近 N 轮并设置单日、单用户调用上限防止恶意刷取。5. 避坑指南三个典型问题内存溢出 (OOM)问题初期我们将所有会话的Message列表都保存在内存的Map里随着用户量增长很快导致 OOM。解决必须将会话状态外部化存储。引入 Redis 并设置 TTL 是根本解决方案。同时在内存中仅缓存最活跃的少量会话。线程阻塞导致响应慢问题ChatClient.call()是同步阻塞调用如果 LLM API 响应慢会迅速占满 Tomcat 线程池导致服务整体不可用。解决使用异步非阻塞模型。将ChatClient的调用封装到Async方法中或者使用 WebFlux 响应式编程。更简单的做法是调整线程池大小并设置合理的调用超时时间如 10秒超时后返回友好提示。上下文混乱 (Context Pollution)问题简单地将所有历史对话都塞进 Prompt会导致 Prompt 过长、成本激增并且可能让 AI 关注到过于久远的不相关信息。解决实现智能上下文窗口。不是存储所有消息而是只保留最近 5-10 轮对话。本轮识别出的intent所必需的slots信息。系统关键指令如用户身份。这样可以精炼上下文提升效果并降低成本。6. 总结与思考通过 Spring AI我们快速构建了一个上下文感知的智能客服系统。它的价值在于让 Java 开发者能用熟悉的方式拥抱 AI 能力将精力集中在业务逻辑和架构设计上。最后留一个开放问题供大家讨论在我们的实践中为了提升响应速度我们牺牲了一点意图识别的精度优先用规则匹配。在模型精度用更强大的 LLM 做意图分类和响应速度用规则或小模型之间你的业务场景是如何权衡的有没有更优雅的混合策略希望这篇笔记能对正在探索 Spring AI 或智能客服系统的你有所帮助。欢迎一起交流更多实战细节。
Spring AI智能客服多轮问答实战:从架构设计到生产环境部署
发布时间:2026/6/6 14:54:34
最近在做一个智能客服项目客户反馈最集中的问题就是“机器人聊着聊着就忘了前面说过什么”。比如用户想订机票先问了“明天北京到上海的航班”接着问“下午的呢”机器人很可能就懵了因为它丢失了“北京到上海”这个关键上下文。这种多轮对话中的上下文断裂、意图漂移问题严重影响了用户体验。为了解决这个问题我们基于 Spring AI 框架进行了一次深度实战目标是构建一个能“记住”对话历史的智能客服系统。下面就把从架构设计到上线的完整过程和踩过的坑分享给大家。1. 为什么选择 Spring AI技术选型的思考在项目初期我们对比了几个主流方案Rasa功能强大NLU和对话管理一体但需要学习其特定的领域语言DSL对纯 Java 技术栈的团队来说集成和定制成本较高。Dialogflow (Google)云服务开箱即用但存在数据隐私、网络延迟问题且定制能力和成本控制较弱。Spring AI作为 Spring 生态的新成员它的最大优势是“原生”。对于已经深度使用 Spring Boot 的团队可以无缝集成用熟悉的Bean、Configuration来管理 AI 组件。它抽象了底层模型如 OpenAI、Azure OpenAI让我们能更专注于业务逻辑对话状态管理而非模型接口的适配。最终我们选择了 Spring AI核心考量是降低集成复杂度充分利用现有 Java 技术资产如 Redis、Spring Security让团队能快速上手和迭代。2. 核心架构三招解决上下文丢失我们的方案围绕三个核心展开对话状态管理、意图识别优化和上下文缓存。对话状态管理 (Dialogue State Management) 这是多轮对话的“大脑”。我们定义了一个ConversationSession对象它不仅保存原始的对话消息列表还额外维护了几个关键状态currentIntent当前识别出的用户意图如BOOK_FLIGHT,QUERY_REFUND。slots一个 Map用于填充意图所需的参数。例如BOOK_FLIGHT意图的 slots 可能包含{departureCity: 北京, arrivalCity: 上海, date: 2023-10-27}。step记录在多轮对话填充 slots 过程中的当前步骤。每次用户输入系统先更新slots和step再根据最新的完整状态生成给 AI 模型的 Prompt。这样AI 每次回复都基于完整的对话上下文和明确的任务状态。意图识别优化 单纯依赖大语言模型LLM做意图分类在特定业务场景下可能不够精确且成本高。我们的策略是“规则 模型”混合规则匹配对于“你好”、“谢谢”等简单意图或“查订单号XXX”这种有固定模式的需求用正则表达式或关键词快速匹配直接返回速度快且准。模型分类对于复杂、模糊的表达才调用 Spring AI 的ChatClient通过精心设计的PromptTemplate让其进行意图分类。Prompt 里会提供例子Few-Shot Learning显著提升准确率。这套混合方案将意图识别准确率提升到了 98% 以上。上下文缓存与持久化 这是保证“记忆”不丢失的基础。我们使用Redis作为会话存储。每个会话有一个唯一sessionId通常由前端生成或基于用户ID创建。ConversationSession对象被序列化后以sessionId为 Key 存入 Redis。设置合理的 TTL如 30 分钟实现会话自动超时清理避免内存泄漏。使用 Redis 的分布式特性天然支持集群部署用户请求打到任何服务实例都能获取到正确的上下文。3. 手把手代码实现下面是一些关键代码片段基于 Spring Boot 3.x 和 Spring AI。1. 应用配置 (application.yml)spring: ai: openai: api-key: ${OPENAI_API_KEY} chat: options: model: gpt-3.5-turbo # 可根据精度和成本选择模型 temperature: 0.2 # 较低的温度值使输出更确定、更专注于业务 max-tokens: 500 # 限制单次响应长度控制成本 redis: host: localhost port: 6379 timeout: 2000ms # 会话缓存配置 conversation: ttl: 30m # 会话存活时间根据业务调整2. 自定义对话历史存储 (RedisConversationStore)这是实现持久化的核心。import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.metadata.ChatResponseMetadata; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.time.Duration; import java.util.ArrayList; import java.util.List; Component public class RedisConversationStore { private final RedisTemplateString, ConversationSession redisTemplate; private final Duration ttl; public RedisConversationStore(RedisTemplateString, ConversationSession redisTemplate, Value(${spring.redis.conversation.ttl}) Duration ttl) { this.redisTemplate redisTemplate; this.ttl ttl; } // 根据sessionId获取完整的会话状态 public ConversationSession getSession(String sessionId) { return redisTemplate.opsForValue().get(buildKey(sessionId)); } // 保存或更新整个会话状态 public void saveSession(String sessionId, ConversationSession session) { String key buildKey(sessionId); redisTemplate.opsForValue().set(key, session, ttl); // 设置TTL } // 向指定会话中添加一条消息并更新状态 public void addMessage(String sessionId, Message message, String recognizedIntent) { ConversationSession session getSession(sessionId); if (session null) { session new ConversationSession(sessionId); } session.addMessage(message); session.setCurrentIntent(recognizedIntent); // 此处可添加更复杂的slot填充逻辑 saveSession(sessionId, session); } private String buildKey(String sessionId) { return conversation: sessionId; } }3. 对话控制器 (ChatController)import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.PromptTemplate; import org.springframework.web.bind.annotation.*; import java.util.Map; RestController RequestMapping(/api/chat) public class ChatController { private final ChatClient chatClient; private final RedisConversationStore conversationStore; private final IntentRecognizer intentRecognizer; // 自定义的意图识别器混合策略 PostMapping(/{sessionId}) public ChatResponse chat(PathVariable String sessionId, RequestBody UserInput userInput) { // 1. 识别用户意图 String intent intentRecognizer.recognize(userInput.getContent()); // 2. 从Redis恢复或创建会话状态 ConversationSession session conversationStore.getSession(sessionId); if (session null) { session new ConversationSession(sessionId); } // 3. 更新会话状态填充slots等 session.updateWithUserInput(userInput.getContent(), intent); // 4. 构建包含上下文的Prompt // 这里从session中提取最近的N条消息作为上下文避免Prompt过长 ListMessage recentMessages session.getRecentMessages(10); String context convertMessagesToContext(recentMessages); PromptTemplate promptTemplate new PromptTemplate( 你是一个专业的客服助手。以下是之前的对话历史 {context} 当前用户的最新问题是{latestQuestion} 已知用户的意图是{intent} 请根据以上信息进行回复。 ); MapString, Object model Map.of( context, context, latestQuestion, userInput.getContent(), intent, intent ); Prompt prompt promptTemplate.create(model); // 5. 调用AI模型 ChatResponse response chatClient.call(prompt); // 6. 将AI的回复也存入会话历史 conversationStore.addMessage(sessionId, new AssistantMessage(response.getResult().getOutput().getContent()), intent); // 7. 返回响应 return response; } }4. 生产环境部署与考量系统上线前我们重点解决了以下几个问题性能压测 我们使用 JMeter 模拟了从 50 到 500 的并发用户。结果发现在并发 200 以下时平均响应时间稳定在 800ms 左右主要耗时在 LLM API 调用。超过 300 并发后由于 Redis 读写竞争和线程池排队响应时间曲线开始陡增。通过优化 Redis 连接池、调整 Web 服务器线程数并将部分非实时的意图识别改为异步处理最终将 300 并发的平均响应时间控制在 1.5 秒内。安全与合规日志脱敏所有对话日志在落盘前会通过正则规则过滤手机号、身份证号、邮箱等敏感信息替换为***。内容审核在将用户输入拼接到 Prompt 前会调用一个简单的关键词过滤服务拦截明显的不当内容。AI 的回复也会经过一次审核后再返回给用户。Token 成本控制监控每次对话的 Token 消耗对上下文长度进行裁剪只保留最近 N 轮并设置单日、单用户调用上限防止恶意刷取。5. 避坑指南三个典型问题内存溢出 (OOM)问题初期我们将所有会话的Message列表都保存在内存的Map里随着用户量增长很快导致 OOM。解决必须将会话状态外部化存储。引入 Redis 并设置 TTL 是根本解决方案。同时在内存中仅缓存最活跃的少量会话。线程阻塞导致响应慢问题ChatClient.call()是同步阻塞调用如果 LLM API 响应慢会迅速占满 Tomcat 线程池导致服务整体不可用。解决使用异步非阻塞模型。将ChatClient的调用封装到Async方法中或者使用 WebFlux 响应式编程。更简单的做法是调整线程池大小并设置合理的调用超时时间如 10秒超时后返回友好提示。上下文混乱 (Context Pollution)问题简单地将所有历史对话都塞进 Prompt会导致 Prompt 过长、成本激增并且可能让 AI 关注到过于久远的不相关信息。解决实现智能上下文窗口。不是存储所有消息而是只保留最近 5-10 轮对话。本轮识别出的intent所必需的slots信息。系统关键指令如用户身份。这样可以精炼上下文提升效果并降低成本。6. 总结与思考通过 Spring AI我们快速构建了一个上下文感知的智能客服系统。它的价值在于让 Java 开发者能用熟悉的方式拥抱 AI 能力将精力集中在业务逻辑和架构设计上。最后留一个开放问题供大家讨论在我们的实践中为了提升响应速度我们牺牲了一点意图识别的精度优先用规则匹配。在模型精度用更强大的 LLM 做意图分类和响应速度用规则或小模型之间你的业务场景是如何权衡的有没有更优雅的混合策略希望这篇笔记能对正在探索 Spring AI 或智能客服系统的你有所帮助。欢迎一起交流更多实战细节。