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 2  
3 3 import dev.langchain4j.service.MemoryId;
4 4 import dev.langchain4j.service.SystemMessage;
  5 +import dev.langchain4j.service.TokenStream;
5 6 import dev.langchain4j.service.UserMessage;
6 7 import dev.langchain4j.service.V;
7   -import reactor.core.publisher.Flux;
8 8  
  9 +/**
  10 + * 闲聊智能体(小羚羊人设)
  11 + * 说明:同步/流式两个方法共用同一份人设提示词,改一处即可,避免两份拷贝走偏
  12 + */
9 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 34 @UserMessage("用户说:{{userInput}}")
19 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 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 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 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 5 import com.alibaba.fastjson2.JSONObject;
6 6 import com.alibaba.fastjson2.JSONWriter;
7 7 import com.xly.entity.ParamRule;
  8 +import com.xly.entity.ToolMeta;
8 9 import com.xly.entity.UserSceneSession;
9 10  
10 11 import java.util.List;
... ... @@ -110,4 +111,60 @@ public class SystemPromptGenerator {
110 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 171 \ No newline at end of file
... ...
src/main/java/com/xly/config/ModelConfig.java
1 1 package com.xly.config;
2 2  
  3 +import com.fasterxml.jackson.databind.DeserializationFeature;
3 4 import com.fasterxml.jackson.databind.ObjectMapper;
4 5 import com.fasterxml.jackson.databind.SerializationFeature;
5 6 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
  7 +import com.xly.agent.ChatiAgent;
6 8 import com.xly.agent.DynamicTableNl2SqlAiAgent;
7 9 import com.xly.agent.SceneSelectorAiAgent;
  10 +import com.xly.agent.SecMethodAiAgent;
  11 +import dev.langchain4j.memory.chat.MessageWindowChatMemory;
8 12 import dev.langchain4j.model.ollama.OllamaChatModel;
9 13 import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
10 14 import dev.langchain4j.service.AiServices;
11   -import dev.langchain4j.memory.chat.MessageWindowChatMemory;
12 15 import org.springframework.beans.factory.annotation.Qualifier;
13 16 import org.springframework.beans.factory.annotation.Value;
14 17 import org.springframework.context.annotation.Bean;
... ... @@ -17,11 +20,17 @@ import org.springframework.context.annotation.Primary;
17 20  
18 21 import java.time.Duration;
19 22  
  23 +/**
  24 + * 模型与 Agent 统一装配(langchain4j 1.14.0 新 API:ChatModel / StreamingChatModel)
  25 + * 约定:
  26 + * - 路由/SQL/参数提取类任务 → 低温(确定性)
  27 + * - 闲聊类任务 → 高温(多样性)
  28 + */
20 29 @Configuration
21 30 public class ModelConfig {
22 31  
23 32 @Value("${langchain4j.ollama.base-url}")
24   - private String chatModelUrl;
  33 + private String baseUrl;
25 34  
26 35 @Value("${langchain4j.ollama.chat-model-name}")
27 36 private String chatModelName;
... ... @@ -29,88 +38,106 @@ public class ModelConfig {
29 38 @Value("${langchain4j.ollama.sql-model-name}")
30 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 80 @Bean
34 81 @Primary
35 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 84 .logRequests(true)
44 85 .logResponses(true)
45 86 .build();
46 87 }
47 88  
48   - // ====================== 自由闲聊模型 ======================
  89 + /** 自由闲聊(非流式) */
49 90 @Bean("chatiModel")
50 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 102 @Bean("sqlChatModel")
63 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 105 .numPredict(4096)
70   - .timeout(Duration.ofSeconds(120))
71   - .maxRetries(3)
72 106 .build();
73 107 }
74 108  
75   - // ====================== 流式对话模型 ======================
  109 + /** 主流式对话 */
76 110 @Bean("streamingChatModel")
77 111 @Primary
78 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 117 @Bean("streamingSqlModel")
91 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 126 @Bean
104 127 @Primary
105 128 public ObjectMapper objectMapper() {
106 129 ObjectMapper mapper = new ObjectMapper();
107 130 mapper.registerModule(new JavaTimeModule());
108 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 133 return mapper;
111 134 }
112 135  
113   - // ====================== 动态 SQL Agent ======================
  136 + // ========================================================
  137 + // 四、Agent Bean
  138 + // ========================================================
  139 +
  140 + /** 动态 SQL Agent */
114 141 @Bean
115 142 public DynamicTableNl2SqlAiAgent dynamicTableNl2SqlAiAgent(
116 143 @Qualifier("sqlChatModel") OllamaChatModel sqlModel) {
... ... @@ -120,13 +147,45 @@ public class ModelConfig {
120 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 168 @Bean
125 169 public SceneSelectorAiAgent sceneSelectorAiAgent(
126 170 @Qualifier("chatLanguageModel") OllamaChatModel chatLanguageModel) {
127 171 return AiServices.builder(SceneSelectorAiAgent.class)
128 172 .chatModel(chatLanguageModel)
129 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 189 .build();
131 190 }
132 191 }
133 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 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 105 \ No newline at end of file
... ...
src/main/java/com/xly/service/UserSceneSessionService.java
1 1 package com.xly.service;
2 2  
3 3 import cn.hutool.core.util.ObjectUtil;
4   -import com.xly.agent.ChatiAgent;
5 4 import com.xly.agent.DynamicTableNl2SqlAiAgent;
6 5 import com.xly.agent.ErpAiAgent;
7 6 import com.xly.config.OperableChatMemoryProvider;
... ... @@ -25,7 +24,7 @@ public class UserSceneSessionService {
25 24  
26 25 // 原有缓存:Agent实例、会话记忆
27 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 28 public static final Map<String, DynamicTableNl2SqlAiAgent> ERP_DynamicTableNl2SqlAiAgent_CACHE = new HashMap<>();
30 29  
31 30  
... ... @@ -72,7 +71,6 @@ public class UserSceneSessionService {
72 71 public void cleanAllSession(){
73 72 USER_SCENE_SESSION_CACHE.clear();
74 73 ERP_AGENT_CACHE.clear();
75   - CHAT_AGENT_CACHE.clear();
76 74 ERP_DynamicTableNl2SqlAiAgent_CACHE.clear();
77 75 }
78 76  
... ... @@ -86,7 +84,6 @@ public class UserSceneSessionService {
86 84 public void cleanUserSession(String sUserId){
87 85 USER_SCENE_SESSION_CACHE.remove(sUserId);
88 86 ERP_AGENT_CACHE.remove(sUserId);
89   - CHAT_AGENT_CACHE.remove(sUserId);
90 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 24 import com.xly.tool.DynamicToolProvider;
25 25 import com.xly.tool.ToolSpecificationHolder;
26 26 import com.xly.util.*;
27   -import dev.langchain4j.agent.tool.ReturnBehavior;
28   -import dev.langchain4j.agent.tool.ToolExecutionRequest;
29 27 import dev.langchain4j.agent.tool.ToolSpecification;
30   -import dev.langchain4j.data.message.AiMessage;
31 28 import dev.langchain4j.data.message.ChatMessage;
32 29  
33 30 import dev.langchain4j.memory.ChatMemory;
... ... @@ -37,10 +34,8 @@ import dev.langchain4j.service.Result;
37 34 import dev.langchain4j.service.tool.ToolExecutor;
38 35 import lombok.RequiredArgsConstructor;
39 36 import lombok.extern.slf4j.Slf4j;
40   -import org.apache.commons.lang3.time.DateFormatUtils;
41 37 import org.springframework.beans.factory.annotation.Value;
42 38 import org.springframework.stereotype.Service;
43   -import org.springframework.util.IdGenerator;
44 39 import reactor.core.publisher.Flux;
45 40  
46 41 import java.time.Duration;
... ... @@ -54,7 +49,7 @@ import java.util.stream.IntStream;
54 49 public class XlyErpService {
55 50 //中文对话模型
56 51 private final OllamaChatModel chatModel;
57   - private final OllamaChatModel chatiModel;
  52 + private final ChatiAgent chatiAgent;
58 53 private final SceneSelectorAiAgent sceneSelectorAiAgent;
59 54 private final UserSceneSessionService userSceneSessionService;
60 55 private final DynamicToolProvider dynamicToolProvider;
... ... @@ -63,6 +58,7 @@ public class XlyErpService {
63 58 private final RedisService redisService;
64 59 private final AiGlobalAgentQuestionSqlEmitterService aiGlobalAgentQuestionSqlEmitterService;
65 60 private final MilvusService milvusService;
  61 + private final SecMethodAiAgent secMethodAiAgent;
66 62  
67 63 //执行动态语句 执行异常的情况下 最多执行次数
68 64 private final Integer maxRetries = 5;
... ... @@ -342,12 +338,49 @@ public class XlyErpService {
342 338 // 3.1 尝试处理场景选择(输入序号则匹配,否则展示选择提示)
343 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 384 String sResponMessage = StrUtil.EMPTY;
352 385 //用户输入添加方法(如果没有方法,动态SQL方法不需要)
353 386 Boolean isConfirmed = dynamicToolProvider.isConfirmed(input,session);
... ... @@ -368,6 +401,7 @@ public class XlyErpService {
368 401 //重新生成新的aiAgent拿新的aiAgent 做选择
369 402 aiAgent = createConfirmeAgent(session);
370 403 }
  404 +
371 405 //报价每次定义参数
372 406 if(!isConfirmed && ObjectUtil.isNotEmpty(session) && ObjectUtil.isNotEmpty(session.getCurrentTool()) && session.getCurrentTool().getBQuo()){
373 407 sSystemPrompt = SystemPromptGenerator.generate(session.getCurrentTool().getSMethodName(),session,session.getCurrentScene().getSKnowledgeBase());
... ... @@ -486,7 +520,6 @@ public class XlyErpService {
486 520 UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session);
487 521 // 清空Agent缓存
488 522 UserSceneSessionService.ERP_AGENT_CACHE.remove(userId);
489   - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId);
490 523 cleanMemory(userId, sUserName, sBrandsId, sSubsidiaryId, sUserType, authorization);
491 524 }
492 525  
... ... @@ -503,7 +536,6 @@ public class XlyErpService {
503 536 session.setArgs(null);
504 537 session.setSUserQuestionList(new ArrayList<>());
505 538 UserSceneSessionService.ERP_AGENT_CACHE.remove(userId);
506   - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId);
507 539 session.setBCleanMemory(false);
508 540 String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY;
509 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 752 session.setArgs(new HashMap<>());
721 753 // session.setSceneSelected(false);
722 754 UserSceneSessionService.ERP_AGENT_CACHE.remove(userId);
723   - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId);
724 755 session.setBCleanMemory(false);
725 756 }
726 757  
... ... @@ -944,16 +975,95 @@ public class XlyErpService {
944 975 aiAgent = AiServices.builder(DynamicTableNl2SqlAiAgent.class)
945 976 .chatModel(ol)
946 977 .chatMemoryProvider(operableChatMemoryProvider)
  978 + .maxSequentialToolsInvocations(1)
947 979 .toolProvider(dynamicToolProvider)
948 980 .build();
949 981 UserSceneSessionService.ERP_DynamicTableNl2SqlAiAgent_CACHE.put(userId, aiAgent);
950 982 }
951 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 1065 // ====================== 动态构建Agent(支持选定场景/未选场景) ======================
955 1066 private ErpAiAgent createErpAiAgent(String userId, String userInput, UserSceneSession session) {
956   -
957 1067 // 1. 已选场景:强制绑定该场景工具
958 1068 if (session.isSceneSelected() && session.getCurrentScene() != null) {
959 1069 dynamicToolProvider.sSceneIdMap.put(userId,session.getCurrentScene().getSId());
... ... @@ -969,7 +1079,7 @@ public class XlyErpService {
969 1079 }
970 1080 // 4. 获取/创建用Agent
971 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 1083 List<ToolSpecificationHolder> dataListOne =
974 1084 dataList.stream().filter(m->
975 1085 userInput.equals(m.getsMethodName().trim()) || ("查询"+userInput).equals(m.getsMethodName().trim())
... ... @@ -992,9 +1102,10 @@ public class XlyErpService {
992 1102 .chatModel(chatModel)
993 1103 .chatMemoryProvider(operableChatMemoryProvider)
994 1104 .tools(executors,immediateReturnToolNames)
  1105 + .maxSequentialToolsInvocations(1)
995 1106 .build();
996 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 1110 return aiAgent;
1000 1111 }
... ... @@ -1021,6 +1132,8 @@ public class XlyErpService {
1021 1132 .chatModel(chatModel)
1022 1133 .chatMemoryProvider(operableChatMemoryProvider)
1023 1134 .tools(executors,immediateReturnToolNames)
  1135 + // ★ 关键:防死循环兜底
  1136 + .maxSequentialToolsInvocations(1)
1024 1137 .build();
1025 1138 return aiAgent;
1026 1139 }
... ... @@ -1073,7 +1186,6 @@ public class XlyErpService {
1073 1186 UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session);
1074 1187 // 清空Agent缓存
1075 1188 UserSceneSessionService.ERP_AGENT_CACHE.remove(userId);
1076   - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId);
1077 1189 return "场景选择已重置!请重新选择业务场景:\n" + session.buildSceneSelectHint();
1078 1190 }
1079 1191  
... ... @@ -1225,18 +1337,15 @@ public class XlyErpService {
1225 1337 String methodName = ObjectUtil.isNotEmpty(session.getCurrentTool())
1226 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 1349 .map(chunk -> AiResponseDTO.builder()
1241 1350 .sSceneName(sceneName)
1242 1351 .sMethodName(methodName)
... ... @@ -1256,13 +1365,6 @@ public class XlyErpService {
1256 1365 private AiResponseDTO getChatiAgent (String input,UserSceneSession session){
1257 1366 String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY;
1258 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 1368 String sChatMessage = chatiAgent.chat(session.getUserId(), input);
1267 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 42  
43 43 import java.util.concurrent.ConcurrentHashMap;
44 44 import java.util.stream.Collectors;
45   -import java.util.stream.IntStream;
46 45  
47 46 @Slf4j
48 47 @Service
... ... @@ -57,8 +56,10 @@ public class DynamicToolProvider implements ToolProvider {
57 56 private final OperableChatMemoryProvider operableChatMemoryProvider;
58 57  
59 58 private final Map<String, ToolSpecificationHolder> toolCache = new ConcurrentHashMap<>();
  59 + public final List<ToolMeta> metaAll = new ArrayList<>();
60 60 public final Map<String, String> sSceneIdMap = new ConcurrentHashMap<>();
61 61 public final Map<String, List<ToolSpecificationHolder>> sceneToolCacheMap = new ConcurrentHashMap<>();
  62 + public final Map<String, List<ToolSpecificationHolder>> sceneToolCacheMapFist = new ConcurrentHashMap<>();
62 63 private final List<ParamRule> paramRuleDataAll = new ArrayList<>();
63 64  
64 65 @Value("${erp.baseurl}")
... ... @@ -73,20 +74,31 @@ public class DynamicToolProvider implements ToolProvider {
73 74 @jakarta.annotation.PostConstruct
74 75 public void init() {
75 76 List<ToolMeta> metas = toolMetaMapper.findAll();
  77 + metaAll.addAll(metas);
76 78 for (ToolMeta meta : metas) {
77 79 try {
78 80 doSetToolAIshowfieldShow(meta);
79 81 ToolSpecification spec = buildToolSpecification(meta);
80 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 84 log.info("已加载动态工具:{}", meta.getSMethodNo());
83 85 String sceneId = meta.getSSceneId();
84 86 List<ToolSpecificationHolder> dataList = new ArrayList<>();
85 87 if(ObjectUtil.isNotEmpty(sceneToolCacheMap.get(sceneId))) {
86 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 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 102 } catch (Exception e) {
91 103 e.printStackTrace();
92 104 log.error("构建工具失败,sMethodNo={}", meta.getSMethodNo(), e);
... ... @@ -153,136 +165,147 @@ public class DynamicToolProvider implements ToolProvider {
153 165 log.info("创建工具执行器: {}", meta.getSMethodNo());
154 166  
155 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 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 206 Map<String,Object> argOld = session.getArgs();
191 207 sDynamicValueOld = ObjectUtil.isNotEmpty(argOld.get("动态部件"))
192 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 283 // Map<String,Object> data = quoService.getQuoData( args, session);
265   - session.getCurrentTool().setDynamicParamRule(dynamicParamRule);
  284 + session.getCurrentTool().setDynamicParamRule(dynamicParamRule);
266 285 // String sSystemPrompt = SystemPromptGenerator.buildMissParamPrompt(session,dynamicParamRule);
267 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 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 815 private String getConstMeg(ParamRule paramRule){
758 816 if(!RuleCode.CONST.getCode().equals(paramRule.getSRule())){
759 817 return StrUtil.EMPTY;
... ... @@ -832,7 +890,7 @@ public class DynamicToolProvider implements ToolProvider {
832 890 private String getMissParamMsg(ToolMeta meta,UserSceneSession session, Map<String, Object> args){
833 891 if(meta.getBQuo()){
834 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 894 List<Map<String,Object>> sEmptyData = sAll.stream().filter(one-> BooleanUtil.toBoolean(one.get("bEmpty").toString())
837 895 && (!Validator.isValid(one.get("sValue")))
838 896 && !"sDynamicPart".equals(one.get("sName"))
... ... @@ -842,9 +900,10 @@ public class DynamicToolProvider implements ToolProvider {
842 900 if(ObjectUtil.isNotEmpty(sEmptyData)){
843 901 List<ParamRule> missData = session.getCurrentTool().getParamRuleListAll().stream().filter(d ->
844 902 d.getBEmpty()
  903 + && (ObjectUtil.isNotEmpty(args))
845 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 907 ).collect(Collectors.toUnmodifiableList());
849 908 data.put("bTs",true);
850 909 String sSystemPrompt = SystemPromptGenerator.buildMissParamPrompt(session,missData);
... ...
src/main/java/com/xly/tool/ToolSpecificationHolder.java
... ... @@ -9,11 +9,13 @@ public class ToolSpecificationHolder {
9 9 private final ToolExecutor toolExecutor;
10 10 private final String sName;
11 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 14 this.toolSpecification = toolSpecification;
14 15 this.toolExecutor = toolExecutor;
15 16 this.sName = sName;
16 17 this.sMethodName = sMethodName;
  18 + this.stoolDesc = stoolDesc;
17 19 }
18 20  
19 21 public ToolSpecification getToolSpecification() {
... ... @@ -31,5 +33,9 @@ public class ToolSpecificationHolder {
31 33 public String getsMethodName() {
32 34 return sMethodName;
33 35 }
  36 +
  37 + public String getStoolDesc() {
  38 + return stoolDesc;
  39 + }
34 40 }
35 41  
... ...