Spring AI 综合项目:AI 智能客服系统

该文章已生成可运行项目,

Spring AI 综合项目:AI 智能客服系统

本文是《Spring AI 实战》系列第 15 章。从第 1 章的环境搭建到第 14 章的生产实战,我们学过了 ChatClient、Prompt Engineering、RAG、Function Calling、多模态、Agent 编排、安全防护、监控告警。这一章,把全书所学融会贯通——用 Spring AI 2.0.0 从零搭建一个生产级 AI 智能客服系统。这不是 Demo,是真正能上线、有架构设计、有安全防护、有监控的完整项目。


一、开篇:全书所学融会贯通

回头看这 15 章的旅程:从配置一个 API Key 开始,到搭建一个能看图、能查订单、能检索知识库、能自动转人工的智能客服系统。Spring AI 让这一切变得自然——作为 Java/Spring 程序员,你不需要学一门新语言,不需要理解复杂的 AI 框架,只需要在熟悉的 Spring 生态中,用你已经掌握的编程模式(依赖注入、AOP、 Advisor 链、配置化管理)来构建 AI 应用。

本章的目标不是"教你再学一个新技术",而是展示如何把前面所有章节的知识组合成一个完整的、可落地的项目。我会从需求分析开始,经过技术选型、架构设计,再到核心代码实现,最后讲测试策略。每个环节都附带可以直接参考的代码和配置。


二、需求分析

2.1 功能需求

功能模块功能描述优先级技术实现
智能对话基于公司知识库回答用户问题P0RAG(VectorStore + Embedding)
订单查询根据订单号查询订单状态和详情P0Function Calling(@Tool)
物流查询根据运单号查询物流信息P0Function Calling(@Tool)
意图识别自动判断用户意图(问答/查单/投诉/转人工)P0Structured Output(BeanOutputConverter)
多轮对话保持上下文,支持追问和澄清P0ChatMemory(MessageChatMemory)
流式输出逐字显示 AI 回答,提升用户体验P1ChatClient.stream() + SSE
转人工AI 无法处理时,平滑转接到人工客服P1意图识别 + 转接逻辑
安全防护防注入、PII 脱敏、输出审查P1Advisor 链(ContentPolicy + PII + OutputReview)
语义缓存相似问题直接返回缓存,节省成本P2VectorStore 语义缓存

2.2 非功能需求

维度指标说明
响应时间首字延迟 < 1s,完整回答 < 5s流式传输保证首字体验
并发能力支持 100 并发用户虚拟线程 + 异步处理
可用性99.9%健康检查 + 限流降级
数据安全对话数据加密存储PII 脱敏 + 输出审查
可观测性全链路监控Micrometer + Prometheus + Grafana

三、技术选型

3.1 技术栈选型表

组件选型理由备选方案
基础框架Spring Boot 3.4 + Spring AI 2.0.0最新稳定版,Java 21Spring Boot 3.3
语言Java 21虚拟线程、Record、Switch 表达式Kotlin
对话模型通义千问 qwen-plus中文能力强、合规、性价比高GPT-4o、Claude
Embedding 模型通义 text-embedding-v3中文语义理解最优OpenAI text-embedding-3
向量数据库Redis Stack(RediSearch)性能好、运维成熟、已有基础设施Milvus、Weaviate
对话记忆Redis ChatMemory和向量数据库统一技术栈JDBC、Cassandra
监控Micrometer + Prometheus + GrafanaSpring 生态标准方案OpenTelemetry
部署Docker + K8s弹性伸缩、蓝绿部署Docker Compose(开发环境)

3.2 Maven 依赖

<dependencies>
    <!-- Spring Boot 3.4 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring AI 核心 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
    <!-- 向量存储(Redis) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-vector-store-redis</artifactId>
    </dependency>
    <!-- Redis(对话记忆 + 缓存) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- 监控 -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>
</dependencies>

四、架构设计

4.1 分层架构

┌─────────────────────────────────────────────┐
│                Controller 层                 │
│   CustomerServiceController                │
│   - 流式对话 /api/chat/stream                 │
│   - 同步对话 /api/chat                        │
│   - 转人工   /api/chat/transfer               │
└──────────────────┬──────────────────────────┘
                   │
