SpringAI智能客服实战:从零构建高可用AI对话系统

最近在做一个智能客服项目,从传统的规则引擎切换到了基于大语言模型的方案,技术栈选用了SpringAI。整个过程踩了不少坑,也积累了一些心得,今天就来分享一下如何从零开始,构建一个高可用的AI对话系统。

智能客服系统架构示意图

1. 背景痛点:为什么传统客服系统越来越吃力?

在开始技术实现之前,我们先聊聊为什么要做这个转型。我接触过不少基于规则引擎的客服系统,它们通常存在以下几个痛点:

硬编码流程的局限性:传统的客服系统大多采用if-else或者决策树的方式处理用户问题。当业务规则复杂时,代码会变得极其臃肿。每次新增一个业务场景,都需要开发人员手动添加规则,维护成本很高。

冷启动成本高:对于一个新业务,需要人工整理大量的问答对(QA Pair)和对话流程,这个过程耗时耗力。而且,用户的问题千变万化,很难通过有限的规则覆盖所有情况,导致意图识别(Intent Detection)准确率低。

响应慢且扩展性差:规则匹配通常涉及复杂的逻辑判断,在并发量高的时候容易成为性能瓶颈。系统难以平滑地扩展,用户体验大打折扣。

正是这些痛点,促使我们寻找更智能、更灵活的解决方案,也就是基于大语言模型(LLM)的智能客服。

2. 技术选型:SpringAI vs. Rasa/Dialogflow

确定方向后,接下来就是技术选型。我们主要对比了SpringAI、Rasa和Google的Dialogflow。

模型训练成本(Model Training Cost)

  • Rasa:它是一个开源框架,需要自己准备语料进行意图识别和实体抽取模型的训练。虽然灵活,但训练成本高,需要NLP专业知识,且模型效果严重依赖标注数据的质量。
  • Dialogflow:谷歌的云服务,提供了可视化的训练界面,降低了使用门槛。但它是闭源的,数据存储在云端,有 vendor lock-in(供应商锁定)的风险,且定制能力有限。
  • SpringAI:它本身不提供模型,而是一个集成层。我们可以轻松接入OpenAI GPT、Azure OpenAI或本地部署的模型(如Ollama)。最大的优势是“零训练”,直接利用大模型已有的强大语义理解能力,通过提示词工程(Prompt Engineering)来引导模型完成特定任务,省去了大量的训练工作和数据准备。

Java生态兼容性(Java Ecosystem Compatibility): 对于我们团队来说,这一点至关重要。我们的技术栈以Spring Boot为主。

  • SpringAI:天然是Spring家族的一员,与Spring Boot、Spring Security等组件无缝集成。配置简单,可以用熟悉的@Bean@Configuration方式来管理AI客户端,学习成本极低。
  • Rasa:核心是Python,虽然提供了HTTP API供Java调用,但需要维护两套技术栈,增加了系统复杂性和运维负担。
  • Dialogflow:通过SDK或API调用,与语言无关,但同样需要处理跨服务调用的问题。

综合来看,对于追求快速落地、且团队以Java为主的我们,SpringAI是性价比最高的选择。它让我们能继续在熟悉的Spring生态里,享受大模型带来的能力飞跃。

3. 核心实现:三步搭建智能对话引擎

选型确定后,我们开始动手实现。核心可以分为三步:集成大模型、定制提示词、管理对话状态。

3.1 使用SpringAI的ChatClient集成GPT模型

SpringAI抽象出了ChatClient接口,让切换不同的模型提供商变得非常容易。以下是一个基础的配置示例:

@Configuration
public class OpenAIConfig {

    @Value("${spring.ai.openai.api-key}")
    private String apiKey;

    @Bean
    public OpenAiChatClient openAiChatClient() {
        OpenAiApi openAiApi = new OpenAiApi("https://api.openai.com/v1", apiKey);
        return new OpenAiChatClient(openAiApi);
    }
}

在Service中,我们就可以直接注入ChatClient来调用:

@Service
public class CustomerService {

