记得去年我们团队接手了一个电商平台的客服系统升级项目。原来的系统是基于关键词匹配的规则引擎,平时应付简单问题还行,但一到促销季就问题频出。用户问“我昨天买的衣服什么时候发货”,客服机器人能识别“发货”关键词,给出标准回答。但如果用户接着问“那能改地址吗”,系统就完全丢失了上下文,不知道“那”指的是什么,只能重新开始对话。更头疼的是,用户说“这个颜色不太喜欢”,系统根本理解不了“这个”指的是之前聊过的商品,意图识别准确率直接掉到60%以下,人工客服介入率飙升,运营成本大幅增加。

智能客服系统架构示意图

面对这些问题,我们决定用Spring AI来重构整个智能客服系统。经过几个月的迭代,系统终于稳定上线,今天我就把其中几个核心模块的实现思路和踩过的坑分享一下。

1. 对话状态管理:用Redis+状态机守住“记忆”

智能客服不像单次问答,它是有状态的。用户可能聊到一半去干别的,半小时后回来继续问,系统得记得之前说到哪了。我们最初用Session存对话状态,但集群环境下同步麻烦,而且用户量一大内存就扛不住。

后来我们改用 Redis + Spring State Machine 的方案,效果立竿见影。Redis做分布式缓存,保证任何一台服务节点都能读到相同的对话状态;Spring State Machine则把复杂的对话流程变成清晰的状态图,维护起来特别直观。

具体实现上,我们为每个会话(Session)定义了几个核心状态:INIT(初始)、WAITING_FOR_INTENT(等待识别意图)、PROCESSING(处理中)、WAITING_FOR_USER(等待用户输入)、COMPLETED(完成)。每次用户发来消息,状态机就驱动对话向下一个状态流转。

下面是我们定义状态机的配置代码:

@Configuration
@EnableStateMachine
public class DialogStateMachineConfig extends StateMachineConfigurerAdapter<String, String> {

    @Override
    public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
        states
            .withStates()
            .initial("INIT")
            .state("WAITING_FOR_INTENT")
            .state("PROCESSING")
            .state("WAITING_FOR_USER")
            .end("COMPLETED");
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
        transitions
            .withExternal()
            .source("INIT").target("WAITING_FOR_INTENT").event("USER_MESSAGE_RECEIVED")
            .and()
            .withExternal()
            .source("WAITING_FOR_INTENT").target("PROCESSING").event("INTENT_IDENTIFIED")
            .and()
            .withExternal()
            .source("PROCESSING").target("WAITING_FOR_USER").event("RESPONSE_READY")
            .and()
            .withExternal()
            .source("WAITING_FOR_USER").target("COMPLETED").event("USER_CONFIRMED");
    }
}

状态本身存在Redis里,我们封装了一个DialogSessionService

@Service
public class DialogSessionService {
    
    @Autowired
    private RedisTemplate<String, DialogContext> redisTemplate;
    
    // 保存或更新对话上下文
    public void saveContext(String sessionId, DialogContext context) {
        String key = "dialog:session:" + sessionId;
        // 设置30分钟过期,避免内存泄漏
        redisTemplate.opsForValue().set(key, context, 30, TimeUnit.MINUTES);
    }
    
    // 获取对话上下文
    public DialogContext getContext(String sessionId) {
        String key = "dialog:session:" + sessionId;
        return redisTemplate.opsForValue().get(key);
    }
}

这样设计后,对话状态清晰可控,而且Redis的持久化机制保证了即使服务重启,未完成的对话也能恢复。

2. 意图识别模块:规则与模型的“组合拳”

意图识别是智能客服的“大脑”。我们调研了两种方案:基于规则的匹配和基于BERT的深度学习模型。实际用下来发现,没有谁是最好的,关键看场景。

规则匹配 速度快、解释性强,适合处理明确、固定的问题。比如“退货流程”、“修改密码”这类标准问题,用正则表达式或者AC自动机就能快速匹配,响应时间在10毫秒以内。我们用了Drools规则引擎,把业务部门经常变化的规则做成可配置的。

