Commit 06ca6a052f1445dcab76b663dd8a6bbf6c57a07a

Authored by qianbao
1 parent 9a985fca

AI 对于时间的处理

src/main/java/com/xly/agent/DynamicTableNl2SqlAiAgent.java
@@ -19,7 +19,7 @@ public interface DynamicTableNl2SqlAiAgent { @@ -19,7 +19,7 @@ public interface DynamicTableNl2SqlAiAgent {
19 */ 19 */
20 @SystemMessage(""" 20 @SystemMessage("""
21 你是资深MySQL数据分析师,严格遵循以下**通用规则**生成SQL,适用于所有业务场景: 21 你是资深MySQL数据分析师,严格遵循以下**通用规则**生成SQL,适用于所有业务场景:
22 - 1. 语法规范:仅生成符合MySQL8.0/5.7的标准SELECT语句,兼容低版本,多表关联用JOIN而非逗号; 22 + 1. 语法规范:仅生成符合MySQL8.0.36的标准SELECT语句,兼容低版本,多表关联用JOIN而非逗号;
23 2. 输出格式:仅返回SQL语句本身,无任何解释、换行、```sql/```包裹、备注、多余空格,直接输出可执行SQL; 23 2. 输出格式:仅返回SQL语句本身,无任何解释、换行、```sql/```包裹、备注、多余空格,直接输出可执行SQL;
24 3. 编写规范: 24 3. 编写规范:
25 3.1 多表关联必须使用 表名+字段名(如表名.字段名),严格按下面[涉及表名]中的表次序关联,聚合函数(SUM/COUNT/AVG/MIN/MAX)必须加业务化别名,日期过滤使用标准DATE格式(yyyy-MM-dd); 25 3.1 多表关联必须使用 表名+字段名(如表名.字段名),严格按下面[涉及表名]中的表次序关联,聚合函数(SUM/COUNT/AVG/MIN/MAX)必须加业务化别名,日期过滤使用标准DATE格式(yyyy-MM-dd);
@@ -75,6 +75,83 @@ public interface DynamicTableNl2SqlAiAgent { @@ -75,6 +75,83 @@ public interface DynamicTableNl2SqlAiAgent {
75 @V("userInput") String userInput); 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 * 动态表结构:自然语言解释SQL执行结果 155 * 动态表结构:自然语言解释SQL执行结果
79 * 入参:用户问题、执行的SQL、表结构、JSON格式结果 156 * 入参:用户问题、执行的SQL、表结构、JSON格式结果
80 */ 157 */
src/main/java/com/xly/service/XlyErpService.java
@@ -18,6 +18,7 @@ import com.xly.exception.sqlexception.SqlGenerateException; @@ -18,6 +18,7 @@ import com.xly.exception.sqlexception.SqlGenerateException;
18 import com.xly.mapper.ToolMetaMapper; 18 import com.xly.mapper.ToolMetaMapper;
19 import com.xly.runner.AppStartupRunner; 19 import com.xly.runner.AppStartupRunner;
20 import com.xly.tool.DynamicToolProvider; 20 import com.xly.tool.DynamicToolProvider;
  21 +import com.xly.util.EnhancedErrorGuidance;
21 import com.xly.util.InputPreprocessor; 22 import com.xly.util.InputPreprocessor;
22 import com.xly.util.SqlValidateUtil; 23 import com.xly.util.SqlValidateUtil;
23 import com.xly.util.ValiDataUtil; 24 import com.xly.util.ValiDataUtil;
@@ -26,6 +27,7 @@ import dev.langchain4j.data.message.AiMessage; @@ -26,6 +27,7 @@ import dev.langchain4j.data.message.AiMessage;
26 import dev.langchain4j.model.chat.ChatLanguageModel; 27 import dev.langchain4j.model.chat.ChatLanguageModel;
27 import dev.langchain4j.model.ollama.OllamaChatModel; 28 import dev.langchain4j.model.ollama.OllamaChatModel;
28 import dev.langchain4j.service.AiServices; 29 import dev.langchain4j.service.AiServices;
  30 +import dev.langchain4j.service.V;
29 import jnr.ffi.annotations.In; 31 import jnr.ffi.annotations.In;
30 import lombok.RequiredArgsConstructor; 32 import lombok.RequiredArgsConstructor;
31 import lombok.extern.slf4j.Slf4j; 33 import lombok.extern.slf4j.Slf4j;
@@ -59,7 +61,6 @@ public class XlyErpService { @@ -59,7 +61,6 @@ public class XlyErpService {
59 public final static Integer maxTollRetries = 1; 61 public final static Integer maxTollRetries = 1;
60 62
61 63
62 -  
63 /*** 64 /***
64 * @Author 钱豹 65 * @Author 钱豹
65 * @Date 19:18 2026/1/27 66 * @Date 19:18 2026/1/27
@@ -150,7 +151,7 @@ public class XlyErpService { @@ -150,7 +151,7 @@ public class XlyErpService {
150 && ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName()) 151 && ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName())
151 && ObjectUtil.isNotEmpty(session.getCurrentTool().getSStructureMemo())) 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 if (ObjectUtil.isEmpty(sResponMessage)){ 157 if (ObjectUtil.isEmpty(sResponMessage)){
@@ -196,18 +197,24 @@ public class XlyErpService { @@ -196,18 +197,24 @@ public class XlyErpService {
196 * @return java.lang.String 197 * @return java.lang.String
197 * @Description 获取执行动态SQL 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 String resultExplain = "信息模糊,请提供更具体的问题或指令"; 201 String resultExplain = "信息模糊,请提供更具体的问题或指令";
201 try{ 202 try{
202 while (attempt < maxRetries) { 203 while (attempt < maxRetries) {
203 try{ 204 try{
204 attempt = attempt+1; 205 attempt = attempt+1;
205 - return getDynamicTableSqlExec( session, input, userId, userInput); 206 + return getDynamicTableSqlExec(session, input, userId, userInput,errorSql,errorMessage);
206 }catch (Exception e){ 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 if (attempt == maxRetries) { 214 if (attempt == maxRetries) {
208 - return resultExplain; 215 + return resultExplain+"查询的SQL语句:"+errorSqlOld;
209 } else { 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,7 +249,7 @@ public class XlyErpService {
242 * @return java.lang.String 249 * @return java.lang.String
243 * @Description 执行动态sSql 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 // 1. 构建自然语言转SQLAgent, 253 // 1. 构建自然语言转SQLAgent,
247 DynamicTableNl2SqlAiAgent aiDynamicTableNl2SqlAiAgent = createDynamicTableNl2SqlAiAgent(userId, input, session); 254 DynamicTableNl2SqlAiAgent aiDynamicTableNl2SqlAiAgent = createDynamicTableNl2SqlAiAgent(userId, input, session);
248 String tableNames = session.getCurrentTool().getSInputTabelName(); 255 String tableNames = session.getCurrentTool().getSInputTabelName();
@@ -250,16 +257,27 @@ public class XlyErpService { @@ -250,16 +257,27 @@ public class XlyErpService {
250 String tableStruct = session.getCurrentTool().getSStructureMemo(); 257 String tableStruct = session.getCurrentTool().getSStructureMemo();
251 String sDataNow = DateUtil.format(new Date(), DatePattern.CHINESE_DATE_TIME_FORMAT); 258 String sDataNow = DateUtil.format(new Date(), DatePattern.CHINESE_DATE_TIME_FORMAT);
252 log.info("当前时间:"+sDataNow); 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 if (rawSql == null || rawSql.trim().isEmpty()) { 267 if (rawSql == null || rawSql.trim().isEmpty()) {
255 - throw new SqlGenerateException("AI服务生成SQL失败,返回结果为空"); 268 + throw new SqlGenerateException("SQL EMPTY");
256 } 269 }
257 // 2. 清理SQL多余符号 + 生产级强校验(核心安全保障,不可省略) 270 // 2. 清理SQL多余符号 + 生产级强校验(核心安全保障,不可省略)
258 String cleanSql = SqlValidateUtil.cleanSqlSymbol(rawSql); 271 String cleanSql = SqlValidateUtil.cleanSqlSymbol(rawSql);
259 SqlValidateUtil.validateMysqlSql(cleanSql); 272 SqlValidateUtil.validateMysqlSql(cleanSql);
260 // 4. 执行SQL获取结构化结果 273 // 4. 执行SQL获取结构化结果
261 // Map<String,Object> params = new HashMap<>(); 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 // 5. 调用AI服务生成自然语言解释(传入表结构,让解释更贴合业务) 281 // 5. 调用AI服务生成自然语言解释(传入表结构,让解释更贴合业务)
264 String resultJson = JSON.toJSONString(sqlResult); 282 String resultJson = JSON.toJSONString(sqlResult);
265 return aiDynamicTableNl2SqlAiAgent.explainSqlResult( 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 \ No newline at end of file 70 \ No newline at end of file