┌──────────────────▼──────────────────────────┐
│              Service 层                      │
│   CustomerServiceService                     │
│   - 意图识别(Structured Output)             │
│   - 路由分发(查订单/查物流/知识库/闲聊)     │
│   - 转人工判断                               │
└──────────────────┬──────────────────────────┘
                   │
┌──────────────────▼──────────────────────────┐
│          Spring AI ChatClient               │
│                                             │
│  System Prompt ─┐                           │
│  ChatMemory  ───┤─→ Advisor 链 → 大模型      │
│  RAG 检索    ───┤   ┌─────────────────┐     │
│  @Tool 工具  ───┘   │ PII 过滤 Advisor│     │
│                     │ 安全策略 Advisor│     │
│                     │ 输出审查 Advisor│     │
│                     │ RAG 检索 Advisor│     │
│                     │ 记忆管理 Advisor│     │
│                     └─────────────────┘     │
└─────────────────────────────────────────────┘
                   │
┌──────────────────▼──────────────────────────┐
│              基础设施层                      │
│  Redis(向量库 + 记忆 + 缓存)               │
│  业务 API(订单系统、物流系统)               │
│  大模型 API(通义千问)                       │
└─────────────────────────────────────────────┘

4.2 模块划分

包名职责核心类
controllerREST API 入口CustomerServiceController
service业务逻辑编排CustomerServiceService
configChatClient 配置CustomerServiceConfig
toolsAI 工具函数CustomerServiceTools
model数据模型IntentTypeCustomerServiceResponse
security安全防护ContentPolicyAdvisorPiiFilterAdvisor
cache语义缓存SemanticCacheService

4.3 数据流

用户消息 → Controller → Service(意图识别)
                           │
              ┌────────────┼────────────┬────────────┐
              ▼            ▼            ▼            ▼
          ORDER_QUERY  KNOWLEDGE_QA  LOGISTICS  HUMAN_SERVICE
              │            │            │            │
         @Tool查订单   RAG检索知识库  @Tool查物流   返回转人工
              │            │            │            │
              └────────────┴────────────┴────────────┘
                           │
                    ChatClient 回答
                           │
                    流式/同步返回给用户

五、核心代码

5.1 IntentType 枚举

package com.example.customerservice.model;

/**
 * 用户意图枚举
 *
 * AI 通过 Structured Output 自动识别用户意图,
 * 系统根据意图类型做不同的处理。
 */
public enum IntentType {
    /** 查询订单(需要调用订单查询工具) */
    ORDER_QUERY,
    /** 知识问答(需要 RAG 检索知识库) */
    KNOWLEDGE_QA,
    /** 查询物流(需要调用物流查询工具) */
    LOGISTICS_QUERY,
    /** 投诉建议(需要转人工) */
    COMPLAINT,
    /** 闲聊/问候(直接回答) */
    CHITCHAT,
    /** 转人工客服 */
    HUMAN_SERVICE,
    /** 无法识别的意图 */
    UNKNOWN
}

5.2 CustomerServiceResponse Record

package com.example.customerservice.model;

/**
 * 客服响应结果 —— 统一的返回格式
 *
 * 使用 Java 21 Record 定义,不可变、简洁。
 * 前端根据 intent 类型决定展示方式:
 * - CHITCHAT/KNOWLEDGE_QA:直接展示 answer
 * - ORDER_QUERY/LOGISTICS_QUERY:展示结构化数据 + answer
 * - HUMAN_SERVICE/COMPLAINT:展示转人工提示
 */
public record CustomerServiceResponse(
    /** 识别到的用户意图 */
    IntentType intent,
    /** AI 的回答内容 */
    String answer,
    /** 置信度(0-1),低于阈值建议转人工 */
    double confidence,
    /** 是否需要转人工 */
    boolean transferToHuman,
    /** 关联的工具调用结果(订单信息、物流信息等) */
    String toolResult
) {
    /** 快速构建纯文本回答 */
    public static CustomerServiceResponse chat(String answer) {
        return new CustomerServiceResponse(
                IntentType.CHITCHAT, answer, 0.9, false, null);
    }

    /** 快速构建转人工响应 */
    public static CustomerServiceResponse transfer(String reason) {
        return new CustomerServiceResponse(
                IntentType.HUMAN_SERVICE, reason, 0.0, true, null);
    }
}

