手把手用 Spring AI 做一个智能客服:意图识别 + 工具调用 + 人工无缝切换

0 阅读7分钟

Spring AI 实战系列 | 进阶篇 第 1 篇

智能客服系统:多轮对话 + 工具调用 + 人工兜底

系列说明:本文为《Spring AI 实战系列 进阶篇》第 1 篇

前置知识:完成入门篇第 2 篇(Tool Calling)和第 5 篇(Advisors)

预计阅读时间:14 分钟


💻 开发环境说明

重要说明:本文章是基于本地模型进行讲解,不依赖任何云端 API Key,真正零成本、可离线运行。(Ps但是电脑配置确实带不动“高智商”的大模型,后面项目源码是换了智谱的大模型去跑的)

项目配置
电脑型号MagicBook Pro 14 2025,14.6 吋
CPUIntel Core Ultra 5(MTL Ultra5)
核显Intel UMA
内存24GB
硬盘1TB SSD
本地模型Ollama + qwen2.5:7b(通义千问)
Embedding 模型qwen3-embedding:0.6b

为什么选择本地模型?

对比项云端 API(OpenAI 等)本地模型(Ollama)
费用按 Token 计费免费
网络需要联网完全离线可用
数据安全数据上传到第三方数据不出本地
响应速度依赖网络延迟本地推理,无网络延迟
适用场景生产环境、高并发开发测试、个人项目

模型下载命令

# 安装 Ollama(macOS/Linux)
curl -fsSL https://ollama.ai/install.sh | sh

# 下载 Chat 模型
ollama pull qwen2.5:7b

# 下载 Embedding 模型(用于 RAG)
ollama pull qwen3-embedding:0.6b

# 验证
ollama list

📖 目录

  1. 业务场景与核心挑战
  2. 系统架构设计
  3. 意图识别:AI 路由分发
  4. 多轮对话与工具调用
  5. 人工接管机制

一、业务场景与核心挑战

1.1 场景描述

搭建一个电商平台的智能客服系统,用户可以通过对话完成:

功能示例对话
订单查询"我的订单 B-12345 到哪里了?"
退换货"我想退货,订单号是 R-98765"
产品咨询"这款手机的电池容量是多少?"
投诉建议"你们的发货太慢了,我要投诉"
转人工"我要找人工客服"

1.2 核心挑战

挑战解决方案
用户意图多样意图识别 + 路由分发
需要查询实时数据Tool Calling 连接后端服务
多轮对话上下文ChatMemory + 会话状态
复杂问题处理不了满意度评分 → 人工接管
系统稳定性降级策略 + 异常处理

二、系统架构设计

2.1 整体架构

┌─────────────────────────────────────────────────────────────────┐
│                         前端 (Vue3)                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐ │
│  │  聊天界面    │  │  满意度评分  │  │  人工客服接入(WebSocket)│ │
│  └─────────────┘  └─────────────┘  └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                      Spring Boot 后端                            │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  IntentRouter(意图识别 + 路由分发)                      │   │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐   │   │
│  │  │ 订单查询  │ │ 退换货   │ │ 产品咨询 │ │ 转人工   │   │   │
│  │  └──────────┘ └──────────┘ └──────────┘ └──────────┘   │   │
│  └─────────────────────────────────────────────────────────┘   │
│                              ↓                                  │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  各场景 ChatClient(配置不同 System Prompt + Tools)      │   │
│  │  - OrderQueryClient(订单查询工具)                       │   │
│  │  - RefundClient(退换货工具)                             │   │
│  │  - ProductClient(知识库 RAG)                            │   │
│  │  - HumanHandoffClient(转人工逻辑)                       │   │
│  └─────────────────────────────────────────────────────────┘   │
│                              ↓                                  │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  SessionManager(会话状态管理)                           │   │
│  │  - ChatMemory 存储(Redis 分布式会话)                     │   │
│  │  - 满意度评分                                            │   │
│  │  - 人工接管标记                                          │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘
                              ↓
          ┌──────────────────┬──────────────────┐
          ↓                  ↓                  ↓
┌─────────────────┐  ┌──────────────┐  ┌─────────────────────────┐
│      Redis      │  │    MySQL    │  │     外部服务             │
│  用户登录会话     │  │  订单数据    │  │  库存/知识库/人工客服    │
│  ChatMemory缓存  │  │  退换货记录  │  │                        │
└─────────────────┘  └──────────────┘  └─────────────────────────┘

2.2 核心组件说明

组件职责技术选型
IntentRouter识别用户意图,路由到对应处理器结构化输出 + 策略模式
ChatClient 实例各场景独立的 AI 客户端Spring AI ChatClient
SessionManager管理多轮对话上下文Redis 分布式 ChatMemory
Tool 方法连接后端业务系统(MySQL 订单数据)@Tool 注解
HumanHandoffService监控满意度,触发人工接管WebSocket + MySQL 记录

三、意图识别:AI 路由分发

3.1 为什么需要意图识别?

用户的一句话可能对应多种处理方式:

