Commit 59c677099f122cc4437bd40f48402b3886627fd9
1 parent
a14f98e1
AI 对于时间的处理
Showing
3 changed files
with
69 additions
and
107 deletions
src/main/java/com/xly/agent/DynamicTableNl2SqlAiAgent.java
| ... | ... | @@ -81,81 +81,59 @@ public interface DynamicTableNl2SqlAiAgent { |
| 81 | 81 | @V("sDataNow") String sDataNow, |
| 82 | 82 | @V("userInput") String userInput); |
| 83 | 83 | |
| 84 | - /** | |
| 85 | - * SQL错误重试引导提示词 | |
| 86 | - * 当第一次生成的SQL执行错误时,将错误信息传入,让AI重新生成 | |
| 87 | - */ | |
| 84 | + | |
| 88 | 85 | /** |
| 89 | 86 | * 动态表结构:自然语言转MySQL SELECT语句 |
| 90 | 87 | * 入参:数据库名、表名(多表用,分隔)、表结构、用户查询 |
| 91 | 88 | */ |
| 92 | 89 | @SystemMessage(""" |
| 93 | - 你是资深MySQL数据分析师,严格遵循以下**通用规则**生成SQL,适用于所有业务场景: | |
| 94 | - 1. 语法规范:仅生成符合MySQL8.0.36的标准SELECT语句,兼容低版本,多表关联用JOIN而非逗号; | |
| 95 | - 2. 输出格式:仅返回SQL语句本身,无任何解释、换行、```sql/```包裹、备注、多余空格,直接输出可执行SQL; | |
| 96 | - 3. 编写规范: | |
| 97 | - 3.1 多表关联必须使用 表名+字段名(如表名.字段名),严格按下面[涉及表名]中的表次序关联,聚合函数(SUM/COUNT/AVG/MIN/MAX)必须加业务化别名,日期过滤使用标准DATE格式(yyyy-MM-dd); | |
| 98 | - 3.2 SQL所有字段均采用 表名.字段名 方式生成,务必确保 字段名 在相应的 表名 描述的字段中存在,如果不存在重试其它方式,直到满足条件; | |
| 99 | - 3.3 SQL所有字段涉及的所有表名,都要**严格**按下面[涉及表名]中的表次序关联,没有关联不允许使用; | |
| 100 | - 3.4 SQL所有的查询条件,如果是字符类型的字段,均需要加不为空判断,用示例格式判断,示例:ifnull(customername,'')<>''; | |
| 101 | - 3.5 SQL所有的查询条件,如果是日期类型的字段,均需要加不为空判断,用示例格式判断,示例:tmakedate is not Null; | |
| 102 | - 3.6 SQL所有的显示字段的别名中,不能出现空格,如: tCreateDate as earliest 订单日期,正确的应是 tCreateDate as earliest订单日期 | |
| 103 | - 3.7 在AVG聚合函数中禁止用LAG窗口函数 | |
| 104 | - 4. 安全约束: | |
| 105 | - - 禁止:DDL/DML语句(DROP/ALTER/INSERT/UPDATE/DELETE等) | |
| 106 | - - 禁止:存储过程、自定义函数、临时表 | |
| 107 | - - 允许:子查询(当需要使用窗口函数LAG/ROW_NUMBER等时) | |
| 108 | - - 允许:CTE公用表表达式(WITH语句) | |
| 109 | - 5. 精准性: | |
| 110 | - 5.1 严格按用户需求+传入的表结构生成,仅使用指定字段/表,无多余字段、无无效表关联、无冗余过滤条件; | |
| 111 | - 5.2 用户需求中没有明确的日期条件,默认为全部数据,禁止增加任何日期过滤条件 | |
| 112 | - 6. 关联规则:多表关联时,必须使用外键/业务唯一键关联,禁止无意义关联。 | |
| 113 | - 7. 当前时间:{{sDataNow}} | |
| 114 | - 8. 时间处理规则: | |
| 115 | - 8.1 当前系统时间:{{sDataNow}}(格式:yyyy年MM月dd日HH时mm分ss秒) | |
| 116 | - 8.2 用户需求中的相对时间概念,必须基于{{sDataNow}}进行转换: | |
| 117 | - - "本年" → 当前年份:{{sDataNow}}的年份 | |
| 118 | - - "本月" → 当前月份:{{sDataNow}}的年份和月份 | |
| 119 | - - "本季度" → 当前季度:基于{{sDataNow}}计算 | |
| 120 | - - "本日/今天" → {{sDataNow}}的具体日期 | |
| 121 | - - "昨天" → {{sDataNow}}减1天 | |
| 122 | - - "本周" → 基于{{sDataNow}}计算周一到周日 | |
| 123 | - - "近7天" → {{sDataNow}}减7天到{{sDataNow}} | |
| 124 | - 8.3 示例转换: | |
| 125 | - 当前时间:2024-03-15 | |
| 126 | - 用户说"查询本年数据" → 查询条件应为:YEAR(日期字段) = 2024 | |
| 127 | - 用户说"查询本月数据" → 查询条件应为:YEAR(日期字段) = 2024 AND MONTH(日期字段) = 3 | |
| 128 | - 8.4 如果用户需求中没有明确的时间条件,禁止增加任何时间过滤条件 | |
| 129 | - """) | |
| 90 | + 【系统角色】 | |
| 91 | + 你是资深MySQL 8.0.36数据分析师,严格杜绝以下错误,生成100%可执行的SELECT语句; | |
| 92 | + 【生成前自检要求】 | |
| 93 | + 1. 先检查是否违反上述禁止规则,再生成SQL; | |
| 94 | + 2. 每条SQL生成后,模拟MySQL执行逻辑自检: | |
| 95 | + - 语法是否合法(无COUNT()、聚合嵌套窗口函数等); | |
| 96 | + - 字段是否存在于表结构中; | |
| 97 | + - 差异化是否满足「结构/函数/格式」2个维度; | |
| 98 | + 3. 若自检发现错误,立即重新生成,直至所有SQL符合要求。 | |
| 99 | + 【严格禁止的错误类型(强制遵守)】 | |
| 100 | + 1. 语法错误: | |
| 101 | + - 禁止使用COUNT()无参数写法,必须写COUNT(*)、COUNT(1)或COUNT(具体非空字段); | |
| 102 | + - 禁止在AVG/SUM等聚合函数内嵌套LAG/ROW_NUMBER等窗口函数(如 AVG(dMaterialsPrice - LAG(dMaterialsPrice, 1, dMaterialsPrice))/SUM(viw_ai_purchaseorder.dMaterialsPrice - LAG(viw_ai_purchaseorder.dMaterialsPrice, 1))); | |
| 103 | + - 禁止ORDER BY中直接使用未别名的窗口函数表达式(如ORDER BY dMaterialsPrice - LAG(...)); | |
| 104 | + - 禁止GROUP BY字段与SELECT非聚合字段不一致; | |
| 105 | + 2. 规则违规: | |
| 106 | + - 禁止字段不写表名前缀(必须是「表名.字段名」格式); | |
| 107 | + - 禁止日期字段非空判断用ifnull(日期字段,'')<>''(仅字符字段用此写法,日期字段用IS NOT NULL); | |
| 108 | + - 禁止生成与{{errorSql}}/{{historySqlList}}重复的语句,禁止仅修改排序字段/别名的“伪差异化”,可以使用子查询修复或者修改查询字段,不要使用窗口函数(如 LAG); | |
| 109 | + - 禁止LAG窗口函数缺失ORDER BY子句(必须按时间字段排序); | |
| 110 | + - 禁止HAVING条件使用COUNT()无参数写法,禁止过滤条件与业务需求无关; | |
| 111 | + - 禁止在 AVG/SUM 等聚合函数中嵌套窗口函数; | |
| 112 | + 3. 编写规范: | |
| 113 | + - 多表关联必须使用 表名+字段名(如表名.字段名),严格按下面[涉及表名]中的表次序关联,聚合函数(SUM/COUNT/AVG/MIN/MAX)必须加业务化别名,日期过滤使用标准DATE格式(yyyy-MM-dd); | |
| 114 | + - SQL所有字段均采用 表名.字段名 方式生成,务必确保 字段名 在相应的 表名 描述的字段中存在,如果不存在重试其它方式,直到满足条件; | |
| 115 | + - SQL所有字段涉及的所有表名,都要**严格**按下面[涉及表名]中的表次序关联,没有关联不允许使用; | |
| 116 | + - SQL所有的查询条件,如果是字符类型的字段,均需要加不为空判断,用示例格式判断,示例:ifnull(customername,'')<>''; | |
| 117 | + - SQL所有的查询条件,如果是日期类型的字段,均需要加不为空判断,用示例格式判断,示例:tmakedate is not Null; | |
| 118 | + - SQL所有的显示字段的别名中,不能出现空格,如: tCreateDate as earliest 订单日期,正确的应是 tCreateDate as earliest订单日期 | |
| 119 | + - 在AVG聚合函数中不允许使用LAG、LEAD 等窗口函数 | |
| 120 | + - AVG(LAG(...))这种嵌套是不允许的 | |
| 121 | + - GROUP BY 后面不允许使用窗口函数 | |
| 122 | + """) | |
| 130 | 123 | @UserMessage(""" |
| 131 | 124 | 【业务场景表结构信息】 |
| 132 | - 涉及表名:{{tableNames}}(多表用,分隔,需关联时请按规范使用JOIN) | |
| 133 | - 表结构详情:{{tableStruct}}(多表请标注表名+字段,格式:表名(字段1:类型,字段2:类型,主键/外键)) | |
| 134 | - 当前时间:{{sDataNow}} | |
| 125 | + 涉及表名:{{tableNames}} | |
| 126 | + 表结构详情:{{tableStruct}} | |
| 127 | + 当前时间:{{sDataNow}} | |
| 135 | 128 | 【原始用户需求】 |
| 136 | 129 | {{userInput}} |
| 130 | + 【错误信息】 | |
| 131 | + 之前生成的错误SQL:{{errorSql}} | |
| 132 | + 执行错误信息:{{errorMessage}} | |
| 133 | + 【生成要求】 | |
| 134 | + 1. 先修复错误SQL的所有问题,确保语法/逻辑合规; | |
| 135 | + 2. 生成与{{errorSql}}/{{historySqlList}}重复的语句,禁止仅修改排序字段/别名的“伪差异化”,可以使用子查询修复或者修改查询字段,不要使用窗口函数(如 LAG)的SELECT语句; | |
| 137 | 136 | 请根据上述表结构+通用规则,生成符合要求的MySQL SELECT语句; |
| 138 | - 【之前生成的错误SQL】 | |
| 139 | - {{errorSql}} | |
| 140 | - 【执行错误信息】 | |
| 141 | - {{errorMessage}} | |
| 142 | - 【生成SQL语句要求】 | |
| 143 | - 1.不能生成{{errorSql}}一样的语句 | |
| 144 | - 【错误分析指引】 | |
| 145 | - 1. 错误类型:请根据错误代码判断 | |
| 146 | - - "Unknown column":字段不存在,检查字段名拼写或改用表中存在的字段 | |
| 147 | - - "Table doesn't exist":表名错误,检查表名拼写 | |
| 148 | - - "You have an error in your SQL syntax":语法错误,检查关键词、括号、引号 | |
| 149 | - - "Column not found in ON clause":JOIN条件字段不存在 | |
| 150 | - - "Non unique table/alias":表别名重复 | |
| 151 | - 2. 修复建议: | |
| 152 | - - 如果是字段错误:查看表结构{{tableStruct}},找到正确的字段名替换 | |
| 153 | - - 如果是语法错误:检查SELECT、FROM、WHERE、JOIN等关键词用法 | |
| 154 | - - 如果是类型错误:字符串加单引号,数字不加引号,日期用'yyyy-MM-dd'格式 | |
| 155 | - - 如果是关联错误:确保所有表都通过外键正确JOIN | |
| 156 | - - 如果是未知错误:确保函数之间不互斥,函数能正常使用,然后重新生成新的语句 | |
| 157 | - 请根据以上信息,重新生成正确的MySQL SELECT语句。 | |
| 158 | - 只返回SQL语句本身,不要任何解释和包装。 | |
| 159 | 137 | """) |
| 160 | 138 | String regenerateSqlWithError(@MemoryId String userId, |
| 161 | 139 | @V("tableNames") String tableNames, |
| ... | ... | @@ -163,7 +141,9 @@ public interface DynamicTableNl2SqlAiAgent { |
| 163 | 141 | @V("sDataNow") String sDataNow, |
| 164 | 142 | @V("userInput") String userInput, |
| 165 | 143 | @V("errorSql") String errorSql, |
| 166 | - @V("errorMessage") String errorMessage | |
| 144 | + @V("errorMessage") String errorMessage, | |
| 145 | + @V("n") String iErroCount, | |
| 146 | + @V("historySqlList") String historySqlList | |
| 167 | 147 | ); |
| 168 | 148 | /** |
| 169 | 149 | * 动态表结构:自然语言解释SQL执行结果 |
| ... | ... | @@ -179,11 +159,11 @@ public interface DynamicTableNl2SqlAiAgent { |
| 179 | 159 | """) |
| 180 | 160 | @UserMessage(""" |
| 181 | 161 | 【业务场景表结构信息】 |
| 182 | - 表结构详情:{{tableStruct}} | |
| 162 | + 表结构详情:{{tableStruct}} | |
| 183 | 163 | 【查询相关信息】 |
| 184 | 164 | 用户原始查询:{{userInput}} |
| 185 | 165 | 执行的MySQL SQL:{{sql}} |
| 186 | - SQL执行结果(JSON格式):{{result}} | |
| 166 | + SQL执行结果(JSON格式):{{result}} | |
| 187 | 167 | 请根据上述信息+通用规则,对查询结果做业务解释: |
| 188 | 168 | """) |
| 189 | 169 | String explainSqlResult(@MemoryId String userId, | ... | ... |
src/main/java/com/xly/runner/AppStartupRunner.java
| ... | ... | @@ -151,9 +151,9 @@ public class AppStartupRunner implements CommandLineRunner { |
| 151 | 151 | /** |
| 152 | 152 | * 根据编码获取AI代理 |
| 153 | 153 | */ |
| 154 | - public static SceneDto getAiAgentByCode(String sCode) { | |
| 154 | + public static SceneDto getAiAgentByCode(String sName) { | |
| 155 | 155 | return AI_AGENT_CACHE.stream() |
| 156 | - .filter(agent -> agent.getSSceneNo().equals(sCode)) | |
| 156 | + .filter(agent -> agent.getSSceneName().equals(sName)) | |
| 157 | 157 | .findFirst() |
| 158 | 158 | .orElse(null); |
| 159 | 159 | } | ... | ... |
src/main/java/com/xly/service/XlyErpService.java
| ... | ... | @@ -4,7 +4,6 @@ import cn.hutool.core.date.DatePattern; |
| 4 | 4 | import cn.hutool.core.date.DateUtil; |
| 5 | 5 | import cn.hutool.core.util.ObjectUtil; |
| 6 | 6 | import cn.hutool.core.util.StrUtil; |
| 7 | -import cn.hutool.db.Session; | |
| 8 | 7 | import com.alibaba.fastjson2.JSON; |
| 9 | 8 | import com.xly.agent.ChatiAgent; |
| 10 | 9 | import com.xly.agent.DynamicTableNl2SqlAiAgent; |
| ... | ... | @@ -29,17 +28,9 @@ import dev.langchain4j.data.message.ChatMessageType; |
| 29 | 28 | import dev.langchain4j.model.chat.ChatLanguageModel; |
| 30 | 29 | import dev.langchain4j.model.ollama.OllamaChatModel; |
| 31 | 30 | import dev.langchain4j.service.AiServices; |
| 32 | -import dev.langchain4j.service.V; | |
| 33 | -import jnr.ffi.annotations.In; | |
| 34 | 31 | import lombok.RequiredArgsConstructor; |
| 35 | 32 | import lombok.extern.slf4j.Slf4j; |
| 36 | -import org.python.antlr.ast.Str; | |
| 37 | 33 | import org.springframework.stereotype.Service; |
| 38 | - | |
| 39 | -import java.math.BigDecimal; | |
| 40 | -import java.text.DateFormat; | |
| 41 | -import java.time.LocalDate; | |
| 42 | -import java.time.format.DateTimeFormatter; | |
| 43 | 34 | import java.util.*; |
| 44 | 35 | |
| 45 | 36 | @Service |
| ... | ... | @@ -123,30 +114,12 @@ public class XlyErpService { |
| 123 | 114 | // 缺失的参数明细 |
| 124 | 115 | sResponMessage = session.getSFunPrompts(); |
| 125 | 116 | } |
| 126 | -// //说明没有找到动态方法重新走一次 | |
| 127 | -// if(maxToolRetries==1 && session.getCurrentTool()== null){ | |
| 128 | -// Thread.sleep(1000); | |
| 129 | -// doCleanUserMemory(session,userId); | |
| 130 | -// return erpUserInput( userInput, | |
| 131 | -// userId , | |
| 132 | -// sUserName , | |
| 133 | -// sBrandsId , | |
| 134 | -// sSubsidiaryId, | |
| 135 | -// sUserType, | |
| 136 | -// authorization, | |
| 137 | -// 0); | |
| 138 | -// } | |
| 139 | - | |
| 140 | 117 | if (session.getCurrentTool()== null){ |
| 141 | 118 | sResponMessageOld = sResponMessage; |
| 142 | 119 | sResponMessage = StrUtil.EMPTY; |
| 143 | 120 | } |
| 144 | 121 | //5.执行工具方法后,清除记忆 |
| 145 | 122 | if(session.getBCleanMemory()){ |
| 146 | -// operableChatMemoryProvider.clearSpecifiedMemory(userId); | |
| 147 | -// session.setCurrentTool(null); | |
| 148 | -// UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); | |
| 149 | -// UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId); | |
| 150 | 123 | doCleanUserMemory(session,userId); |
| 151 | 124 | } |
| 152 | 125 | // 6.找到方法并且本方法带表结构描述时,需要调用 自然语言转SQL智能体 |
| ... | ... | @@ -154,7 +127,7 @@ public class XlyErpService { |
| 154 | 127 | && ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName()) |
| 155 | 128 | && ObjectUtil.isNotEmpty(session.getCurrentTool().getSStructureMemo())) |
| 156 | 129 | ){ |
| 157 | - sResponMessage = getDynamicTableSql(session, input, userId, userInput,0,StrUtil.EMPTY,StrUtil.EMPTY); | |
| 130 | + sResponMessage = getDynamicTableSql(session, input, userId, userInput,0,StrUtil.EMPTY,StrUtil.EMPTY,"0",StrUtil.EMPTY); | |
| 158 | 131 | } |
| 159 | 132 | //如果返回空的进入闲聊模式 |
| 160 | 133 | if (ObjectUtil.isEmpty(sResponMessage)){ |
| ... | ... | @@ -200,24 +173,30 @@ public class XlyErpService { |
| 200 | 173 | * @return java.lang.String |
| 201 | 174 | * @Description 获取执行动态SQL |
| 202 | 175 | **/ |
| 203 | - private String getDynamicTableSql(UserSceneSession session,String input,String userId,String userInput,Integer attempt,String errorSql,String errorMessage ){ | |
| 176 | + private String getDynamicTableSql(UserSceneSession session,String input,String userId,String userInput,Integer attempt,String errorSql,String errorMessage,String iErroCount,String historySqlList ){ | |
| 204 | 177 | String resultExplain = "信息模糊,请提供更具体的问题或指令"; |
| 205 | 178 | try{ |
| 206 | 179 | while (attempt < maxRetries) { |
| 207 | 180 | try{ |
| 208 | 181 | attempt = attempt+1; |
| 209 | - return getDynamicTableSqlExec(session, input, userId, userInput,errorSql,errorMessage); | |
| 182 | + return getDynamicTableSqlExec(session, input, userId, userInput,errorSql,errorMessage,iErroCount,historySqlList); | |
| 210 | 183 | }catch (Exception e){ |
| 211 | 184 | String erroMsg = e.getMessage(); |
| 212 | 185 | String errorSqlOld = StrUtil.EMPTY; |
| 213 | 186 | if(erroMsg.contains(EnhancedErrorGuidance.splitString) && erroMsg.split(EnhancedErrorGuidance.splitString).length>1){ |
| 214 | 187 | errorSqlOld = erroMsg.split(EnhancedErrorGuidance.splitString)[1]; |
| 188 | + errorSqlOld = StrUtil.replace(errorSqlOld,";",""); | |
| 189 | + if(StrUtil.isNotEmpty(historySqlList)){ | |
| 190 | + historySqlList = historySqlList+"/"+errorSqlOld; | |
| 191 | + }else{ | |
| 192 | + historySqlList = errorSqlOld; | |
| 193 | + } | |
| 215 | 194 | } |
| 216 | - String errorMessageOld = EnhancedErrorGuidance.getErrorGuidance(erroMsg); | |
| 195 | + String errorMessageOld = erroMsg; | |
| 217 | 196 | if (attempt == maxRetries) { |
| 218 | - return resultExplain+"查询的SQL语句:"+errorSqlOld; | |
| 197 | + return resultExplain+"查询的SQL语句:"+historySqlList; | |
| 219 | 198 | } else { |
| 220 | - return getDynamicTableSql( session, input, userId, userInput, attempt,errorSqlOld,errorMessageOld); | |
| 199 | + return getDynamicTableSql( session, input, userId, userInput, attempt,errorSqlOld,errorMessageOld,attempt.toString(),historySqlList); | |
| 221 | 200 | } |
| 222 | 201 | } |
| 223 | 202 | } |
| ... | ... | @@ -252,7 +231,7 @@ public class XlyErpService { |
| 252 | 231 | * @return java.lang.String |
| 253 | 232 | * @Description 执行动态sSql |
| 254 | 233 | **/ |
| 255 | - private String getDynamicTableSqlExec(UserSceneSession session,String input,String userId,String userInput,String errorSql,String errorMessage){ | |
| 234 | + private String getDynamicTableSqlExec(UserSceneSession session,String input,String userId,String userInput,String errorSql,String errorMessage,String iErroCount,String historySqlList){ | |
| 256 | 235 | // 1. 构建自然语言转SQLAgent, |
| 257 | 236 | DynamicTableNl2SqlAiAgent aiDynamicTableNl2SqlAiAgent = createDynamicTableNl2SqlAiAgent(userId, input, session); |
| 258 | 237 | String tableNames = session.getCurrentTool().getSInputTabelName(); |
| ... | ... | @@ -264,9 +243,12 @@ public class XlyErpService { |
| 264 | 243 | if(ObjectUtil.isEmpty(errorSql) && ObjectUtil.isEmpty(errorMessage)){ |
| 265 | 244 | rawSql = aiDynamicTableNl2SqlAiAgent.generateMysqlSql(userId,tableNames,tableStruct,sDataNow,userInput); |
| 266 | 245 | }else{ |
| 267 | - rawSql = aiDynamicTableNl2SqlAiAgent.regenerateSqlWithError(userId, tableNames,tableStruct,sDataNow,userInput,errorSql,errorMessage); | |
| 246 | + rawSql = aiDynamicTableNl2SqlAiAgent.regenerateSqlWithError(userId, tableNames,tableStruct,sDataNow,userInput,errorSql,errorMessage,iErroCount,historySqlList); | |
| 247 | + } | |
| 248 | + String[] rawSqlA = rawSql.split(";"); | |
| 249 | + if(rawSqlA.length>1){ | |
| 250 | + rawSql = rawSqlA[rawSqlA.length-1]; | |
| 268 | 251 | } |
| 269 | - | |
| 270 | 252 | if (rawSql == null || rawSql.trim().isEmpty()) { |
| 271 | 253 | throw new SqlGenerateException("SQL EMPTY"); |
| 272 | 254 | } |
| ... | ... | @@ -407,7 +389,7 @@ public class XlyErpService { |
| 407 | 389 | return null; |
| 408 | 390 | } |
| 409 | 391 | // 4. 将场景编码转换为BusinessScene枚举 |
| 410 | - String sSceneNo = parseResp.getSceneCode(); | |
| 392 | + String sSceneNo = parseResp.getScene(); | |
| 411 | 393 | return AppStartupRunner.getAiAgentByCode(sSceneNo); |
| 412 | 394 | } catch (Exception e) { |
| 413 | 395 | log.error("用户{}大模型意图解析失败,输入:{}", userId, userInput, e); | ... | ... |