背景痛点:规则引擎的“硬编码地狱”
去年双十一,公司老客服系统直接“罢工”。
那套基于正则+关键词的“古董”规则引擎,平时还能应付,一到大促就露馅:
- 运营同学凌晨两点还在加规则,一条“满300减50”的文案要配十几条正则,一不小心就把“满300减0”也匹配进去。
- 用户问“我买的鞋能换码吗?”和“鞋子码数不合适能换吗?”在系统眼里完全是两句话,得写两条几乎一样的规则。
- 最惨的是,一旦并发飙到 200 TPS,后端 DSL 引擎就疯狂 GC,CPU 像火箭一样窜到 90%。
一句话:维护成本高、泛化能力差、性能瓶颈明显。于是,我们决定用 LLM 把“人工智障”升级成“人工智能”。
技术选型:为什么 SpringBoot + GPT-3.5 能打
先放对比表,直观感受:
| 模型 | 意图识别 F1 | 中文支持 | 调用延迟 | 成本/1000 次 |
|---|---|---|---|---|
| GPT-3.5-turbo | 0.94 | 原生 | 600 ms | 0.14 $ |
| Claude-3 | 0.92 | 需 prompt 中英混写 | 900 ms | 0.32 $ |
| 自研 6B | 0.86 | 完全中文 | 350 ms | 2 卡 A100 |
结论:GPT-3.5 在中文场景下 F1 最高,延迟可接受,成本也低。
框架侧,SpringBoot 3.x 已稳定支持虚拟线程(Project Loom),配合 WebFlux 能做到“异步非阻塞 + 注解式编程”两头甜,比 Vert.x 心智负担低,比 Django 生态更熟,Java 组直接上手。
核心实现:三步搭好骨架
1. 异步入口:Spring WebFlux
@RestController @RequestMapping("/bot") public class ChatController { private final ChatService chatService; public ChatController(ChatService chatService) { this.chatService = chatService; } @PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ServerSentEvent<String>> chat(@RequestBody ChatRequest req) { return chatService.streamAnswer(req) .map(ans -> ServerSentEvent.builder(ans).build()); } }全链路 Reactive,Tomcat 线程 0 阻塞,单机压测 TPS 直接翻倍。
2. 对话上下文 DTO
public class ChatContext { private String sessionId; // 会话标识 private Deque<Message> history; // 循环队列,长度=10 private Map<String, Object> slots; // 抽取的实体 private Instant expireAt; // TTL,默认 30 min }- 历史记录用
ArrayDeque,超过 10 条自动丢最老 的,防止 token 爆炸。 - 实体槽位存在用
Map<String,Object>,后续可对接 NLU 插件。 - 过期时间存 Redis,利用
KeyspaceNotification做自动清理。
3. 带重试的 OpenAI 客户端
/** * 调用 OpenAI ChatCompletion,自带指数退避重试 * 最大重试 3 次,首次延迟 200 ms,最大延迟 2 s */ @Component public class OpenAiClient { private final WebClient webClient = WebClient.builder() .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + System.getenv("OPENAI_KEY")) .build(); public Mono<String> chat(ChatContext ctx) { return webClient .post() .uri("https://api.openai.com/v1/chat/completions") .bodyValue(buildRequest(ctx)) .retrieve() .bodyToMono(OpenAiResponse.class) .map(r -> r.getChoices().get(0).getMessage().getContent()) .retryWhen(Retry.backoff(3, Duration.ofMillis(200)) .maxBackoff(Duration.ofSeconds(2)) .filter(this::isRetryable)); } private boolean isRetryable(Throwable t) { return t instanceof WebClientResponseException && ((WebClientResponseException) t).getStatusCode().is5xxServerError(); } }小提示:OpenAI 返回 429/500 时别急着重试,先退避,否则容易被限 IP。
性能优化:缓存 + 熔断双保险
1. Caffeine 本地缓存
高频“密码怎么重置?”这类标准 FAQ,命中率高达 68%,直接把 600 ms 的 LLM 调用省掉。
Cache<String, String> cache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(1, TimeUnit.HOURS) .build();2. Hystrix 熔断
LLM 抖动时,fallback 返回静态文案“客服忙,请稍后再试”,保证核心链路不雪崩。
@HystrixCommand(fallbackMethod = "fallbackAnswer") public Mono<String> askLlm(ChatContext ctx) { return openAiClient.chat(ctx); } private Mono<String> fallbackAnswer(ChatContext ctx) { return Mono.just("客服忙,请稍后再试"); }压测结果:线程池 20 核心,错误率>5% 时 5 s 内自动熔断,恢复时间 10 s。
避坑指南:token、敏感词与合规
1. token 长度限制
GPT-3.5 最大 4096 token,历史记录超长会直接报错。
解决思路:
- 循环队列只保留最近 10 轮。
- 用
tiktoken库提前计算,超长就摘要:history.removeFirst()。 - 对返回也做截断,设置
max_tokens=1000,留 500 token 给 prompt。
2. 敏感词过滤
用 AOP 拦截入站消息:
@Aspect @Component public class SensitiveFilterAspect { @Around("@annotation(SensitiveCheck)") public Object filter(ProceedingJoinPoint pjp) throws Throwable { String text = (String) pjp.getArgs()[0]; if (SensitiveWordHolder.contains(text)) { throw new BusinessException("输入包含敏感词"); } return pjp.proceed(); } }敏感词库每日离线更新,Hash 匹配 O(1)。
3. GDPR 合规日志
- 对话内容写进
chat_log表前,先 AES 加密,密钥放 KMS。 - 提供“一键导出”与“删除”接口,满足用户数据可携带权。
- 设置 30 天自动匿名化,把
sessionId做 SHA-256 哈希,原 ID 丢弃。
验证指标:JMeter 压测一览
测试环境:4C8G Docker 容器,MySQL 8.0,Redis 6.2,并发 500 线程,持续 5 min。
| 指标 | 平均值 | 95 线 | 99 线 | 错误率 |
|---|---|---|---|---|
| RT (ms) | 320 | 580 | 720 | 0.3 % |
| TPS | 510 | — | — | — |
CPU 占用 55 %,内存 3.2 G,GC 停顿 < 30 ms。
对比老系统 RT 1100 ms、错误率 3 %,提升肉眼可见。
延伸思考:多模态的下一站
- 语音接入:集成阿里一句话识别,把 ASR 文本直接送进现有
ChatContext,回答后用 TTS 回读,链路不变。 - 图像接入:用户拍商品吊牌,OCR 提取款号 → 查库存 → 返回“有货/缺货”。
- 视频客服:WebRTC 推流,LLM 实时生成字幕,辅助人工坐席快速回复。
想象一个场景:用户发语音“我买的这双鞋开胶了”,系统先语音转文字,再意图识别到“质量问题”,自动推送“退换货入口 + 附近门店地图”,全程 3 s 完成——这就是多模态的杀伤力。
写在最后的碎碎念
整套系统上线三个月,已接过 120 万次对话,把人工坐席从 60 人减到 15 人,运营成本降了 70 %。
LLM 不是银弹,但用对了场景,再配好 SpringBoot 的成熟“工具箱”,确实能让老系统“枯木逢春”。
下一步,我们打算把模型换成 GPT-4-turbo,再试试函数调用(Function Calling),让机器人直接帮用户改地址、发优惠券——如果踩到新坑,再来和大家分享。