用户:"我的订单在哪里?"
  → 意图:ORDER_QUERY → 调用订单查询工具

用户:"这款手机支持快充吗?"
  → 意图:PRODUCT_QA → 查询知识库 RAG

用户:"我要找人工"
  → 意图:HUMAN_HANDOFF → 转人工客服

3.2 意图识别实现

使用结构化输出让 AI 直接返回意图分类:

public enum Intent {
    ORDER_QUERY,      // 订单查询
    REFUND_REQUEST,   // 退换货
    PRODUCT_QA,       // 产品咨询
    COMPLAINT,        // 投诉建议
    HUMAN_HANDOFF,    // 转人工
    GREETING,         // 问候
    UNKNOWN           // 未知意图
}

public record IntentResult(
    Intent intent,           // 识别到的意图
    double confidence,       // 置信度 0.0-1.0
    String extractedParams   // 提取的参数(如订单号)
) {}

IntentRecognizer 服务:

@Service
public class IntentRecognizer {

    private final ChatClient chatClient;

    public IntentRecognizer(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultSystem("""
                你是智能客服的意图识别专家。
                分析用户输入,判断属于以下哪种意图:
                - ORDER_QUERY: 查询订单状态、物流信息
                - REFUND_REQUEST: 申请退货、换货、退款
                - PRODUCT_QA: 询问产品规格、功能、价格
                - COMPLAINT: 投诉、抱怨、建议
                - HUMAN_HANDOFF: 明确要求人工客服
                - GREETING: 打招呼、问候
                - UNKNOWN: 无法判断的意图
                
                同时提取关键参数(如订单号、产品名称)。
                confidence 是 0.0-1.0 的置信度。
                """)
            .build();
    }

    public IntentResult recognize(String userInput) {
        return chatClient.prompt()
            .user(userInput)
            .call()
            .entity(IntentResult.class);
    }
}

3.3 路由分发器

@Service
public class IntentRouter {

    private final IntentRecognizer recognizer;
    private final Map<Intent, ChatHandler> handlers;

    public IntentRouter(IntentRecognizer recognizer,
                       OrderQueryHandler orderHandler,
                       RefundHandler refundHandler,
                       ProductQAHandler productHandler,
                       HumanHandoffHandler humanHandler) {
        this.recognizer = recognizer;
        this.handlers = Map.of(
            Intent.ORDER_QUERY, orderHandler,
            Intent.REFUND_REQUEST, refundHandler,
            Intent.PRODUCT_QA, productHandler,
            Intent.HUMAN_HANDOFF, humanHandler
        );
    }

    public ChatResponse handle(String sessionId, String userInput) {
        // 1. 识别意图
        IntentResult intent = recognizer.recognize(userInput);
        
        // 2. 置信度低时,用通用回复
        if (intent.confidence() < 0.6) {
            return ChatResponse.builder()
                .reply("抱歉,我不太理解您的问题。您可以问:订单查询、退换货、产品咨询,或输入'人工'转接客服。")
                .intent(Intent.UNKNOWN)
                .build();
        }

        // 3. 路由到对应处理器
        ChatHandler handler = handlers.getOrDefault(
            intent.intent(), 
            handlers.get(Intent.PRODUCT_QA)  // 默认走产品咨询
        );

        return handler.handle(sessionId, userInput, intent);
    }
}

四、多轮对话与工具调用

4.1 订单查询场景

工具定义:

@Component
public class OrderTools {

    private final OrderService orderService;  // 真实订单服务

    public OrderTools(OrderService orderService) {
        this.orderService = orderService;
    }

    @Tool(description = "根据订单号查询订单详情和物流状态")
    public String queryOrder(@ToolParam(description = "订单号,如 B-12345") String orderNo) {
        Order order = orderService.findByOrderNo(orderNo);
        if (order == null) {
            return "未找到订单:" + orderNo + ",请检查订单号是否正确。";
        }
        return String.format(
            "订单号:%s\n状态:%s\n商品:%s\n物流:%s",
            order.getOrderNo(),
            order.getStatus(),
            order.getProductName(),
            order.getLogisticsInfo()
        );
    }

    @Tool(description = "查询用户最近的订单列表")
    public String queryRecentOrders(@ToolParam(description = "用户ID") String userId) {
        List<Order> orders = orderService.findRecentByUserId(userId, 5);
        // 格式化返回...
        return formatOrderList(orders);
    }
}

订单查询处理器:

@Component
public class OrderQueryHandler implements ChatHandler {

    private final ChatClient chatClient;
    private final OrderTools orderTools;
    private final ChatMemoryStore memoryStore;

    public OrderQueryHandler(ChatClient.Builder builder,
                            OrderTools orderTools,
                            ChatMemoryStore memoryStore) {
        this.orderTools = orderTools;
        this.memoryStore = memoryStore;
        this.chatClient = builder
            .defaultSystem("""
                你是订单查询助手,帮助用户查询订单状态和物流信息。
                如果用户没有提供订单号,询问用户订单号。
                回复要简洁友好,关键信息(状态、物流)要突出显示。
                """)
            .defaultTools(orderTools)
            .build();
    }