BERT模型 理解能力强,能处理“我这个东西不太想要了”这种口语化、多样化的表达。但模型需要训练数据,部署资源要求也高。我们在关键场景(如售前咨询、投诉处理)用了微调的BERT模型,准确率能从70%提升到90%以上。

生产环境中,我们搞了个混合路由策略:先走规则引擎,如果置信度高于阈值(比如0.9)就直接返回;否则再走BERT模型。这样既保证了高频简单问题的响应速度,又兼顾了复杂语句的识别精度。

集成NLP服务(我们用的是国内一家云服务商)的Spring Boot配置如下:

@Configuration
public class NlpServiceConfig {
    
    @Value("${nlp.service.endpoint}")
    private String endpoint;
    
    @Value("${nlp.service.api-key}")
    private String apiKey;
    
    @Bean
    public RestTemplate nlpRestTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        
        // 设置连接超时和读取超时
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(5000); // 5秒连接超时
        factory.setReadTimeout(10000);   // 10秒读取超时
        
        restTemplate.setRequestFactory(factory);
        
        // 添加统一的请求头,比如认证信息
        restTemplate.getInterceptors().add((request, body, execution) -> {
            request.getHeaders().add("Authorization", "Bearer " + apiKey);
            return execution.execute(request, body);
        });
        
        return restTemplate;
    }
    
    @Bean
    public NlpService nlpService(RestTemplate nlpRestTemplate) {
        return new NlpServiceImpl(endpoint, nlpRestTemplate);
    }
}

3. 异步响应处理:别让用户干等着

客服系统经常要调用外部服务,比如查订单、查物流、调用NLP接口。如果全都同步处理,一个慢接口就会卡住整个线程。我们用 CompletableFuture 做了异步化改造,效果很明显。

核心思路是:主线程收到用户请求后,快速生成一个任务提交到线程池,然后立即返回(比如先回复“正在查询,请稍候”)。后台线程并行调用各个依赖服务,等所有结果都齐了,再组装最终回复推送给用户。

@Service
public class AsyncDialogService {
    
    @Autowired
    private ThreadPoolTaskExecutor dialogTaskExecutor;
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private LogisticsService logisticsService;
    
    @Autowired
    private NlpService nlpService;
    
    public CompletableFuture<DialogResponse> processUserMessage(UserMessage message) {
        return CompletableFuture.supplyAsync(() -> {
            // 1. 并行调用:识别意图
            CompletableFuture<Intent> intentFuture = CompletableFuture
                .supplyAsync(() -> nlpService.identifyIntent(message.getContent()), dialogTaskExecutor);
            
            // 2. 并行调用:如果消息里提到订单,查订单信息
            CompletableFuture<OrderInfo> orderFuture = CompletableFuture
                .supplyAsync(() -> extractOrderId(message.getContent())
                    .map(orderService::getOrderInfo)
                    .orElse(null), dialogTaskExecutor);
            
            // 3. 等所有并行任务完成
            return CompletableFuture.allOf(intentFuture, orderFuture)
                .thenApply(v -> {
                    try {
                        Intent intent = intentFuture.get();
                        OrderInfo orderInfo = orderFuture.get();
                        
                        // 4. 组装响应
                        return buildResponse(intent, orderInfo, message);
                    } catch (Exception e) {
                        throw new CompletionException(e);
                    }
                }).join();
        }, dialogTaskExecutor);
    }
    
    private Optional<String> extractOrderId(String content) {
        // 简单用正则提取订单号,实际会更复杂
        Pattern pattern = Pattern.compile("订单[::]?(\\w{10,})");
        Matcher matcher = pattern.matcher(content);
        if (matcher.find()) {
            return Optional.of(matcher.group(1));
        }
        return Optional.empty();
    }
}

这样改造后,95%的请求响应时间都控制在1秒以内,用户体验提升很明显。

4. 性能优化:从压测数据到参数调优

