diff --git a/src/main/java/com/xly/agent/DynamicTableNl2SqlAiAgent.java b/src/main/java/com/xly/agent/DynamicTableNl2SqlAiAgent.java index 105ff3f..fb04ff3 100644 --- a/src/main/java/com/xly/agent/DynamicTableNl2SqlAiAgent.java +++ b/src/main/java/com/xly/agent/DynamicTableNl2SqlAiAgent.java @@ -19,7 +19,7 @@ public interface DynamicTableNl2SqlAiAgent { */ @SystemMessage(""" 你是资深MySQL数据分析师,严格遵循以下**通用规则**生成SQL,适用于所有业务场景: - 1. 语法规范:仅生成符合MySQL8.0/5.7的标准SELECT语句,兼容低版本,多表关联用JOIN而非逗号; + 1. 语法规范:仅生成符合MySQL8.0.36的标准SELECT语句,兼容低版本,多表关联用JOIN而非逗号; 2. 输出格式:仅返回SQL语句本身,无任何解释、换行、```sql/```包裹、备注、多余空格,直接输出可执行SQL; 3. 编写规范: 3.1 多表关联必须使用 表名+字段名(如表名.字段名),严格按下面[涉及表名]中的表次序关联,聚合函数(SUM/COUNT/AVG/MIN/MAX)必须加业务化别名,日期过滤使用标准DATE格式(yyyy-MM-dd); @@ -75,6 +75,83 @@ public interface DynamicTableNl2SqlAiAgent { @V("userInput") String userInput); /** + * SQL错误重试引导提示词 + * 当第一次生成的SQL执行错误时,将错误信息传入,让AI重新生成 + */ + /** + * 动态表结构:自然语言转MySQL SELECT语句 + * 入参:数据库名、表名(多表用,分隔)、表结构、用户查询 + */ + @SystemMessage(""" + 你是资深MySQL数据分析师,严格遵循以下**通用规则**生成SQL,适用于所有业务场景: + 1. 语法规范:仅生成符合MySQL8.0.36的标准SELECT语句,兼容低版本,多表关联用JOIN而非逗号; + 2. 输出格式:仅返回SQL语句本身,无任何解释、换行、```sql/```包裹、备注、多余空格,直接输出可执行SQL; + 3. 编写规范: + 3.1 多表关联必须使用 表名+字段名(如表名.字段名),严格按下面[涉及表名]中的表次序关联,聚合函数(SUM/COUNT/AVG/MIN/MAX)必须加业务化别名,日期过滤使用标准DATE格式(yyyy-MM-dd); + 3.2 SQL所有字段均采用 表名.字段名 方式生成,务必确保 字段名 在相应的 表名 描述的字段中存在,如果不存在重试其它方式,直到满足条件; + 3.3 SQL所有字段涉及的所有表名,都要**严格**按下面[涉及表名]中的表次序关联,没有关联不允许使用; + 3.4 SQL所有的查询条件,如果是字符类型的字段,均需要加不为空判断,用示例格式判断,示例:ifnull(customername,'')<>''; + 3.5 SQL所有的查询条件,如果是日期类型的字段,均需要加不为空判断,用示例格式判断,示例:tmakedate is not Null; + 3.6 SQL所有的显示字段的别名中,不能出现空格,如: tCreateDate as earliest 订单日期,正确的应是 tCreateDate as earliest订单日期 + 4. 安全约束:禁止生成任何DDL/DML语句(DROP/ALTER/INSERT/UPDATE/DELETE等),禁止使用子查询、存储过程、自定义函数、临时表; + 5. 精准性: + 5.1 严格按用户需求+传入的表结构生成,仅使用指定字段/表,无多余字段、无无效表关联、无冗余过滤条件; + 5.2 用户需求中没有明确的日期条件,默认为全部数据,禁止增加任何日期过滤条件 + 6. 关联规则:多表关联时,必须使用外键/业务唯一键关联,禁止无意义关联。 + 7. 当前时间:{{sDataNow}} + 8. 时间处理规则: + 8.1 当前系统时间:{{sDataNow}}(格式:yyyy年MM月dd日HH时mm分ss秒) + 8.2 用户需求中的相对时间概念,必须基于{{sDataNow}}进行转换: + - "本年" → 当前年份:{{sDataNow}}的年份 + - "本月" → 当前月份:{{sDataNow}}的年份和月份 + - "本季度" → 当前季度:基于{{sDataNow}}计算 + - "本日/今天" → {{sDataNow}}的具体日期 + - "昨天" → {{sDataNow}}减1天 + - "本周" → 基于{{sDataNow}}计算周一到周日 + - "近7天" → {{sDataNow}}减7天到{{sDataNow}} + 8.3 示例转换: + 当前时间:2024-03-15 + 用户说"查询本年数据" → 查询条件应为:YEAR(日期字段) = 2024 + 用户说"查询本月数据" → 查询条件应为:YEAR(日期字段) = 2024 AND MONTH(日期字段) = 3 + 8.4 如果用户需求中没有明确的时间条件,禁止增加任何时间过滤条件 + """) + @UserMessage(""" + 【业务场景表结构信息】 + 涉及表名:{{tableNames}}(多表用,分隔,需关联时请按规范使用JOIN) + 表结构详情:{{tableStruct}}(多表请标注表名+字段,格式:表名(字段1:类型,字段2:类型,主键/外键)) + 当前时间:{{sDataNow}} + 【原始用户需求】 + {{userInput}} + 请根据上述表结构+通用规则,生成符合要求的MySQL SELECT语句; + 【之前生成的错误SQL】 + {{errorSql}} + 【执行错误信息】 + {{errorMessage}} + 【错误分析指引】 + 1. 错误类型:请根据错误代码判断 + - "Unknown column":字段不存在,检查字段名拼写或改用表中存在的字段 + - "Table doesn't exist":表名错误,检查表名拼写 + - "You have an error in your SQL syntax":语法错误,检查关键词、括号、引号 + - "Column not found in ON clause":JOIN条件字段不存在 + - "Non unique table/alias":表别名重复 + + 2. 修复建议: + - 如果是字段错误:查看表结构{{tableStruct}},找到正确的字段名替换 + - 如果是语法错误:检查SELECT、FROM、WHERE、JOIN等关键词用法 + - 如果是类型错误:字符串加单引号,数字不加引号,日期用'yyyy-MM-dd'格式 + - 如果是关联错误:确保所有表都通过外键正确JOIN + 请根据以上信息,重新生成正确的MySQL SELECT语句。 + 只返回SQL语句本身,不要任何解释和包装。 + """) + String regenerateSqlWithError(@MemoryId String userId, + @V("tableNames") String tableNames, + @V("tableStruct") String tableStruct, + @V("sDataNow") String sDataNow, + @V("userInput") String userInput, + @V("errorSql") String errorSql, + @V("errorMessage") String errorMessage + ); + /** * 动态表结构:自然语言解释SQL执行结果 * 入参:用户问题、执行的SQL、表结构、JSON格式结果 */ diff --git a/src/main/java/com/xly/service/XlyErpService.java b/src/main/java/com/xly/service/XlyErpService.java index 40fe55b..46ae6fb 100644 --- a/src/main/java/com/xly/service/XlyErpService.java +++ b/src/main/java/com/xly/service/XlyErpService.java @@ -18,6 +18,7 @@ import com.xly.exception.sqlexception.SqlGenerateException; import com.xly.mapper.ToolMetaMapper; import com.xly.runner.AppStartupRunner; import com.xly.tool.DynamicToolProvider; +import com.xly.util.EnhancedErrorGuidance; import com.xly.util.InputPreprocessor; import com.xly.util.SqlValidateUtil; import com.xly.util.ValiDataUtil; @@ -26,6 +27,7 @@ import dev.langchain4j.data.message.AiMessage; import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.ollama.OllamaChatModel; import dev.langchain4j.service.AiServices; +import dev.langchain4j.service.V; import jnr.ffi.annotations.In; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -59,7 +61,6 @@ public class XlyErpService { public final static Integer maxTollRetries = 1; - /*** * @Author 钱豹 * @Date 19:18 2026/1/27 @@ -150,7 +151,7 @@ public class XlyErpService { && ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName()) && ObjectUtil.isNotEmpty(session.getCurrentTool().getSStructureMemo())) ){ - sResponMessage = getDynamicTableSql(session, input, userId, userInput,0); + sResponMessage = getDynamicTableSql(session, input, userId, userInput,0,StrUtil.EMPTY,StrUtil.EMPTY); } //如果返回空的进入闲聊模式 if (ObjectUtil.isEmpty(sResponMessage)){ @@ -196,18 +197,24 @@ public class XlyErpService { * @return java.lang.String * @Description 获取执行动态SQL **/ - private String getDynamicTableSql(UserSceneSession session,String input,String userId,String userInput,Integer attempt){ + private String getDynamicTableSql(UserSceneSession session,String input,String userId,String userInput,Integer attempt,String errorSql,String errorMessage ){ String resultExplain = "信息模糊,请提供更具体的问题或指令"; try{ while (attempt < maxRetries) { try{ attempt = attempt+1; - return getDynamicTableSqlExec( session, input, userId, userInput); + return getDynamicTableSqlExec(session, input, userId, userInput,errorSql,errorMessage); }catch (Exception e){ + String erroMsg = e.getMessage(); + String errorSqlOld = StrUtil.EMPTY; + if(erroMsg.contains(EnhancedErrorGuidance.splitString) && erroMsg.split(EnhancedErrorGuidance.splitString).length>1){ + errorSqlOld = erroMsg.split(EnhancedErrorGuidance.splitString)[1]; + } + String errorMessageOld = EnhancedErrorGuidance.getErrorGuidance(erroMsg); if (attempt == maxRetries) { - return resultExplain; + return resultExplain+"查询的SQL语句:"+errorSqlOld; } else { - return getDynamicTableSql( session, input, userId, userInput, attempt); + return getDynamicTableSql( session, input, userId, userInput, attempt,errorSqlOld,errorMessageOld); } } } @@ -242,7 +249,7 @@ public class XlyErpService { * @return java.lang.String * @Description 执行动态sSql **/ - private String getDynamicTableSqlExec(UserSceneSession session,String input,String userId,String userInput){ + private String getDynamicTableSqlExec(UserSceneSession session,String input,String userId,String userInput,String errorSql,String errorMessage){ // 1. 构建自然语言转SQLAgent, DynamicTableNl2SqlAiAgent aiDynamicTableNl2SqlAiAgent = createDynamicTableNl2SqlAiAgent(userId, input, session); String tableNames = session.getCurrentTool().getSInputTabelName(); @@ -250,16 +257,27 @@ public class XlyErpService { String tableStruct = session.getCurrentTool().getSStructureMemo(); String sDataNow = DateUtil.format(new Date(), DatePattern.CHINESE_DATE_TIME_FORMAT); log.info("当前时间:"+sDataNow); - String rawSql = aiDynamicTableNl2SqlAiAgent.generateMysqlSql(userId,tableNames,tableStruct,sDataNow,userInput); + String rawSql = StrUtil.EMPTY; + if(ObjectUtil.isEmpty(errorSql) && ObjectUtil.isEmpty(errorMessage)){ + rawSql = aiDynamicTableNl2SqlAiAgent.generateMysqlSql(userId,tableNames,tableStruct,sDataNow,userInput); + }else{ + rawSql = aiDynamicTableNl2SqlAiAgent.regenerateSqlWithError(userId, tableNames,tableStruct,sDataNow,userInput,errorSql,errorMessage); + } + if (rawSql == null || rawSql.trim().isEmpty()) { - throw new SqlGenerateException("AI服务生成SQL失败,返回结果为空"); + throw new SqlGenerateException("SQL EMPTY"); } // 2. 清理SQL多余符号 + 生产级强校验(核心安全保障,不可省略) String cleanSql = SqlValidateUtil.cleanSqlSymbol(rawSql); SqlValidateUtil.validateMysqlSql(cleanSql); // 4. 执行SQL获取结构化结果 // Map params = new HashMap<>(); - List> sqlResult = dynamicExeDbService.findSql(new HashMap<>(),cleanSql); + List> sqlResult = new ArrayList<>(); + try{ + sqlResult = dynamicExeDbService.findSql(new HashMap<>(),cleanSql); + }catch (Exception e){ + throw new SqlGenerateException(e.getMessage()+" OLDSQL "+cleanSql); + } // 5. 调用AI服务生成自然语言解释(传入表结构,让解释更贴合业务) String resultJson = JSON.toJSONString(sqlResult); return aiDynamicTableNl2SqlAiAgent.explainSqlResult( diff --git a/src/main/java/com/xly/util/EnhancedErrorGuidance.java b/src/main/java/com/xly/util/EnhancedErrorGuidance.java new file mode 100644 index 0000000..a3811d8 --- /dev/null +++ b/src/main/java/com/xly/util/EnhancedErrorGuidance.java @@ -0,0 +1,69 @@ +package com.xly.util; + +/** + * 更详细的错误分类引导提示词 + */ +public class EnhancedErrorGuidance { + + public static String splitString = " OLDSQL "; + /** + * 根据错误类型生成针对性的引导信息 + */ + public static String getErrorGuidance(String errorMessageAll) { + + StringBuilder guidance = new StringBuilder(); + String errorMessage = errorMessageAll.split(splitString)[0]; + + if (errorMessage.contains("Unknown column")) { + guidance.append("【字段不存在错误】\n"); + guidance.append("- 错误原因:SQL中使用了表中不存在的字段\n"); + guidance.append("- 修复方法:从以下表结构中选择正确的字段名\n"); + guidance.append(" {{tableStruct}}"); + guidance.append("\n- 注意:字段名区分大小写,请核对拼写"); + + } else if (errorMessage.contains("Table doesn't exist")) { + guidance.append("【表不存在错误】\n"); + guidance.append("- 错误原因:SQL中使用了不存在的表名\n"); + guidance.append("- 修复方法:检查表名拼写,确保与传入的表名完全一致\n"); + + } else if (errorMessage.contains("Syntax error")) { + guidance.append("【SQL语法错误】\n"); + guidance.append("- 错误原因:SQL语句不符合MySQL语法规范\n"); + guidance.append("- 常见问题:\n"); + guidance.append(" 1. 关键词拼写错误(SELEC、FORM等)\n"); + guidance.append(" 2. 缺少必要的关键词(JOIN、ON等)\n"); + guidance.append(" 3. 括号不匹配\n"); + guidance.append(" 4. 字符串缺少引号\n"); + + } else if (errorMessage.contains("Column not found in ON clause")) { + guidance.append("【JOIN关联字段错误】\n"); + guidance.append("- 错误原因:ON条件中使用的字段不存在\n"); + guidance.append("- 修复方法:检查JOIN条件字段是否正确\n"); + + } else if (errorMessage.contains("Non unique table/alias")) { + guidance.append("【表别名重复错误】\n"); + guidance.append("- 错误原因:多个表使用了相同的别名\n"); + guidance.append("- 修复方法:为每个表使用唯一的别名\n"); + + } else if (errorMessage.contains("Unknown table")) { + guidance.append("【未知表错误】\n"); + guidance.append("- 错误原因:SQL中引用了未在FROM/JOIN中声明的表\n"); + guidance.append("- 修复方法:确保所有使用的表都已正确JOIN\n"); + + } else if (errorMessage.contains("Incorrect integer value")) { + guidance.append("【数据类型不匹配错误】\n"); + guidance.append("- 错误原因:字符串值赋给了数字字段\n"); + guidance.append("- 修复方法:数字类型不加引号,字符串类型加单引号\n"); + + } else if(errorMessage.contains("SQL EMPTY")){ + guidance.append("【生成SQL】\n"); + guidance.append("- 错误原因:生成的SQL语句为空\n"); + guidance.append("- 修复方法:请重新生成\n"); + } else { + guidance.append("【未知错误】\n"); + guidance.append("- 错误信息:").append(errorMessage).append("\n"); + guidance.append("- 请仔细检查SQL语句的每一个部分\n"); + } + return guidance.toString(); + } +} \ No newline at end of file