最近在做一个智能客服系统的升级项目,之前的老系统用的是规则匹配,用户问得稍微复杂点就“听不懂”了,维护规则库也让人头大。正好Spring AI这个项目越来越成熟,就决定用它结合DeepSeek的模型来试试水,目标是打造一个响应快、听得懂、能聊下去的智能客服。整个过程踩了不少坑,也积累了一些心得,记录下来和大家分享。

智能客服系统架构示意图

1. 为什么选择 Spring AI + DeepSeek?

在做技术选型时,我们主要考虑了三个方向:直接用TensorFlow/PyTorch自研、用成熟的NLP平台API、以及Spring AI这类集成框架。

  • 自研模型:虽然灵活度高,但对我们Java团队来说,从数据标注、模型训练到服务部署,链路太长,维护成本高,且难以快速响应业务变化。
  • 通用NLP平台API:调用简单,但按量计费,长期来看成本不可控,且数据出域存在合规风险。
  • Spring AI + 特定模型:最终我们选择了这个组合。Spring AI提供了一套标准的、Spring风格的API(如ChatClient),让我们可以像调用本地服务一样使用AI能力,底层模型可以灵活切换(比如从DeepSeek换到其他兼容API的模型)。选择DeepSeek主要是看中它对中文语义的理解和生成能力相当不错,而且提供了稳定、性价比高的API服务,非常适合处理中文客服场景。

2. 核心实现:三层架构与对话管理

我们的系统核心分为三层:接入层、AI服务层和对话管理层。

接入层就是普通的Spring Boot Controller,负责接收用户请求。这里重点做了两件事:限流异步响应

  • 限流:用了Bucket4j,防止突发流量打垮服务或触发DeepSeek的API调用限制。
  • 异步响应:客服场景用户能容忍短暂等待,但不能阻塞线程。我们用CompletableFuture包装AI调用,立即返回一个“正在思考”的提示,等AI结果出来后再通过WebSocket或轮询推给前端。
@RestController
@RequestMapping("/api/chat")
public class ChatController {
    @Autowired
    private ChatService chatService;
    @Autowired
 private RateLimiterService rateLimiterService; // Bucket4j封装

    @PostMapping
    public CompletableFuture<ResponseEntity<ChatResponse>> chat(@RequestBody ChatRequest request, HttpServletRequest servletRequest) {
        // 1. 限流检查
        if (!rateLimiterService.tryConsume(servletRequest.getRemoteAddr())) {
            return CompletableFuture.completedFuture(ResponseEntity.status(429).body(ChatResponse.ofError("请求过于频繁,请稍后再试")));
        }

        // 2. 异步处理对话
        return chatService.processAsync(request.getSessionId(), request.getMessage())
                .thenApply(response -> ResponseEntity.ok(response))
                .exceptionally(e -> ResponseEntity.status(500).body(ChatResponse.ofError("系统繁忙,请稍后重试")));
    }
}

AI服务层的核心是封装ChatClient。Spring AI的ChatClient接口是门面,我们只需要配置好DeepSeek的API地址和密钥。

@Service
public class DeepSeekChatService implements ChatService {
    @Autowired
    private ChatClient chatClient;
    @Autowired
    private ConversationCache cache; // 用Caffeine缓存对话上下文

    @Override
    public CompletableFuture<ChatResponse> processAsync(String sessionId, String userMessage) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                // 1. 从缓存获取历史对话
                List<Message> conversationHistory = cache.get(sessionId);
                conversationHistory.add(new UserMessage(userMessage));

                // 2. 调用AI
                ChatResponse aiResponse = chatClient.call(new Prompt(conversationHistory));

                // 3. 更新缓存(只保留最近N轮,防止上下文过长)
                conversationHistory.add(new AssistantMessage(aiResponse.getContent()));
                cache.put(sessionId, trimConversationHistory(conversationHistory));

                // 4. 返回结果(可在这里加入敏感词过滤等后处理)
                return ChatResponse.ofSuccess(filterSensitiveWords(aiResponse.getContent()));
            } catch (Exception e) {
                log.error("AI处理失败, sessionId: {}", sessionId, e);
                // 这里可以加入降级策略,例如返回预设话术
                return ChatResponse.ofError("AI助手暂时无法响应,请稍后再试。");
            }
        });
    }
}

对话管理层是大脑,我们设计了一个简单的对话状态机。客服不是一问一答,而是有流程的。比如用户要“退货”,状态会从INIT(初始)进入QUERY_ORDER(查询订单)状态,客服AI会主动询问订单号。

我们用一个ConversationState类来封装状态,并用Redis或Caffeine缓存起来。状态机的转移逻辑可以放在Service里,根据AI的回复意图(可以通过在Prompt里要求模型返回结构化意图,或者用另一个小的分类模型来识别)和当前状态,决定下一个状态和回复。