系统上线前,我们做了全面的压力测试。用JMeter模拟了不同并发用户数下的表现,这里分享一些关键数据:

  • 单机配置:4核8G,Spring Boot 2.7,JDK 11

  • 基准场景:简单问答(规则匹配)

    • 100并发:平均响应时间 45ms,错误率 0%
    • 500并发:平均响应时间 120ms,错误率 0.2%
    • 1000并发:平均响应时间 350ms,错误率 1.5%
  • 复杂场景:需要调用NLP服务+订单查询

    • 100并发:平均响应时间 280ms,错误率 0.5%
    • 500并发:平均响应时间 850ms,错误率 3.2%
    • 1000并发:系统开始出现超时,错误率升至8%

从数据可以看出,外部服务调用是主要瓶颈。针对这个问题,我们做了几件事:

连接池优化:默认的HikariCP配置不适合高并发,我们调整了关键参数:

spring:
  datasource:
    hikari:
      maximum-pool-size: 20           # 根据实际测试调整,不是越大越好
      minimum-idle: 5                 # 保持最小空闲连接
      connection-timeout: 30000       # 连接超时30秒
      idle-timeout: 600000            # 空闲连接10分钟后回收
      max-lifetime: 1800000           # 连接最大生命周期30分钟
      connection-test-query: SELECT 1 # MySQL健康检查语句

线程池优化:异步处理用的线程池也需要精心配置:

@Configuration
public class ThreadPoolConfig {
    
    @Bean("dialogTaskExecutor")
    public ThreadPoolTaskExecutor dialogTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数 = CPU核数 * 2
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2);
        // 最大线程数,根据业务特点设置
        executor.setMaxPoolSize(50);
        // 队列容量,太大了会耗内存,太小了容易触发拒绝策略
        executor.setQueueCapacity(1000);
        // 线程名前缀,方便日志追踪
        executor.setThreadNamePrefix("dialog-async-");
        // 拒绝策略:调用者线程直接执行,避免任务丢失
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

缓存优化:对于频繁查询且变化不大的数据(比如商品信息、常见问题库),我们加了多级缓存。先用本地Caffeine缓存,过期或没有的话再查Redis,最后才查数据库。这样大部分请求根本不用走到数据库。

性能监控仪表盘

5. 生产环境Checklist:监控与排错指南

系统上线只是开始,稳定运行才是关键。这是我们团队维护半年多总结的Checklist:

必须监控的Metrics

  1. 系统层面

    • 平均响应时间(P50、P95、P99):关注长尾请求
    • QPS(每秒查询数):了解系统负载
    • 错误率:特别是5xx错误
    • JVM内存使用率:避免GC问题
  2. 业务层面

    • 意图识别准确率:低于阈值要告警
    • 人工转接率:突然升高可能意味着模型出问题了
    • 会话超时率:用户是否经常聊到一半离开
  3. 外部依赖

    • NLP服务调用耗时和成功率
    • 数据库连接池使用情况
    • Redis缓存命中率

常见故障排查步骤

当收到告警时,我们一般按这个顺序排查:

  1. 确认现象:是单个用户问题还是全局问题?错误信息是什么?
  2. 检查依赖:NLP服务是否正常?数据库连接是否够用?
  3. 查看日志:搜索错误时间点的异常日志,特别是ERROR级别的
  4. 分析监控:看故障时间点的CPU、内存、响应时间曲线
  5. 回滚预案:如果最近有发布,考虑快速回滚到上一个稳定版本

有一次线上故障让我们印象深刻:突然大量用户投诉客服机器人答非所问。查监控发现意图识别服务响应时间从平均200ms飙升到5秒以上。进一步排查,原来是NLP服务提供商那边出了故障。幸好我们做了降级策略,自动切回了规则引擎模式,虽然体验降级了,但至少服务没完全挂掉。

写在最后

构建Spring AI智能客服系统,技术选型只是第一步,更重要的是根据业务场景做权衡和调整。规则引擎快但不够智能,深度学习模型聪明但成本高,混合方案往往是最实用的。异步化、缓存、连接池这些“基础设施”的优化,带来的性能提升可能比算法优化更明显。

这套系统上线后,我们的客服人力成本降低了40%,用户满意度还提升了15%。技术的价值,最终还是要体现在业务成果上。希望这些实践经验对你有帮助,少踩一些我们踩过的坑。

Logo

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

更多推荐