    private final ChatClient chatClient;

    public CustomerService(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    public String handleUserQuery(String userMessage) {
        // 基础调用
        Prompt prompt = new Prompt(new UserMessage(userMessage));
        ChatResponse response = chatClient.call(prompt);
        return response.getResult().getOutput().getContent();
    }
}

3.2 自定义PromptTemplate实现领域知识注入

直接让大模型回答,它可能不了解我们的具体业务。这时就需要通过提示词(Prompt)来注入领域知识(Domain Knowledge)。SpringAI提供了强大的PromptTemplate

例如,我们要做一个电商客服,可以这样设计提示词:

@Service
public class CustomerService {

    private final ChatClient chatClient;
    private final PromptTemplate promptTemplate;

    public CustomerService(ChatClient chatClient) {
        this.chatClient = chatClient;
        // 定义提示词模板, {context}和{question}是占位符
        this.promptTemplate = new PromptTemplate("""
            你是一个专业的{company}电商客服助手。请根据以下已知信息,用中文简洁、专业地回答用户的问题。
            如果无法从已知信息中得到答案,请明确告知用户“根据现有信息,我暂时无法回答这个问题,建议您联系人工客服”。
            
            已知公司信息:
            {companyContext}
            
            当前用户问题:
            {userQuestion}
            """);
    }

    public String handleQuery(String userQuestion) {
        // 从数据库或配置中加载公司特定的上下文信息
        String companyContext = loadCompanyContext();
        
        // 构建消息,填充模板
        Message message = promptTemplate.createMessage(Map.of(
            "company", "某电商平台",
            "companyContext", companyContext,
            "userQuestion", userQuestion
        ));
        
        Prompt prompt = new Prompt(message);
        ChatResponse response = chatClient.call(prompt);
        return response.getResult().getOutput().getContent();
    }
}

通过这种方式,我们将产品目录、退货政策、活动规则等知识结构化地提供给模型,极大地提升了回答的准确性和专业性。

3.3 对话状态机设计(附UML状态图描述)

智能客服不是单轮问答,而是多轮对话(Multi-Turn Dialogue)。我们需要跟踪对话状态(Dialogue State)。这里我设计了一个简单的状态机(State Machine)。

核心状态

  • GREETING:欢迎状态,用户刚进入。
  • IDENTIFYING_INTENT:识别用户意图。
  • HANDLING_QUERY:处理具体查询(如查订单、退换货)。
  • COLLECTING_INFO:收集必要信息(如订单号、手机号)。
  • RESOLVING:解决问题中。
  • CONFIRMING:向用户确认解决方案。
  • CLOSED:对话结束。

对话状态机UML图

我们用枚举和实体类来实现:

public enum ConversationState {
    GREETING,
    IDENTIFYING_INTENT,
    HANDLING_QUERY,
    COLLECTING_INFO,
    RESOLVING,
    CONFIRMING,
    CLOSED
}

@Entity
public class ConversationSession {
    @Id
    private String sessionId;
    private String userId;
    @Enumerated(EnumType.STRING)
    private ConversationState currentState;
    @Lob
    private String contextHistory; // 可存储结构化或序列化的对话历史
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    // ... getters and setters
}

在每次处理用户消息时,服务层会根据当前状态、用户输入和模型的分析结果,决定状态如何转移,并更新会话上下文。

4. 代码示例:可复用的Spring Boot Starter配置

为了让项目更整洁,我将通用配置打包成了一个自定义的Spring Boot Starter。这里分享两个关键配置。

4.1 带重试机制的RestTemplate配置

调用外部AI服务API,网络不稳定是常事,必须加入重试机制。

@Configuration
public class AIClientConfig {

    @Bean
    public RestTemplate aiRestTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        
        // 添加重试拦截器
        HttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler(3, true);
        HttpClientBuilder clientBuilder = HttpClients.custom()
                .setRetryHandler(retryHandler);
        restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(clientBuilder.build()));
        
