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

智能客服系统架构示意图

1. 为什么必须换掉传统规则引擎?

先说说我们之前的痛点,这也是很多公司客服系统的通病:

  1. 长尾问题处理能力为零:规则引擎只能处理预设好的问题模板。比如,我们预设了“如何重置密码?”的规则,但用户问“我忘了登录口令怎么办?”,系统就识别不了。现实中的用户提问千奇百怪,穷举规则根本不可能。
  2. 多轮对话维护是噩梦:想实现一个简单的业务办理流程,比如“退换货”,需要引导用户提供订单号、问题描述等信息。用规则引擎实现,需要定义大量的状态和跳转逻辑,代码臃肿,改起来牵一发而动全身。
  3. 知识更新成本高:产品功能一变,客服话术和知识库就要更新,需要开发人员手动修改规则代码,响应慢,效率低。
  4. 扩展性差:想接入新的渠道(比如从网页扩展到微信小程序),或者增加新的智能功能(比如情感分析),在原有架构上改造非常困难。

所以,我们的核心目标就是利用大语言模型(LLM)的理解和生成能力,让系统能“听懂”用户的自然语言,并“聪明地”进行多轮交互,从根本上解决灵活性和扩展性问题。

2. 技术选型:为什么是 DeepSeek?

市面上可选的模型很多,我们重点对比了 DeepSeek、通义千问和文心一言在中文客服场景下的几个关键指标:

  1. 中文理解与生成能力:DeepSeek 在中文常识推理、上下文理解和指令跟随方面表现非常出色,特别是在处理客服场景中常见的口语化、省略句时,准确率明显更高。这是我们最看重的点。
  2. QPS(每秒查询率)与延迟:通过压力测试,在同等配置下,DeepSeek API 的响应 P95 延迟更稳定。对于客服系统,稳定的低延迟比绝对的高吞吐更重要。
  3. Token 成本:DeepSeek 的定价策略非常有竞争力。客服对话通常单轮交互不长,但并发量可能不小,长期来看成本优势明显。
  4. 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模型进行评价?很想听听大家的实践和想法。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