package com.superbiz.agent.service; import com.alibaba.cloud.ai.graph.OverAllState; import com.alibaba.cloud.ai.graph.RunnableConfig; import com.alibaba.cloud.ai.graph.agent.ReactAgent; import com.alibaba.cloud.ai.graph.agent.flow.agent.SupervisorAgent; import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.superbiz.agent.agent.tool.DateTimeTools; import com.superbiz.agent.agent.tool.InternalDocsTools; import com.superbiz.agent.agent.tool.QueryLogsTools; import com.superbiz.agent.agent.tool.QueryMetricsTools; import com.superbiz.agent.domain.entity.DiagnosisSession; import com.superbiz.agent.hook.AgentLoggingHook; import com.superbiz.agent.hook.TokenTrackingChatModel; import com.superbiz.agent.hook.TokenUsageHolder; import com.superbiz.agent.hook.VerifierInputHook; import com.superbiz.agent.repository.AgentStepRepository; import com.superbiz.agent.repository.DiagnosisSessionRepository; import com.superbiz.agent.tool.LookupKnowledgeTool; import com.superbiz.agent.tool.RetrievedDocTracker; import com.superbiz.agent.util.QuestionComplexity; import com.superbiz.agent.util.SessionContextHolder; import com.superbiz.agent.util.VerifierContextHolder; import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Service; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; /** * 聊天服务 * 封装 ReactAgent 对话的公共逻辑,包括模型创建、系统提示词构建、Agent 配置等 */ @Service public class ChatService { private static final Logger logger = LoggerFactory.getLogger(ChatService.class); private static final String LOW_CONFID_DISCLAIMER = "以下结论基于当前已获取证据,仍存在部分证据缺口,请谨慎参考。"; private static final String DEGRADED_PREFIX = "当前无法基于已获取证据生成可靠结论,建议人工介入。"; /** 封装 answer + 后端生成的 sessionId,用于 feedback 关联 */ public record ChatResult(String answer, String sessionId) {} @Autowired private InternalDocsTools internalDocsTools; @Autowired private DateTimeTools dateTimeTools; @Autowired private QueryMetricsTools queryMetricsTools; @Autowired(required = false) // Mock 模式下才注册,所以设置为 optional,真实环境通过mcp配置注入 private QueryLogsTools queryLogsTools; @Autowired(required = false) private ToolCallbackProvider tools; @Autowired private ChatModel chatModel; @Autowired private LookupKnowledgeTool lookupKnowledgeTool; @Autowired private DiagnosisSessionRepository diagnosisSessionRepository; @Autowired private AgentStepRepository agentStepRepository; @Autowired private EvaluationService evaluationService; @Autowired private RetrievedDocTracker retrievedDocTracker; @Autowired private KnowledgeDomainService knowledgeDomainService; @Autowired private ToolTraceSummaryService toolTraceSummaryService; @Autowired private SelfEvaluationMergeService selfEvaluationMergeService; @Value("${verifier.low-confidence-threshold:0.5}") private double verifierLowConfidenceThreshold; /** 多 Agent Chat 的 Prompt */ private String chatPlannerPrompt; private String chatExecutorPrompt; private String chatVerifierPrompt; private final ObjectMapper objectMapper = new ObjectMapper(); @PostConstruct public void init() { // 加载 Prompt try { chatPlannerPrompt = new String( new ClassPathResource("prompts/chat-planner-prompt.md").getInputStream().readAllBytes(), StandardCharsets.UTF_8); chatExecutorPrompt = new String( new ClassPathResource("prompts/chat-executor-prompt.md").getInputStream().readAllBytes(), StandardCharsets.UTF_8); chatVerifierPrompt = new String( new ClassPathResource("prompts/chat-verifier-prompt.md").getInputStream().readAllBytes(), StandardCharsets.UTF_8); logger.info("Chat 多 Agent Prompts 加载成功"); } catch (IOException e) { logger.error("加载 Chat Prompt 文件失败", e); throw new RuntimeException("Failed to load chat prompts", e); } // 包装 ChatModel 以捕获 token 用量 chatModel = new TokenTrackingChatModel(chatModel); logger.info("ChatModel 已包装 TokenTrackingChatModel"); } /** * 获取注入的 ChatModel */ public ChatModel getChatModel() { return chatModel; } /** * 构建系统提示词(包含历史消息) * @param history 历史消息列表 * @return 完整的系统提示词 */ public String buildSystemPrompt(List> history) { StringBuilder systemPromptBuilder = new StringBuilder(); // 基础系统提示 systemPromptBuilder.append("你是一个专业的智能助手,可以获取当前时间、查询天气信息、搜索内部文档知识库,以及查询 Prometheus 告警信息。\n"); systemPromptBuilder.append("当用户询问时间相关问题时,**必须每次都调用 getCurrentDateTime 工具**,因为时间会不断变化。即使历史消息中有时间信息,也不要直接复用,必须重新查询最新时间。\n"); systemPromptBuilder.append("当用户需要查询公司内部文档、流程、最佳实践或技术指南时,使用 lookupKnowledgeTool 工具。\n"); systemPromptBuilder.append("当用户需要查询 Prometheus 告警、监控指标或系统告警状态时,使用 queryPrometheusAlerts 工具。\n"); systemPromptBuilder.append("当用户需要查询腾讯云日志时,请调用腾讯云mcp服务查询,默认查询地域ap-guangzhou,查询时间范围为近一个月。\n\n"); // 添加历史消息(过滤时间查询相关内容) if (!history.isEmpty()) { systemPromptBuilder.append("--- 对话历史 ---\n"); for (Map msg : history) { String role = msg.get("role"); String content = msg.get("content"); // 🔧 过滤时间查询相关的历史消息,避免 LLM 复用旧的时间信息 if ("user".equals(role) && isTimeQuery(content)) { continue; // 跳过时间查询问题 } if ("assistant".equals(role) && containsTimeInfo(content)) { continue; // 跳过包含时间信息的回答 } if ("user".equals(role)) { systemPromptBuilder.append("用户: ").append(content).append("\n"); } else if ("assistant".equals(role)) { systemPromptBuilder.append("助手: ").append(content).append("\n"); } } systemPromptBuilder.append("--- 对话历史结束 ---\n\n"); } systemPromptBuilder.append("请基于以上对话历史,回答用户的新问题。"); return systemPromptBuilder.toString(); } /** * 判断是否为时间查询问题 */ private boolean isTimeQuery(String content) { if (content == null) { return false; } // 匹配常见的时间查询模式 return content.matches(".*(现在|当前|此时).*(几点|时间).*") || content.matches(".*(几点|时间).*(了|呢|[??]).*") || content.toLowerCase().matches(".*(what.*time|current.*time).*"); } /** * 判断是否包含时间信息 */ private boolean containsTimeInfo(String content) { if (content == null) { return false; } // 匹配日期时间格式:2026年5月31日、15:57、下午3点 等 return content.matches(".*(\\d{4}年\\d{1,2}月\\d{1,2}日|\\d{1,2}:\\d{2}|[上下午]+\\d{1,2}[点时]).*"); } /** * 动态构建方法工具数组 * 根据 cls.mock-enabled 决定是否包含 QueryLogsTools */ public Object[] buildMethodToolsArray() { if (queryLogsTools != null) { // Mock 模式:包含 QueryLogsTools return new Object[]{dateTimeTools, lookupKnowledgeTool}; } else { // 真实模式:不包含 QueryLogsTools(由 MCP 提供日志查询功能) return new Object[]{dateTimeTools, lookupKnowledgeTool, queryMetricsTools}; } } /** * 获取工具回调列表,mcp服务提供的工具 */ public ToolCallback[] getToolCallbacks() { if (tools == null) { return new ToolCallback[0]; } return tools.getToolCallbacks(); } /** * 记录可用工具列表:mcp服务提供的工具 */ public void logAvailableTools() { if (tools == null) { logger.info("MCP 未启用,无远程工具"); return; } ToolCallback[] toolCallbacks = tools.getToolCallbacks(); logger.info("可用工具列表:"); for (ToolCallback toolCallback : toolCallbacks) { logger.info(">>> {}", toolCallback.getToolDefinition().name()); } } /** * 创建 ReactAgent * @param chatModel 聊天模型 * @param systemPrompt 系统提示词 * @return 配置好的 ReactAgent */ public ReactAgent createReactAgent(ChatModel chatModel, String systemPrompt) { return ReactAgent.builder() .name("intelligent_assistant") .model(chatModel) .systemPrompt(systemPrompt) .methodTools(buildMethodToolsArray()) .tools(getToolCallbacks()) .hooks(new AgentLoggingHook(agentStepRepository, "intelligent_assistant")) .build(); } /** * 执行 ReactAgent 对话(非流式) * @param agent ReactAgent 实例 * @param question 用户问题 * @return ChatResult(answer + sessionId) */ public ChatResult executeChat(ReactAgent agent, String question) throws GraphRunnerException { logger.info("========================================"); logger.info("📝 用户问题: {}", question); String sessionId = UUID.randomUUID().toString().substring(0, 8); long startTime = System.currentTimeMillis(); // 创建诊断会话 DiagnosisSession session = DiagnosisSession.builder() .sessionId(sessionId) .query(question) .status("RUNNING") .agentFlow("CHAT") .build(); diagnosisSessionRepository.save(session); // 设置 ThreadLocal 上下文(LookupKnowledgeTool 通过此获取 sessionId) SessionContextHolder.setSessionId(sessionId); try { // 通过 RunnableConfig 将 sessionId 传入 Hook(线程安全,异步也兼容) var config = RunnableConfig.builder() .addMetadata("sessionId", sessionId) .build(); var response = agent.call(question, config); long duration = System.currentTimeMillis() - startTime; String answer = response.getText(); // 更新诊断会话 session.setStatus("SUCCESS"); session.setAnswer(answer); session.setTotalDurationMs((int) duration); backfillSessionMetrics(session); diagnosisSessionRepository.save(session); evaluationService.evaluate(sessionId, answer); logger.info("⏱️ 总耗时: {} ms", duration); logger.info("📏 输出长度: {} 字符", answer.length()); logger.info("========================================"); return new ChatResult(answer, sessionId); } catch (Exception e) { session.setStatus("FAILED"); diagnosisSessionRepository.save(session); throw e; } finally { retrievedDocTracker.clearSession(sessionId); SessionContextHolder.clear(); } } /** * 根据问题复杂度自动选择执行策略 * @param chatModel 聊天模型 * @param toolCallbacks 工具回调 * @param question 用户问题 * @param history 历史消息 * @return ChatResult(answer + sessionId) */ public ChatResult executeChatWithStrategy(ChatModel chatModel, ToolCallback[] toolCallbacks, String question, List> history) throws GraphRunnerException { if (QuestionComplexity.isComplex(question)) { logger.info("📊 问题判定为复杂,使用多 Agent(Planner + Executor)执行"); return executeChatComplex(chatModel, toolCallbacks, question, history); } else { logger.info("📊 问题判定为简单,使用单 Agent 执行"); String systemPrompt = buildSystemPrompt(history); ReactAgent agent = createReactAgent(chatModel, systemPrompt); return executeChat(agent, question); } } /** * 多 Agent 复杂对话执行(Planner + Executor + Supervisor) */ public ChatResult executeChatComplex(ChatModel chatModel, ToolCallback[] toolCallbacks, String question, List> history) throws GraphRunnerException { String sessionId = UUID.randomUUID().toString().substring(0, 8); long startTime = System.currentTimeMillis(); DiagnosisSession session = DiagnosisSession.builder() .sessionId(sessionId) .query(question) .status("RUNNING") .agentFlow("CHAT") .build(); diagnosisSessionRepository.save(session); SessionContextHolder.setSessionId(sessionId); VerifierContextHolder.setOriginalQuery(question); VerifierContextHolder.setRetryContext(null); VerifierContextHolder.setExecutorFinalAnswer(null); try { VerifierDecision finalDecision = null; String retryContext = null; String answer = null; RunnableConfig config = RunnableConfig.builder() .addMetadata("sessionId", sessionId) .build(); for (int round = 1; round <= 2; round++) { VerifierContextHolder.setRetryContext(retryContext); VerifierContextHolder.setToolTraceSummary(null); ReactAgent planner = buildChatPlannerAgent(chatModel, history, retryContext); ReactAgent executor = buildChatExecutorAgent(chatModel, toolCallbacks, history, retryContext); ReactAgent verifier = buildChatVerifierAgent(chatModel); SupervisorAgent supervisor = SupervisorAgent.builder() .name("chat_supervisor") .description("负责按单轮顺序调度 Planner、Executor、Verifier 的多 Agent 控制器") .model(chatModel) .systemPrompt(buildSupervisorPrompt(round)) .subAgents(List.of(planner, executor, verifier)) .build(); String plannerPlan = callAgent(planner, buildPlannerInput(question, retryContext), config); answer = callAgent(executor, buildExecutorInput(question, plannerPlan, retryContext), config); VerifierContextHolder.setExecutorFinalAnswer(answer); String verifierOutput = callAgent(verifier, "VERIFY", config); finalDecision = parseVerifierDecision(verifierOutput, round); if (finalDecision == null) { finalDecision = buildVerifierFallbackDecision(round, "verifier_output 缺失或无法解析"); answer = buildLowConfidenceOutput(answer, finalDecision); persistVerifierEvaluation(session, finalDecision, round); break; } if ("PASS".equals(finalDecision.verdict())) { answer = answer == null || answer.isBlank() ? "抱歉,多 Agent 分析未能生成有效结论。" : answer; persistVerifierEvaluation(session, finalDecision, round); break; } if ("REJECT".equals(finalDecision.verdict())) { answer = buildDegradedOutput(finalDecision); persistVerifierEvaluation(session, finalDecision, round); break; } if (finalDecision.groundednessScore() >= verifierLowConfidenceThreshold || round == 2) { answer = buildLowConfidenceOutput(answer, finalDecision); persistVerifierEvaluation(session, finalDecision, round); break; } retryContext = buildRetryContext(finalDecision); persistVerifierEvaluation(session, finalDecision, round); } long duration = System.currentTimeMillis() - startTime; if (answer == null || answer.isBlank()) { answer = "抱歉,多 Agent 分析未能生成有效结论。"; } session.setStatus("SUCCESS"); session.setAnswer(answer); session.setTotalDurationMs((int) duration); backfillSessionMetrics(session); diagnosisSessionRepository.save(session); evaluationService.evaluate(sessionId, answer); logger.info("⏱️ 多 Agent 总耗时: {} ms", duration); logger.info("📏 输出长度: {} 字符", answer.length()); return new ChatResult(answer, sessionId); } catch (Exception e) { session.setStatus("FAILED"); diagnosisSessionRepository.save(session); logger.error("多 Agent 执行失败", e); return new ChatResult("执行失败: " + e.getMessage(), sessionId); } finally { retrievedDocTracker.clearSession(sessionId); SessionContextHolder.clear(); VerifierContextHolder.clear(); } } private ReactAgent buildChatPlannerAgent(ChatModel chatModel, List> history, String retryContext) { StringBuilder prompt = new StringBuilder(chatPlannerPrompt); // 注入 knowledge map String knowledgeMap = knowledgeDomainService.buildKnowledgeMap(); if (!knowledgeMap.isBlank()) { prompt.append("\n\n## 可用知识库\n\n").append(knowledgeMap); } if (!history.isEmpty()) { prompt.append("\n\n--- 对话历史 ---\n"); for (Map msg : history) { prompt.append(msg.get("role")).append(": ").append(msg.get("content")).append("\n"); } prompt.append("--- 对话历史结束 ---\n"); } if (retryContext != null && !retryContext.isBlank()) { prompt.append("\n\n--- 本轮补证据约束 ---\n").append(retryContext).append("\n"); } return ReactAgent.builder() .name("chat_planner") .description("负责拆解问题、规划步骤") .model(chatModel) .systemPrompt(prompt.toString()) .hooks(new AgentLoggingHook(agentStepRepository, "planner")) .outputKey("planner_plan") .build(); } private ReactAgent buildChatVerifierAgent(ChatModel chatModel) { return ReactAgent.builder() .name("chat_verifier") .description("负责验证 Executor 答案的事实准确性") .model(chatModel) .systemPrompt(chatVerifierPrompt) .hooks(new AgentLoggingHook(agentStepRepository, "verifier"), new VerifierInputHook(toolTraceSummaryService)) .outputKey("verifier_output") .build(); } private ReactAgent buildChatExecutorAgent(ChatModel chatModel, ToolCallback[] toolCallbacks, List> history, String retryContext) { StringBuilder prompt = new StringBuilder(chatExecutorPrompt); if (!history.isEmpty()) { prompt.append("\n\n--- 对话历史 ---\n"); for (Map msg : history) { prompt.append(msg.get("role")).append(": ").append(msg.get("content")).append("\n"); } prompt.append("--- 对话历史结束 ---\n"); } if (retryContext != null && !retryContext.isBlank()) { prompt.append("\n\n--- 本轮补证据约束 ---\n").append(retryContext).append("\n"); } return ReactAgent.builder() .name("chat_executor") .description("负责执行具体步骤并及时反馈") .model(chatModel) .systemPrompt(prompt.toString()) .methodTools(buildMethodToolsArray()) .tools(toolCallbacks) .hooks(new AgentLoggingHook(agentStepRepository, "executor")) .outputKey("executor_feedback") .build(); } private String callAgent(ReactAgent agent, String input, RunnableConfig config) throws GraphRunnerException { return agent.call(input, config).getText(); } private String buildPlannerInput(String question, String retryContext) { if (retryContext == null || retryContext.isBlank()) { return question; } return question + "\n\n--- 补充约束 ---\n" + retryContext; } private String buildExecutorInput(String question, String plannerPlan, String retryContext) { StringBuilder input = new StringBuilder(question); if (plannerPlan != null && !plannerPlan.isBlank()) { input.append("\n\n--- planner_plan ---\n").append(plannerPlan); } if (retryContext != null && !retryContext.isBlank()) { input.append("\n\n--- retry_context ---\n").append(retryContext); } return input.toString(); } private VerifierDecision parseVerifierDecision(String verifierOutput, int round) { if (verifierOutput == null || verifierOutput.isBlank()) { return null; } try { JsonNode root = objectMapper.readTree(sanitizeJsonPayload(verifierOutput)); List> factsChecked = parseFactsChecked(root.path("facts_checked")); return new VerifierDecision( root.path("verdict").asText("LOW_CONFID"), root.path("groundedness_score").asDouble(0.0), root.path("critical_fact_count").asInt(0), factsChecked, root.path("rationale").asText(""), round ); } catch (Exception e) { logger.error("解析 verifier_output 失败: {}", verifierOutput, e); return null; } } private String sanitizeJsonPayload(String raw) { String trimmed = raw.trim(); if (trimmed.startsWith("```")) { int firstNewline = trimmed.indexOf('\n'); int lastFence = trimmed.lastIndexOf("```"); if (firstNewline >= 0 && lastFence > firstNewline) { return trimmed.substring(firstNewline + 1, lastFence).trim(); } } return trimmed; } private List> parseFactsChecked(JsonNode factsNode) { List> factsChecked = new ArrayList<>(); if (!factsNode.isArray()) { return factsChecked; } for (JsonNode factNode : factsNode) { Map fact = new LinkedHashMap<>(); fact.put("fact", factNode.path("fact").asText("")); fact.put("is_critical", factNode.path("is_critical").asBoolean(false)); fact.put("verification", factNode.path("verification").asText("")); fact.put("detail", factNode.path("detail").asText("")); fact.put("evidence_refs", parseEvidenceRefs(factNode.path("evidence_refs"))); factsChecked.add(fact); } return factsChecked; } private List> parseEvidenceRefs(JsonNode evidenceRefsNode) { List> evidenceRefs = new ArrayList<>(); if (!evidenceRefsNode.isArray()) { return evidenceRefs; } for (JsonNode refNode : evidenceRefsNode) { Map evidenceRef = new LinkedHashMap<>(); evidenceRef.put("trace_ref", refNode.path("trace_ref").asText("")); evidenceRef.put("tool_name", refNode.path("tool_name").asText("")); evidenceRef.put("topic_domain", refNode.path("topic_domain").asText("")); evidenceRef.put("note", refNode.path("note").asText("")); List sourceInvocationIds = new ArrayList<>(); JsonNode idsNode = refNode.path("source_invocation_ids"); if (idsNode.isArray()) { for (JsonNode idNode : idsNode) { if (idNode.canConvertToLong()) { sourceInvocationIds.add(idNode.asLong()); } } } evidenceRef.put("source_invocation_ids", sourceInvocationIds); evidenceRefs.add(evidenceRef); } return evidenceRefs; } private VerifierDecision buildVerifierFallbackDecision(int round, String rationale) { return new VerifierDecision("LOW_CONFID", 0.0, 0, List.of(), rationale, round); } private String buildSupervisorPrompt(int round) { return """ 你是一个多 Agent 调度器。每一轮必须严格按顺序完成以下动作: 1. 先调用 chat_planner 生成执行计划 2. 再调用 chat_executor 执行计划并形成最终答案 3. 最后调用 chat_verifier 对 executor 最终答案做事实核查 规则: - 本轮只允许完成一次 Planner -> Executor -> Verifier 链路 - Verifier 完成后立即停止,不要继续调用任何 Agent - 不要自己编造答案,最终用户输出由外层代码根据 verifier_output 决定 - 当前是第 %d 轮,保持单轮内顺序稳定 """.formatted(round); } private String buildRoundInput(String question, String retryContext) { if (retryContext == null || retryContext.isBlank()) { return question; } return question + "\n\n--- 补充约束 ---\n" + retryContext; } private String extractExecutorAnswer(Optional stateOptional) { if (stateOptional.isEmpty()) { return null; } return stateOptional.get().value("executor_feedback") .filter(AssistantMessage.class::isInstance) .map(AssistantMessage.class::cast) .map(AssistantMessage::getText) .orElse(null); } private VerifierDecision parseVerifierDecision(Optional stateOptional, int round) { if (stateOptional.isEmpty()) { return null; } Optional verifierOutput = stateOptional.get().value("verifier_output") .filter(AssistantMessage.class::isInstance) .map(AssistantMessage.class::cast); if (verifierOutput.isEmpty() || verifierOutput.get().getText() == null || verifierOutput.get().getText().isBlank()) { return null; } try { JsonNode root = objectMapper.readTree(verifierOutput.get().getText()); List> factsChecked = parseFactsChecked(root.path("facts_checked")); return new VerifierDecision( root.path("verdict").asText("LOW_CONFID"), root.path("groundedness_score").asDouble(0.0), root.path("critical_fact_count").asInt(0), factsChecked, root.path("rationale").asText(""), round ); } catch (Exception e) { logger.error("解析 verifier_output 失败: {}", verifierOutput.get().getText(), e); return null; } } private void persistVerifierEvaluation(DiagnosisSession session, VerifierDecision decision, int round) { if (decision == null) { return; } Map verifierEvaluation = new LinkedHashMap<>(); verifierEvaluation.put("verdict", decision.verdict()); verifierEvaluation.put("groundedness_score", decision.groundednessScore()); verifierEvaluation.put("critical_fact_count", decision.criticalFactCount()); verifierEvaluation.put("facts_checked", decision.factsChecked()); verifierEvaluation.put("rationale", decision.rationale()); verifierEvaluation.put("round", round); verifierEvaluation.put("traceability_version", "v1"); verifierEvaluation.put("tool_trace_summary", Optional.ofNullable(VerifierContextHolder.getToolTraceSummary()).orElse(List.of())); String merged = selfEvaluationMergeService.mergeVerifierEvaluation(session.getSelfEvaluation(), verifierEvaluation); session.setSelfEvaluation(merged); diagnosisSessionRepository.save(session); } private String buildRetryContext(VerifierDecision decision) { try { List missingFacts = extractEvidenceGaps(decision); Map retryContext = new LinkedHashMap<>(); retryContext.put("round", decision.round()); retryContext.put("missing_evidence_facts", missingFacts); retryContext.put("instruction", "仅补充以上断言相关证据,不要重复已完成检索"); return objectMapper.writeValueAsString(retryContext); } catch (Exception e) { logger.error("构造 retry_context 失败", e); return "{\"round\":1,\"missing_evidence_facts\":[],\"instruction\":\"仅补充缺失证据\"}"; } } private String buildLowConfidenceOutput(String executorAnswer, VerifierDecision decision) { StringBuilder output = new StringBuilder(LOW_CONFID_DISCLAIMER); output.append("\n\n").append(executorAnswer == null ? "" : executorAnswer); List gaps = extractEvidenceGaps(decision); if (!gaps.isEmpty()) { output.append("\n\n当前缺口:"); for (String gap : gaps) { output.append("\n- ").append(gap); } } return output.toString(); } private String buildDegradedOutput(VerifierDecision decision) { StringBuilder output = new StringBuilder(DEGRADED_PREFIX); List confirmedFacts = extractConfirmedFacts(decision); List gaps = extractEvidenceGaps(decision); List suggestions = buildNextStepSuggestions(decision); output.append("\n\n已确认信息:"); if (confirmedFacts.isEmpty()) { output.append("\n- 暂无可稳定确认的信息"); } else { for (String fact : confirmedFacts) { output.append("\n- ").append(fact); } } output.append("\n\n证据缺口:"); if (gaps.isEmpty()) { output.append("\n- 当前缺少足够的直接证据支撑核心结论"); } else { for (String gap : gaps) { output.append("\n- ").append(gap); } } output.append("\n\n建议下一步:"); for (String suggestion : suggestions) { output.append("\n- ").append(suggestion); } return output.toString(); } private List extractConfirmedFacts(VerifierDecision decision) { List confirmedFacts = new ArrayList<>(); for (Map fact : decision.factsChecked()) { String verification = String.valueOf(fact.get("verification")); boolean critical = Boolean.TRUE.equals(fact.get("is_critical")); if (critical && ("direct_evidence".equals(verification) || "indirect_support".equals(verification))) { confirmedFacts.add(String.valueOf(fact.get("fact"))); } } return confirmedFacts; } private List extractEvidenceGaps(VerifierDecision decision) { List gaps = new ArrayList<>(); for (Map fact : decision.factsChecked()) { String verification = String.valueOf(fact.get("verification")); boolean critical = Boolean.TRUE.equals(fact.get("is_critical")); if (critical && ("no_evidence".equals(verification) || "contradicted".equals(verification))) { gaps.add(String.valueOf(fact.get("fact")) + ":" + String.valueOf(fact.get("detail"))); } } if (gaps.isEmpty() && "LOW_CONFID".equals(decision.verdict())) { for (Map fact : decision.factsChecked()) { String verification = String.valueOf(fact.get("verification")); boolean critical = Boolean.TRUE.equals(fact.get("is_critical")); if (critical && "indirect_support".equals(verification)) { gaps.add(String.valueOf(fact.get("fact")) + ":缺少直接证据锚点"); } } } return gaps; } private List buildNextStepSuggestions(VerifierDecision decision) { List suggestions = new ArrayList<>(); List> toolSummary = toolTraceSummaryService.buildVerifierTraceSummary(SessionContextHolder.getSessionId(), null); boolean hasKnowledgeTool = toolSummary.stream().anyMatch(item -> "lookup_knowledge".equals(item.get("tool_name"))); boolean hasFailedEvidence = toolSummary.stream().anyMatch(item -> !Boolean.TRUE.equals(item.get("success"))); if (!hasKnowledgeTool) { suggestions.add("补充知识库或业务文档检索结果,建立可引用的证据锚点"); } if (hasFailedEvidence) { suggestions.add("优先重试失败的证据型查询,补齐日志、指标或知识库侧证据"); } if (suggestions.isEmpty()) { suggestions.add("围绕上述证据缺口补充只读查询,再由人工复核最终结论"); } return suggestions; } private record VerifierDecision( String verdict, double groundednessScore, int criticalFactCount, List> factsChecked, String rationale, int round ) { } /** 从 agent_step 汇总 token、步数等指标回填 diagnosis_session */ private void backfillSessionMetrics(DiagnosisSession session) { try { List steps = agentStepRepository.findBySessionIdOrderByStepIndex(session.getSessionId()); if (steps.isEmpty()) return; int totalTokens = 0; int stepCount = 0; int toolCallCount = 0; for (var s : steps) { stepCount++; if (s.getTokenCount() != null) totalTokens += s.getTokenCount(); if (Boolean.TRUE.equals(s.getHasToolCall())) toolCallCount++; } session.setTotalTokenCount(totalTokens); session.setStepCount(stepCount); session.setToolCallCount(toolCallCount); } catch (Exception e) { logger.warn("回填会话指标失败: sessionId={}", session.getSessionId(), e); } } }