        // 设置通用超时
        SimpleClientHttpRequestFactory factory = (SimpleClientHttpRequestFactory) restTemplate.getRequestFactory();
        factory.setConnectTimeout(5000);
        factory.setReadTimeout(30000);
        
        return restTemplate;
    }
}

4.2 敏感词过滤拦截器代码(O(n)时间复杂度)

用户输入必须经过过滤,防止违规内容。这里使用Trie树(字典树)实现高效过滤。

@Component
public class SensitiveWordFilter {
    
    private final TrieNode root = new TrieNode();
    
    // 初始化敏感词库
    @PostConstruct
    public void init() {
        List<String> sensitiveWords = loadSensitiveWords(); // 从数据库或文件加载
        for (String word : sensitiveWords) {
            insertWord(word);
        }
    }
    
    private void insertWord(String word) {
        TrieNode node = root;
        for (char c : word.toCharArray()) {
            node.children.putIfAbsent(c, new TrieNode());
            node = node.children.get(c);
        }
        node.isEnd = true;
    }
    
    /**
     * 过滤文本中的敏感词,替换为*
     * 时间复杂度 O(n),n为文本长度
     */
    public String filter(String text) {
        if (text == null || text.isEmpty()) {
            return text;
        }
        char[] chars = text.toCharArray();
        StringBuilder result = new StringBuilder();
        int i = 0;
        while (i < chars.length) {
            TrieNode node = root;
            int j = i;
            int matchEnd = -1;
            // 检查从i开始的所有可能子串
            while (j < chars.length && node.children.containsKey(chars[j])) {
                node = node.children.get(chars[j]);
                j++;
                if (node.isEnd) {
                    matchEnd = j; // 记录最长匹配的结束位置
                }
            }
            if (matchEnd != -1) {
                // 发现敏感词,替换为*
                result.append("*".repeat(matchEnd - i));
                i = matchEnd;
            } else {
                result.append(chars[i]);
                i++;
            }
        }
        return result.toString();
    }
    
    private static class TrieNode {
        Map<Character, TrieNode> children = new HashMap<>();
        boolean isEnd = false;
    }
}

在Controller层或Service层调用filter()方法,即可对用户输入进行清洗。

5. 生产考量:稳定性与合规性

系统上线,稳定和合规是生命线。

5.1 使用Hystrix实现熔断降级

当AI服务响应过慢或不可用时,需要有备用方案。我们集成Hystrix。

@Service
public class CustomerService {
    
    private final ChatClient chatClient;
    private final FallbackResponseService fallbackService;
    
    @HystrixCommand(fallbackMethod = "handleQueryFallback",
                   commandProperties = {
                       @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000"),
                       @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10")
                   })
    public String handleQueryWithHystrix(String userQuestion) {
        return handleQuery(userQuestion); // 调用原有的AI处理方法
    }
    
    // 降级方法
    public String handleQueryFallback(String userQuestion, Throwable t) {
        log.warn("AI服务调用失败,启用降级策略。问题:{}", userQuestion, t);
        // 1. 返回预定义的通用话术
        // 2. 从本地知识库进行简单关键词匹配
        // 3. 引导用户使用其他渠道(如留言)
        return fallbackService.getFallbackResponse(userQuestion);
    }
}

5.2 对话日志的GDPR合规存储方案

用户对话数据属于个人数据,存储必须合规(如GDPR)。我们采取以下措施:

  1. 数据脱敏:在存储前,对日志中的姓名、手机号、邮箱、订单号等个人可识别信息(PII)进行掩码或哈希处理。
  2. 加密存储:日志数据库的磁盘加密必须开启,敏感字段建议应用层额外加密。
  3. 访问控制与审计:严格限制日志数据的访问权限,所有查询和导出操作必须有完整的审计日志。
  4. 设置保留期限:根据法律要求,制定明确的日志保留策略,到期自动删除。
