ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Java与Spring Boot的聊天机器人数据查询系统实战解析

基于Java与Spring Boot的聊天机器人数据查询系统实战解析 简介软件杯竞赛项目基于Java的聊天机器人数据查询系统源码包面向正在做毕业设计和需要实战项目的Java、Android开发者。系统以Android为客户端采用MVVMRxJavaRetrofitGSON架构服务端基于SSM框架借助TensorFlow与Seq2Seq模型及图灵语料库训练出具备学习能力的机器人用户通过聊天指令即可完成企业数据查询。压缩包共1969个文件以class、java、xml源码和png界面资源为主包含66个jar依赖库、68个Python脚本、42个JSP页面以及可直接安装的apk和演示视频整体约276MB。已有245人学习下载。内含完整项目源码、说明文档及演示视频可直接用于课程设计、期末大作业或毕设答辩也可作为企业数据查询场景的参考实现。1. 软件杯项目里的 Java 聊天机器人数据查询系统它到底在解决什么问题软件杯这类竞赛里“基于 Java 的聊天机器人数据查询系统”是出镜率很高的选题——它看起来人人能做真到答辩和演示视频录制时翻车的比跑通的还多。难点不在聊天本身而在两处一是把用户随口说的自然语言翻译成可执行的查询条件二是保证翻译结果不会在数据层面出错。这篇文章按我自己的落地路径来讲清楚这套东西先定架构再跑通最小版本接着提升查询准确率最后把演示现场最容易踩的坑列出来。适合准备软件杯或课程设计的 Java 学生也适合想用 Java 快速搭一个内部数据问答原型的工程师。2. 架构与选型把“聊天”和“查询”拆成两件事2.1 为什么是 Java Spring Boot 而不是纯 Python先说结论这个系统用 Python 写聊天部分一天就能搞定但加上数据查询、权限控制、和已有业务系统的对接Python 方案很容易在运维和交付上失控。常见做法是用 Java Spring Boot 做服务端聊天机器人作为对话入口数据查询走 MyBatis 访问 MySQL关键词匹配加正则规则完成意图识别。整套链路全部跑在 JVM 里提交源码时只需要一个 Spring Boot 工程加一个 SQL 脚本演示时一台笔记本加一条网线就能起来。这一点对软件杯这类需要提交源码和演示视频的竞赛场景很重要。评委不关心模型多复杂关心的是能不能跑通、能不能解释、边界在哪。选 Java 的另外一层原因是大部分参赛者的课程体系里 Java 基础更扎实环境按 JDK 8/11 加 Maven 就能走通网上对应的 java 环境变量配置教程也很全。如果团队里有人熟 Python我的建议是让他负责离线阶段的语料整理和规则提炼不要让他把聊天核心写成 Python 服务再和 Java 对接——多一个中间服务就多一个演示时启动失败的可能。很多人会问我为什么不上大模型。原因很实际软件杯要求提交源码和演示视频演示现场的局域网不一定能访问外部的模型 API规则方案完全离线可跑而且每一次查询结果都能解释清楚来龙去脉。真到答辩被追问“这个分类结果怎么来的”你可以直接说哪条正则命中了哪个词而不是解释一个黑匣子。后面如果想换成模型只要保住 QueryParam 这个中间对象不变替换解析模块内部实现就行不影响其他层。2.2 对话到 SQL 的链路拆解意图识别、槽位抽取、结果格式化把整个流程画成一条流水线依次是接收消息、预处理分词和词性标注、意图识别、槽位抽取、查询参数组装、SQL 生成、执行查询、结果格式化、回复用户。其中最容易写坏的是槽位抽取和 SQL 生成之间的映射关系。很多人把用户输入直接拼进 SQL结果一句话里带个“比”“大于”就出语法错误或者查出远超预期的数据量。我的经验是把整条链路拆成两个黑匣子前一个负责把自然语言变成结构化的 QueryParam 对象后一个负责把 QueryParam 变成安全的 SQL。中间用对象传递不要用字符串传递。QueryParam 至少包含这几个字段查询维度按部门、按产品、按时间段、时间范围起止、聚合方式总数、平均值、最大值、过滤条件列表、返回条数上限。这样每个环节都能单独写单元测试也方便在后面做规则模板时快速定位是识别错了还是 SQL 拼错了。分词环节我一般用 HanLP 的默认标准分词器加上自定义词典。原因很简单它是纯 jar 依赖离线可用不像某些方案要单独起一个 Python 进程。人名、产品名、部门名放进自定义词典准确率能明显提升。要注意的是分词只是预处理不要把分词结果直接当槽位值一定要经过后面的词表和正则映射否则“研发部”被切成一截一截之后槽位抽取会乱掉。2.3 模块边界与接口约定把系统拆成四个可独立测试的工程这里给出一种经过验证的工程结构虽然它不是唯一方案但很适合软件杯这种需要把源码讲清楚的场景。我的习惯是拆成四个 Maven 模块父模块统一管理版本smart-query/ ├── pom.xml # 父模块统一依赖版本 ├── chatbot-core/ # 会话管理、消息收发、意图识别 ├── query-parser/ # 自然语言到 QueryParam 的转换 ├── query-executor/ # QueryParam 到 SQL 的执行层 └── web/ # Spring Boot 启动模块WebSocket 入口chatbot-core 不知道数据库里有什么表它只负责把用户消息转成一个语义对象query-parser 不碰 HTTP 和 WebSocket只做文本处理query-executor 只接收 QueryParam不让任何自然语言字符串进来。web 模块负责把三者串起来并暴露成服务。模块间的核心接口定义如下代码里我加了关键注释public interface QueryParser { /** * 把用户原话解析成查询参数。 * 解析失败时返回 null由上层决定走兜底回复。 */ QueryParam parse(String userInput); } public interface QueryExecutor { /** * 根据查询参数执行数据查询。 * result 统一为 ListMapString, Object避免和具体实体类耦合。 */ ListMapString, Object execute(QueryParam param); } public interface ChatService { // 收到用户消息返回回复文本内部串起 parser - executor - formatter String reply(String userId, String message); }参数说明QueryParser 返回 null 而不是抛异常是为了在规则匹配不到时能自然走兜底回复否则用户随便问一句“今天天气怎么样”系统就报错演示时很尴尬。QueryExecutor 统一返回 Map 列表是为了适配不同统计口径不需要为每张表写一个 DTO。这样的接口约定在团队协作时也清晰三个人分别做三个模块联调时只需要对齐 QueryParam 的字段名谁都不用等谁。3. 跑通最小可运行版本从空项目到第一个查询请求3.1 依赖与配置pom.xml 和 application.yml 的最终形态依赖列表要克制。很多新手把 spring-boot-starter-web 和 spring-boot-starter-webflux 同时引进来端口和容器冲突排查半天。我给出一份能直接启动的 pom 依赖清单其余注释掉的依赖先不要加dependencies !-- Web 服务基础 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- WebSocket 对话通道 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency !-- MyBatis 与 MySQL 驱动 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- HanLP 纯 Java 离线版用于分词和词性标注 -- dependency groupIdcom.hankcs/groupId artifactIdhanlp/artifactId versionportable-1.8.4/version /dependency /dependencies逻辑说明spring-boot-starter-websocket 必须和 starter-web 配合不要只引 websocket 而漏掉 web。MyBatis 版本 2.3.2 对应 Spring Boot 2.7.x如果用 Boot 3.x包名和 starter 坐标更换较大建议保守用 Boot 2.7.18。HanLP 的 portable 包体积小、离线可用自定义词典放在 resources 目录里由配置加载。application.yml 里最需要注意的是数据源连接参数和 MyBatis 的 map-underscore-to-camel-caseserver: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/smart_query?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true default-statement-timeout: 10参数说明characterEncodingutf8 必须写否则中文条件容易乱码serverTimezone 不配的话高版本 MySQL 驱动会报时区错误。default-statement-timeout 设成 10 秒演示时一旦出现慢查询后端能主动断开而不是让前端一直转圈。mapper-locations 指向 resources/mapper 目录后面动态 SQL 都放这里。3.2 WebSocket 对话入口建立会话、收消息、回消息选用 WebSocket 而不是 HTTP 轮询是因为聊天场景的请求密度高HTTP 接口每次都要重新建立连接演示视频里看起来也不够流畅。配置类里注册一个 /chat 端点前端小程序或网页连上后用 JSON 消息体交互。Configuration public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new ChatWebSocketHandler(), /chat) // 本地演示允许跨域上线前必须收紧 .setAllowedOrigins(*); } }public class ChatWebSocketHandler extends TextWebSocketHandler { private final ChatService chatService SpringUtils.getBean(ChatService.class); Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 约定消息格式{userId:u001,text:上个月各部门的平均销售额} JSONObject req JSON.parseObject(message.getPayload()); String reply chatService.reply(req.getString(userId), req.getString(text)); JSONObject resp new JSONObject(); resp.put(type, reply); resp.put(data, reply); session.sendMessage(new TextMessage(resp.toJSONString())); } }逻辑说明这里把 ChatService 通过自定义的 SpringUtils 拿出来是为了让 Handler 不被 Spring 容器管理也拿得到依赖。如果你不想用工具类就把 ChatWebSocketHandler 注册成 Component用构造器注入 ChatService更符合 Spring 的习惯。参数说明setAllowedOrigins(*) 在本地演示最省事但上线前必须改成实际域名消息体里的 userId 先透传后面的行级权限就靠它来过滤数据范围。3.3 最小意图识别关键词表与优先级策略最小可用版本不需要机器学习用一张关键词表就能覆盖大部分查询意图。我把意图分成四类统计查询、趋势查询、明细查询、闲聊。分类依据是用户句子里的动词和疑问词实现一个 IntentClassifierpublic class IntentClassifier { // 命中即返回意图的强规则模式用 LinkedHashMap 保证顺序 private static final MapString, IntentType STRONG_RULES new LinkedHashMap(); static { STRONG_RULES.put(趋势|走势|变化|环比|同比, IntentType.TREND); STRONG_RULES.put(平均|均值|总计|总数|合计|查询|统计, IntentType.STATISTIC); STRONG_RULES.put(明细|列表|详情|哪些|分别|每, IntentType.DETAIL); } public static IntentType classify(String sentence) { // 强规则优先遍历顺序即优先级 for (Map.EntryString, IntentType entry : STRONG_RULES.entrySet()) { if (Pattern.compile(entry.getKey()).matcher(sentence).find()) { return entry.getValue(); } } return IntentType.CHITCHAT; } }逻辑说明LinkedHashMap 保证匹配顺序可控“趋势”和“统计”同时出现时先命中趋势这是为了让“统计最近三个月销售趋势”这样的句子不会走错分支。参数说明正则里的竖线是“或”关系不建议把规则写得太长超过四五个词就拆成两条方便排查“哪条规则误伤了”。比如“平均”和“均值”保留一个就行保留太多同义词反而容易和“平均值排序”这种句子冲突。3.4 用 MyBatis 动态 SQL 把查询条件落到 MySQL最后一个节点是执行查询。这里强调一点不要让 SQL 里的表名和排序字段走 #{} 参数MyBatis 的 #{} 会加引号表名带引号直接报错。常见做法是白名单映射后拼接或者用 ${} 并在传入前做严格校验。值过滤全部走 #{}这是底线。select idselectStatistic resultTypemap SELECT COUNT(*) AS total_count, IFNULL(AVG(sale_amount), 0) AS avg_amount FROM sale_record where if teststartTime ! null AND sale_date gt; #{startTime} /if if testendTime ! null AND sale_date lt; #{endTime} /if if testdeptName ! null and deptName ! AND dept_name #{deptName} /if /where /select逻辑说明 标签会自动去掉多余的 AND避免“WHERE and”这类低级 SQL 错误。时间范围用大于等于和小于等于保证边界日期不丢数据。参数说明startTime 和 endTime 在 service 层必须格式化成年月日否则传入“2024-1-1”这种写法MySQL 的隐式转换会把索引用废掉演示数据量一大就慢。如果查询目标是“最近7天”endTime 就是今天startTime 是今天减 6 天。4. 把查询准确率提上去自然语言到查询条件的规则模板设计4.1 槽位识别时间范围、统计维度、统计口径的正则与词表槽位抽取是整条链路里最值得花时间的部分。时间我拆成绝对时间和相对时间绝对时间支持“2024年3月”相对时间支持“上周”“最近7天”“上个月”。实现上正则负责格式化的日期词表加日期计算负责相对时间。public class SlotExtractor { private static final Pattern YEAR_MONTH Pattern.compile((\\d{4})年(\\d{1,2})月); private static final Pattern RECENT_DAYS Pattern.compile(最近(\\d{1,3})天); public static void extract(String sentence, QueryParam param) { Matcher ym YEAR_MONTH.matcher(sentence); if (ym.find()) { param.setStartTime(ym.group(1) - ym.group(2) -01); param.setEndTime(ym.group(1) - ym.group(2) -31); } Matcher rd RECENT_DAYS.matcher(sentence); if (rd.find()) { // 最近7天包含今天所以减 6 天 int days Integer.parseInt(rd.group(1)); param.setStartTime(LocalDate.now().minusDays(days - 1).toString()); param.setEndTime(LocalDate.now().toString()); } } }参数说明月份结束日期写死 31 是故意的查询时用小于等于 31 能自然兼容 30 天和 2 月的特殊情况不用额外做闰年判断。相对时间的“最近7天”我按包含今天来算如果需求要排除今天改成 minusDays(days) 就行。这个口径在答辩时一定会被问到提前想好说法。统计维度部门、产品、客户用自定义词典解决。词典文件一行一个词HanLP 加载后分词结果就会把这些词当成整体避免“研发部”被切分成“研发”和“部”。注意词典文件必须 UTF-8 编码Windows 记事本默认带 BOM会导致词典加载成乱码这里提前打个预防针。4.2 模板匹配的优先级与兜底策略槽位抽完下一步是决定怎么聚合。我的做法是定义若干查询模板每个模板声明自己支持的槽位和输出列。匹配时按“槽位完整度”排序能匹配的槽位越多优先级越高这样“2024年3月每个部门的销售总额”会先匹配部门维度模板而不是时间维度模板。public class TemplateMatcher { private final ListQueryTemplate templates Arrays.asList( new DeptStatisticTemplate(), // 按部门统计 new ProductStatisticTemplate(), // 按产品统计 new TimeTrendTemplate() // 按时序趋势 ); public QueryTemplate match(QueryParam param) { return templates.stream() .filter(t - t.supports(param)) .max(Comparator.comparingInt(t - t.slotScore(param))) .orElse(new FallbackTemplate()); } }逻辑说明slotScore 统计的是这个模板能消费的槽位数量比如参数里既有部门又有时间DeptStatisticTemplate 两个都消费得分就比只消费时间的 TimeTrendTemplate 高。兜底模板不执行 SQL回复“这个问题我还没学会换个问法试试”同时把原始输入打印到日志留着扩充规则。这套策略不是万能的但覆盖竞赛演示场景足够。多问句拆解也是这里要做的事。用户说“2024年3月和4月的销售总额分别是多少”规则模板不能直接处理。常见做法是先用句号或“和”把句子切成两个子句逐句走一遍抽取再把多个 QueryParam 合并成一次对比查询。字段映射和结果显示都会复杂一截建议放到进阶版本做先保住单句查询的准确率。4.3 查询权限与白名单规则系统的最后一道防线规则系统再准也不能让用户问出“把所有用户密码查出来”。实际系统的做法是列一个可查询字段白名单任何槽位值必须经过映射表转换成数据库字段名映射表里没有的字段直接拒绝不让它进入 SQL 拼接环节。public class FieldWhitelist { private static final MapString, String FIELD_MAP new HashMap(); static { FIELD_MAP.put(部门, dept_name); FIELD_MAP.put(产品, product_name); FIELD_MAP.put(销售额, sale_amount); FIELD_MAP.put(订单数, order_count); // 不在映射表里的词一律不允许进入 SQL } public static String resolve(String slotValue) { String field FIELD_MAP.get(slotValue); if (field null) { throw new IllegalSlotException(字段不在白名单: slotValue); } return t. field; // 强制加表别名防止多表 join 时字段歧义 } }逻辑说明resolve 返回的字段加了表别名 t.这样 join 多表时不会因为两个表都有 id 字段而报字段歧义。参数说明白名单要在需求评审阶段就让业务方确认不要开发到一半再补否则演示时临时加字段容易把 SQL 写错。捕获 IllegalSlotException 后返回统一提示“这个字段暂不支持查询”比抛 500 更体面。5. 避坑与常见问题排查演示前最容易翻车的四个点5.1 WebSocket 连接建立后频繁掉线现象前端连上 /chat 接口发第一条消息能收到回复十几秒后连接断开刷新页面又能用。原因WebSocket 没有心跳机制浏览器或中间层的空闲连接超时会把会话断开。解决前端每隔 30 秒发一个 ping后端收到 ping 回一个 pong同时后端给会话设置空闲超时。如果不想自己维护心跳可以开启 SockJSregistry.addHandler(new ChatWebSocketHandler(), /chat) .setAllowedOrigins(*) .withSockJS();注意开了 withSockJS()前端就不能用原生 WebSocket 直连必须引入 sockjs-client 库。如果前端只想用原生 WebSocket不要开这个开关后端用定时任务轮询 session 是否仍然打开超过 90 秒没消息就主动发一个空包维持连接。心跳间隔设置的原则是小于中间层超时时间的一半直播演示网络抖动时不会被打断。5.2 中文分词把部门名切成碎片现象输入“研发部的销售额”分词结果是“研发/部/的/销售/额”槽位抽取把“部”当成噪音department 字段为空查询直接退化成全表统计。原因HanLP 默认模型根本不认识项目里的专有名词。解决在 resources 下放自定义词典文件并在 HanLP.properties 里指定路径。词典格式一行一个词不要带词性标注例如研发部 产品一部 华东大区注意词典文件必须是 UTF-8 无 BOM 编码IDEA 默认没问题Windows 记事本另存为时容易带 BOM加载时词条变成乱码分词效果比不加载还差。改完词典要清掉 HanLP 的缓存目录再重启否则旧词表还在生效这个问题经常让人误以为配置没写上。5.3 动态 SQL 的 #{} 与 ${} 混用现象按时间过滤正常按表名拼接报错或者代码审查时被指出有 SQL 注入风险。原因表名和 ORDER BY 字段不能走 #{}它会把参数当字符串加引号但直接用 ${} 拼接用户输入就是注入漏洞。解决表名和排序字段走白名单映射值过滤全走 #{}。我见过一个翻车案例同事把 ${} 用在 like 查询里用户输入“%’ or 11 -- ”整个用户表被查出来了。凡是用户输入一律先过白名单再拼 SQL这条规则没有任何例外。如果你要支持按不同字段排序别直接拼接列名先取白名单里的 key再映射成“表名.字段名”最后拼进 ORDER BY。这样即使传入的排序词是“销售额”落到 SQL 里也是 t.sale_amount而不是用户写的任意字符串。另外MyBatis 日志里默认不打印完整 SQL排查拼装问题时把 mapper 接口所在包的日志级别调成 DEBUG看到的才是真实执行语句。5.4 演示视频录制时查询卡顿现象“最近一年销售明细”需要分页查询演示时一次请求等五秒页面空白视频录出来很难看。原因深分页在 MySQL 里要扫描前面所有行数据量到几十万就明显变慢。解决分页用游标方式或者限制单次返回条数。演示前先跑一遍慢日志把查询超时配好超时直接返回友好提示别让前端一直转圈。实践里我见过最简单的有效做法是两层限制if testlimit ! null and limit gt; 0 LIMIT #{limit} /if把 limit 上限写死在查询参数里最大 1000 条。所有明细查询必须带时间范围否则直接拒绝。这样即使评委现场输入一句“查询全部销售记录”后端也只返回最近一段时间的部分数据不会因为全表扫描把数据库拖垮。视频录制前把演示库里造的数据控制在 5 万行以内索引建好查询基本都在 200 毫秒内返回画面流畅度完全不一样。6. 验收与进阶用测试集、压测和行级权限让系统更耐看系统跑通只是第一关软件杯答辩时评委最爱问的是“准确率多少”“并发怎么样”“不同身份看到的数据一样吗”。所以验收不能只靠试几条问句要建立一个小的测试集准备 20 到 30 条有代表性的问句人工标注标准答案跑一遍记录意图识别准确率、槽位抽取准确率和最终查询结果一致率。下表是我常用的记录格式测试问句期望意图实际意图槽位完整结果一致上个月各部门平均销售额STATISTICSTATISTIC是是最近7天产品一部的订单数STATISTICSTATISTIC是是研发部的销售趋势如何TRENDDETAIL否否表格里第三行就是典型失败案例句子带“趋势”但没写“统计”被 DETAIL 抢走了。这类问题通常在测试集跑完一轮之后集中暴露比现场演示时发现要划算得多。压测也别忽视用 JDK 自带的并发工具模拟几十个用户同时发消息观察响应时间分布和有没有线程异常。抢答类问题取决于执行线程池的设置Spring Boot 默认线程池在聊天场景下够用但如果查询里锁表几十个请求就可能把连接池占满。进阶方向我给两个建议。第一个是行级权限把 userId 传进 QueryParam在 SQL 里追加“AND dept_id 当前用户所属部门ID”这样销售经理和普通员工查同一句话结果范围不同答辩时这是一个很加分的点。第二个是查询审计把每次查询的原始问句、最终 SQL、耗时写进日志表这既是答辩素材也是后续优化规则的第一手语料。我最早做这套系统时把精力全放在规则引擎上结果演示时 WebSocket 掉线差点收不了场。后来把三成时间拿出来做心跳、超时、白名单这些不起眼的基础能力演示才真正顺下来。坑总是出在最容易被忽略的地方希望帮到你。本文还有配套的精品资源点击获取
返回列表