Commit 06ca6a052f1445dcab76b663dd8a6bbf6c57a07a
1 parent
9a985fca
AI 对于时间的处理
Showing
3 changed files
with
175 additions
and
11 deletions
src/main/java/com/xly/agent/DynamicTableNl2SqlAiAgent.java
| ... | ... | @@ -19,7 +19,7 @@ public interface DynamicTableNl2SqlAiAgent { |
| 19 | 19 | */ |
| 20 | 20 | @SystemMessage(""" |
| 21 | 21 | 你是资深MySQL数据分析师,严格遵循以下**通用规则**生成SQL,适用于所有业务场景: |
| 22 | - 1. 语法规范:仅生成符合MySQL8.0/5.7的标准SELECT语句,兼容低版本,多表关联用JOIN而非逗号; | |
| 22 | + 1. 语法规范:仅生成符合MySQL8.0.36的标准SELECT语句,兼容低版本,多表关联用JOIN而非逗号; | |
| 23 | 23 | 2. 输出格式:仅返回SQL语句本身,无任何解释、换行、```sql/```包裹、备注、多余空格,直接输出可执行SQL; |
| 24 | 24 | 3. 编写规范: |
| 25 | 25 | 3.1 多表关联必须使用 表名+字段名(如表名.字段名),严格按下面[涉及表名]中的表次序关联,聚合函数(SUM/COUNT/AVG/MIN/MAX)必须加业务化别名,日期过滤使用标准DATE格式(yyyy-MM-dd); |
| ... | ... | @@ -75,6 +75,83 @@ public interface DynamicTableNl2SqlAiAgent { |
| 75 | 75 | @V("userInput") String userInput); |
| 76 | 76 | |
| 77 | 77 | /** |
| 78 | + * SQL错误重试引导提示词 | |
| 79 | + * 当第一次生成的SQL执行错误时,将错误信息传入,让AI重新生成 | |
| 80 | + */ | |
| 81 | + /** | |
| 82 | + * 动态表结构:自然语言转MySQL SELECT语句 | |
| 83 | + * 入参:数据库名、表名(多表用,分隔)、表结构、用户查询 | |
| 84 | + */ | |
| 85 | + @SystemMessage(""" | |
| 86 | + 你是资深MySQL数据分析师,严格遵循以下**通用规则**生成SQL,适用于所有业务场景: | |
| 87 | + 1. 语法规范:仅生成符合MySQL8.0.36的标准SELECT语句,兼容低版本,多表关联用JOIN而非逗号; | |
| 88 | + 2. 输出格式:仅返回SQL语句本身,无任何解释、换行、```sql/```包裹、备注、多余空格,直接输出可执行SQL; | |
| 89 | + 3. 编写规范: | |
| 90 | + 3.1 多表关联必须使用 表名+字段名(如表名.字段名),严格按下面[涉及表名]中的表次序关联,聚合函数(SUM/COUNT/AVG/MIN/MAX)必须加业务化别名,日期过滤使用标准DATE格式(yyyy-MM-dd); | |
| 91 | + 3.2 SQL所有字段均采用 表名.字段名 方式生成,务必确保 字段名 在相应的 表名 描述的字段中存在,如果不存在重试其它方式,直到满足条件; | |
| 92 | + 3.3 SQL所有字段涉及的所有表名,都要**严格**按下面[涉及表名]中的表次序关联,没有关联不允许使用; | |
| 93 | + 3.4 SQL所有的查询条件,如果是字符类型的字段,均需要加不为空判断,用示例格式判断,示例:ifnull(customername,'')<>''; | |
| 94 | + 3.5 SQL所有的查询条件,如果是日期类型的字段,均需要加不为空判断,用示例格式判断,示例:tmakedate is not Null; | |
| 95 | + 3.6 SQL所有的显示字段的别名中,不能出现空格,如: tCreateDate as earliest 订单日期,正确的应是 tCreateDate as earliest订单日期 | |
| 96 | + 4. 安全约束:禁止生成任何DDL/DML语句(DROP/ALTER/INSERT/UPDATE/DELETE等),禁止使用子查询、存储过程、自定义函数、临时表; | |
| 97 | + 5. 精准性: | |
| 98 | + 5.1 严格按用户需求+传入的表结构生成,仅使用指定字段/表,无多余字段、无无效表关联、无冗余过滤条件; | |
| 99 | + 5.2 用户需求中没有明确的日期条件,默认为全部数据,禁止增加任何日期过滤条件 | |
| 100 | + 6. 关联规则:多表关联时,必须使用外键/业务唯一键关联,禁止无意义关联。 | |
| 101 | + 7. 当前时间:{{sDataNow}} | |
| 102 | + 8. 时间处理规则: | |
| 103 | + 8.1 当前系统时间:{{sDataNow}}(格式:yyyy年MM月dd日HH时mm分ss秒) | |
| 104 | + 8.2 用户需求中的相对时间概念,必须基于{{sDataNow}}进行转换: | |
| 105 | + - "本年" → 当前年份:{{sDataNow}}的年份 | |
| 106 | + - "本月" → 当前月份:{{sDataNow}}的年份和月份 | |
| 107 | + - "本季度" → 当前季度:基于{{sDataNow}}计算 | |
| 108 | + - "本日/今天" → {{sDataNow}}的具体日期 | |
| 109 | + - "昨天" → {{sDataNow}}减1天 | |
| 110 | + - "本周" → 基于{{sDataNow}}计算周一到周日 | |
| 111 | + - "近7天" → {{sDataNow}}减7天到{{sDataNow}} | |
| 112 | + 8.3 示例转换: | |
| 113 | + 当前时间:2024-03-15 | |
| 114 | + 用户说"查询本年数据" → 查询条件应为:YEAR(日期字段) = 2024 | |
| 115 | + 用户说"查询本月数据" → 查询条件应为:YEAR(日期字段) = 2024 AND MONTH(日期字段) = 3 | |
| 116 | + 8.4 如果用户需求中没有明确的时间条件,禁止增加任何时间过滤条件 | |
| 117 | + """) | |
| 118 | + @UserMessage(""" | |
| 119 | + 【业务场景表结构信息】 | |
| 120 | + 涉及表名:{{tableNames}}(多表用,分隔,需关联时请按规范使用JOIN) | |
| 121 | + 表结构详情:{{tableStruct}}(多表请标注表名+字段,格式:表名(字段1:类型,字段2:类型,主键/外键)) | |
| 122 | + 当前时间:{{sDataNow}} | |
| 123 | + 【原始用户需求】 | |
| 124 | + {{userInput}} | |
| 125 | + 请根据上述表结构+通用规则,生成符合要求的MySQL SELECT语句; | |
| 126 | + 【之前生成的错误SQL】 | |
| 127 | + {{errorSql}} | |
| 128 | + 【执行错误信息】 | |
| 129 | + {{errorMessage}} | |
| 130 | + 【错误分析指引】 | |
| 131 | + 1. 错误类型:请根据错误代码判断 | |
| 132 | + - "Unknown column":字段不存在,检查字段名拼写或改用表中存在的字段 | |
| 133 | + - "Table doesn't exist":表名错误,检查表名拼写 | |
| 134 | + - "You have an error in your SQL syntax":语法错误,检查关键词、括号、引号 | |
| 135 | + - "Column not found in ON clause":JOIN条件字段不存在 | |
| 136 | + - "Non unique table/alias":表别名重复 | |
| 137 | + | |
| 138 | + 2. 修复建议: | |
| 139 | + - 如果是字段错误:查看表结构{{tableStruct}},找到正确的字段名替换 | |
| 140 | + - 如果是语法错误:检查SELECT、FROM、WHERE、JOIN等关键词用法 | |
| 141 | + - 如果是类型错误:字符串加单引号,数字不加引号,日期用'yyyy-MM-dd'格式 | |
| 142 | + - 如果是关联错误:确保所有表都通过外键正确JOIN | |
| 143 | + 请根据以上信息,重新生成正确的MySQL SELECT语句。 | |
| 144 | + 只返回SQL语句本身,不要任何解释和包装。 | |
| 145 | + """) | |
| 146 | + String regenerateSqlWithError(@MemoryId String userId, | |
| 147 | + @V("tableNames") String tableNames, | |
| 148 | + @V("tableStruct") String tableStruct, | |
| 149 | + @V("sDataNow") String sDataNow, | |
| 150 | + @V("userInput") String userInput, | |
| 151 | + @V("errorSql") String errorSql, | |
| 152 | + @V("errorMessage") String errorMessage | |
| 153 | + ); | |
| 154 | + /** | |
| 78 | 155 | * 动态表结构:自然语言解释SQL执行结果 |
| 79 | 156 | * 入参:用户问题、执行的SQL、表结构、JSON格式结果 |
| 80 | 157 | */ | ... | ... |
src/main/java/com/xly/service/XlyErpService.java
| ... | ... | @@ -18,6 +18,7 @@ import com.xly.exception.sqlexception.SqlGenerateException; |
| 18 | 18 | import com.xly.mapper.ToolMetaMapper; |
| 19 | 19 | import com.xly.runner.AppStartupRunner; |
| 20 | 20 | import com.xly.tool.DynamicToolProvider; |
| 21 | +import com.xly.util.EnhancedErrorGuidance; | |
| 21 | 22 | import com.xly.util.InputPreprocessor; |
| 22 | 23 | import com.xly.util.SqlValidateUtil; |
| 23 | 24 | import com.xly.util.ValiDataUtil; |
| ... | ... | @@ -26,6 +27,7 @@ import dev.langchain4j.data.message.AiMessage; |
| 26 | 27 | import dev.langchain4j.model.chat.ChatLanguageModel; |
| 27 | 28 | import dev.langchain4j.model.ollama.OllamaChatModel; |
| 28 | 29 | import dev.langchain4j.service.AiServices; |
| 30 | +import dev.langchain4j.service.V; | |
| 29 | 31 | import jnr.ffi.annotations.In; |
| 30 | 32 | import lombok.RequiredArgsConstructor; |
| 31 | 33 | import lombok.extern.slf4j.Slf4j; |
| ... | ... | @@ -59,7 +61,6 @@ public class XlyErpService { |
| 59 | 61 | public final static Integer maxTollRetries = 1; |
| 60 | 62 | |
| 61 | 63 | |
| 62 | - | |
| 63 | 64 | /*** |
| 64 | 65 | * @Author 钱豹 |
| 65 | 66 | * @Date 19:18 2026/1/27 |
| ... | ... | @@ -150,7 +151,7 @@ public class XlyErpService { |
| 150 | 151 | && ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName()) |
| 151 | 152 | && ObjectUtil.isNotEmpty(session.getCurrentTool().getSStructureMemo())) |
| 152 | 153 | ){ |
| 153 | - sResponMessage = getDynamicTableSql(session, input, userId, userInput,0); | |
| 154 | + sResponMessage = getDynamicTableSql(session, input, userId, userInput,0,StrUtil.EMPTY,StrUtil.EMPTY); | |
| 154 | 155 | } |
| 155 | 156 | //如果返回空的进入闲聊模式 |
| 156 | 157 | if (ObjectUtil.isEmpty(sResponMessage)){ |
| ... | ... | @@ -196,18 +197,24 @@ public class XlyErpService { |
| 196 | 197 | * @return java.lang.String |
| 197 | 198 | * @Description 获取执行动态SQL |
| 198 | 199 | **/ |
| 199 | - private String getDynamicTableSql(UserSceneSession session,String input,String userId,String userInput,Integer attempt){ | |
| 200 | + private String getDynamicTableSql(UserSceneSession session,String input,String userId,String userInput,Integer attempt,String errorSql,String errorMessage ){ | |
| 200 | 201 | String resultExplain = "信息模糊,请提供更具体的问题或指令"; |
| 201 | 202 | try{ |
| 202 | 203 | while (attempt < maxRetries) { |
| 203 | 204 | try{ |
| 204 | 205 | attempt = attempt+1; |
| 205 | - return getDynamicTableSqlExec( session, input, userId, userInput); | |
| 206 | + return getDynamicTableSqlExec(session, input, userId, userInput,errorSql,errorMessage); | |
| 206 | 207 | }catch (Exception e){ |
| 208 | + String erroMsg = e.getMessage(); | |
| 209 | + String errorSqlOld = StrUtil.EMPTY; | |
| 210 | + if(erroMsg.contains(EnhancedErrorGuidance.splitString) && erroMsg.split(EnhancedErrorGuidance.splitString).length>1){ | |
| 211 | + errorSqlOld = erroMsg.split(EnhancedErrorGuidance.splitString)[1]; | |
| 212 | + } | |
| 213 | + String errorMessageOld = EnhancedErrorGuidance.getErrorGuidance(erroMsg); | |
| 207 | 214 | if (attempt == maxRetries) { |
| 208 | - return resultExplain; | |
| 215 | + return resultExplain+"查询的SQL语句:"+errorSqlOld; | |
| 209 | 216 | } else { |
| 210 | - return getDynamicTableSql( session, input, userId, userInput, attempt); | |
| 217 | + return getDynamicTableSql( session, input, userId, userInput, attempt,errorSqlOld,errorMessageOld); | |
| 211 | 218 | } |
| 212 | 219 | } |
| 213 | 220 | } |
| ... | ... | @@ -242,7 +249,7 @@ public class XlyErpService { |
| 242 | 249 | * @return java.lang.String |
| 243 | 250 | * @Description 执行动态sSql |
| 244 | 251 | **/ |
| 245 | - private String getDynamicTableSqlExec(UserSceneSession session,String input,String userId,String userInput){ | |
| 252 | + private String getDynamicTableSqlExec(UserSceneSession session,String input,String userId,String userInput,String errorSql,String errorMessage){ | |
| 246 | 253 | // 1. 构建自然语言转SQLAgent, |
| 247 | 254 | DynamicTableNl2SqlAiAgent aiDynamicTableNl2SqlAiAgent = createDynamicTableNl2SqlAiAgent(userId, input, session); |
| 248 | 255 | String tableNames = session.getCurrentTool().getSInputTabelName(); |
| ... | ... | @@ -250,16 +257,27 @@ public class XlyErpService { |
| 250 | 257 | String tableStruct = session.getCurrentTool().getSStructureMemo(); |
| 251 | 258 | String sDataNow = DateUtil.format(new Date(), DatePattern.CHINESE_DATE_TIME_FORMAT); |
| 252 | 259 | log.info("当前时间:"+sDataNow); |
| 253 | - String rawSql = aiDynamicTableNl2SqlAiAgent.generateMysqlSql(userId,tableNames,tableStruct,sDataNow,userInput); | |
| 260 | + String rawSql = StrUtil.EMPTY; | |
| 261 | + if(ObjectUtil.isEmpty(errorSql) && ObjectUtil.isEmpty(errorMessage)){ | |
| 262 | + rawSql = aiDynamicTableNl2SqlAiAgent.generateMysqlSql(userId,tableNames,tableStruct,sDataNow,userInput); | |
| 263 | + }else{ | |
| 264 | + rawSql = aiDynamicTableNl2SqlAiAgent.regenerateSqlWithError(userId, tableNames,tableStruct,sDataNow,userInput,errorSql,errorMessage); | |
| 265 | + } | |
| 266 | + | |
| 254 | 267 | if (rawSql == null || rawSql.trim().isEmpty()) { |
| 255 | - throw new SqlGenerateException("AI服务生成SQL失败,返回结果为空"); | |
| 268 | + throw new SqlGenerateException("SQL EMPTY"); | |
| 256 | 269 | } |
| 257 | 270 | // 2. 清理SQL多余符号 + 生产级强校验(核心安全保障,不可省略) |
| 258 | 271 | String cleanSql = SqlValidateUtil.cleanSqlSymbol(rawSql); |
| 259 | 272 | SqlValidateUtil.validateMysqlSql(cleanSql); |
| 260 | 273 | // 4. 执行SQL获取结构化结果 |
| 261 | 274 | // Map<String,Object> params = new HashMap<>(); |
| 262 | - List<Map<String, Object>> sqlResult = dynamicExeDbService.findSql(new HashMap<>(),cleanSql); | |
| 275 | + List<Map<String, Object>> sqlResult = new ArrayList<>(); | |
| 276 | + try{ | |
| 277 | + sqlResult = dynamicExeDbService.findSql(new HashMap<>(),cleanSql); | |
| 278 | + }catch (Exception e){ | |
| 279 | + throw new SqlGenerateException(e.getMessage()+" OLDSQL "+cleanSql); | |
| 280 | + } | |
| 263 | 281 | // 5. 调用AI服务生成自然语言解释(传入表结构,让解释更贴合业务) |
| 264 | 282 | String resultJson = JSON.toJSONString(sqlResult); |
| 265 | 283 | return aiDynamicTableNl2SqlAiAgent.explainSqlResult( | ... | ... |
src/main/java/com/xly/util/EnhancedErrorGuidance.java
0 → 100644
| 1 | +package com.xly.util; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * 更详细的错误分类引导提示词 | |
| 5 | + */ | |
| 6 | +public class EnhancedErrorGuidance { | |
| 7 | + | |
| 8 | + public static String splitString = " OLDSQL "; | |
| 9 | + /** | |
| 10 | + * 根据错误类型生成针对性的引导信息 | |
| 11 | + */ | |
| 12 | + public static String getErrorGuidance(String errorMessageAll) { | |
| 13 | + | |
| 14 | + StringBuilder guidance = new StringBuilder(); | |
| 15 | + String errorMessage = errorMessageAll.split(splitString)[0]; | |
| 16 | + | |
| 17 | + if (errorMessage.contains("Unknown column")) { | |
| 18 | + guidance.append("【字段不存在错误】\n"); | |
| 19 | + guidance.append("- 错误原因:SQL中使用了表中不存在的字段\n"); | |
| 20 | + guidance.append("- 修复方法:从以下表结构中选择正确的字段名\n"); | |
| 21 | + guidance.append(" {{tableStruct}}"); | |
| 22 | + guidance.append("\n- 注意:字段名区分大小写,请核对拼写"); | |
| 23 | + | |
| 24 | + } else if (errorMessage.contains("Table doesn't exist")) { | |
| 25 | + guidance.append("【表不存在错误】\n"); | |
| 26 | + guidance.append("- 错误原因:SQL中使用了不存在的表名\n"); | |
| 27 | + guidance.append("- 修复方法:检查表名拼写,确保与传入的表名完全一致\n"); | |
| 28 | + | |
| 29 | + } else if (errorMessage.contains("Syntax error")) { | |
| 30 | + guidance.append("【SQL语法错误】\n"); | |
| 31 | + guidance.append("- 错误原因:SQL语句不符合MySQL语法规范\n"); | |
| 32 | + guidance.append("- 常见问题:\n"); | |
| 33 | + guidance.append(" 1. 关键词拼写错误(SELEC、FORM等)\n"); | |
| 34 | + guidance.append(" 2. 缺少必要的关键词(JOIN、ON等)\n"); | |
| 35 | + guidance.append(" 3. 括号不匹配\n"); | |
| 36 | + guidance.append(" 4. 字符串缺少引号\n"); | |
| 37 | + | |
| 38 | + } else if (errorMessage.contains("Column not found in ON clause")) { | |
| 39 | + guidance.append("【JOIN关联字段错误】\n"); | |
| 40 | + guidance.append("- 错误原因:ON条件中使用的字段不存在\n"); | |
| 41 | + guidance.append("- 修复方法:检查JOIN条件字段是否正确\n"); | |
| 42 | + | |
| 43 | + } else if (errorMessage.contains("Non unique table/alias")) { | |
| 44 | + guidance.append("【表别名重复错误】\n"); | |
| 45 | + guidance.append("- 错误原因:多个表使用了相同的别名\n"); | |
| 46 | + guidance.append("- 修复方法:为每个表使用唯一的别名\n"); | |
| 47 | + | |
| 48 | + } else if (errorMessage.contains("Unknown table")) { | |
| 49 | + guidance.append("【未知表错误】\n"); | |
| 50 | + guidance.append("- 错误原因:SQL中引用了未在FROM/JOIN中声明的表\n"); | |
| 51 | + guidance.append("- 修复方法:确保所有使用的表都已正确JOIN\n"); | |
| 52 | + | |
| 53 | + } else if (errorMessage.contains("Incorrect integer value")) { | |
| 54 | + guidance.append("【数据类型不匹配错误】\n"); | |
| 55 | + guidance.append("- 错误原因:字符串值赋给了数字字段\n"); | |
| 56 | + guidance.append("- 修复方法:数字类型不加引号,字符串类型加单引号\n"); | |
| 57 | + | |
| 58 | + } else if(errorMessage.contains("SQL EMPTY")){ | |
| 59 | + guidance.append("【生成SQL】\n"); | |
| 60 | + guidance.append("- 错误原因:生成的SQL语句为空\n"); | |
| 61 | + guidance.append("- 修复方法:请重新生成\n"); | |
| 62 | + } else { | |
| 63 | + guidance.append("【未知错误】\n"); | |
| 64 | + guidance.append("- 错误信息:").append(errorMessage).append("\n"); | |
| 65 | + guidance.append("- 请仔细检查SQL语句的每一个部分\n"); | |
| 66 | + } | |
| 67 | + return guidance.toString(); | |
| 68 | + } | |
| 69 | +} | |
| 0 | 70 | \ No newline at end of file | ... | ... |