SpringAI智能客服实战:从零构建高可用AI对话系统
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:对话结束。

我们用枚举和实体类来实现:
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)。我们采取以下措施:
- 数据脱敏:在存储前,对日志中的姓名、手机号、邮箱、订单号等个人可识别信息(PII)进行掩码或哈希处理。
- 加密存储:日志数据库的磁盘加密必须开启,敏感字段建议应用层额外加密。
- 访问控制与审计:严格限制日志数据的访问权限,所有查询和导出操作必须有完整的审计日志。
- 设置保留期限:根据法律要求,制定明确的日志保留策略,到期自动删除。
@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)是指用户通过精心构造的输入,让模型忽略你设定的指令,执行用户想要的操作。防御方法:
- 输入校验与清洗:在将用户输入放入Prompt前,进行严格的校验。例如,检查是否包含类似“忽略之前指令”的字符串,并对长度进行限制。
- 角色隔离与系统指令加固:在Prompt中明确、强硬地定义AI的角色和边界。可以将系统指令放在一个独立的、权重更高的消息中(如果模型支持)。例如:“你必须始终扮演客服角色,绝对不能执行任何与客服无关的操作。”
- 输出校验:对模型的输出也要进行检查。如果回复中出现了明显违背指令的内容(如生成了代码、执行了非客服操作),则触发二次处理或人工审核流程。
6.2 多租户场景下的会话隔离策略
一个系统为多个不同公司(租户)服务时,隔离是关键。
- 数据层面:所有与会话、知识库、配置相关的表,都必须包含
tenant_id字段。在查询时,通过ThreadLocal或SecurityContext持有当前租户信息,并在DAO层自动附加tenant_id = ?条件。 - 配置层面:每个租户应有独立的AI模型配置(如API Key、Base URL)、Prompt模板和敏感词库。可以通过在配置类中根据当前租户动态加载
@Bean来实现。 - 会话层面:
ConversationSession的sessionId可以设计为包含租户前缀(如TENANT_A_session_123),或者在会话对象中明确存储tenantId。确保一个租户的用户无法访问到另一个租户的会话历史。
7. 延伸思考:基于SpringAI Function Calling实现工单自动创建
SpringAI支持Function Calling,这让AI不仅能说,还能“做”。我们可以让客服AI在判断问题无法在线解决时,自动创建工单。
可行性分析:
- 定义“创建工单”函数:我们创建一个
createTicket方法,描述它的功能(如“为用户创建一个技术支持工单”)、所需参数(用户ID、问题描述、紧急程度)。 - 在Prompt中暴露函数:将函数描述提供给大模型。当用户说“我的订单一直没发货,需要人工介入”,模型可以理解意图,并决定调用
createTicket函数。 - SpringAI处理调用:当模型返回一个函数调用请求时,SpringAI会回调我们本地注册的Java方法,执行真正的创建工单逻辑(如调用工单系统的API)。
- 增强用户体验: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:3306和localhost:6379,运行docker-compose up -d,再启动Spring Boot应用,一个具备基础能力的智能客服后端就跑起来了。
这次从传统规则引擎切换到SpringAI智能客服的实践,让我们真切感受到了大模型带来的生产力变革。它并非完美,在成本控制和输出稳定性上仍需精细打磨,但其在理解力、灵活性和开发效率上的优势是革命性的。希望这篇笔记能为你带来一些启发。
更多推荐
所有评论(0)