基于Spring Boot的智能客服问答系统:架构设计与工程实践

📅 发布时间:2026/7/11 14:58:36 👁️ 浏览次数:
基于Spring Boot的智能客服问答系统:架构设计与工程实践
最近在做一个智能客服问答系统的项目从零到一踩了不少坑也积累了一些心得。传统客服系统依赖人工配置的规则用户问题稍微变个说法可能就匹配不上响应慢、扩展性差维护成本还高。这次我们基于 Spring Boot 来构建目标是实现一个意图识别准、响应快、能支撑高并发的智能系统。下面就把整个设计思路和工程实践过程梳理一下希望能给有类似需求的同学一些参考。1. 为什么不用现成框架技术选型的思考市面上其实有不少成熟的对话机器人框架比如开源的 Rasa还有云服务商提供的 DialogFlow。在做技术选型时我们主要从几个维度进行了对比Rasa功能强大NLU和对话管理一体但部署和定制相对复杂对中文社区的支持和性能调优需要投入较多精力。DialogFlow谷歌的产品上手快但属于黑盒服务数据安全、定制化程度和长期成本是主要顾虑。自研方案基于 Spring Boot TensorFlow Serving。选择这条路主要是考虑到团队技术栈以 Java 为主Spring Boot 的生态和微服务治理能力成熟。同时将核心的 NLP 模型如意图识别通过 TensorFlow Serving 独立部署既能利用其高性能的模型服务能力又能通过 Spring Boot 灵活构建业务逻辑和集成其他组件如知识图谱、缓存等在自主可控性、性能优化和系统集成度上找到了平衡点。最终我们决定采用微服务架构将系统拆分为对话网关、意图识别服务、对话管理服务、知识检索服务等核心的智能部分交给专业的 TensorFlow Serving。2. 系统核心架构与实现细节整个系统的架构图大致如下想象一下用户请求首先到达基于 Spring WebFlux 的异步网关然后路由到意图识别服务识别出的意图和实体再驱动对话状态机最终从知识库Neo4j或缓存中获取答案返回。下面分模块看看关键实现。2.1 异步对话接口Spring WebFlux 的应用为了应对高并发对话入口接口我们使用了 Spring WebFlux 来实现非阻塞 IO。相比传统的 Spring MVC它在处理大量并发连接时资源消耗更低。import org.springframework.web.bind.annotation.*; import org.springframework.http.MediaType; import reactor.core.publisher.Mono; RestController RequestMapping(/api/v1/chat) public class ChatController { PostMapping(value /query, produces MediaType.APPLICATION_JSON_VALUE) public MonoChatResponse handleQuery(RequestBody ChatRequest request) { // 1. 异步调用意图识别服务 return intentService.asyncRecognize(request.getQuery()) .flatMap(intentResult - { // 2. 根据意图异步处理对话逻辑 return dialogService.process(intentResult, request.getSessionId()); }) .onErrorResume(e - { // 3. 异常处理返回兜底回复 return Mono.just(ChatResponse.defaultFallback()); }); } }2.2 意图识别服务BERT模型的服务化这是系统的“大脑”。我们使用 BERT 预训练模型进行微调来做意图分类。模型训练完成后通过 TensorFlow Serving 部署。Spring Boot 服务通过 gRPC 客户端调用它。import org.tensorflow.framework.DataType; import org.tensorflow.framework.TensorProto; import org.tensorflow.framework.TensorShapeProto; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import tensorflow.serving.Predict; import tensorflow.serving.PredictionServiceGrpc; Service public class IntentRecognitionService { private PredictionServiceGrpc.PredictionServiceBlockingStub stub; PostConstruct public void init() { ManagedChannel channel ManagedChannelBuilder.forAddress(tf-serving-host, 8500) .usePlaintext() .build(); stub PredictionServiceGrpc.newBlockingStub(channel); } public IntentResult recognize(String query) { // 文本预处理分词、转换为ID序列等 ListInteger inputIds textPreprocessor.process(query); // 构建请求 Tensor TensorProto.Builder tensorBuilder TensorProto.newBuilder() .setDtype(DataType.DT_INT32) .setTensorShape(TensorShapeProto.newBuilder() .addDim(TensorShapeProto.Dim.newBuilder().setSize(1)) .addDim(TensorShapeProto.Dim.newBuilder().setSize(inputIds.size())) .build()); for (Integer id : inputIds) { tensorBuilder.addIntVal(id); } Predict.PredictRequest request Predict.PredictRequest.newBuilder() .setModelSpec(ModelSpec.newBuilder().setName(intent_bert).setSignatureName(serving_default)) .putInputs(input_ids, tensorBuilder.build()) .build(); // 调用预测 Predict.PredictResponse response stub.predict(request); // 解析响应获取概率最高的意图标签 MapString, TensorProto outputs response.getOutputsMap(); TensorProto logitsProto outputs.get(intent_logits); // ... 解析logitsProto得到意图ID和置信度 int predictedIntentId // 解析逻辑; float confidence // 解析逻辑; return new IntentResult(predictedIntentId, getIntentLabel(predictedIntentId), confidence); } }2.3 知识存储与检索Spring Data Neo4j对于产品问答、故障排查这类结构化知识我们使用 Neo4j 图数据库来存储。利用 Spring Data Neo4j可以很方便地进行 ORM 操作。首先定义实体节点和关系import org.springframework.data.neo4j.core.schema.*; Node(FAQ) Data public class FaqNode { Id GeneratedValue private Long id; Property(question) private String question; Property(answer) private String answer; Property(category) private String category; } Node(Solution) Data public class SolutionNode { Id GeneratedValue private Long id; private String title; private String steps; } // 可以定义关系例如 (:FAQ)-[:BELONGS_TO]-(:Category)然后通过 Repository 进行查询import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Query; import org.springframework.data.repository.query.Param; public interface KnowledgeRepository extends Neo4jRepositoryFaqNode, Long { Query(MATCH (f:FAQ) WHERE f.question CONTAINS $keyword RETURN f ORDER BY f.question LIMIT 5) ListFaqNode findFaqByKeyword(Param(keyword) String keyword); // 更复杂的图谱查询例如通过关系寻找解决方案 Query(MATCH (f:FAQ {id: $faqId})-[:RELATED_TO]-(s:Solution) RETURN s) ListSolutionNode findRelatedSolutions(Param(faqId) Long faqId); }3. 性能优化实战缓存与压测系统上线性能是关键。我们主要做了两件事引入本地缓存和进行全面的压力测试。3.1 使用 Caffeine 缓存高频问答很多用户问的是热门问题比如“怎么重置密码”、“运费多少”。这些问题的答案相对固定每次都要走一遍完整的 NLP 和知识检索流程太浪费。我们在对话管理服务中引入了 Caffeine 本地缓存。import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; Component public class ResponseCacheManager { // 缓存Key为问题文本的MD5Value为标准答案 private CacheString, String qaCache Caffeine.newBuilder() .maximumSize(10000) // 缓存最大条目 .expireAfterWrite(10, TimeUnit.MINUTES) // 写入10分钟后过期 .recordStats() // 记录统计信息 .build(); public String getCachedAnswer(String questionHash) { return qaCache.getIfPresent(questionHash); } public void putAnswer(String questionHash, String answer) { qaCache.put(questionHash, answer); } // 可以定期打印缓存命中率用于监控 public CacheStats getStats() { return qaCache.stats(); } }3.2 压力测试与结果分析使用 JMeter 进行压测是必不可少的环节。我们模拟了用户并发提问的场景。JMeter 配置线程组设置 500 个线程在 30 秒内启动循环 100 次。HTTP 请求指向我们的/api/v1/chat/query接口Body Data 中放入 JSON 格式的典型用户问题。监听器添加聚合报告、查看结果树、响应时间图等。测试结果与分析在未开启缓存的情况下系统在 5000 QPS 的压力下平均响应时间在 120ms 左右P95 响应时间约 250ms。瓶颈主要在意图识别服务的 gRPC 调用和 Neo4j 查询。开启 Caffeine 缓存高频问答后我们预设了约 1000 个高频问题同样压力下平均响应时间直接降到了 35msP95 在 80ms 以内吞吐量显著提升。缓存命中率稳定在 65%-70%。这个结果验证了缓存策略的有效性也说明对于智能客服“智能”与“效率”需要结合并非所有请求都需要动用最重的模型。4. 开发中的“坑”与安全考量4.1 避坑指南对话状态管理的内存泄漏最初我们用ConcurrentHashMap在内存中保存用户对话上下文session。运行一段时间后发现内存持续增长。原因是有些会话结束后没有及时清理。后来引入了带有过期时间的缓存如 Guava Cache 或 Caffeine来管理会话状态并设置了合理的 TTL如30分钟无活动则清除问题得以解决。多轮对话上下文的设计陷阱设计上下文数据结构时一开始只简单保存了最近几轮问答。但当用户问题涉及指代如“它”、“上面说的那个”时系统就懵了。后来我们改进了上下文对象不仅保存历史对话还额外保存了上一轮识别出的关键“实体”如产品名、订单号并在新一轮识别时将这些实体作为特征一并输入模型显著提升了多轮对话的连贯性。4.2 安全考量用户输入过滤用户什么都会输入必须防范注入攻击和恶意脚本。我们在网关层对用户输入的文本进行了基本的正则过滤剔除明显的 SQL 注入、JavaScript 代码片段等危险字符。但这只是第一道防线更复杂的语义攻击需要在 NLP 模型层面有更强的鲁棒性。对话日志脱敏对话日志对于优化模型至关重要但里面可能包含手机号、身份证号等敏感信息。我们在日志存储前增加了一个脱敏处理环节使用正则表达式匹配并替换这些敏感信息为***既满足了审计和训练需求又符合数据安全规范。5. 总结与思考通过这个项目我们成功构建了一个响应迅速、意图识别准确的智能客服系统。Spring Boot 提供了稳固的微服务底座TensorFlow Serving 承载了核心的 AI 能力Neo4j 让知识关联查询变得高效再加上 Caffeine 缓存这把“快刀”整体性能达到了预期。最后抛出一个我们在项目中持续思考的开放性问题如何平衡模型精度与响应延迟用更大的模型如 BERT-Large固然能提升几个点的准确率但推理时间也成倍增加。在线上系统中我们最终选择了 BERT-Base 模型并通过知识蒸馏训练了一个更小的“学生模型”用于对延迟要求极高的场景。未来模型量化、剪枝以及使用更高效的 Attention 机制如 Linformer可能是我们继续探索的方向。毕竟在真实的用户体验面前有时 100ms 的延迟差异比 1% 的精度提升感受更明显。这其中的权衡永远值得深入琢磨。