5.3 CustomerServiceTools(@Tool 工具函数)

package com.example.customerservice.tools;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

/**
 * 客服系统工具集 —— AI 可以调用的外部能力
 *
 * 通过 @Tool 注解,Spring AI 自动将这些方法注册为大模型可调用的工具。
 * AI 根据用户意图,自主决定是否调用、调用哪个工具。
 */
@Component
public class CustomerServiceTools {

    private final RestClient restClient;

    public CustomerServiceTools(RestClient.Builder builder) {
        // 调用内部业务系统的 API
        this.restClient = builder
                .baseUrl("http://internal-api.company.com")
                .build();
    }

    /**
     * 查询订单信息
     *
     * AI 在识别到用户要查订单时自动调用。
     * 返回订单状态、商品、金额、收货地址等结构化信息。
     */
    @Tool(description = "根据订单号查询订单的详细信息。" +
            "返回订单状态、商品列表、金额、收货地址等。" +
            "当用户提到'订单'、'下单'、'查订单'时调用。")
    public String queryOrder(
            @ToolParam(description = "订单号,如 ORD-20260101-001") 
            String orderId) {
        try {
            // 调用内部订单系统 API
            return restClient.get()
                    .uri("/orders/{id}", orderId)
                    .retrieve()
                    .body(String.class);
        } catch (Exception e) {
            return "订单查询失败:%s。请确认订单号是否正确。"
                    .formatted(e.getMessage());
        }
    }

    /**
     * 查询物流信息
     *
     * AI 在识别到用户要查物流时自动调用。
     * 返回快递公司、运单号、当前位置、预计到达时间。
     */
    @Tool(description = "根据运单号查询物流信息。" +
            "返回快递公司、当前位置、预计到达时间等。" +
            "当用户提到'物流'、'快递'、'到哪了'时调用。")
    public String trackPackage(
            @ToolParam(description = "运单号,如 SF1234567890")
            String trackingNumber) {
        try {
            return restClient.get()
                    .uri("/logistics/{trackingNo}", trackingNumber)
                    .retrieve()
                    .body(String.class);
        } catch (Exception e) {
            return "物流查询失败:%s。请确认运单号是否正确。"
                    .formatted(e.getMessage());
        }
    }

    /**
     * 查询知识库
     *
     * 注意:RAG 检索通常通过 Advisor 自动完成,
     * 这个工具用于 AI 需要主动搜索特定主题时。
     */
    @Tool(description = "在知识库中搜索特定主题的信息。" +
            "当用户的提问涉及公司政策、产品规格、售后规则时调用。" +
            "返回与查询最相关的知识库条目。")
    public String searchKnowledgeBase(
            @ToolParam(description = "搜索关键词,如'退换货政策'")
            String query) {
        try {
            return restClient.get()
                    .uri("/knowledge/search?q={query}", query)
                    .retrieve()
                    .body(String.class);
        } catch (Exception e) {
            return "知识库搜索失败。";
        }
    }
}

5.4 CustomerServiceConfig(ChatClient 配置)

package com.example.customerservice.config;

import com.example.customerservice.model.IntentType;
import com.example.customerservice.security.*;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.*;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 客服系统 ChatClient 配置
 *
 * 核心:System Prompt + Advisor 链 + 工具注册
 *
 * Advisor 链执行顺序(从外到内):
 * 1. PiiFilterAdvisor    —— 脱敏用户输入中的敏感信息
 * 2. ContentPolicyAdvisor —— 拦截 Prompt 注入攻击
 * 3. SafeGuardAdvisor     —— 安全护栏,限制输出范围
 * 4. QuestionAnswerAdvisor —— RAG 检索知识库增强
 * 5. MessageChatMemoryAdvisor —— 对话记忆管理
 *
 * 工具注册:
 * - queryOrder:查订单
 * - trackPackage:查物流
 * - searchKnowledgeBase:搜索知识库
 */