// 简化的状态枚举和状态机逻辑
public enum DialogState {
    INIT, GREETING, QUERY_ORDER, HANDLE_COMPLAINT, CONFIRM_RESOLUTION, END;
}

@Service
public class DialogStateMachine {
    public DialogState nextState(DialogState currentState, String userIntent) {
        switch (currentState) {
            case INIT:
                if ("greeting".equals(userIntent)) return DialogState.GREETING;
                if ("refund".equals(userIntent)) return DialogState.QUERY_ORDER;
                break;
            case QUERY_ORDER:
                if ("provide_order_no".equals(userIntent)) return DialogState.HANDLE_COMPLAINT;
                // ... 其他转移逻辑
        }
        // 默认回到初始或结束状态
        return DialogState.END;
    }
}

3. 生产环境必须考虑的几点

系统能跑起来只是第一步,要上线还得过好几关。

压力测试:我们用JMeter模拟了用户并发提问。关键参数是设置合理的思考时间(Think Time),因为用户打字需要时间。测试目标是保证在预期QPS(比如100)下,P99响应时间在3秒以内。Spring AI的ChatClient本身是阻塞的,所以我们的异步封装和线程池配置就很重要。

模型热更新与降级:我们不能保证DeepSeek API永远稳定。所以配置了熔断器(如Resilience4j),当连续失败次数达到阈值就熔断,直接走降级逻辑(返回预设回复或转人工)。同时,我们在配置中心(如Nacos)里放了备用API的密钥和地址,必要时可以快速切换。

敏感信息过滤:AI可能会生成一些不合规的内容。我们在AI回复返回前,会经过一个敏感词过滤组件。这个组件内置了一个基础词库,同时会动态从管理后台拉取最新的敏感词规则进行匹配和替换。

日志与监控:所有AI请求和回复都必须脱敏后再打印日志。我们用了AOP切面,在ChatClient.call方法前后拦截,将请求和响应中的手机号、身份证号等信息替换为*。同时,关键指标如调用耗时、成功率、各意图分布等都通过Micrometer暴露给Prometheus进行监控。

4. 避坑指南:都是实战踩出来的

  1. API调用频率限制:DeepSeek等公有API都有频率限制。除了在网关和业务层做限流,更关键的是缓存。对于常见、标准的问题(如“营业时间”、“联系方式”),答案可以直接缓存,根本不用问AI。我们用Caffeine做本地缓存,Guava Cache也可以。
  2. 对话超时与上下文管理:用户可能聊到一半走了。我们为每个会话sessionId设置了30分钟的超时,超时后缓存内的对话历史会被清除,下次用户再来视为新会话。同时,上下文历史不能无限增长(会消耗大量Token且可能影响效果),我们只保留最近10轮对话。
  3. 异常处理的粒度:网络超时、API限额耗尽、模型返回内容格式错误……每种异常都要有不同的处理策略和告警级别。我们定义了一个统一的AiException异常体系,并在@ControllerAdvice中做精细化的处理,确保用户体验和问题可追溯。

系统监控仪表盘示意图

5. 总结与展望

通过Spring AI集成DeepSeek,我们确实比较快地构建了一个可用的智能客服核心,意图识别的准确率相比老系统有显著提升。Spring AI的抽象让我们不必过于关心底层模型的具体调用方式,开发体验比较顺畅。

当然,这个方案也有其边界。它严重依赖外部模型的性能和稳定性。未来,我们考虑在几个方向做深化:

  • 混合模型策略:简单问题用本地小模型(通过Spring AI集成ONNX Runtime运行的轻量模型)快速响应,复杂问题再fallback到DeepSeek大模型,平衡成本和体验。
  • 知识库增强:将产品文档、FAQ做成向量知识库(用Spring AI的VectorStore)。AI生成回答时,先检索相关知识片段作为参考,让回答更精准。
  • 多模态交互:这可能是下一个挑战。如何优化“语音+文本”的客服场景?比如,用户上传一张问题商品的图片,或者直接发送语音。这可能需要:
    • 前端进行语音识别(STT)转成文本再发送,或者后端集成语音识别服务。
    • 对于图片,可以先用CV模型识别物体和问题,将识别结果作为文本描述连同用户问题一起送给AI模型。
    • Spring AI目前对多模态的支持还在演进中,可能需要我们自己封装多模态模型的调用,或者等待社区更成熟的方案。

这次项目让我感受到,AI工程化和传统后端开发既有相通之处(比如高可用、缓存、限流),又有其特殊性(如提示词工程、上下文管理、模型评估)。希望这篇笔记对想尝试Spring AI的朋友有所帮助。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