智能客服项目
大约 4 分钟
第十七章:智能客服系统
17.1 需求分析:客服系统真正要解决什么
很多“客服机器人”失败,并不是模型不够强,而是需求没有拆清楚。一个可落地的智能客服,通常至少要满足:
- 能回答“规则类问题”(退换货、发票、运费)
- 能处理“数据类问题”(订单状态、物流进度、余额)
- 能正确“转人工”(用户强烈不满/高风险操作/多次失败)
- 能被治理:审计、限流、成本控制、质量评估
17.2 系统设计:从最小可用开始
建议第一版只做三件事:
- 统一入口 API:
/cs/chat - 两个工具:
getOrderStatus、handoffToHuman - 会话上下文:按
sessionId保存最近 N 条消息
17.3 核心功能实现(可复制运行)
下面给出一个“单模块 Spring Boot”示例,特点是:
- 用
ChatClient做对话 - 用函数调用让模型“主动”查订单/转人工
- 用内存 Map 做会话存储(便于学习,生产请换 Redis)
17.3.1 会话存储:只保留最近 N 条
package com.example.customerservice.memory;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Component;
@Component
public class SessionMemoryStore {
private static final int MAX_MESSAGES = 12;
private final ConcurrentHashMap<String, Deque<String>> store = new ConcurrentHashMap<>();
public List<String> appendAndGet(String sessionId, String message) {
Deque<String> deque = store.computeIfAbsent(sessionId, k -> new ArrayDeque<>());
synchronized (deque) {
deque.addLast(message);
while (deque.size() > MAX_MESSAGES) {
deque.removeFirst();
}
return List.copyOf(deque);
}
}
}17.3.2 工具集:查订单 + 转人工(返回结构化结果)
package com.example.customerservice.tools;
import java.util.Map;
import org.springframework.stereotype.Service;
@Service
public class CustomerServiceTools {
public Map<String, Object> getOrderStatus(Map<String, Object> arguments) {
String orderId = String.valueOf(arguments.getOrDefault("orderId", "")).trim();
if (orderId.isBlank()) {
throw new IllegalArgumentException("orderId 不能为空");
}
String status = switch (orderId) {
case "A1001" -> "SHIPPED";
case "A1002" -> "DELIVERED";
default -> "UNKNOWN";
};
return Map.of("orderId", orderId, "status", status);
}
public Map<String, Object> handoffToHuman(Map<String, Object> arguments) {
String reason = String.valueOf(arguments.getOrDefault("reason", "用户请求人工")).trim();
return Map.of("handoff", true, "reason", reason, "ticketId", "T-" + System.currentTimeMillis());
}
}17.3.3 Controller:对话入口 + 工具注册
package com.example.customerservice.api;
import com.example.customerservice.memory.SessionMemoryStore;
import com.example.customerservice.tools.CustomerServiceTools;
import java.util.List;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.Schema;
import org.springframework.ai.model.function.SchemaProperty;
import org.springframework.ai.model.function.SchemaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CustomerServiceController {
private final ChatClient chatClient;
private final SessionMemoryStore memoryStore;
private final CustomerServiceTools tools;
public CustomerServiceController(ChatClient chatClient, SessionMemoryStore memoryStore, CustomerServiceTools tools) {
this.chatClient = chatClient;
this.memoryStore = memoryStore;
this.tools = tools;
}
@PostMapping("/cs/chat")
public ApiResponse<String> chat(@RequestBody ChatRequest request) {
List<String> history = memoryStore.appendAndGet(request.sessionId(), "User: " + request.message());
FunctionCallback getOrderStatus = new FunctionCallback(
"getOrderStatus",
"查询订单状态。参数 orderId 形如 A1001。",
new Schema(SchemaType.OBJECT)
.addProperty("orderId", SchemaProperty.of(SchemaType.STRING, "订单号,例如 A1001"))
.addRequired("orderId"),
tools::getOrderStatus
);
FunctionCallback handoffToHuman = new FunctionCallback(
"handoffToHuman",
"转人工。当用户情绪激烈、涉及退款纠纷、或多次失败时调用。",
new Schema(SchemaType.OBJECT)
.addProperty("reason", SchemaProperty.of(SchemaType.STRING, "转人工原因"))
.addRequired("reason"),
tools::handoffToHuman
);
String answer = chatClient.prompt()
.system("""
你是电商平台客服。规则:
1) 遇到订单状态查询必须调用 getOrderStatus。
2) 用户要求人工、或出现强烈不满、或你无法解决时调用 handoffToHuman。
3) 输出要简洁,先给结论,再给操作建议。
""")
.user("历史对话:\n" + String.join("\n", history))
.user("本轮问题:" + request.message())
.functions(getOrderStatus, handoffToHuman)
.call()
.content();
memoryStore.appendAndGet(request.sessionId(), "Assistant: " + answer);
return ApiResponse.ok(answer);
}
public record ChatRequest(String sessionId, String message) {
}
}ApiResponse 的写法可复用第十章的封装方式。
17.4 部署上线:别忽略治理
上线前至少要具备:
- 鉴权与限流:防止被刷
- 审计日志:工具调用必须可追踪
- 兜底策略:供应商故障时返回固定提示或转人工
- 成本控制:记录 token 用量,设置配额与告警 点击这里👇🏻获取:100万QPS短链系统、复杂的商城微服务系统、智能翻译助手AI Agent、SaaS点餐系统、刷题吧小程序、商城系统、秒杀系统、AI项目、代码生成神器、苏三demo项目、智能天气播报AI Agent、智能代码审查AI Agent等 10 个项目的:项目源代码、开发教程和技术答疑