@Configuration
public class CustomerServiceConfig {

    @Bean
    public ChatClient customerServiceClient(
            ChatClient.Builder builder,
            VectorStore vectorStore,
            CustomerServiceTools tools,
            PiiFilterAdvisor piiAdvisor,
            ContentPolicyAdvisor contentPolicyAdvisor,
            SafeGuardAdvisor safeGuardAdvisor) {

        return builder
                // ---- System Prompt:定义 AI 的角色、行为、规则 ----
                .defaultSystem("""
                        你是"小橙",一个专业的电商客服助手。
                        
                        ## 你的职责
                        1. 回答用户关于商品、订单、物流、售后的问题
                        2. 帮助用户查询订单状态和物流信息
                        3. 基于知识库回答公司政策和常见问题
                        
                        ## 工具使用规则
                        - 用户提到"订单号"或"我的订单"时,调用 queryOrder
                        - 用户提到"物流"或"快递"或"到哪了"时,调用 trackPackage
                        - 涉及公司政策、售后规则时,调用 searchKnowledgeBase
                        
                        ## 转人工条件(满足任一即转人工)
                        - 用户明确要求转人工
                        - 用户表示不满或要投诉
                        - 涉及退款金额 > 500 元
                        - 你连续两次无法理解用户意图
                        
                        ## 回答风格
                        - 语气友好、专业、有同理心
                        - 回答简洁,避免过长
                        - 不确定的信息要说明"建议以实际情况为准"
                        - 用中文回答
                        """)
                // ---- 注册工具 ----
                .defaultTools(tools)
                // ---- Advisor 链(按顺序执行)----
                .defaultAdvisors(
                        piiAdvisor,            // 1. PII 脱敏
                        contentPolicyAdvisor,  // 2. 安全策略
                        safeGuardAdvisor       // 3. 安全护栏
                )
                .build();
    }
}

5.5 CustomerServiceController

package com.example.customerservice.controller;

import com.example.customerservice.model.CustomerServiceResponse;
import com.example.customerservice.service.CustomerServiceService;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;

import java.util.Map;

/**
 * 客服系统 REST 接口
 *
 * 提供三种交互方式:
 * 1. 流式对话(推荐):逐 Token 返回,用户体验最好
 * 2. 同步对话:等待完整回答后返回,适合后台调用
 * 3. 转人工:当 AI 判断需要转人工时,前端调用此接口
 */
@RestController
@RequestMapping("/api/chat")
public class CustomerServiceController {

    private final CustomerServiceService chatService;

    public CustomerServiceController(CustomerServiceService chatService) {
        this.chatService = chatService;
    }

    /**
     * 流式对话(推荐方式)
     *
     * 返回 Server-Sent Events 流,前端逐字展示。
     * 首字延迟通常 < 500ms,用户体验远优于同步方式。
     *
     * curl 示例:
     * curl -N "http://localhost:8080/api/chat/stream" \
     *   -H "Content-Type: application/json" \
     *   -d '{"message":"我的订单ORD-001到哪了?","sessionId":"user123"}'
     */
    @PostMapping(value = "/stream",
                 produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<CustomerServiceResponse> streamChat(
            @RequestBody ChatRequest request) {
        return chatService.streamChat(
                request.message(),
                request.sessionId()
        );
    }

    /**
     * 同步对话(等待完整回答)
     *
     * 适用于后台任务或不需要流式展示的场景。
     * 响应时间通常 3-10 秒。
     */
    @PostMapping("/sync")
    public CustomerServiceResponse syncChat(
            @RequestBody ChatRequest request) {
        return chatService.syncChat(
                request.message(),
                request.sessionId()
        );
    }

    /**
     * 主动转人工
     *
     * 用户在前端点击"转人工"按钮时调用。
     * 返回人工客服的接入信息(队列号、等待时间等)。
     */
    @PostMapping("/transfer")
    public Map<String, Object> transferToHuman(
            @RequestBody Map<String, String> request) {
        String sessionId = request.get("sessionId");
        return chatService.transferToHuman(sessionId);
    }

    /** 聊天请求参数 */
    public record ChatRequest(
            String message,   // 用户消息
            String sessionId   // 会话 ID(用于多轮对话记忆)
    ) {}
}

5.6 CustomerServiceService

package com.example.customerservice.service;

import com.example.customerservice.model.CustomerServiceResponse;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

/**
 * 客服系统核心服务
 *
 * 编排 ChatClient 的调用,处理意图识别和响应生成。
 * 对外提供流式和同步两种调用方式。
 */
@Service
public class CustomerServiceService {

