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 功能需求
| 功能模块 | 功能描述 | 优先级 | 技术实现 |
|---|---|---|---|
| 智能对话 | 基于公司知识库回答用户问题 | P0 | RAG(VectorStore + Embedding) |
| 订单查询 | 根据订单号查询订单状态和详情 | P0 | Function Calling(@Tool) |
| 物流查询 | 根据运单号查询物流信息 | P0 | Function Calling(@Tool) |
| 意图识别 | 自动判断用户意图(问答/查单/投诉/转人工) | P0 | Structured Output(BeanOutputConverter) |
| 多轮对话 | 保持上下文,支持追问和澄清 | P0 | ChatMemory(MessageChatMemory) |
| 流式输出 | 逐字显示 AI 回答,提升用户体验 | P1 | ChatClient.stream() + SSE |
| 转人工 | AI 无法处理时,平滑转接到人工客服 | P1 | 意图识别 + 转接逻辑 |
| 安全防护 | 防注入、PII 脱敏、输出审查 | P1 | Advisor 链(ContentPolicy + PII + OutputReview) |
| 语义缓存 | 相似问题直接返回缓存,节省成本 | P2 | VectorStore 语义缓存 |
2.2 非功能需求
| 维度 | 指标 | 说明 |
|---|---|---|
| 响应时间 | 首字延迟 < 1s,完整回答 < 5s | 流式传输保证首字体验 |
| 并发能力 | 支持 100 并发用户 | 虚拟线程 + 异步处理 |
| 可用性 | 99.9% | 健康检查 + 限流降级 |
| 数据安全 | 对话数据加密存储 | PII 脱敏 + 输出审查 |
| 可观测性 | 全链路监控 | Micrometer + Prometheus + Grafana |
三、技术选型
3.1 技术栈选型表
| 组件 | 选型 | 理由 | 备选方案 |
|---|---|---|---|
| 基础框架 | Spring Boot 3.4 + Spring AI 2.0.0 | 最新稳定版,Java 21 | Spring 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 + Grafana | Spring 生态标准方案 | OpenTelemetry |
| 部署 | Docker + K8s | 弹性伸缩、蓝绿部署 | Docker Compose(开发环境) |
3.2 Maven 依赖
xml
<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 模块划分
| 包名 | 职责 | 核心类 |
|---|---|---|
controller |
REST API 入口 | CustomerServiceController |
service |
业务逻辑编排 | CustomerServiceService |
config |
ChatClient 配置 | CustomerServiceConfig |
tools |
AI 工具函数 | CustomerServiceTools |
model |
数据模型 | IntentType、CustomerServiceResponse |
security |
安全防护 | ContentPolicyAdvisor、PiiFilterAdvisor |
cache |
语义缓存 | SemanticCacheService |
4.3 数据流
用户消息 → Controller → Service(意图识别)
│
┌────────────┼────────────┬────────────┐
▼ ▼ ▼ ▼
ORDER_QUERY KNOWLEDGE_QA LOGISTICS HUMAN_SERVICE
│ │ │ │
@Tool查订单 RAG检索知识库 @Tool查物流 返回转人工
│ │ │ │
└────────────┴────────────┴────────────┘
│
ChatClient 回答
│
流式/同步返回给用户
五、核心代码
5.1 IntentType 枚举
java
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
java
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 工具函数)
java
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 配置)
java
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
java
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
java
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 关键测试代码
java
/**
* 客服系统核心测试
*
* 测试策略:
* 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 调整不会导致回答质量下降。