@Entity
public class ConversationLog {
    @Id
    private String logId;
    private String sessionId;
    @Column(columnDefinition = "TEXT")
    private String userInput; // 存储脱敏后的输入
    @Column(columnDefinition = "TEXT")
    private String aiResponse;
    private String userIdHash; // 存储用户ID的哈希值,而非原始ID
    private LocalDateTime timestamp;
    private boolean containsPii; // 标记是否包含过PII
    // ... getters and setters
}

6. 避坑指南:安全与多租户

6.1 避免Prompt注入攻击的3种方法

Prompt注入(Prompt Injection)是指用户通过精心构造的输入,让模型忽略你设定的指令,执行用户想要的操作。防御方法:

  1. 输入校验与清洗:在将用户输入放入Prompt前,进行严格的校验。例如,检查是否包含类似“忽略之前指令”的字符串,并对长度进行限制。
  2. 角色隔离与系统指令加固:在Prompt中明确、强硬地定义AI的角色和边界。可以将系统指令放在一个独立的、权重更高的消息中(如果模型支持)。例如:“你必须始终扮演客服角色,绝对不能执行任何与客服无关的操作。”
  3. 输出校验:对模型的输出也要进行检查。如果回复中出现了明显违背指令的内容(如生成了代码、执行了非客服操作),则触发二次处理或人工审核流程。

6.2 多租户场景下的会话隔离策略

一个系统为多个不同公司(租户)服务时,隔离是关键。

  1. 数据层面:所有与会话、知识库、配置相关的表,都必须包含tenant_id字段。在查询时,通过ThreadLocal或SecurityContext持有当前租户信息,并在DAO层自动附加tenant_id = ?条件。
  2. 配置层面:每个租户应有独立的AI模型配置(如API Key、Base URL)、Prompt模板和敏感词库。可以通过在配置类中根据当前租户动态加载@Bean来实现。
  3. 会话层面ConversationSessionsessionId可以设计为包含租户前缀(如TENANT_A_session_123),或者在会话对象中明确存储tenantId。确保一个租户的用户无法访问到另一个租户的会话历史。

7. 延伸思考:基于SpringAI Function Calling实现工单自动创建

SpringAI支持Function Calling,这让AI不仅能说,还能“做”。我们可以让客服AI在判断问题无法在线解决时,自动创建工单。

可行性分析

  1. 定义“创建工单”函数:我们创建一个createTicket方法,描述它的功能(如“为用户创建一个技术支持工单”)、所需参数(用户ID、问题描述、紧急程度)。
  2. 在Prompt中暴露函数:将函数描述提供给大模型。当用户说“我的订单一直没发货,需要人工介入”,模型可以理解意图,并决定调用createTicket函数。
  3. SpringAI处理调用:当模型返回一个函数调用请求时,SpringAI会回调我们本地注册的Java方法,执行真正的创建工单逻辑(如调用工单系统的API)。
  4. 增强用户体验:AI可以在创建工单后,将工单号、预计处理时间等信息流畅地组织成自然语言回复给用户。

这实现了从“智能问答”到“智能处理”的跨越,是提升客服系统自动化程度的关键一步。

本地快速启动模板

最后,附上一个简单的docker-compose.yml,可以快速拉起一个包含MySQL(存储会话和日志)和Redis(缓存会话状态)的本地开发环境。

version: '3.8'
services:
  mysql:
    image: mysql:8.0
    container_name: ai-customer-mysql
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: customer_ai
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    command: --default-authentication-plugin=mysql_native_password

  redis:
    image: redis:7-alpine
    container_name: ai-customer-redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

volumes:
  mysql_data:
  redis_data:

将项目配置文件中的数据库和Redis地址指向localhost:3306localhost:6379,运行docker-compose up -d,再启动Spring Boot应用,一个具备基础能力的智能客服后端就跑起来了。

这次从传统规则引擎切换到SpringAI智能客服的实践,让我们真切感受到了大模型带来的生产力变革。它并非完美,在成本控制和输出稳定性上仍需精细打磨,但其在理解力、灵活性和开发效率上的优势是革命性的。希望这篇笔记能为你带来一些启发。

Logo

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

更多推荐