Spring Boot + AI + DeepSeek 实战:构建高可用智能客服系统的架构设计与避坑指南
最近在做一个智能客服系统的升级项目,之前那套基于规则引擎的老系统实在是让人头疼。用户的问题稍微复杂点或者不在预设的规则库里,系统就直接“装死”或者答非所问,维护那成千上万条规则的成本高得吓人。正好 Spring AI 项目越来越成熟,DeepSeek 模型在中文场景下表现又很亮眼,就决定用 Spring Boot + Spring AI + DeepSeek 这套组合拳来重构。折腾了几个月,总算上线稳定运行了,这里把整个架构设计和踩过的坑梳理一下,希望能帮到有类似需求的同学。

1. 为什么必须换掉传统规则引擎?
先说说我们之前的痛点,这也是很多公司客服系统的通病:
- 长尾问题处理能力为零:规则引擎只能处理预设好的问题模板。比如,我们预设了“如何重置密码?”的规则,但用户问“我忘了登录口令怎么办?”,系统就识别不了。现实中的用户提问千奇百怪,穷举规则根本不可能。
- 多轮对话维护是噩梦:想实现一个简单的业务办理流程,比如“退换货”,需要引导用户提供订单号、问题描述等信息。用规则引擎实现,需要定义大量的状态和跳转逻辑,代码臃肿,改起来牵一发而动全身。
- 知识更新成本高:产品功能一变,客服话术和知识库就要更新,需要开发人员手动修改规则代码,响应慢,效率低。
- 扩展性差:想接入新的渠道(比如从网页扩展到微信小程序),或者增加新的智能功能(比如情感分析),在原有架构上改造非常困难。
所以,我们的核心目标就是利用大语言模型(LLM)的理解和生成能力,让系统能“听懂”用户的自然语言,并“聪明地”进行多轮交互,从根本上解决灵活性和扩展性问题。
2. 技术选型:为什么是 DeepSeek?
市面上可选的模型很多,我们重点对比了 DeepSeek、通义千问和文心一言在中文客服场景下的几个关键指标:
- 中文理解与生成能力:DeepSeek 在中文常识推理、上下文理解和指令跟随方面表现非常出色,特别是在处理客服场景中常见的口语化、省略句时,准确率明显更高。这是我们最看重的点。
- QPS(每秒查询率)与延迟:通过压力测试,在同等配置下,DeepSeek API 的响应 P95 延迟更稳定。对于客服系统,稳定的低延迟比绝对的高吞吐更重要。
- Token 成本:DeepSeek 的定价策略非常有竞争力。客服对话通常单轮交互不长,但并发量可能不小,长期来看成本优势明显。
- API 友好度与生态:DeepSeek 的 API 设计简洁,文档清晰,并且与 Spring AI 的适配工作已经比较完善,集成起来更顺畅。
综合来看,在预算和性能的平衡上,DeepSeek 成为了我们的首选。当然,在架构设计上,我们通过抽象层将模型调用隔离,未来切换模型也不会太痛苦。
3. 核心实现:三驾马车驱动智能对话
整个系统的核心可以概括为三个部分:模型调用层、对话管理层和异步响应层。
3.1 使用 Spring AI ChatClient 统一模型调用
Spring AI 提供了 ChatClient 这个抽象接口,让我们可以用几乎相同的方式调用不同的模型。封装 DeepSeek 的关键在于正确配置 ChatModel。
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import lombok.RequiredArgsConstructor;
@Configuration
@RequiredArgsConstructor
public class DeepSeekConfig {
private final DeepSeekProperties deepSeekProperties; // 从配置中心读取
@Bean
@Scope("prototype") // 重要!避免并发对话间的上下文污染
public ChatClient deepSeekChatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("你是一个专业、友善的客服助手。请用简洁清晰的中文回答用户问题。")
.defaultOptions(DeepSeekApi.ChatOptions.builder()
.temperature(0.7) // 创造性适中
.maxTokens(500) // 控制回复长度
.build())
.build();
}
@Bean
public ChatModel deepSeekChatModel(DeepSeekApi deepSeekApi) {
return new DeepSeekChatModel(deepSeekApi);
}
@Bean
public DeepSeekApi deepSeekApi() {
return new DeepSeekApi(deepSeekProperties.getApiKey());
}
}
这里将 ChatClient 的 Bean 作用域设为 prototype 非常重要。因为每个用户会话都是独立的,如果使用默认的单例,不同用户的对话历史会混在一起,导致严重的上下文混乱。
3.2 设计轻量级对话状态机
多轮对话的核心是状态管理。我们设计了一个基于枚举的轻量级状态机,而不是引入复杂的规则引擎。
public enum DialogState {
INITIAL, // 初始状态,等待用户输入
GREETING_RECEIVED, // 已问候
INTENT_IDENTIFIED, // 意图已识别(如:查询订单、退货)
AWAITING_PARAMETER, // 等待用户提供必要参数(如:订单号)
PARAMETER_CONFIRMATION, // 向用户确认参数
PROCESSING, // 正在调用后端服务处理
RESOLVED, // 问题已解决
ESCALATED_TO_HUMAN // 需转人工
// ... 其他业务状态
}
// 对话上下文对象,贯穿整个会话生命周期
@Data
public class DialogContext {
private String sessionId;
private String userId;
private DialogState currentState;
private List<Message> history; // 对话历史
private Map<String, Object> slots; // 槽位,用于存储收集到的参数(如订单号、手机号)
private Long lastActiveTime;
}
状态转移由专门的 DialogStateManager 来驱动,它根据当前状态、用户最新输入和模型识别的意图,决定下一个状态是什么,并触发相应的动作(比如调用知识库查询、请求用户确认等)。
3.3 实现带背压控制的异步响应
客服场景下,用户可能连续快速发送消息。如果每个请求都同步阻塞地等待模型返回(可能耗时2-10秒),会迅速耗尽服务器线程,导致服务雪崩。我们必须采用异步非阻塞的方式。
我们使用 Project Reactor 的 Mono/Flux 配合 Spring WebFlux 来实现响应式流,并加入简单的背压控制——当处理队列过长时,直接拒绝新请求,返回友好提示。
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestController
@RequestMapping("/api/chat")
@RequiredArgsConstructor
public class ChatController {
private final ChatClient chatClient;
private final DialogManager dialogManager;
private final RateLimiter rateLimiter; // 背压/限流器
@PostMapping("/stream")
public Flux<String> streamChat(@RequestBody UserRequest request) {
// 1. 背压检查:如果系统负载过高,快速失败
if (!rateLimiter.tryAcquire()) {
return Flux.just("当前咨询用户较多,请稍后再试。");
}
// 2. 异步处理:将耗时的LLM调用和业务逻辑放到弹性线程池中,不阻塞Netty事件循环线程
return Mono.fromCallable(() -> {
// 更新对话状态,生成最终的Prompt
Prompt finalPrompt = dialogManager.processRequest(request);
return finalPrompt;
})
.subscribeOn(Schedulers.boundedElastic()) // 使用有界弹性线程池,防止线程爆炸
.flatMapMany(prompt ->
// 3. 流式调用ChatClient,实现逐词输出
chatClient.prompt(prompt)
.stream()
.content()
)
.onErrorResume(e -> {
log.error("对话处理异常", e);
return Flux.just("系统开小差了,请稍后重试。");
});
}
}
这段代码的关键点:
Schedulers.boundedElastic():将阻塞操作(IO、复杂计算)转移到专门的线程池,避免阻塞响应式框架的核心线程。rateLimiter:基于令牌桶或信号量实现,保护下游的 DeepSeek API 和自身系统。onErrorResume:优雅降级,任何环节出错都给用户一个友好回复,而不是抛出晦涩的异常。
4. 生产环境必须考虑的三大问题
系统能跑起来只是第一步,要稳定上线,还得过下面几关。
4.1 对话日志的幂等性处理
网络可能不稳定,用户可能连续点击发送,客户端可能重试。这会导致完全相同的请求被处理多次,造成重复回答、重复执行操作(如创建工单)。我们的解决方案是为每条用户消息生成一个唯一 clientMsgId(由前端生成),并在服务端做去重。
@Service
public class ChatService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public Mono<String> handleMessage(ChatRequest request) {
String dedupeKey = "msg_dedup:" + request.getSessionId() + ":" + request.getClientMsgId();
// 使用Redis setnx 实现原子性判断
Boolean isNew = redisTemplate.opsForValue().setIfAbsent(dedupeKey, "1", Duration.ofSeconds(30));
if (Boolean.FALSE.equals(isNew)) {
// 重复请求,直接返回缓存的结果
return Mono.just((String) redisTemplate.opsForValue().get(dedupeKey + ":result"));
}
// ... 正常处理逻辑,处理完后将结果存入 cache
}
}
4.2 基于Hystrix的降级策略
虽然 DeepSeek API 很稳定,但总有万一。我们不能因为一个外部服务挂掉导致整个客服系统瘫痪。我们为 ChatClient 的调用包装了 Hystrix 命令。
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
@Service
public class FallbackChatService {
@HystrixCommand(fallbackMethod = "fallbackResponse")
public String getAiResponse(String userInput) {
// 正常调用 DeepSeek API
return chatClient.prompt(userInput).call().content();
}
// 降级方法
public String fallbackResponse(String userInput, Throwable t) {
log.warn("AI服务降级触发,输入:{}", userInput, t);
// 1. 首先尝试从本地高频QA缓存中获取答案
String cachedAnswer = localCache.get(matchQuestion(userInput));
if (cachedAnswer != null) {
return cachedAnswer;
}
// 2. 缓存未命中,返回引导语,引导用户使用自助服务或稍后重试
return "当前AI助手正在优化中,您可以通过【帮助中心】搜索常见问题,或直接描述您的问题,我们将为您转接人工客服。";
}
}
4.3 敏感词过滤的AOP实现
AI可能生成不受控的内容,必须在最终回复给用户前进行一道安全检查。我们用 Spring AOP 实现了一个全局过滤器。
@Aspect
@Component
@Slf4j
public class SensitiveWordAspect {
@Autowired
private SensitiveWordFilter filter;
// 环绕所有对外提供对话回复的方法
@Around("execution(* com.yourcompany..*Service.*getReply*(..))")
public Object filterSensitiveWords(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = joinPoint.proceed(); // 执行原方法,获取AI回复
if (result instanceof String) {
String originalText = (String) result;
String filteredText = filter.doFilter(originalText);
if (!originalText.equals(filteredText)) {
log.info("对话内容触发敏感词过滤,已替换。原内容片段:{}", originalText.substring(0, Math.min(50, originalText.length())));
}
return filteredText;
}
return result;
}
}
SensitiveWordFilter 内部可以使用 DFA 算法加载词库,实现高效匹配和替换(如替换为***)。
5. 避坑指南:那些我们踩过的“深坑”
5.1 模型冷启动超时 首次调用 DeepSeek API 或长时间无调用后的首次请求,耗时可能特别长(超过10秒),触发超时。我们的预热方案是在应用启动后,用一个后台线程定时(如每5分钟)发送一个简单的“你好”请求,保持连接活跃。同时,在健康检查接口中集成该预热逻辑。
5.2 上下文窗口溢出 DeepSeek 模型有上下文长度限制(如 128K tokens)。长时间的对话历史积累会耗尽窗口,导致模型“忘记”前面的内容。我们采用了智能裁剪算法:
- 优先保留最近 N 轮对话。
- 利用 Embedding 计算历史对话与当前问题的相关性,保留最相关的片段。
- 将超长的历史总结成一段摘要,放入上下文。这个摘要可以由另一个轻量级模型生成。
5.3 GPU资源监控(如果你部署了私有模型) 如果自己部署模型,监控至关重要。除了常规的 CPU、内存,要重点关注:
- GPU 利用率:持续高于80%可能意味着需要扩容。
- GPU 内存使用率:防止内存溢出导致服务崩溃。
- 每请求平均延迟和 P99延迟:监控模型推理性能的稳定性。 我们使用 Prometheus + Grafana 来采集和展示这些指标,并设置了相应的告警规则。
写在最后
这套基于 Spring Boot + Spring AI + DeepSeek 的智能客服系统上线后,意图识别的准确率提升了约40%,大部分常见问题都能得到满意解答,人工客服的压力减轻了不少。整个开发过程,让我们深刻体会到,用好AI不仅仅是调个API,更需要扎实的软件工程能力——合理的架构设计、稳定的异步处理、完善的降级熔断和监控体系,这些都是系统能稳定服务于生产环境的基石。
最后,留一个我们正在思考的开放性问题:如何设计一个有效的对话质量评估体系? 准确率、响应时间这些基础指标容易监控,但“回答是否真正解决了用户问题”、“话术是否友好自然”这类主观性很强的质量维度,该如何自动化或半自动化地评估?是结合用户反馈(点赞/点踩),还是引入另一个AI模型进行评价?很想听听大家的实践和想法。
更多推荐


所有评论(0)