|
|
@@ -1,25 +1,24 @@
|
|
|
package com.zsjz.ai.module.agent.intent;
|
|
|
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
-import com.zsjz.ai.common.utils.Json;
|
|
|
+import com.google.common.collect.Maps;
|
|
|
import com.zsjz.ai.module.agent.config.EtlProperties;
|
|
|
import com.zsjz.ai.module.agent.entity.AgentMessage;
|
|
|
import com.zsjz.ai.module.agent.entity.AgentModel;
|
|
|
import com.zsjz.ai.module.agent.mapper.AgentMessageMapper;
|
|
|
import com.zsjz.ai.module.agent.mapper.AgentModelMapper;
|
|
|
+import com.zsjz.ai.module.agent.prompt.PromptHelper;
|
|
|
+import com.zsjz.ai.module.agent.prompt.PromptLoader;
|
|
|
import com.zsjz.ai.module.agent.service.AgentModelFactory;
|
|
|
-
|
|
|
+import io.agentscope.core.ReActAgent;
|
|
|
import io.agentscope.core.message.Msg;
|
|
|
-import io.agentscope.core.message.MsgRole;
|
|
|
-import io.agentscope.core.message.TextBlock;
|
|
|
-import io.agentscope.core.model.GenerateOptions;
|
|
|
+import io.agentscope.core.message.UserMessage;
|
|
|
import io.agentscope.core.model.Model;
|
|
|
+import io.agentscope.core.tool.Toolkit;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
-import org.springframework.util.StringUtils;
|
|
|
|
|
|
-import java.time.Duration;
|
|
|
-import java.util.ArrayList;
|
|
|
import java.util.Collections;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
@@ -35,10 +34,14 @@ import java.util.Map;
|
|
|
@Service
|
|
|
public class IntentService {
|
|
|
|
|
|
- /** 单条历史消息参与上下文的截断长度 */
|
|
|
+ /**
|
|
|
+ * 单条历史消息参与上下文的截断长度
|
|
|
+ */
|
|
|
private static final int HISTORY_CONTENT_LIMIT = 300;
|
|
|
|
|
|
- /** 实体列表上限(防止 LLM 输出超长) */
|
|
|
+ /**
|
|
|
+ * 实体列表上限(防止 LLM 输出超长)
|
|
|
+ */
|
|
|
private static final int MAX_ENTITIES = 10;
|
|
|
|
|
|
private final AgentModelMapper agentModelMapper;
|
|
|
@@ -61,67 +64,41 @@ public class IntentService {
|
|
|
*
|
|
|
* @param modelId 会话当前模型 ID
|
|
|
* @param sessionId 会话 ID(用于加载最近对话历史补全指代/省略)
|
|
|
- * @param question 用户原始问题
|
|
|
+ * @param question 用户原始问题
|
|
|
* @return 识别结果;模型配置缺失/调用失败/超时/解析失败时返回 null
|
|
|
*/
|
|
|
public IntentResult safeAnalyze(Long modelId, Long sessionId, String question) {
|
|
|
- try {
|
|
|
- AgentModel config = agentModelMapper.selectById(modelId);
|
|
|
- if (config == null) {
|
|
|
+ AgentModel config = agentModelMapper.selectById(modelId);
|
|
|
+ if (config == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ Model model = agentModelFactory.create(config);
|
|
|
+ try (ReActAgent agent =
|
|
|
+ ReActAgent.builder()
|
|
|
+ .name("IntentAgent")
|
|
|
+ .model(model)
|
|
|
+ .toolkit(new Toolkit())
|
|
|
+ .build()) {
|
|
|
+
|
|
|
+ Msg userMsg = new UserMessage(buildSystemPrompt(sessionId, question));
|
|
|
+ Msg response = agent.call(userMsg, IntentResult.class).block();
|
|
|
+ if (response == null) {
|
|
|
return null;
|
|
|
}
|
|
|
- Model model = agentModelFactory.create(config);
|
|
|
-
|
|
|
- List<Msg> msgs = List.of(
|
|
|
- Msg.builder().role(MsgRole.SYSTEM).textContent(buildSystemPrompt()).build(),
|
|
|
- Msg.builder().role(MsgRole.USER).textContent(buildUserPrompt(sessionId, question)).build());
|
|
|
-
|
|
|
- StringBuilder sb = new StringBuilder();
|
|
|
- model.stream(msgs, List.of(), GenerateOptions.builder()
|
|
|
- .temperature(0.1)
|
|
|
- .maxTokens(etlProperties.getIntent().getMaxTokens())
|
|
|
- .build())
|
|
|
- .doOnNext(resp -> {
|
|
|
- if (resp.getContent() != null) {
|
|
|
- resp.getContent().forEach(block -> {
|
|
|
- if (block instanceof TextBlock t && t.getText() != null) {
|
|
|
- sb.append(t.getText());
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- })
|
|
|
- .blockLast(Duration.ofSeconds(etlProperties.getIntent().getTimeoutSeconds()));
|
|
|
-
|
|
|
- return parse(sb.toString().trim(), question);
|
|
|
+ return response.getStructuredData(IntentResult.class);
|
|
|
} catch (Exception e) {
|
|
|
- log.warn("意图识别降级(fail-open): modelId={}, sessionId={}, error={}",
|
|
|
+ log.error("意图识别降级(fail-open): modelId={}, sessionId={}, error={}",
|
|
|
modelId, sessionId, e.getMessage());
|
|
|
- return null;
|
|
|
}
|
|
|
+ return null;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 意图识别系统提示词:5 类标签 + 严格 JSON 输出约束
|
|
|
- */
|
|
|
- private static String buildSystemPrompt() {
|
|
|
- return "你是 Excel ETL 数据分析助手的问题理解模块,负责分析用户当前问题。\n"
|
|
|
- + "将问题意图分类为以下标签之一:\n"
|
|
|
- + "- data_query:数据查询/统计/聚合(需写 SQL 分析数据)\n"
|
|
|
- + "- schema_explore:表结构探索(了解有哪些表/字段/含义)\n"
|
|
|
- + "- etl_clean:数据清洗/转换/ETL 处理任务\n"
|
|
|
- + "- visualization:图表/可视化需求\n"
|
|
|
- + "- general_chat:通用问答/闲聊/与数据无关\n\n"
|
|
|
- + "输出严格 JSON(不要输出任何其他内容,不要用 markdown 代码块包裹):\n"
|
|
|
- + "{\"intent\":\"<上述标签之一>\",\"enhanced\":\"<语义增强后的问题>\",\"entities\":[\"<关键数据实体/业务概念>\"]}\n\n"
|
|
|
- + "enhanced 规则:结合对话历史补全当前问题中的指代和省略(如\"那个表\"\"上次的结果\"具体指什么),"
|
|
|
- + "保留用户原意,输出一句完整清晰的中文问题;无历史可参考时,将口语化表述改写为更完整清晰的表述。\n"
|
|
|
- + "entities 规则:提取问题涉及的数据实体与业务概念(如:客户、手机号、订单金额),最多 10 个,没有则为空数组。";
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
* 用户提示词:最近对话历史 + 当前问题
|
|
|
*/
|
|
|
- private String buildUserPrompt(Long sessionId, String question) {
|
|
|
+ private String buildSystemPrompt(Long sessionId, String question) {
|
|
|
+ String rawPrompt = PromptLoader.loadPrompt("intent-recognition");
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
List<AgentMessage> history = loadHistory(sessionId);
|
|
|
if (!history.isEmpty()) {
|
|
|
@@ -136,10 +113,15 @@ public class IntentService {
|
|
|
}
|
|
|
sb.append('\n');
|
|
|
}
|
|
|
- sb.append("[当前问题]\n").append(question);
|
|
|
- return sb.toString();
|
|
|
+ String outputSchema = PromptHelper.generateSchema(IntentResult.class);
|
|
|
+ Map<String, Object> variable = Maps.newLinkedHashMap();
|
|
|
+ variable.put("multi_turn", sb.toString());
|
|
|
+ variable.put("format", outputSchema);
|
|
|
+ variable.put("latest_query", question);
|
|
|
+ return StrUtil.format(rawPrompt, variable);
|
|
|
}
|
|
|
|
|
|
+
|
|
|
/**
|
|
|
* 加载最近 N 条会话消息(时间正序返回;排除当前正在提问的消息——历史均在本次落库前查询)
|
|
|
*/
|
|
|
@@ -161,53 +143,4 @@ public class IntentService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 宽松解析 LLM 输出:剥 markdown 围栏 → 截取首尾大括号 → Jackson 解析 → 标签校验回落
|
|
|
- */
|
|
|
- private IntentResult parse(String raw, String question) {
|
|
|
- if (!StringUtils.hasText(raw)) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- String json = extractJson(raw);
|
|
|
- if (json == null) {
|
|
|
- log.warn("意图识别输出非 JSON(降级): {}", abbreviate(raw));
|
|
|
- return null;
|
|
|
- }
|
|
|
- try {
|
|
|
- Map<String, Object> map = Json.objectMapper().readValue(json, Map.class);
|
|
|
- IntentEnum intent = IntentEnum.fromCode((String) map.get("intent"));
|
|
|
-
|
|
|
- String enhanced = map.get("enhanced") instanceof String s && StringUtils.hasText(s)
|
|
|
- ? s.trim() : question;
|
|
|
-
|
|
|
- List<String> entities = new ArrayList<>();
|
|
|
- if (map.get("entities") instanceof List<?> list) {
|
|
|
- for (Object item : list) {
|
|
|
- if (item instanceof String s && StringUtils.hasText(s) && entities.size() < MAX_ENTITIES) {
|
|
|
- entities.add(s.trim());
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- return new IntentResult(intent.getCode(), intent.getLabel(), enhanced, entities);
|
|
|
- } catch (Exception e) {
|
|
|
- log.warn("意图识别结果解析失败(降级): error={}", e.getMessage());
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 提取 JSON 主体:兼容 ```json 围栏与前后多余文本
|
|
|
- */
|
|
|
- private static String extractJson(String raw) {
|
|
|
- int start = raw.indexOf('{');
|
|
|
- int end = raw.lastIndexOf('}');
|
|
|
- if (start < 0 || end <= start) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- return raw.substring(start, end + 1);
|
|
|
- }
|
|
|
-
|
|
|
- private static String abbreviate(String s) {
|
|
|
- return s.length() > 120 ? s.substring(0, 120) + "..." : s;
|
|
|
- }
|
|
|
}
|