Commit 1599dfd45f2f5b31a14d4849a23d5878239cf570

Authored by qianbao
1 parent 37c6147d

添加未清选择 改成动态引导语

src/main/java/com/xly/agent/ChatiAgent.java
@@ -2,30 +2,45 @@ package com.xly.agent; @@ -2,30 +2,45 @@ package com.xly.agent;
2 2
3 import dev.langchain4j.service.MemoryId; 3 import dev.langchain4j.service.MemoryId;
4 import dev.langchain4j.service.SystemMessage; 4 import dev.langchain4j.service.SystemMessage;
  5 +import dev.langchain4j.service.TokenStream;
5 import dev.langchain4j.service.UserMessage; 6 import dev.langchain4j.service.UserMessage;
6 import dev.langchain4j.service.V; 7 import dev.langchain4j.service.V;
7 -import reactor.core.publisher.Flux;  
8 8
  9 +/**
  10 + * 闲聊智能体(小羚羊人设)
  11 + * 说明:同步/流式两个方法共用同一份人设提示词,改一处即可,避免两份拷贝走偏
  12 + */
9 public interface ChatiAgent { 13 public interface ChatiAgent {
10 - @SystemMessage("""  
11 - 你是一个轻松自然的聊天伙伴,语气亲切口语化,像朋友一样闲聊。  
12 - 要求:1. 不生硬、不说教,避免书面化表达;  
13 - 2. 主动接梗,适当延伸话题,不一问一答;  
14 - 3. 偶尔带点小幽默,保持轻松无压力的氛围;  
15 - 4. 回答简洁,符合日常聊天的语气,不啰嗦。  
16 - 5. 首次沟通时发现称呼不是“小羚羊”时,请回复“我不是..,我是小羚羊”,语气俏皮。  
17 - """) 14 +
  15 +
  16 + /** 闲聊人设提示词(编译期常量,可直接用于 @SystemMessage) */
  17 + String CHAT_SYSTEM_PROMPT = """
  18 + 你叫“小羚羊”,是用户身边轻松自然的聊天伙伴,语气亲切口语化,像朋友一样闲聊。
  19 + 【聊天风格】
  20 + 1. 不生硬、不说教,避免书面化表达;
  21 + 2. 主动接梗,适当延伸话题,不要一问一答;
  22 + 3. 偶尔带点小幽默,保持轻松无压力的氛围。
  23 + 【身份规则】
  24 + 4. 只有当用户把你叫成别的名字或别的助手时,才俏皮地回一句“我不是…,我是小羚羊~”;
  25 + 用户没有称呼你的时候,绝对不要主动提这句话。
  26 + 【输出要求】
  27 + 5. 回答控制在 60 字以内,一到两句话,不啰嗦、不分点、不做总结;
  28 + 6. 回复会被转成语音播报,只输出纯文本:不要 Markdown、标题、列表、表情符号和特殊符号;
  29 + 7. 不要编造业务数据。用户问到订单、报价、客户、库存等业务问题时,
  30 + 提示他输入“重置”重新选择业务场景,不要自己瞎答。
  31 + """;
  32 +
  33 + @SystemMessage(CHAT_SYSTEM_PROMPT)
18 @UserMessage("用户说:{{userInput}}") 34 @UserMessage("用户说:{{userInput}}")
19 String chat(@MemoryId String userId, @V("userInput") String userInput); 35 String chat(@MemoryId String userId, @V("userInput") String userInput);
20 36
21 - @SystemMessage("""  
22 - 你是一个轻松自然的聊天伙伴,语气亲切口语化,像朋友一样闲聊。  
23 - 要求:1. 不生硬、不说教,避免书面化表达;  
24 - 2. 主动接梗,适当延伸话题,不一问一答;  
25 - 3. 偶尔带点小幽默,保持轻松无压力的氛围;  
26 - 4. 回答简洁,符合日常聊天的语气,不啰嗦。  
27 - 5. 首次沟通时发现称呼不是“小羚羊”时,请回复“我不是..,我是小羚羊”,语气俏皮。  
28 - """) 37 + /**
  38 + * 流式闲聊。
  39 + * 注意:返回 TokenStream 而不是 Flux —— Flux 返回值依赖 langchain4j-reactor 模块提供的
  40 + * TokenStreamAdapter(SPI),本项目未引入该依赖,直接返回 Flux 会在调用时抛
  41 + * “Can't find suitable TokenStreamAdapter”。调用方用 Flux.create 桥接即可。
  42 + */
  43 + @SystemMessage(CHAT_SYSTEM_PROMPT)
29 @UserMessage("用户说:{{userInput}}") 44 @UserMessage("用户说:{{userInput}}")
30 - Flux<String> chatStream(@MemoryId String userId, @V("userInput") String userInput);  
31 -} 45 + TokenStream chatStream(@MemoryId String userId, @V("userInput") String userInput);
  46 +}
32 \ No newline at end of file 47 \ No newline at end of file
src/main/java/com/xly/agent/SecMethodAiAgent.java 0 → 100644
  1 +package com.xly.agent;
  2 +
  3 +import dev.langchain4j.service.*;
  4 +
  5 +public interface SecMethodAiAgent {
  6 +
  7 + @SystemMessage("{{sSystemPrompt}}")
  8 + @UserMessage("用户输入:{{userInput}}")
  9 + Result<String> choiceMethod(
  10 + @MemoryId String userId,
  11 + @V("userInput") String userInput,
  12 + @V("sSystemPrompt") String sSystemPrompt
  13 + );
  14 +}
0 \ No newline at end of file 15 \ No newline at end of file
src/main/java/com/xly/agent/SystemPromptGenerator.java
@@ -5,6 +5,7 @@ import com.alibaba.fastjson2.JSON; @@ -5,6 +5,7 @@ import com.alibaba.fastjson2.JSON;
5 import com.alibaba.fastjson2.JSONObject; 5 import com.alibaba.fastjson2.JSONObject;
6 import com.alibaba.fastjson2.JSONWriter; 6 import com.alibaba.fastjson2.JSONWriter;
7 import com.xly.entity.ParamRule; 7 import com.xly.entity.ParamRule;
  8 +import com.xly.entity.ToolMeta;
8 import com.xly.entity.UserSceneSession; 9 import com.xly.entity.UserSceneSession;
9 10
10 import java.util.List; 11 import java.util.List;
@@ -110,4 +111,60 @@ public class SystemPromptGenerator { @@ -110,4 +111,60 @@ public class SystemPromptGenerator {
110 """.formatted(methodNo); 111 """.formatted(methodNo);
111 } 112 }
112 113
  114 +
  115 + /**
  116 + * 方法选择阶段:从可选方法树中,让大模型选出唯一最匹配的一个 sMethodNo。
  117 + * 与 generate()(参数提取)配套,是它的上游一步。
  118 + *
  119 + * @param toolList 当前场景可选的方法列表(方法树扁平化后的叶子集合)
  120 + * @param sKnowledgeBase 行业知识库,可为空
  121 + * @return 供 choiceMethod 使用的 sSystemPrompt
  122 + */
  123 + public static String generateChoiceMethodPrompt(List<ToolMeta> toolList, String sKnowledgeBase) {
  124 + StringBuilder sb = new StringBuilder();
  125 +
  126 + sb.append("你是一个方法路由分类器。你的职责是理解用户意图,从候选方法中选出唯一最匹配的一项。\n\n");
  127 +
  128 + sb.append("【任务】\n");
  129 + sb.append("阅读用户输入,与【可选方法列表】逐项比对语义,选出意图最贴合的一个方法,输出它的 sMethodNo。\n");
  130 + sb.append("判断时以用户的核心操作意图为准,只依据用户明确表达的内容,不推测未提及的需求。\n\n");
  131 +
  132 + sb.append("【输出要求】\n");
  133 + sb.append("- 只输出一个 JSON 对象,形如:{\"sMethodNo\": \"方法编号\", \"confidence\": 0.0}\n");
  134 + sb.append("- sMethodNo 取自【可选方法列表】中的编号;均不匹配时取 \"none\"。\n");
  135 + sb.append("- confidence 为 0~1 的匹配置信度:意图明确取高值,表述模糊取低值。\n");
  136 + sb.append("- 不要输出解释、思考过程或 JSON 以外的任何文字。\n\n");
  137 +
  138 + if (ObjectUtil.isNotEmpty(sKnowledgeBase)) {
  139 + sb.append("【行业背景知识】\n");
  140 + sb.append("以下知识用于帮助你理解用户输入中的行业术语,请结合它判断意图:\n");
  141 + sb.append(sKnowledgeBase).append("\n\n");
  142 + }
  143 +
  144 + sb.append("【可选方法列表】\n");
  145 + for (ToolMeta tool : toolList) {
  146 + sb.append("- sMethodNo: ").append(tool.getSMethodNo())
  147 + .append(" 名称: ").append(tool.getSMethodName());
  148 + // 描述为空时降级用场景名,保证每个方法都有判别依据
  149 + String desc = ObjectUtil.isNotEmpty(tool.getStoolDesc())
  150 + ? tool.getStoolDesc() : tool.getSceneName();
  151 + if (ObjectUtil.isNotEmpty(desc)) {
  152 + sb.append(" 适用场景: ").append(desc);
  153 + }
  154 + sb.append("\n");
  155 + }
  156 +
  157 + sb.append("\n【示例】\n");
  158 + int count = 0;
  159 + for (ToolMeta tool : toolList) {
  160 + if (count++ >= 3) break;
  161 + sb.append("- 用户输入涉及\"").append(tool.getSMethodName())
  162 + .append("\" → {\"sMethodNo\": \"").append(tool.getSMethodNo())
  163 + .append("\", \"confidence\": 0.9}\n");
  164 + }
  165 + sb.append("- 用户输入\"你好\" → {\"sMethodNo\": \"none\", \"confidence\": 0}\n");
  166 +
  167 + return sb.toString();
  168 + }
  169 +
113 } 170 }
114 \ No newline at end of file 171 \ No newline at end of file
src/main/java/com/xly/config/ModelConfig.java
1 package com.xly.config; 1 package com.xly.config;
2 2
  3 +import com.fasterxml.jackson.databind.DeserializationFeature;