    @Override
    public ChatResponse handle(String sessionId, String userInput, IntentResult intent) {
        // 获取或创建该会话的 ChatMemory
        ChatMemory chatMemory = memoryStore.getOrCreate(sessionId);

        String reply = chatClient.prompt()
            .user(userInput)
            .advisors(new MessageChatMemoryAdvisor(chatMemory))
            .call()
            .content();

        return ChatResponse.builder()
            .reply(reply)
            .intent(Intent.ORDER_QUERY)
            .build();
    }
}

4.2 退换货场景(带状态机)

退换货流程较长,需要会话状态机管理:

public enum RefundState {
    INIT,           // 初始状态
    ORDER_CHECKED,  // 已确认订单
    REASON_COLLECTED, // 已收集退货原因
    CONFIRMED,      // 用户确认
    COMPLETED       // 完成
}

@Component
public class RefundHandler implements ChatHandler {

    private final Map<String, RefundSession> sessions = new ConcurrentHashMap<>();

    @Override
    public ChatResponse handle(String sessionId, String userInput, IntentResult intent) {
        RefundSession session = sessions.computeIfAbsent(sessionId, 
            k -> new RefundSession(sessionId));

        return switch (session.getState()) {
            case INIT -> handleInit(session, userInput, intent);
            case ORDER_CHECKED -> handleReasonCollect(session, userInput);
            case REASON_COLLECTED -> handleConfirm(session, userInput);
            case CONFIRMED -> handleSubmit(session);
            default -> ChatResponse.builder()
                .reply("您的退换货申请已提交,客服将在 24 小时内处理。")
                .build();
        };
    }

    private ChatResponse handleInit(RefundSession session, String input, IntentResult intent) {
        // 提取订单号,查询订单
        String orderNo = extractOrderNo(input, intent.extractedParams());
        Order order = orderService.findByOrderNo(orderNo);
        
        if (order == null) {
            return ChatResponse.builder()
                .reply("未找到订单,请提供正确的订单号。")
                .build();
        }

        session.setOrderNo(orderNo);
        session.setOrder(order);
        session.setState(RefundState.ORDER_CHECKED);

        return ChatResponse.builder()
            .reply(String.format(
                "已找到订单:%s(%s)\n请问退货原因是什么?",
                order.getProductName(),
                order.getOrderNo()
            ))
            .build();
    }

    // ... 其他状态处理
}

五、人工接管机制

5.1 触发条件

触发方式说明
用户主动用户输入"人工"、"找客服"等关键词
满意度低用户对 AI 回复评分 ≤ 2 星
多次失败同一意图识别失败 3 次以上
系统异常AI 服务异常或工具调用失败

5.2 实现代码

@Service
public class HumanHandoffService {

    private final SimpMessagingTemplate messagingTemplate;  // WebSocket
    private final HumanAgentQueue agentQueue;

    /**
     * 触发人工接管
     */
    public void handoffToHuman(String sessionId, String reason) {
        // 1. 标记会话状态
        SessionManager.markAsHumanHandoff(sessionId);

        // 2. 获取会话历史
        List<Message> history = ChatMemoryStore.getHistory(sessionId);

        // 3. 加入人工客服队列
        HandoffRequest request = HandoffRequest.builder()
            .sessionId(sessionId)
            .reason(reason)
            .chatHistory(history)
            .timestamp(LocalDateTime.now())
            .build();
        
        agentQueue.enqueue(request);

        // 4. 通知前端切换界面
        messagingTemplate.convertAndSend(
            "/topic/handoff/" + sessionId,
            Map.of("status", "HANDOFF", "reason", reason)
        );
    }

    /**
     * 满意度评分监控
     */
    public void recordSatisfaction(String sessionId, int score) {
        if (score <= 2) {
            handoffToHuman(sessionId, "用户满意度低(" + score + "星)");
        }
    }
}

5.3 WebSocket 实时通信

@Controller
public class ChatWebSocketController {

    private final IntentRouter router;
    private final HumanHandoffService handoffService;

    @MessageMapping("/chat/{sessionId}")
    @SendTo("/topic/chat/{sessionId}")
    public ChatMessage handleChat(@DestinationVariable String sessionId,
                                   ChatMessage message) {
        
        // 检查是否已转人工
        if (SessionManager.isHumanHandoff(sessionId)) {
            // 直接转发给人工客服系统
            return forwardToHumanAgent(sessionId, message);
        }

        // AI 处理
        ChatResponse response = router.handle(sessionId, message.getContent());

        // 检查是否需要转人工(如 AI 无法处理)
        if (response.getIntent() == Intent.HUMAN_HANDOFF) {
            handoffService.handoffToHuman(sessionId, "用户要求转人工");
        }

        return ChatMessage.builder()
            .sender("AI")
            .content(response.getReply())
            .timestamp(LocalDateTime.now())
            .build();
    }
}

关注公众号「AI日撰」,点击菜单「获取源码」获取完整代码(Gitee 仓库)。