    private final ChatClient chatClient;

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

    /**
     * 流式对话
     * 逐 Token 返回,前端通过 SSE 接收。
     */
    public Flux<CustomerServiceResponse> streamChat(
            String message, String sessionId) {
        return chatClient.prompt()
                .user(message)
                .advisors(a -> a.param("chat_memory_conversation_id", sessionId))
                .stream()
                .chatResponse()
                .map(response -> CustomerServiceResponse.chat(
                        response.getResult().getOutput().getText()));
    }

    /**
     * 同步对话
     * 等待完整回答后一次性返回。
     */
    public CustomerServiceResponse syncChat(
            String message, String sessionId) {
        String answer = chatClient.prompt()
                .user(message)
                .advisors(a -> a.param(
                        "chat_memory_conversation_id", sessionId))
                .call()
                .content();
        return CustomerServiceResponse.chat(answer);
    }

    /**
     * 转人工
     * 返回转接信息,前端引导用户排队等候。
     */
    public Map<String, Object> transferToHuman(String sessionId) {
        return Map.of(
                "status", "queued",
                "message", "正在为您转接人工客服,请稍候...",
                "queuePosition", 3,
                "estimatedWaitMinutes", 5
        );
    }
}

六、测试策略

6.1 AI 应用测试的特殊性

AI 应用的测试和传统应用有本质区别——AI 的输出不是确定性的。同一个输入,两次调用可能得到不同(但都正确)的答案。这意味着你不能用传统的 assertEquals(expected, actual) 来测试。

测试类型传统应用AI 应用工具
单元测试断言精确返回值断言返回值包含关键信息AssertJ 的 contains()
集成测试Mock 外部依赖Mock 大模型返回固定答案WireMock + 固定响应
端到端测试自动化 UI 测试人工评估回答质量评估数据集 + 人工标注
回归测试修改代码后功能不变修改 Prompt 后回答质量不下降LLM-as-Judge 自动评估

6.2 关键测试代码

/**
 * 客服系统核心测试
 *
 * 测试策略:
 * 1. Mock 大模型返回固定答案,测试业务逻辑的正确性
 * 2. 测试意图识别的准确性
 * 3. 测试转人工的触发条件
 */
@SpringBootTest
class CustomerServiceServiceTest {

    @MockBean
    private ChatClient chatClient;

    @Autowired
    private CustomerServiceService service;

    @Test
    void shouldReturnOrderInfo_WhenUserAsksAboutOrder() {
        // 模拟用户询问订单
        String answer = service.syncChat(
                "我的订单 ORD-001 状态是什么?", "session-1");
        // 断言回答中包含订单相关信息(不要求精确匹配)
        assertThat(answer.answer())
                .contains("ORD-001");
        assertThat(answer.intent())
                .isEqualTo(IntentType.ORDER_QUERY);
    }

    @Test
    void shouldTransferToHuman_WhenUserComplains() {
        String answer = service.syncChat(
                "你们的服务太差了,我要投诉!", "session-2");
        assertThat(answer.transferToHuman()).isTrue();
    }
}

生产建议:上线后持续收集用户反馈,建立"黄金问答数据集"(标注了期望回答的 Q&A 对)。用这个数据集做回归测试,确保每次 Prompt 调整不会导致回答质量下降。


在这里插入图片描述

Spring AI 实战 – 第15章:完整内容与源码

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值