背景与痛点
去年“618”大促,公司客服通道被挤爆,平均响应时间飙到 18 秒,后台工单积压 3 万条。人工坐席成本占运营预算 42%,老板一句“降本增效”把压力直接甩给技术部。传统 FAQ 机器人只能命中 60% 的问题,剩下 40% 还得人工兜底,等于“半自动”。更尴尬的是,业务查询接口分散在订单、库存、会员三个系统,客服小姐姐要在 6 个界面来回切换,效率低到怀疑人生。
痛点总结:
- 响应链路长:浏览器→客服后台→业务中台→数据库,层层加码。
- 意图识别弱:关键词匹配,用户换个说法就失效。
- 数据孤岛:订单、库存、积分接口无统一封装,查询一次要 3~5 次 RPC。
- 扩容成本高:人工坐席线性扩容,双 11 前临时招 200 人,节后裁员赔偿 N+1。
技术选型
Java 圈能玩的大模型方案其实不多,我踩坑后把主流三条路跑了一遍:
| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| OpenAI GPT-4 Java SDK | 模型能力强,社区资料多 | 官方没 SDK,得用三方封装;网络延迟 400 ms+ | 放弃 |
| Claude 官方 API | 支持 100 k token,长对话爽 | 国内直连 503 概率高,需要代理 | 放弃 |
| 阿里云 DashScope(通义) | 国内 VPC 内网调用,RT 120 ms;有 Java SDK;按量计费 0.0012 元/1k token | 模型尺寸略小于 GPT-4,但业务场景够用 | 采用 |
最终选型:Spring Boot 3.2 + Alibaba DashScope + Redis 缓存 + Hystrix 熔断。
核心实现
整体架构一张图先说明白:
浏览器 → 智能客服网关 → 大模型意图识别 → 业务查询聚合服务 → 各域微服务。
- 用户问:“我的订单怎么还没发货?”
- 大模型返回结构化 JSON:
{intent:"ORDER_QUERY", entities:{orderNo:"12345"}} - 聚合服务拿 orderNo 去订单中心拉数据,把结果拼成自然语言再返回。
这样客服系统只负责“对话管理”,业务查询被拆成独立服务,可独立扩容,责任清晰。
代码示例
下面代码可直接拷贝到 IDEA 跑起来,依赖版本:Spring Boot 3.2.5、spring-ai 1.0.0-SNAPSHOT(社区版,封装了 DashScope)。
1. Controller 层
@RestController @RequestMapping("/bot") @RequiredArgsConstructor @Slf4j public class ChatController { private final ChatService chatService; @PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> chat(@RequestBody ChatRequest request) { return chatService.chat(request.getSessionId(), request.getQuery()) .doOnError(e -> log.error("session:{} error", request.getSessionId(), e)); } }2. Service 层
@Service @Slf4j @RequiredArgsConstructor public class ChatService { private final DashScopeChatModel chatModel; private final BusinessQueryService businessQueryService; private final RedisTemplate<String, String> redis; public Flux<String> chat(String sessionId, String query) { // 1. 缓存 30 秒相同的问法,直接返回,防重复调用 String cacheKey = "bot:answer:" + DigestUtils.md5DigestAsHex(query.getBytes()); String cached = redis.opsForValue().get(cacheKey); if (cached != null) { return Flux.just(cached); } // 2. 构造 Prompt,让模型返回固定 JSON String prompt = """ 你是官方客服,请根据用户问题提取意图和实体,返回纯 JSON,不要多余解释。 意图列表:[ORDER_QUERY, STOCK_QUERY, COUPON_QUERY] 用户问题:%s """.formatted(query); return chatModel.stream(prompt) .map(resp -> { String json = resp.getResult().getOutput().getContent(); log.info("model output:{}", json); // 3. 解析意图并查询业务 IntentPayload payload = parsePayload(json); String answer = businessQueryService.query(payload); // 4. 写缓存 redis.opsForValue().set(cacheKey, answer, Duration.ofSeconds(30)); return answer; }); } private IntentPayload parsePayload(String json) { try { return new ObjectMapper().readValue(json, IntentPayload.class); } catch (Exception e) { throw new BotException("意图解析失败", e); } } }3. 业务查询聚合
@Service @Slf4j @RequiredArgsConstructor public class BusinessQueryService { private final OrderClient orderClient; private final StockClient stockClient; @Cached(cacheNames = "business", key = "#payload.toString()", unless = "#result==null") public String query(IntentPayload payload) { return switch (payload.getIntent()) { case "ORDER_QUERY" -> { OrderDTO order = orderClient.getOrder(payload.getEntities().getOrderNo()); yield "订单%s 当前状态:%s,预计发货时间:%s" .formatted(order.getOrderNo(), order.getStatus(), order.getEstimateDelivery()); } case "STOCK_QUERY" -> { StockDTO stock = stockClient.getStock(payload.getEntities().getSkuId()); yield "商品%s 现货库存:%d 件".formatted(stock.getSkuName(), stock.getAvailable()); } default -> "暂不支持该查询,请联系人工客服。"; }; } }Clean Code 要点:
- 所有外部调用都封装到 Client 接口,Service 只负责编排。
- 异常统一转译成 BotException,再由
@ControllerAdvice统一返回 200 带错误码,前端好处理。 - 日志用占位符,避免字符串拼接。
性能优化
异步化:
上面代码返回Flux<String>,Spring WebFlux 自动把每段答案按 SSE 推给前端,首字节时间(TTFB)从 1.8 s 降到 320 ms,用户体感“秒回”。缓存:
30 秒本地缓存 + 5 分钟 Redis 缓存,命中率 68%,大模型调用量直接降一半,账单肉眼可见地瘦下去。并发控制:
大模型 SDK 默认 200 连接,高并发下被打爆会抛PoolTimeoutException。我把连接池提到 500,同时用 Sentinel 做 QPS 限流:单机 50 req/s,超量直接降级到“关键词+缓存”模式,保证核心链路不挂。批查询:
订单、库存、优惠券三个接口支持批量,in 查询一次 30 条,把 3 次 RPC 合并成 1 次,RT 再降 40%。
避坑指南
- 超时设置:DashScope 内网 RT 平均 120 ms,但偶发抖动到 2 s。把
readTimeout设 3 s,重试 1 次,防止长尾拖死线程池。 - Token 限制:通义 7B 版本最大 4 k token,长对话容易爆。用滑动窗口保留最近 3 轮,历史摘要只保留关键实体,节省 30% token。
- JSON 幻觉:模型偶尔在 JSON 前后加 ```,导致解析失败。在 Prompt 里加“返回纯 JSON,不要 markdown 包裹”后,错误率从 5% 降到 0.3%。
- 线程隔离:大模型调用是 IO 密集,单独给一个 Elatic 线程池,不与业务线程混用,避免阻塞 WebFlux 事件循环。
- 灰度发布:先用 5% 流量实验,对比人工坐席解决率,达到 90% 再全量,防止“智能客服”变“智障客服”。
效果与展望
上线两周,核心指标对比:
| 指标 | 上线前 | 上线后 |
|---|---|---|
| 平均响应时间 | 18 s | 2.3 s |
| 人工介入率 | 42% | 11% |
| 坐席成本 | 100% | 65% |
| 用户满意度 | 78% | 92% |
老板看完报表只说了两个字:“加人”。不过这次是“加机器”——直接把 Pod 副本数从 10 扩到 30,成本不到原来人工的 1/5。
下一步打算:
- 多轮对话:把 session 存到 Redis Stream,支持上下文追问“那订单还有货吗?”。
- 插件化:用 Java SPI 机制把“业务查询”做成插件,运营同学上传 Jar 就能扩展新意图,零代码发布。
- 语音输入:集成阿里一句话识别,把语音转文本后直接走现有流程,让 60 岁阿姨也能“说”客服。
如果你也卡在“人工客服太贵、FAQ 太蠢”的泥潭,不妨把这套代码拉下来改两行配置,先让查询速度翻倍,再慢慢迭代多轮对话。智能客服的坑还有很多,但“让机器先跑起来”永远是最重要的一步。祝你上线不踩雷, 7×24 小时零投诉。