3 import com.fasterxml.jackson.databind.ObjectMapper; 4 import com.fasterxml.jackson.databind.ObjectMapper;
4 import com.fasterxml.jackson.databind.SerializationFeature; 5 import com.fasterxml.jackson.databind.SerializationFeature;
5 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 6 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
  7 +import com.xly.agent.ChatiAgent;
6 import com.xly.agent.DynamicTableNl2SqlAiAgent; 8 import com.xly.agent.DynamicTableNl2SqlAiAgent;
7 import com.xly.agent.SceneSelectorAiAgent; 9 import com.xly.agent.SceneSelectorAiAgent;
  10 +import com.xly.agent.SecMethodAiAgent;
  11 +import dev.langchain4j.memory.chat.MessageWindowChatMemory;
8 import dev.langchain4j.model.ollama.OllamaChatModel; 12 import dev.langchain4j.model.ollama.OllamaChatModel;
9 import dev.langchain4j.model.ollama.OllamaStreamingChatModel; 13 import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
10 import dev.langchain4j.service.AiServices; 14 import dev.langchain4j.service.AiServices;
11 -import dev.langchain4j.memory.chat.MessageWindowChatMemory;  
12 import org.springframework.beans.factory.annotation.Qualifier; 15 import org.springframework.beans.factory.annotation.Qualifier;
13 import org.springframework.beans.factory.annotation.Value; 16 import org.springframework.beans.factory.annotation.Value;
14 import org.springframework.context.annotation.Bean; 17 import org.springframework.context.annotation.Bean;
@@ -17,11 +20,17 @@ import org.springframework.context.annotation.Primary; @@ -17,11 +20,17 @@ import org.springframework.context.annotation.Primary;
17 20
18 import java.time.Duration; 21 import java.time.Duration;
19 22
  23 +/**
  24 + * 模型与 Agent 统一装配(langchain4j 1.14.0 新 API:ChatModel / StreamingChatModel)
  25 + * 约定:
  26 + * - 路由/SQL/参数提取类任务 → 低温(确定性)
  27 + * - 闲聊类任务 → 高温(多样性)
  28 + */
20 @Configuration 29 @Configuration
21 public class ModelConfig { 30 public class ModelConfig {
22 31
23 @Value("${langchain4j.ollama.base-url}") 32 @Value("${langchain4j.ollama.base-url}")
24 - private String chatModelUrl; 33 + private String baseUrl;
25 34
26 @Value("${langchain4j.ollama.chat-model-name}") 35 @Value("${langchain4j.ollama.chat-model-name}")
27 private String chatModelName; 36 private String chatModelName;
@@ -29,88 +38,106 @@ public class ModelConfig { @@ -29,88 +38,106 @@ public class ModelConfig {
29 @Value("${langchain4j.ollama.sql-model-name}") 38 @Value("${langchain4j.ollama.sql-model-name}")
30 private String sqlModelName; 39 private String sqlModelName;
31 40
32 - // ====================== 主对话模型 ====================== 41 + // ========================================================
  42 + // 一、私有 builder 工厂:消除重复配置
  43 + // ========================================================
  44 +
  45 + /** 非流式 Ollama 模型的公共构造 */
  46 + private OllamaChatModel.OllamaChatModelBuilder chatBuilder(String modelName,
  47 + double temperature,
  48 + double topP,
  49 + long timeoutSeconds,
  50 + int maxRetries) {
  51 + return OllamaChatModel.builder()
  52 + .baseUrl(baseUrl)
  53 + .modelName(modelName)
  54 + .temperature(temperature)
  55 + .topP(topP)
  56 + .timeout(Duration.ofSeconds(timeoutSeconds))
  57 + .maxRetries(maxRetries);
  58 + }
  59 +
  60 + /** 流式 Ollama 模型的公共构造 */
  61 + private OllamaStreamingChatModel.OllamaStreamingChatModelBuilder streamingBuilder(String modelName,
  62 + double temperature,
  63 + double topP,
  64 + int numPredict,
  65 + long timeoutSeconds) {
  66 + return OllamaStreamingChatModel.builder()
  67 + .baseUrl(baseUrl)
  68 + .modelName(modelName)
  69 + .temperature(temperature)
  70 + .topP(topP)
  71 + .numPredict(numPredict)
  72 + .timeout(Duration.ofSeconds(timeoutSeconds));
  73 + }
  74 +
  75 + // ========================================================
  76 + // 二、模型 Bean
  77 + // ========================================================
  78 +
  79 + /** 主对话模型:路由/场景/方法选择等确定性任务共用 */
33 @Bean 80 @Bean
34 @Primary 81 @Primary
35 public OllamaChatModel chatLanguageModel() { 82 public OllamaChatModel chatLanguageModel() {
36 - return OllamaChatModel.builder()  
37 - .baseUrl(chatModelUrl)  
38 - .modelName(chatModelName)  
39 - .temperature(0.1)  
40 - .topP(0.95)  
41 - .timeout(Duration.ofSeconds(120))  
42 - .maxRetries(2) 83 + return chatBuilder(chatModelName, 0.1, 0.95, 120, 2)
43 .logRequests(true) 84 .logRequests(true)
44 .logResponses(true) 85 .logResponses(true)
45 .build(); 86 .build();
46 } 87 }
47 88
48 - // ====================== 自由闲聊模型 ====================== 89 + /** 自由闲聊(非流式) */
49 @Bean("chatiModel") 90 @Bean("chatiModel")
50 public OllamaChatModel chatiModel() { 91 public OllamaChatModel chatiModel() {
51 - return OllamaChatModel.builder()  
52 - .baseUrl(chatModelUrl)  
53 - .modelName(chatModelName)  
54 - .temperature(0.7)  
55 - .topP(0.9)  
56 - .timeout(Duration.ofSeconds(60))  
57 - .maxRetries(2)  
58 - .build(); 92 + return chatBuilder(chatModelName, 0.7, 0.9, 60, 2).build();
  93 + }
  94 +
  95 + /** 自由闲聊(流式) */
  96 + @Bean("chatiStreamingModel")
  97 + public OllamaStreamingChatModel chatiStreamingModel() {
  98 + return streamingBuilder(chatModelName, 0.7, 0.9, 512, 60).build();
59 } 99 }
60 100
61 - // ====================== SQL 专用模型 ====================== 101 + /** SQL 专用(非流式,零温 + 大输出) */
62 @Bean("sqlChatModel") 102 @Bean("sqlChatModel")
63 public OllamaChatModel sqlChatModel() { 103 public OllamaChatModel sqlChatModel() {
64 - return OllamaChatModel.builder()  
65 - .baseUrl(chatModelUrl)  
66 - .modelName(sqlModelName)  
67 - .temperature(0.0)  
68 - .topP(0.95) 104 + return chatBuilder(sqlModelName, 0.0, 0.95, 120, 3)
69 .numPredict(4096) 105 .numPredict(4096)
70 - .timeout(Duration.ofSeconds(120))  
71 - .maxRetries(3)  
72 .build(); 106 .build();
73 } 107 }
74 108
75 - // ====================== 流式对话模型 ====================== 109 + /** 主流式对话 */
76 @Bean("streamingChatModel") 110 @Bean("streamingChatModel")
77 @Primary 111 @Primary
78 public OllamaStreamingChatModel streamingChatModel() { 112 public OllamaStreamingChatModel streamingChatModel() {
79 - return OllamaStreamingChatModel.builder()  
80 - .baseUrl(chatModelUrl)  
81 - .modelName(chatModelName)  
82 - .temperature(0.3)  
83 - .topP(0.9)  
84 - .numPredict(1024)  
85 - .timeout(Duration.ofSeconds(60))  
86 - .build(); 113 + return streamingBuilder(chatModelName, 0.3, 0.9, 1024, 60).build();
87 } 114 }
88 115
89 - // ====================== 流式 SQL 模型 ====================== 116 + /** SQL 流式 */
90 @Bean("streamingSqlModel") 117 @Bean("streamingSqlModel")
91 public OllamaStreamingChatModel streamingSqlModel() { 118 public OllamaStreamingChatModel streamingSqlModel() {
92 - return OllamaStreamingChatModel.builder()  
93 - .baseUrl(chatModelUrl)  
94 - .modelName(sqlModelName)  
95 - .temperature(0.2)  
96 - .topP(0.95)  
97 - .numPredict(2048)  
98 - .timeout(Duration.ofSeconds(120))  
99 - .build(); 119 + return streamingBuilder(sqlModelName, 0.2, 0.95, 2048, 120).build();
100 } 120 }
101 121
102 - // ====================== JSON ====================== 122 + // ========================================================
  123 + // 三、JSON
  124 + // ========================================================
  125 +
103 @Bean 126 @Bean
104 @Primary 127 @Primary
105 public ObjectMapper objectMapper() { 128 public ObjectMapper objectMapper() {
106 ObjectMapper mapper = new ObjectMapper(); 129 ObjectMapper mapper = new ObjectMapper();
107 mapper.registerModule(new JavaTimeModule()); 130 mapper.registerModule(new JavaTimeModule());
108 mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); 131 mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
109 - mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 132 + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
110 return mapper; 133 return mapper;
111 } 134 }
112 135
113 - // ====================== 动态 SQL Agent ====================== 136 + // ========================================================
  137 + // 四、Agent Bean
  138 + // ========================================================
  139 +
  140 + /** 动态 SQL Agent */
114 @Bean 141 @Bean
115 public DynamicTableNl2SqlAiAgent dynamicTableNl2SqlAiAgent( 142 public DynamicTableNl2SqlAiAgent dynamicTableNl2SqlAiAgent(
116 @Qualifier("sqlChatModel") OllamaChatModel sqlModel) { 143 @Qualifier("sqlChatModel") OllamaChatModel sqlModel) {
@@ -120,13 +147,45 @@ public class ModelConfig { @@ -120,13 +147,45 @@ public class ModelConfig {
120 .build(); 147 .build();
121 } 148 }
122 149
123 - // ====================== 场景选择 Agent ====================== 150 + /**
  151 + * 闲聊 Agent:会话隔离由 @MemoryId + OperableChatMemoryProvider 完成,
  152 + * 全局共用一个实例即可
  153 + */
  154 + @Bean
  155 + public ChatiAgent chatiAgent(
  156 + @Qualifier("chatiModel") OllamaChatModel chatiModel,
  157 + @Qualifier("chatiStreamingModel") OllamaStreamingChatModel chatiStreamingModel,
  158 + OperableChatMemoryProvider operableChatMemoryProvider) {
  159 + return AiServices.builder(ChatiAgent.class)
  160 + .chatModel(chatiModel)
  161 + .streamingChatModel(chatiStreamingModel)
  162 + .chatMemoryProvider(operableChatMemoryProvider)
  163 + .maxSequentialToolsInvocations(1)
  164 + .build();
  165 + }
  166 +
  167 + /** 场景选择 Agent(一级路由) */
124 @Bean 168 @Bean
125 public SceneSelectorAiAgent sceneSelectorAiAgent( 169 public SceneSelectorAiAgent sceneSelectorAiAgent(
126 @Qualifier("chatLanguageModel") OllamaChatModel chatLanguageModel) { 170 @Qualifier("chatLanguageModel") OllamaChatModel chatLanguageModel) {
127 return AiServices.builder(SceneSelectorAiAgent.class) 171 return AiServices.builder(SceneSelectorAiAgent.class)
128 .chatModel(chatLanguageModel) 172 .chatModel(chatLanguageModel)
129 .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) 173 .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
  174 + .maxSequentialToolsInvocations(1)
  175 + .build();
  176 + }
  177 +
  178 + /**
  179 + * 方法选择 Agent(二级路由):从场景内方法树中路由出 sMethodNo。
  180 + * 分类任务,用极短记忆窗口避免被上文惯性带偏。
  181 + */
  182 + @Bean
  183 + public SecMethodAiAgent secMethodAiAgent(
  184 + @Qualifier("chatLanguageModel") OllamaChatModel chatLanguageModel) {
  185 + return AiServices.builder(SecMethodAiAgent.class)
  186 + .chatModel(chatLanguageModel)
  187 + .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(2))
  188 + .maxSequentialToolsInvocations(1)
130 .build(); 189 .build();
131 } 190 }
132 } 191 }
133 \ No newline at end of file 192 \ No newline at end of file
src/main/java/com/xly/entity/MethodChoiceResult.java 0 → 100644
  1 +package com.xly.entity;
  2 +
  3 +import lombok.Data;
  4 +
  5 +@Data
  6 +public class MethodChoiceResult {
  7 + private String sMethodNo;
  8 + private Double confidence;
  9 +}
0 \ No newline at end of file 10 \ No newline at end of file
src/main/java/com/xly/service/MethodRouterService.java 0 → 100644
  1 +package com.xly.service;
  2 +
  3 +import cn.hutool.core.util.ObjectUtil;
  4 +import com.alibaba.fastjson2.JSON;
  5 +import com.xly.agent.SecMethodAiAgent;
  6 +import com.xly.agent.SystemPromptGenerator;
  7 +import com.xly.entity.MethodChoiceResult;
  8 +import com.xly.entity.ToolMeta;
  9 +import com.xly.entity.UserSceneSession;
  10 +import lombok.RequiredArgsConstructor;
  11 +import lombok.extern.slf4j.Slf4j;
  12 +import org.springframework.stereotype.Service;
  13 +
  14 +import java.util.List;
  15 +import java.util.Optional;
  16 +
  17 +@Slf4j
  18 +@Service
  19 +@RequiredArgsConstructor
  20 +public class MethodRouterService {
  21 +
  22 + private final SecMethodAiAgent aiAgent;
  23 +
  24 + /** confidence 低于此值视为未命中,走澄清兜底 */
  25 + private static final double CONFIDENCE_THRESHOLD = 0.5;
  26 +
  27 + /**
  28 + * 根据用户输入路由到具体方法,命中则写入 session.currentTool 并返回该方法。
  29 + *
  30 + * @return 命中的 ToolMeta;未命中返回 null(由上层决定澄清或走通用 chat)
  31 + */
  32 + public ToolMeta route(UserSceneSession session, String userInput,
  33 + List<ToolMeta> toolList, String sKnowledgeBase) {
  34 +
  35 + if (ObjectUtil.isEmpty(toolList)) {
  36 + log.warn("方法路由:可选方法列表为空, userId={}", session.getUserId());
  37 + return null;
  38 + }
  39 +
  40 + // ① 生成方法选择 prompt
  41 + String sSystemPrompt = SystemPromptGenerator
  42 + .generateChoiceMethodPrompt(toolList, sKnowledgeBase);
  43 +
  44 + // ② 调大模型
  45 + String raw = aiAgent.choiceMethod(session.getUserId(), userInput, sSystemPrompt).content();
  46 + log.debug("方法路由原始输出: {}", raw);
  47 +
  48 + // ③ 解析(含 ```json 容错)
  49 + MethodChoiceResult result = parse(raw);
  50 + if (result == null || ObjectUtil.isEmpty(result.getSMethodNo())) {
  51 + return null;
  52 + }
  53 +
  54 + String methodNo = result.getSMethodNo();
  55 + Double confidence = result.getConfidence();
  56 +
  57 + // ④ none / 阈值 / 白名单三重校验
  58 + if ("none".equalsIgnoreCase(methodNo)) {
  59 + log.info("方法路由:无匹配方法, input={}", userInput);
  60 + return null;
  61 + }
  62 + if (confidence == null || confidence < CONFIDENCE_THRESHOLD) {
  63 + log.info("方法路由:置信度不足({}), input={}", confidence, userInput);
  64 + return null;
  65 + }
  66 + Optional<ToolMeta> hit = toolList.stream()
  67 + .filter(t -> methodNo.equals(t.getSMethodNo()))
  68 + .findFirst();
  69 + if (hit.isEmpty()) {
  70 + log.warn("方法路由:模型返回了不存在的编号 {}", methodNo); // 防编造
  71 + return null;
  72 + }
  73 +
  74 + // ⑤ 命中,写入当前工具,交给下游参数提取
  75 + ToolMeta chosen = hit.get();
  76 + session.setCurrentTool(chosen);
  77 + log.info("方法路由命中: {} - {}, confidence={}",
  78 + chosen.getSMethodNo(), chosen.getSMethodName(), confidence);
  79 + return chosen;
  80 + }
  81 +
  82 + /** 解析模型输出,兼容被 ```json ... ``` 包裹或前后带杂字符的情况 */
  83 + private MethodChoiceResult parse(String raw) {
  84 + if (ObjectUtil.isEmpty(raw)) return null;
  85 + try {
  86 + String json = stripCodeFence(raw);
  87 + return JSON.parseObject(json, MethodChoiceResult.class);
  88 + } catch (Exception e) {
  89 + log.error("方法路由结果解析失败, raw={}", raw, e);
  90 + return null;
  91 + }
  92 + }
  93 +
  94 + /** 去掉 markdown 代码块,并截取第一个 { 到最后一个 } */
  95 + private String stripCodeFence(String raw) {
  96 + String s = raw.trim()
  97 + .replaceAll("(?s)```json", "")
  98 + .replaceAll("(?s)```", "")
  99 + .trim();
  100 + int start = s.indexOf('{');
  101 + int end = s.lastIndexOf('}');
  102 + return (start >= 0 && end > start) ? s.substring(start, end + 1) : s;
  103 + }
  104 +}
0 \ No newline at end of file 105 \ No newline at end of file
src/main/java/com/xly/service/UserSceneSessionService.java
1 package com.xly.service; 1 package com.xly.service;
2 2
3 import cn.hutool.core.util.ObjectUtil; 3 import cn.hutool.core.util.ObjectUtil;
4 -import com.xly.agent.ChatiAgent;  
5 import com.xly.agent.DynamicTableNl2SqlAiAgent; 4 import com.xly.agent.DynamicTableNl2SqlAiAgent;
6 import com.xly.agent.ErpAiAgent; 5 import com.xly.agent.ErpAiAgent;
7 import com.xly.config.OperableChatMemoryProvider; 6 import com.xly.config.OperableChatMemoryProvider;
@@ -25,7 +24,7 @@ public class UserSceneSessionService { @@ -25,7 +24,7 @@ public class UserSceneSessionService {
25 24
26 // 原有缓存:Agent实例、会话记忆 25 // 原有缓存:Agent实例、会话记忆
27 public static final Map<String, ErpAiAgent> ERP_AGENT_CACHE = new HashMap<>(); 26 public static final Map<String, ErpAiAgent> ERP_AGENT_CACHE = new HashMap<>();
28 - public static final Map<String, ChatiAgent> CHAT_AGENT_CACHE = new HashMap<>(); 27 + // 闲聊Agent已改为全局单例Bean(会话隔离由 @MemoryId 完成),无需再按用户缓存
29 public static final Map<String, DynamicTableNl2SqlAiAgent> ERP_DynamicTableNl2SqlAiAgent_CACHE = new HashMap<>(); 28 public static final Map<String, DynamicTableNl2SqlAiAgent> ERP_DynamicTableNl2SqlAiAgent_CACHE = new HashMap<>();
30 29
31 30
@@ -72,7 +71,6 @@ public class UserSceneSessionService { @@ -72,7 +71,6 @@ public class UserSceneSessionService {
72 public void cleanAllSession(){ 71 public void cleanAllSession(){
73 USER_SCENE_SESSION_CACHE.clear(); 72 USER_SCENE_SESSION_CACHE.clear();
74 ERP_AGENT_CACHE.clear(); 73 ERP_AGENT_CACHE.clear();
75 - CHAT_AGENT_CACHE.clear();  
76 ERP_DynamicTableNl2SqlAiAgent_CACHE.clear(); 74 ERP_DynamicTableNl2SqlAiAgent_CACHE.clear();
77 } 75 }
78 76
@@ -86,7 +84,6 @@ public class UserSceneSessionService { @@ -86,7 +84,6 @@ public class UserSceneSessionService {
86 public void cleanUserSession(String sUserId){ 84 public void cleanUserSession(String sUserId){
87 USER_SCENE_SESSION_CACHE.remove(sUserId); 85 USER_SCENE_SESSION_CACHE.remove(sUserId);
88 ERP_AGENT_CACHE.remove(sUserId); 86 ERP_AGENT_CACHE.remove(sUserId);
89 - CHAT_AGENT_CACHE.remove(sUserId);  
90 ERP_DynamicTableNl2SqlAiAgent_CACHE.remove(sUserId); 87 ERP_DynamicTableNl2SqlAiAgent_CACHE.remove(sUserId);
91 } 88 }
92 89
src/main/java/com/xly/service/XlyErpService.java
@@ -24,10 +24,7 @@ import com.xly.thread.MultiThreadPoolServer; @@ -24,10 +24,7 @@ import com.xly.thread.MultiThreadPoolServer;
24 import com.xly.tool.DynamicToolProvider; 24 import com.xly.tool.DynamicToolProvider;
25 import com.xly.tool.ToolSpecificationHolder; 25 import com.xly.tool.ToolSpecificationHolder;
26 import com.xly.util.*; 26 import com.xly.util.*;
27 -import dev.langchain4j.agent.tool.ReturnBehavior;  
28 -import dev.langchain4j.agent.tool.ToolExecutionRequest;  
29 import dev.langchain4j.agent.tool.ToolSpecification; 27 import dev.langchain4j.agent.tool.ToolSpecification;
30 -import dev.langchain4j.data.message.AiMessage;  
31 import dev.langchain4j.data.message.ChatMessage; 28 import dev.langchain4j.data.message.ChatMessage;
32 29
33 import dev.langchain4j.memory.ChatMemory; 30 import dev.langchain4j.memory.ChatMemory;
@@ -37,10 +34,8 @@ import dev.langchain4j.service.Result; @@ -37,10 +34,8 @@ import dev.langchain4j.service.Result;
37 import dev.langchain4j.service.tool.ToolExecutor; 34 import dev.langchain4j.service.tool.ToolExecutor;
38 import lombok.RequiredArgsConstructor; 35 import lombok.RequiredArgsConstructor;
39 import lombok.extern.slf4j.Slf4j; 36 import lombok.extern.slf4j.Slf4j;
40 -import org.apache.commons.lang3.time.DateFormatUtils;  
41 import org.springframework.beans.factory.annotation.Value; 37 import org.springframework.beans.factory.annotation.Value;
42 import org.springframework.stereotype.Service; 38 import org.springframework.stereotype.Service;
43 -import org.springframework.util.IdGenerator;  
44 import reactor.core.publisher.Flux; 39 import reactor.core.publisher.Flux;
45 40
46 import java.time.Duration; 41 import java.time.Duration;
@@ -54,7 +49,7 @@ import java.util.stream.IntStream; @@ -54,7 +49,7 @@ import java.util.stream.IntStream;
54 public class XlyErpService { 49 public class XlyErpService {
55 //中文对话模型 50 //中文对话模型
56 private final OllamaChatModel chatModel; 51 private final OllamaChatModel chatModel;
57 - private final OllamaChatModel chatiModel; 52 + private final ChatiAgent chatiAgent;
58 private final SceneSelectorAiAgent sceneSelectorAiAgent; 53 private final SceneSelectorAiAgent sceneSelectorAiAgent;
59 private final UserSceneSessionService userSceneSessionService; 54 private final UserSceneSessionService userSceneSessionService;
60 private final DynamicToolProvider dynamicToolProvider; 55 private final DynamicToolProvider dynamicToolProvider;
@@ -63,6 +58,7 @@ public class XlyErpService { @@ -63,6 +58,7 @@ public class XlyErpService {
63 private final RedisService redisService; 58 private final RedisService redisService;
64 private final AiGlobalAgentQuestionSqlEmitterService aiGlobalAgentQuestionSqlEmitterService; 59 private final AiGlobalAgentQuestionSqlEmitterService aiGlobalAgentQuestionSqlEmitterService;
65 private final MilvusService milvusService; 60 private final MilvusService milvusService;
  61 + private final SecMethodAiAgent secMethodAiAgent;
66 62
67 //执行动态语句 执行异常的情况下 最多执行次数 63 //执行动态语句 执行异常的情况下 最多执行次数
68 private final Integer maxRetries = 5; 64 private final Integer maxRetries = 5;
@@ -342,12 +338,49 @@ public class XlyErpService { @@ -342,12 +338,49 @@ public class XlyErpService {
342 // 3.1 尝试处理场景选择(输入序号则匹配,否则展示选择提示) 338 // 3.1 尝试处理场景选择(输入序号则匹配,否则展示选择提示)
343 return handleSceneSelect(userId, input, session,1,0); 339 return handleSceneSelect(userId, input, session,1,0);
344 } 340 }
345 - // 4. 构建Agent,执行业务交互,如果返回为null,说明大模型没有判段出场景,必判断出后才能继续  
346 - ErpAiAgent aiAgent = createErpAiAgent(userId, input, session);  
347 - // 没有选择到场景,进闲聊模式  
348 - if (aiAgent == null){  
349 - return getChatiAgent (input,session); 341 + List<ToolMeta> metaAll = dynamicToolProvider.metaAll;
  342 + if(ObjectUtil.isNotEmpty(metaAll)){
  343 + List<ToolMeta> metaOne = metaAll.stream().filter(m->
  344 + userInput.equals(m.getSMethodName().trim()) || ("查询"+userInput).equals(m.getSMethodName().trim())
  345 + ).collect(Collectors.toUnmodifiableList());
  346 + if(ObjectUtil.isNotEmpty(metaOne)){
  347 + session.setCurrentTool(metaOne.get(0));
  348 + String sResponMessage = metaOne.get(0).getSTsMemo();
  349 + methodName = metaOne.get(0).getSMethodName();
  350 + return AiResponseDTO.builder().sSceneName(sceneName)
  351 + .sMethodName(methodName)
  352 + .aiText(sResponMessage)
  353 + .sCopyTo(session.getSCopyTo())
  354 + .sCopyToSrcId(session.getSCopyToSrcId())
  355 + .dbType(session.getDbType())
  356 + .dbCach(session.getDbCach())
  357 + .sReturnType(sReturnType)
  358 + .build();
  359 + }
350 } 360 }
  361 +
  362 + //获取方法
  363 + ErpAiAgent aiAgent = null;
  364 + //不存在方法需要选择方法
  365 + if(ObjectUtil.isEmpty(session.getCurrentTool())){
  366 + MethodChoiceResult mr = methodChoiceResult(userId, userInput, session);
  367 + if(ObjectUtil.isNotEmpty(mr) && ObjectUtil.isNotEmpty(mr.getSMethodNo())){
  368 + List<ToolMeta> metaOne = metaAll.stream().filter(m-> mr.getSMethodNo().equals(m.getSMethodNo().trim())
  369 + ).collect(Collectors.toUnmodifiableList());
  370 + session.setCurrentTool(metaOne.get(0));
  371 + }
  372 + }
  373 + //还是不存在方法走老的方式兜底
  374 + if(ObjectUtil.isEmpty(session.getCurrentTool())){
  375 + // 4. 构建Agent,执行业务交互,如果返回为null,说明大模型没有判段出场景,必判断出后才能继续
  376 + aiAgent = createErpAiAgent(userId, input, session);
  377 + // 没有选择到场景,进闲聊模式
  378 + if (aiAgent == null){
  379 + return getChatiAgent (input,session);
  380 + }
  381 + }
  382 + //如果输入的文字等于方法描述的文字,直接锁定方法 返回提示语
  383 +// String sSceneId = session.getCurrentScene().getSId();
351 String sResponMessage = StrUtil.EMPTY; 384 String sResponMessage = StrUtil.EMPTY;
352 //用户输入添加方法(如果没有方法,动态SQL方法不需要) 385 //用户输入添加方法(如果没有方法,动态SQL方法不需要)
353 Boolean isConfirmed = dynamicToolProvider.isConfirmed(input,session); 386 Boolean isConfirmed = dynamicToolProvider.isConfirmed(input,session);
@@ -368,6 +401,7 @@ public class XlyErpService { @@ -368,6 +401,7 @@ public class XlyErpService {
368 //重新生成新的aiAgent拿新的aiAgent 做选择 401 //重新生成新的aiAgent拿新的aiAgent 做选择
369 aiAgent = createConfirmeAgent(session); 402 aiAgent = createConfirmeAgent(session);
370 } 403 }
  404 +
371 //报价每次定义参数 405 //报价每次定义参数
372 if(!isConfirmed && ObjectUtil.isNotEmpty(session) && ObjectUtil.isNotEmpty(session.getCurrentTool()) && session.getCurrentTool().getBQuo()){ 406 if(!isConfirmed && ObjectUtil.isNotEmpty(session) && ObjectUtil.isNotEmpty(session.getCurrentTool()) && session.getCurrentTool().getBQuo()){
373 sSystemPrompt = SystemPromptGenerator.generate(session.getCurrentTool().getSMethodName(),session,session.getCurrentScene().getSKnowledgeBase()); 407 sSystemPrompt = SystemPromptGenerator.generate(session.getCurrentTool().getSMethodName(),session,session.getCurrentScene().getSKnowledgeBase());
@@ -486,7 +520,6 @@ public class XlyErpService { @@ -486,7 +520,6 @@ public class XlyErpService {
486 UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session); 520 UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session);
487 // 清空Agent缓存 521 // 清空Agent缓存
488 UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); 522 UserSceneSessionService.ERP_AGENT_CACHE.remove(userId);
489 - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId);  
490 cleanMemory(userId, sUserName, sBrandsId, sSubsidiaryId, sUserType, authorization); 523 cleanMemory(userId, sUserName, sBrandsId, sSubsidiaryId, sUserType, authorization);
491 } 524 }
492 525
@@ -503,7 +536,6 @@ public class XlyErpService { @@ -503,7 +536,6 @@ public class XlyErpService {
503 session.setArgs(null); 536 session.setArgs(null);
504 session.setSUserQuestionList(new ArrayList<>()); 537 session.setSUserQuestionList(new ArrayList<>());
505 UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); 538 UserSceneSessionService.ERP_AGENT_CACHE.remove(userId);
506 - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId);  
507 session.setBCleanMemory(false); 539 session.setBCleanMemory(false);
508 String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY; 540 String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY;
509 return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(StrUtil.EMPTY).aiText(StrUtil.EMPTY).systemText("清除记忆成功!").sReturnType(ReturnTypeCode.HTML.getCode()).build(); 541 return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(StrUtil.EMPTY).aiText(StrUtil.EMPTY).systemText("清除记忆成功!").sReturnType(ReturnTypeCode.HTML.getCode()).build();
@@ -720,7 +752,6 @@ public class XlyErpService { @@ -720,7 +752,6 @@ public class XlyErpService {
720 session.setArgs(new HashMap<>()); 752 session.setArgs(new HashMap<>());
721 // session.setSceneSelected(false); 753 // session.setSceneSelected(false);
722 UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); 754 UserSceneSessionService.ERP_AGENT_CACHE.remove(userId);
723 - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId);  
724 session.setBCleanMemory(false); 755 session.setBCleanMemory(false);
725 } 756 }
726 757
@@ -944,16 +975,95 @@ public class XlyErpService { @@ -944,16 +975,95 @@ public class XlyErpService {
944 aiAgent = AiServices.builder(DynamicTableNl2SqlAiAgent.class) 975 aiAgent = AiServices.builder(DynamicTableNl2SqlAiAgent.class)
945 .chatModel(ol) 976 .chatModel(ol)
946 .chatMemoryProvider(operableChatMemoryProvider) 977 .chatMemoryProvider(operableChatMemoryProvider)
  978 + .maxSequentialToolsInvocations(1)
947 .toolProvider(dynamicToolProvider) 979 .toolProvider(dynamicToolProvider)
948 .build(); 980 .build();
949 UserSceneSessionService.ERP_DynamicTableNl2SqlAiAgent_CACHE.put(userId, aiAgent); 981 UserSceneSessionService.ERP_DynamicTableNl2SqlAiAgent_CACHE.put(userId, aiAgent);
950 } 982 }
951 return aiAgent; 983 return aiAgent;
952 } 984 }
  985 + /***
  986 + * @Author 钱豹
  987 + * @Date 23:07 2026/7/29
  988 + * @Description AI识别获取方法(二级路由:在当前场景内选出一个 sMethodNo)
  989 + **/
  990 + private MethodChoiceResult methodChoiceResult(String userId, String userInput, UserSceneSession session) {
  991 +
  992 + // 1. 取当前场景下的可选方法(方法树扁平集合)
  993 + List<ToolMeta> metaOne = dynamicToolProvider.metaAll.stream()
  994 + .filter(m -> session.getCurrentScene().getSId().equals(m.getSSceneId()))
  995 + .collect(Collectors.toList());
  996 +
  997 + if (ObjectUtil.isEmpty(metaOne)) {
  998 + log.warn("方法路由:当前场景无可选方法, sceneId={}", session.getCurrentScene().getSId());
  999 + return none();
  1000 + }
  1001 +
  1002 + // 2. 生成方法选择 prompt(行业知识库按需注入)
  1003 + String sKnowledgeBase = session.getCurrentScene().getSKnowledgeBase(); // 无此字段就传 null
  1004 + String sSystemPrompt = SystemPromptGenerator.generateChoiceMethodPrompt(metaOne, sKnowledgeBase);
  1005 +
  1006 + // 3. 调大模型
  1007 + String raw = secMethodAiAgent.choiceMethod(userId, userInput, sSystemPrompt).content();
  1008 + log.debug("方法路由原始输出: userId={}, raw={}", userId, raw);
  1009 +
  1010 + // 4. 解析(含 ```json 容错)
  1011 + MethodChoiceResult result = parseChoice(raw);
  1012 + if (result == null || ObjectUtil.isEmpty(result.getSMethodNo())) {
  1013 + return none();
  1014 + }
  1015 +
  1016 + String methodNo = result.getSMethodNo();
  1017 + Double confidence = result.getConfidence();
  1018 +
  1019 + // 5. none / 阈值 / 白名单 三重校验
  1020 + if ("none".equalsIgnoreCase(methodNo)) {
  1021 + log.info("方法路由:无匹配方法, input={}", userInput);
  1022 + return none();
  1023 + }
  1024 + if (confidence == null || confidence < 0.5) {
  1025 + log.info("方法路由:置信度不足({}), input={}", confidence, userInput);
  1026 + return none();
  1027 + }
  1028 + boolean exists = metaOne.stream().anyMatch(m -> methodNo.equals(m.getSMethodNo()));
  1029 + if (!exists) {
  1030 + log.warn("方法路由:模型返回了不存在的编号 {}", methodNo); // 防编造
  1031 + return none();
  1032 + }
  1033 +
  1034 + log.info("方法路由命中: sMethodNo={}, confidence={}", methodNo, confidence);
  1035 + return result;
  1036 + }
  1037 +
  1038 + /** 解析模型输出,兼容被 ```json ... ``` 包裹或前后带杂字符的情况 */
  1039 + private MethodChoiceResult parseChoice(String raw) {
  1040 + if (ObjectUtil.isEmpty(raw)) return null;
  1041 + try {
  1042 + String s = raw.trim()
  1043 + .replaceAll("(?s)```json", "")
  1044 + .replaceAll("(?s)```", "")
  1045 + .trim();
  1046 + int start = s.indexOf('{');
  1047 + int end = s.lastIndexOf('}');
  1048 + String json = (start >= 0 && end > start) ? s.substring(start, end + 1) : s;
  1049 + return JSON.parseObject(json, MethodChoiceResult.class);
  1050 + } catch (Exception e) {
  1051 + log.error("方法路由结果解析失败, raw={}", raw, e);
  1052 + return null;
  1053 + }
  1054 + }
  1055 +
  1056 + /** 未命中统一返回值 */
  1057 + private MethodChoiceResult none() {
  1058 + MethodChoiceResult r = new MethodChoiceResult();
  1059 + r.setSMethodNo("none");
  1060 + r.setConfidence(0.0);
  1061 + return r;
  1062 + }
  1063 +
953 1064
954 // ====================== 动态构建Agent(支持选定场景/未选场景) ====================== 1065 // ====================== 动态构建Agent(支持选定场景/未选场景) ======================
955 private ErpAiAgent createErpAiAgent(String userId, String userInput, UserSceneSession session) { 1066 private ErpAiAgent createErpAiAgent(String userId, String userInput, UserSceneSession session) {
956 -  
957 // 1. 已选场景:强制绑定该场景工具 1067 // 1. 已选场景:强制绑定该场景工具
958 if (session.isSceneSelected() && session.getCurrentScene() != null) { 1068 if (session.isSceneSelected() && session.getCurrentScene() != null) {
959 dynamicToolProvider.sSceneIdMap.put(userId,session.getCurrentScene().getSId()); 1069 dynamicToolProvider.sSceneIdMap.put(userId,session.getCurrentScene().getSId());
@@ -969,7 +1079,7 @@ public class XlyErpService { @@ -969,7 +1079,7 @@ public class XlyErpService {
969 } 1079 }
970 // 4. 获取/创建用Agent 1080 // 4. 获取/创建用Agent
971 ErpAiAgent aiAgent = UserSceneSessionService.ERP_AGENT_CACHE.get(userId); 1081 ErpAiAgent aiAgent = UserSceneSessionService.ERP_AGENT_CACHE.get(userId);
972 - List<ToolSpecificationHolder> dataList = dynamicToolProvider.sceneToolCacheMap.get(session.getCurrentScene().getSId()); 1082 + List<ToolSpecificationHolder> dataList = dynamicToolProvider.sceneToolCacheMapFist.get(session.getCurrentScene().getSId());
973 List<ToolSpecificationHolder> dataListOne = 1083 List<ToolSpecificationHolder> dataListOne =
974 dataList.stream().filter(m-> 1084 dataList.stream().filter(m->
975 userInput.equals(m.getsMethodName().trim()) || ("查询"+userInput).equals(m.getsMethodName().trim()) 1085 userInput.equals(m.getsMethodName().trim()) || ("查询"+userInput).equals(m.getsMethodName().trim())
@@ -992,9 +1102,10 @@ public class XlyErpService { @@ -992,9 +1102,10 @@ public class XlyErpService {
992 .chatModel(chatModel) 1102 .chatModel(chatModel)
993 .chatMemoryProvider(operableChatMemoryProvider) 1103 .chatMemoryProvider(operableChatMemoryProvider)
994 .tools(executors,immediateReturnToolNames) 1104 .tools(executors,immediateReturnToolNames)
  1105 + .maxSequentialToolsInvocations(1)
995 .build(); 1106 .build();
996 UserSceneSessionService.ERP_AGENT_CACHE.put(userId, aiAgent); 1107 UserSceneSessionService.ERP_AGENT_CACHE.put(userId, aiAgent);
997 -// log.info("用户{}Agent构建完成,已选场景:{},场景ID{}", userId, session.isSceneSelected() ? session.getCurrentScene().getSSceneName() : "未选(全场景匹配)", dynamicToolProvider.sSceneIdMap.get(userId)); 1108 +
998 } 1109 }
999 return aiAgent; 1110 return aiAgent;
1000 } 1111 }
@@ -1021,6 +1132,8 @@ public class XlyErpService { @@ -1021,6 +1132,8 @@ public class XlyErpService {
1021 .chatModel(chatModel) 1132 .chatModel(chatModel)
1022 .chatMemoryProvider(operableChatMemoryProvider) 1133 .chatMemoryProvider(operableChatMemoryProvider)
1023 .tools(executors,immediateReturnToolNames) 1134 .tools(executors,immediateReturnToolNames)
  1135 + // ★ 关键:防死循环兜底
  1136 + .maxSequentialToolsInvocations(1)
1024 .build(); 1137 .build();
1025 return aiAgent; 1138 return aiAgent;
1026 } 1139 }
@@ -1073,7 +1186,6 @@ public class XlyErpService { @@ -1073,7 +1186,6 @@ public class XlyErpService {
1073 UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session); 1186 UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session);
1074 // 清空Agent缓存 1187 // 清空Agent缓存
1075 UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); 1188 UserSceneSessionService.ERP_AGENT_CACHE.remove(userId);
1076 - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId);  
1077 return "场景选择已重置!请重新选择业务场景:\n" + session.buildSceneSelectHint(); 1189 return "场景选择已重置!请重新选择业务场景:\n" + session.buildSceneSelectHint();
1078 } 1190 }
1079 1191
@@ -1225,18 +1337,15 @@ public class XlyErpService { @@ -1225,18 +1337,15 @@ public class XlyErpService {
1225 String methodName = ObjectUtil.isNotEmpty(session.getCurrentTool()) 1337 String methodName = ObjectUtil.isNotEmpty(session.getCurrentTool())
1226 ? session.getCurrentTool().getSMethodName() : "随便聊聊"; 1338 ? session.getCurrentTool().getSMethodName() : "随便聊聊";
1227 1339
1228 - // 从缓存获取或创建ChatiAgent  
1229 - ChatiAgent chatiAgent = UserSceneSessionService.CHAT_AGENT_CACHE.get(session.getUserId());  
1230 - if (ObjectUtil.isEmpty(chatiAgent)) {  
1231 - chatiAgent = AiServices.builder(ChatiAgent.class)  
1232 - .chatModel(chatiModel)  
1233 - .chatMemoryProvider(operableChatMemoryProvider)  
1234 - .build();  
1235 - UserSceneSessionService.CHAT_AGENT_CACHE.put(session.getUserId(), chatiAgent);  
1236 - }  
1237 -  
1238 - // 调用流式聊天方法  
1239 - return chatiAgent.chatStream(session.getUserId(), input) 1340 + // TokenStream -> Flux 桥接:onPartialResponse 推片段,完成/异常时结束流
  1341 + return Flux.<String>create(sink -> chatiAgent.chatStream(session.getUserId(), input)
  1342 + .onPartialResponse(sink::next)
  1343 + .onCompleteResponse(response -> sink.complete())
  1344 + .onError(error -> {
  1345 + log.error("闲聊流式返回异常, userId={}", session.getUserId(), error);
  1346 + sink.complete();
  1347 + })
  1348 + .start())
1240 .map(chunk -> AiResponseDTO.builder() 1349 .map(chunk -> AiResponseDTO.builder()
1241 .sSceneName(sceneName) 1350 .sSceneName(sceneName)
1242 .sMethodName(methodName) 1351 .sMethodName(methodName)
@@ -1256,13 +1365,6 @@ public class XlyErpService { @@ -1256,13 +1365,6 @@ public class XlyErpService {
1256 private AiResponseDTO getChatiAgent (String input,UserSceneSession session){ 1365 private AiResponseDTO getChatiAgent (String input,UserSceneSession session){
1257 String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY; 1366 String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY;
1258 String methodName = ObjectUtil.isNotEmpty(session.getCurrentTool())?session.getCurrentTool().getSMethodName():"随便聊聊"; 1367 String methodName = ObjectUtil.isNotEmpty(session.getCurrentTool())?session.getCurrentTool().getSMethodName():"随便聊聊";
1259 - ChatiAgent chatiAgent = UserSceneSessionService.CHAT_AGENT_CACHE.get(session.getUserId());  
1260 - if(ObjectUtil.isEmpty(chatiAgent)){  
1261 - chatiAgent = AiServices.builder(ChatiAgent.class)  
1262 - .chatModel(chatiModel)  
1263 - .chatMemoryProvider(operableChatMemoryProvider)  
1264 - .build();  
1265 - UserSceneSessionService.CHAT_AGENT_CACHE.put(session.getUserId(), chatiAgent); }  
1266 String sChatMessage = chatiAgent.chat(session.getUserId(), input); 1368 String sChatMessage = chatiAgent.chat(session.getUserId(), input);
1267 return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(methodName).aiText(sChatMessage).systemText(StrUtil.EMPTY).sReturnType(ReturnTypeCode.HTML.getCode()).build(); 1369 return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(methodName).aiText(sChatMessage).systemText(StrUtil.EMPTY).sReturnType(ReturnTypeCode.HTML.getCode()).build();
1268 } 1370 }
src/main/java/com/xly/tool/DynamicToolProvider.java
@@ -42,7 +42,6 @@ import java.util.*; @@ -42,7 +42,6 @@ import java.util.*;
42 42
43 import java.util.concurrent.ConcurrentHashMap; 43 import java.util.concurrent.ConcurrentHashMap;
44 import java.util.stream.Collectors; 44 import java.util.stream.Collectors;
45 -import java.util.stream.IntStream;  
46 45
47 @Slf4j 46 @Slf4j
48 @Service 47 @Service
@@ -57,8 +56,10 @@ public class DynamicToolProvider implements ToolProvider { @@ -57,8 +56,10 @@ public class DynamicToolProvider implements ToolProvider {
57 private final OperableChatMemoryProvider operableChatMemoryProvider; 56 private final OperableChatMemoryProvider operableChatMemoryProvider;
58 57
59 private final Map<String, ToolSpecificationHolder> toolCache = new ConcurrentHashMap<>(); 58 private final Map<String, ToolSpecificationHolder> toolCache = new ConcurrentHashMap<>();
  59 + public final List<ToolMeta> metaAll = new ArrayList<>();
60 public final Map<String, String> sSceneIdMap = new ConcurrentHashMap<>(); 60 public final Map<String, String> sSceneIdMap = new ConcurrentHashMap<>();
61 public final Map<String, List<ToolSpecificationHolder>> sceneToolCacheMap = new ConcurrentHashMap<>(); 61 public final Map<String, List<ToolSpecificationHolder>> sceneToolCacheMap = new ConcurrentHashMap<>();
  62 + public final Map<String, List<ToolSpecificationHolder>> sceneToolCacheMapFist = new ConcurrentHashMap<>();
62 private final List<ParamRule> paramRuleDataAll = new ArrayList<>(); 63 private final List<ParamRule> paramRuleDataAll = new ArrayList<>();
63 64
64 @Value("${erp.baseurl}") 65 @Value("${erp.baseurl}")
@@ -73,20 +74,31 @@ public class DynamicToolProvider implements ToolProvider { @@ -73,20 +74,31 @@ public class DynamicToolProvider implements ToolProvider {
73 @jakarta.annotation.PostConstruct 74 @jakarta.annotation.PostConstruct
74 public void init() { 75 public void init() {
75 List<ToolMeta> metas = toolMetaMapper.findAll(); 76 List<ToolMeta> metas = toolMetaMapper.findAll();
  77 + metaAll.addAll(metas);
76 for (ToolMeta meta : metas) { 78 for (ToolMeta meta : metas) {
77 try { 79 try {
78 doSetToolAIshowfieldShow(meta); 80 doSetToolAIshowfieldShow(meta);
79 ToolSpecification spec = buildToolSpecification(meta); 81 ToolSpecification spec = buildToolSpecification(meta);
80 ToolExecutor executor = createToolExecutor(meta); 82 ToolExecutor executor = createToolExecutor(meta);
81 - toolCache.put(meta.getSMethodNo(), new ToolSpecificationHolder(spec, executor,meta.getSMethodNo(),meta.getSMethodName())); 83 + toolCache.put(meta.getSMethodNo(), new ToolSpecificationHolder(spec, executor,meta.getSMethodNo(),meta.getSMethodName(),meta.getStoolDesc()));
82 log.info("已加载动态工具:{}", meta.getSMethodNo()); 84 log.info("已加载动态工具:{}", meta.getSMethodNo());
83 String sceneId = meta.getSSceneId(); 85 String sceneId = meta.getSSceneId();
84 List<ToolSpecificationHolder> dataList = new ArrayList<>(); 86 List<ToolSpecificationHolder> dataList = new ArrayList<>();
85 if(ObjectUtil.isNotEmpty(sceneToolCacheMap.get(sceneId))) { 87 if(ObjectUtil.isNotEmpty(sceneToolCacheMap.get(sceneId))) {
86 dataList = sceneToolCacheMap.get(sceneId); 88 dataList = sceneToolCacheMap.get(sceneId);
87 } 89 }
88 - dataList.add(new ToolSpecificationHolder(spec, executor,meta.getSMethodNo(),meta.getSMethodName())); 90 + dataList.add(new ToolSpecificationHolder(spec, executor,meta.getSMethodNo(),meta.getSMethodName(),meta.getStoolDesc()));
89 sceneToolCacheMap.put(sceneId, dataList); 91 sceneToolCacheMap.put(sceneId, dataList);
  92 +
  93 + //
  94 + ToolSpecification specF = buildToolSpecificationF(meta);
  95 + List<ToolSpecificationHolder> dataListF = new ArrayList<>();
  96 + if(ObjectUtil.isNotEmpty(sceneToolCacheMapFist.get(sceneId))) {
  97 + dataListF = sceneToolCacheMapFist.get(sceneId);
  98 + }
  99 + dataListF.add(new ToolSpecificationHolder(specF, executor,meta.getSMethodNo(),meta.getSMethodName(),meta.getStoolDesc()));
  100 + sceneToolCacheMapFist.put(sceneId, dataListF);
  101 +
90 } catch (Exception e) { 102 } catch (Exception e) {
91 e.printStackTrace(); 103 e.printStackTrace();
92 log.error("构建工具失败,sMethodNo={}", meta.getSMethodNo(), e); 104 log.error("构建工具失败,sMethodNo={}", meta.getSMethodNo(), e);
@@ -153,136 +165,147 @@ public class DynamicToolProvider implements ToolProvider { @@ -153,136 +165,147 @@ public class DynamicToolProvider implements ToolProvider {
153 log.info("创建工具执行器: {}", meta.getSMethodNo()); 165 log.info("创建工具执行器: {}", meta.getSMethodNo());
154 166
155 return (toolExecutionRequest, memoryId) -> { 167 return (toolExecutionRequest, memoryId) -> {
156 - log.info("===== 工具执行器开始执行 =====");  
157 - log.info("工具编号: {}", meta.getSMethodNo());  
158 - log.info("工具名称: {}", meta.getSMethodName());  
159 - log.info("memoryId: {}", memoryId);  
160 - log.info("请求参数: {}", toolExecutionRequest.arguments());  
161 - UserSceneSession session = UserSceneSessionService.USER_SCENE_SESSION_CACHE.get(memoryId.toString());  
162 - session.setCurrentTool(meta);  
163 -  
164 - // ====================== 防重复调用:立即上锁 ======================  
165 - session.setToolExecuted(true);  
166 - if (StrUtil.isNotBlank(session.getSFunPrompts())) {  
167 - // 关键:返回 工具执行失败 = 框架强制停止循环 168 + try{
  169 + log.info("===== 工具执行器开始执行 =====");
  170 + log.info("工具编号: {}", meta.getSMethodNo());
  171 + log.info("工具名称: {}", meta.getSMethodName());
  172 + log.info("memoryId: {}", memoryId);
  173 + log.info("请求参数: {}", toolExecutionRequest.arguments());
  174 + UserSceneSession session = UserSceneSessionService.USER_SCENE_SESSION_CACHE.get(memoryId.toString());
  175 + session.setCurrentTool(meta);
  176 + //如果不存在放入数据
  177 + if (session.getArgs() == null) {
  178 + session.setArgs(new HashMap<>());
  179 + }
  180 + // ====================== 防重复调用:立即上锁 ======================
  181 + session.setToolExecuted(true);
  182 + if (StrUtil.isNotBlank(session.getSFunPrompts())) {
  183 + // 关键:返回 工具执行失败 = 框架强制停止循环
168 // throw new IllegalStateException("STOP_INVOCATION: 任务已完成,停止调用"); 184 // throw new IllegalStateException("STOP_INVOCATION: 任务已完成,停止调用");
169 - return "【任务已完成】请勿重复调用工具,请直接总结结果回复用户:"+session.getSFunPrompts();  
170 - }  
171 - //解析参数失败  
172 - Map<String, Object> argsNew;  
173 - try {  
174 - argsNew = objectMapper.readValue(toolExecutionRequest.arguments(), new TypeReference<>() {});  
175 - log.info("解析后的参数: {}", argsNew);  
176 - } catch (Exception e) {  
177 - log.error("参数解析失败", e);  
178 - // 抛异常  
179 - throw new RuntimeException("参数解析失败,请重新输入");  
180 - } 185 + return "【任务已完成】请勿重复调用工具,请直接总结结果回复用户:"+session.getSFunPrompts();
  186 + }
  187 + //解析参数失败
  188 + Map<String, Object> argsNew;
  189 + try {
  190 + argsNew = objectMapper.readValue(toolExecutionRequest.arguments(), new TypeReference<>() {});
  191 + log.info("解析后的参数: {}", argsNew);
  192 + } catch (Exception e) {
  193 + log.error("参数解析失败", e);
  194 + // 抛异常
  195 + throw new RuntimeException("参数解析失败,请重新输入");
  196 + }
181 197
182 - //合并动态部件  
183 - String sDynamicValue = ObjectUtil.isNotEmpty(argsNew.get("动态部件"))  
184 - ? argsNew.get("动态部件").toString():( ObjectUtil.isNotEmpty(argsNew.get("sDynamicPart"))? argsNew.get("sDynamicPart").toString():StrUtil.EMPTY);  
185 - String sDynamicValueOld =StrUtil.EMPTY;  
186 - if(sDynamicValue.contains("{")){  
187 - sDynamicValue = StrUtil.EMPTY;  
188 - }  
189 - if(ObjectUtil.isNotEmpty(session.getArgs())){ 198 + //合并动态部件
  199 + String sDynamicValue = ObjectUtil.isNotEmpty(argsNew.get("动态部件"))
  200 + ? argsNew.get("动态部件").toString():( ObjectUtil.isNotEmpty(argsNew.get("sDynamicPart"))? argsNew.get("sDynamicPart").toString():StrUtil.EMPTY);
  201 + String sDynamicValueOld =StrUtil.EMPTY;
  202 + if(sDynamicValue.contains("{")){
  203 + sDynamicValue = StrUtil.EMPTY;
  204 + }
  205 + //获取参数 如果不存在放入空
190 Map<String,Object> argOld = session.getArgs(); 206 Map<String,Object> argOld = session.getArgs();
191 sDynamicValueOld = ObjectUtil.isNotEmpty(argOld.get("动态部件")) 207 sDynamicValueOld = ObjectUtil.isNotEmpty(argOld.get("动态部件"))
192 ? argOld.get("动态部件").toString():( ObjectUtil.isNotEmpty(argOld.get("sDynamicPart"))? argOld.get("sDynamicPart").toString():StrUtil.EMPTY); 208 ? argOld.get("动态部件").toString():( ObjectUtil.isNotEmpty(argOld.get("sDynamicPart"))? argOld.get("sDynamicPart").toString():StrUtil.EMPTY);
193 - }  
194 - Set<String> sDynamicData = new HashSet<>();  
195 - if(ObjectUtil.isNotEmpty(sDynamicValueOld)){  
196 - List<String> data = new ArrayList<>();  
197 - if (JSONUtil.isTypeJSONArray(sDynamicValueOld)) {  
198 - data = JSONUtil.toList(sDynamicValueOld, String.class);  
199 - } else {  
200 - data.add(sDynamicValueOld); 209 +
  210 + Set<String> sDynamicData = new HashSet<>();
  211 + if(ObjectUtil.isNotEmpty(sDynamicValueOld)){
  212 + List<String> data = new ArrayList<>();
  213 + if (JSONUtil.isTypeJSONArray(sDynamicValueOld)) {
  214 + data = JSONUtil.toList(sDynamicValueOld, String.class);
  215 + } else {
  216 + data.add(sDynamicValueOld);
  217 + }
  218 + sDynamicData.addAll(data);
201 } 219 }
202 - sDynamicData.addAll(data);  
203 - }  
204 - if(ObjectUtil.isNotEmpty(sDynamicValue)){  
205 - List<String> data = new ArrayList<>();  
206 - if (JSONUtil.isTypeJSONArray(sDynamicValue)) {  
207 - data = JSONUtil.toList(sDynamicValue, String.class);  
208 - } else {  
209 - data.add(sDynamicValue); 220 + if(ObjectUtil.isNotEmpty(sDynamicValue)){
  221 + List<String> data = new ArrayList<>();
  222 + if (JSONUtil.isTypeJSONArray(sDynamicValue)) {
  223 + data = JSONUtil.toList(sDynamicValue, String.class);
  224 + } else {
  225 + data.add(sDynamicValue);
  226 + }
  227 + sDynamicData.addAll(data);
210 } 228 }
211 - sDynamicData.addAll(data);  
212 - }  
213 - if(ObjectUtil.isNotEmpty(sDynamicData)){  
214 - session.getArgs().remove("动态部件");  
215 - session.getArgs().remove("sDynamicPart");  
216 - argsNew.put("动态部件",JSONUtil.toJsonStr(sDynamicData));  
217 - argsNew.put("sDynamicPart",JSONUtil.toJsonStr(sDynamicData));  
218 - }  
219 - //获取之前获取的参数  
220 - Map<String, Object> args = session.getArgs();  
221 - if(ObjectUtil.isEmpty(args)) args = new HashMap<>();  
222 - Map<String, Object> finalArgs = args;  
223 - argsNew.forEach((k, v)->{  
224 - if(Validator.isValid(v)){  
225 - finalArgs.put(k,v); 229 + if(ObjectUtil.isNotEmpty(sDynamicData)){
  230 + session.getArgs().remove("动态部件");
  231 + session.getArgs().remove("sDynamicPart");
  232 + argsNew.put("动态部件",JSONUtil.toJsonStr(sDynamicData));
  233 + argsNew.put("sDynamicPart",JSONUtil.toJsonStr(sDynamicData));
226 } 234 }
227 - });  
228 - if(argsNew.containsKey("删除动态部件")){  
229 - argsNew.put("sDynamicPartDel",argsNew.get("删除动态部件"));  
230 - }  
231 - if(!argsNew.containsKey("sDynamicPartDel")){  
232 - finalArgs.remove("sDynamicPartDel");  
233 - finalArgs.remove("删除动态部件");  
234 - }  
235 - // 2 【补全动态参数】动态参数补全(中->英文)  
236 - try{  
237 - args = applyValues(args, meta.getParamRuleListCheck());  
238 - }catch (Exception e){  
239 - log.error("返回信息",e);  
240 - String askMsg = e.getMessage();  
241 - session.setSFunPrompts(askMsg);  
242 - // 需要提问用户 → 抛异常停止循环  
243 - throw new RuntimeException(askMsg);  
244 - }  
245 - // 2.1 【自动补全】应用参数的默认值  
246 - List<ParamRule> paramRuleData = meta.getParamRuleListAll();  
247 - //添加默认值  
248 - args = applyDefaultValues(args, paramRuleData);  
249 - //客户选择行号转sSLaveId  
250 - if(ObjectUtil.isNotEmpty(args)  
251 - &&(  
252 - (args.containsKey("rowNumbers") && ObjectUtil.isNotEmpty(args.get("rowNumbers")))  
253 - || (args.containsKey("operateType") && "全部确认".equals(args.get("operateType")))  
254 - )&& ObjectUtil.isNotEmpty(session.getCurrentRowData())  
255 - ){  
256 - doSetSlaveIdToArgs( args, session);  
257 - }  
258 - //存入已采集参数  
259 - session.setArgs(args);  
260 - // 2.1.1 根据当前获取的新参数判断是否存在有值必填例如动态参数、动态部件  
261 - if(meta.getBQuo()){  
262 - List<ParamRule> dynamicParamRule = doSetParamgetArgsAfter(args, session);  
263 - if(ObjectUtil.isNotEmpty(dynamicParamRule)){ 235 + //获取之前获取的参数
  236 + Map<String, Object> args = session.getArgs();
  237 + if(ObjectUtil.isEmpty(args)) args = new HashMap<>();
  238 + Map<String, Object> finalArgs = args;
  239 + argsNew.forEach((k, v)->{
  240 + if(Validator.isValid(v)){
  241 + finalArgs.put(k,v);
  242 + }
  243 + });
  244 + if(argsNew.containsKey("删除动态部件")){
  245 + argsNew.put("sDynamicPartDel",argsNew.get("删除动态部件"));
  246 + }
  247 + if(!argsNew.containsKey("sDynamicPartDel")){
  248 + finalArgs.remove("sDynamicPartDel");
  249 + finalArgs.remove("删除动态部件");
  250 + }
  251 + // 2 【补全动态参数】动态参数补全(中->英文)
  252 + try{
  253 + args = applyValues(args, meta.getParamRuleListCheck());
  254 + }catch (Exception e){
  255 + log.error("返回信息",e);
  256 + String askMsg = e.getMessage();
  257 + session.setSFunPrompts(askMsg);
  258 + // 需要提问用户 → 抛异常停止循环
  259 + throw new RuntimeException(askMsg);
  260 + }
  261 + // 2.1 【自动补全】应用参数的默认值
  262 + List<ParamRule> paramRuleData = meta.getParamRuleListAll();
  263 + //添加默认值
  264 + args = applyDefaultValues(args, paramRuleData);
  265 + //客户选择行号转sSLaveId
  266 + if(ObjectUtil.isNotEmpty(args)
  267 + &&(
  268 + (args.containsKey("rowNumbers") && ObjectUtil.isNotEmpty(args.get("rowNumbers")))
  269 + || (args.containsKey("operateType") && "全部确认".equals(args.get("operateType")))
  270 + )&& ObjectUtil.isNotEmpty(session.getCurrentRowData())
  271 + ){
  272 + doSetSlaveIdToArgs( args, session);
  273 + }
  274 + if(ObjectUtil.isEmpty(args)){
  275 + args = new HashMap<>();
  276 + }
  277 + //存入已采集参数
  278 + session.setArgs(args);
  279 + // 2.1.1 根据当前获取的新参数判断是否存在有值必填例如动态参数、动态部件
  280 + if(meta.getBQuo()){
  281 + List<ParamRule> dynamicParamRule = doSetParamgetArgsAfter(args, session);
  282 + if(ObjectUtil.isNotEmpty(dynamicParamRule)){
264 // Map<String,Object> data = quoService.getQuoData( args, session); 283 // Map<String,Object> data = quoService.getQuoData( args, session);
265 - session.getCurrentTool().setDynamicParamRule(dynamicParamRule); 284 + session.getCurrentTool().setDynamicParamRule(dynamicParamRule);
266 // String sSystemPrompt = SystemPromptGenerator.buildMissParamPrompt(session,dynamicParamRule); 285 // String sSystemPrompt = SystemPromptGenerator.buildMissParamPrompt(session,dynamicParamRule);
267 // session.setSSystemPrompt(sSystemPrompt); 286 // session.setSSystemPrompt(sSystemPrompt);
  287 + }
268 } 288 }
  289 + //获取是否缺失参数
  290 + String askMsg = getMissParamMsg( meta, session, args);
  291 + if(ObjectUtil.isNotEmpty(askMsg)){
  292 + return askMsg;
  293 + }
  294 + String resp = StrUtil.EMPTY;
  295 + if(session.getCurrentTool().getBQuo()){
  296 + resp = JSONUtil.toJsonStr(session.getQuoData());
  297 + session.setSFunPrompts(resp);
  298 + session.getCurrentTool().setDynamicParamRule(null);
  299 + return resp;
  300 + }else{
  301 + resp = JSONUtil.toJsonStr(args) ;
  302 + }
  303 + // ====================== 返回时带终止指令 ======================
  304 + return String.valueOf(successResult(toolExecutionRequest, resp));
  305 + }catch (Exception e){
  306 + return String.valueOf(successResult(toolExecutionRequest,"执行方法异常"+e.getMessage()));
269 } 307 }
270 - //获取是否缺失参数  
271 - String askMsg = getMissParamMsg( meta, session, args);  
272 - if(ObjectUtil.isNotEmpty(askMsg)){  
273 - return askMsg;  
274 - }  
275 - String resp = StrUtil.EMPTY;  
276 - if(session.getCurrentTool().getBQuo()){  
277 - resp = JSONUtil.toJsonStr(session.getQuoData());  
278 - session.setSFunPrompts(resp);  
279 - session.getCurrentTool().setDynamicParamRule(null);  
280 - return resp;  
281 - }else{  
282 - resp = JSONUtil.toJsonStr(args) ;  
283 - }  
284 - // ====================== 返回时带终止指令 ======================  
285 - return String.valueOf(successResult(toolExecutionRequest, resp)); 308 +
286 }; 309 };
287 } 310 }
288 311
@@ -754,6 +777,41 @@ public class DynamicToolProvider implements ToolProvider { @@ -754,6 +777,41 @@ public class DynamicToolProvider implements ToolProvider {
754 .build(); 777 .build();
755 } 778 }
756 779
  780 + public ToolSpecification buildToolSpecificationF(ToolMeta meta) {
  781 + ToolSpecification.Builder builder = ToolSpecification.builder()
  782 + .name(meta.getSMethodNo());
  783 +
  784 + StringBuffer stoolDesc = new StringBuffer();
  785 + String forceToolPrompt = """
  786 + 【工具调用规则】
  787 + 1. 每个自定义方法最多只能调用 1 次
  788 + 2. 只要方法返回结果,必须停止调用
  789 + 3. 当用户输入包含【数据确认】、确认数据、确认、第*条数据确认时,必须调用本工具
  790 + """;
  791 + stoolDesc.append(forceToolPrompt);
  792 + if (ObjectUtil.isNotEmpty(meta.getStoolDesc())) {
  793 + stoolDesc.append("MethodNo:").append(meta.getSMethodNo())
  794 + .append(",当用户").append(meta.getSMethodName())
  795 + .append("时,必须调用本工具").append(meta.getSMethodNo())
  796 + .append(",").append(meta.getStoolDesc());
  797 + }
  798 + JsonObjectSchema.Builder schemaBuilder = JsonObjectSchema.builder();
  799 + List<String> requiredParams = new ArrayList<>();
  800 + List<String> requiredParamsNew = new ArrayList<>();
  801 + if(requiredParams!=null && requiredParams.size()>0){
  802 + requiredParamsNew.add(requiredParams.get(0));
  803 + }
  804 + if (!requiredParams.isEmpty()) {
  805 + schemaBuilder.required(requiredParamsNew);
  806 + }
  807 + JsonObjectSchema parameters = schemaBuilder.build();
  808 + return builder
  809 + .description(stoolDesc.toString())
  810 + .parameters(parameters)
  811 + .build();
  812 + }
  813 +
  814 +
757 private String getConstMeg(ParamRule paramRule){ 815 private String getConstMeg(ParamRule paramRule){
758 if(!RuleCode.CONST.getCode().equals(paramRule.getSRule())){ 816 if(!RuleCode.CONST.getCode().equals(paramRule.getSRule())){
759 return StrUtil.EMPTY; 817 return StrUtil.EMPTY;
@@ -832,7 +890,7 @@ public class DynamicToolProvider implements ToolProvider { @@ -832,7 +890,7 @@ public class DynamicToolProvider implements ToolProvider {
832 private String getMissParamMsg(ToolMeta meta,UserSceneSession session, Map<String, Object> args){ 890 private String getMissParamMsg(ToolMeta meta,UserSceneSession session, Map<String, Object> args){
833 if(meta.getBQuo()){ 891 if(meta.getBQuo()){
834 Map<String,Object> data = quoService.getQuoData(args, session); 892 Map<String,Object> data = quoService.getQuoData(args, session);
835 - List<Map<String,Object>> sAll = (List<Map<String, Object>>) data.get("sAll"); 893 + List<Map<String,Object>> sAll = (ObjectUtil.isEmpty(data.get("sAll")))?new ArrayList<> ():(List<Map<String, Object>>) data.get("sAll");
836 List<Map<String,Object>> sEmptyData = sAll.stream().filter(one-> BooleanUtil.toBoolean(one.get("bEmpty").toString()) 894 List<Map<String,Object>> sEmptyData = sAll.stream().filter(one-> BooleanUtil.toBoolean(one.get("bEmpty").toString())
837 && (!Validator.isValid(one.get("sValue"))) 895 && (!Validator.isValid(one.get("sValue")))
838 && !"sDynamicPart".equals(one.get("sName")) 896 && !"sDynamicPart".equals(one.get("sName"))
@@ -842,9 +900,10 @@ public class DynamicToolProvider implements ToolProvider { @@ -842,9 +900,10 @@ public class DynamicToolProvider implements ToolProvider {
842 if(ObjectUtil.isNotEmpty(sEmptyData)){ 900 if(ObjectUtil.isNotEmpty(sEmptyData)){
843 List<ParamRule> missData = session.getCurrentTool().getParamRuleListAll().stream().filter(d -> 901 List<ParamRule> missData = session.getCurrentTool().getParamRuleListAll().stream().filter(d ->
844 d.getBEmpty() 902 d.getBEmpty()
  903 + && (ObjectUtil.isNotEmpty(args))
845 && (!Validator.isValid(args.get(d.getSParam()))) 904 && (!Validator.isValid(args.get(d.getSParam())))
846 - && !"sDynamicPart".equals(d.getSParamValue())  
847 - && !"sDynamicPartDel".equals(d.getSParamValue()) 905 + && !"sDynamicPart".equals(d.getSParamValue())
  906 + && !"sDynamicPartDel".equals(d.getSParamValue())
848 ).collect(Collectors.toUnmodifiableList()); 907 ).collect(Collectors.toUnmodifiableList());
849 data.put("bTs",true); 908 data.put("bTs",true);
850 String sSystemPrompt = SystemPromptGenerator.buildMissParamPrompt(session,missData); 909 String sSystemPrompt = SystemPromptGenerator.buildMissParamPrompt(session,missData);
src/main/java/com/xly/tool/ToolSpecificationHolder.java
@@ -9,11 +9,13 @@ public class ToolSpecificationHolder { @@ -9,11 +9,13 @@ public class ToolSpecificationHolder {
9 private final ToolExecutor toolExecutor; 9 private final ToolExecutor toolExecutor;
10 private final String sName; 10 private final String sName;
11 private final String sMethodName; 11 private final String sMethodName;
12 - public ToolSpecificationHolder(ToolSpecification toolSpecification, ToolExecutor toolExecutor,String sName,String sMethodName) { 12 + private final String stoolDesc;
  13 + public ToolSpecificationHolder(ToolSpecification toolSpecification, ToolExecutor toolExecutor,String sName,String sMethodName,String stoolDesc) {
13 this.toolSpecification = toolSpecification; 14 this.toolSpecification = toolSpecification;
14 this.toolExecutor = toolExecutor; 15 this.toolExecutor = toolExecutor;
15 this.sName = sName; 16 this.sName = sName;
16 this.sMethodName = sMethodName; 17 this.sMethodName = sMethodName;
  18 + this.stoolDesc = stoolDesc;
17 } 19 }
18 20
19 public ToolSpecification getToolSpecification() { 21 public ToolSpecification getToolSpecification() {
@@ -31,5 +33,9 @@ public class ToolSpecificationHolder { @@ -31,5 +33,9 @@ public class ToolSpecificationHolder {
31 public String getsMethodName() { 33 public String getsMethodName() {
32 return sMethodName; 34 return sMethodName;
33 } 35 }
  36 +
  37 + public String getStoolDesc() {
  38 + return stoolDesc;
  39 + }
34 } 40 }
35 41