feat: add traceable scoped AIOps diagnosis
This commit is contained in:
@@ -10,11 +10,12 @@ 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.AgentStep;
|
||||
import com.superbiz.agent.domain.entity.AgentStep;
|
||||
import com.superbiz.agent.domain.entity.DiagnosisSession;
|
||||
import com.superbiz.agent.dto.AIOpsRequest;
|
||||
import com.superbiz.agent.hook.AgentLoggingHook;
|
||||
import com.superbiz.agent.repository.AgentStepRepository;
|
||||
import com.superbiz.agent.repository.DiagnosisSessionRepository;
|
||||
import com.superbiz.agent.repository.ToolInvocationRepository;
|
||||
import com.superbiz.agent.util.SessionContextHolder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -62,6 +63,9 @@ public class AiOpsService {
|
||||
@Autowired
|
||||
private AgentStepRepository agentStepRepository;
|
||||
|
||||
@Autowired
|
||||
private ToolInvocationRepository toolInvocationRepository;
|
||||
|
||||
/**
|
||||
* 执行 AI Ops 告警分析流程
|
||||
*
|
||||
@@ -71,22 +75,22 @@ public class AiOpsService {
|
||||
* @throws GraphRunnerException 如果 Agent 执行失败
|
||||
*/
|
||||
public Optional<OverAllState> executeAiOpsAnalysis(ChatModel chatModel, ToolCallback[] toolCallbacks) throws GraphRunnerException {
|
||||
return executeAiOpsAnalysis(chatModel, toolCallbacks, null, resolveSessionId(null));
|
||||
}
|
||||
|
||||
public Optional<OverAllState> executeAiOpsAnalysis(ChatModel chatModel, ToolCallback[] toolCallbacks,
|
||||
AIOpsRequest request, String sessionId) throws GraphRunnerException {
|
||||
logger.info("开始执行 AI Ops 多 Agent 协作流程");
|
||||
|
||||
String sessionId = UUID.randomUUID().toString().substring(0, 8);
|
||||
String resolvedSessionId = isBlank(sessionId) ? resolveSessionId(request) : sessionId.trim();
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 创建诊断会话
|
||||
DiagnosisSession session = DiagnosisSession.builder()
|
||||
.sessionId(sessionId)
|
||||
.query("AI Ops 告警分析")
|
||||
.status("RUNNING")
|
||||
.agentFlow("AI_OPS")
|
||||
.build();
|
||||
// 创建或更新诊断会话
|
||||
DiagnosisSession session = startDiagnosisSession(resolvedSessionId, request);
|
||||
diagnosisSessionRepository.save(session);
|
||||
|
||||
// 设置 ThreadLocal 上下文(LookupKnowledgeTool 通过此获取 sessionId)
|
||||
SessionContextHolder.setSessionId(sessionId);
|
||||
SessionContextHolder.setSessionId(resolvedSessionId);
|
||||
|
||||
try {
|
||||
// 构建 Planner 和 Executor Agent(每个 Agent 各自带 Hook)
|
||||
@@ -102,7 +106,7 @@ public class AiOpsService {
|
||||
.subAgents(List.of(plannerAgent, executorAgent))
|
||||
.build();
|
||||
|
||||
String taskPrompt = "你是企业级 SRE,接到了自动化告警排查任务。请结合工具调用,执行**规划→执行→再规划**的闭环,并最终按照固定模板输出《告警分析报告》。禁止编造虚假数据,如连续多次查询失败需诚实反馈无法完成的原因。";
|
||||
String taskPrompt = buildTaskPrompt(request);
|
||||
|
||||
logger.info("调用 Supervisor Agent 开始编排...");
|
||||
|
||||
@@ -158,6 +162,87 @@ public class AiOpsService {
|
||||
}
|
||||
}
|
||||
|
||||
public String resolveSessionId(AIOpsRequest request) {
|
||||
if (request != null && !isBlank(request.getSessionId())) {
|
||||
return request.getSessionId().trim();
|
||||
}
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
public void persistFinalReport(String sessionId, String finalReport) {
|
||||
if (isBlank(sessionId) || isBlank(finalReport)) {
|
||||
return;
|
||||
}
|
||||
diagnosisSessionRepository.findBySessionId(sessionId.trim()).ifPresent(session -> {
|
||||
session.setAnswer(finalReport);
|
||||
diagnosisSessionRepository.save(session);
|
||||
});
|
||||
}
|
||||
|
||||
String buildQuerySummary(AIOpsRequest request) {
|
||||
if (request == null) {
|
||||
return "AI Ops 告警分析";
|
||||
}
|
||||
|
||||
StringBuilder summary = new StringBuilder("AI Ops 告警分析");
|
||||
appendField(summary, "告警", request.getAlertName());
|
||||
appendField(summary, "服务", request.getService());
|
||||
appendField(summary, "等级", request.getSeverity());
|
||||
appendField(summary, "时间范围", request.getTimeRange());
|
||||
appendField(summary, "描述", request.getDescription());
|
||||
appendField(summary, "请求", request.getUserRequest());
|
||||
return summary.toString();
|
||||
}
|
||||
|
||||
boolean hasAlertPayload(AIOpsRequest request) {
|
||||
if (request == null) {
|
||||
return false;
|
||||
}
|
||||
return !isBlank(request.getAlertName())
|
||||
|| !isBlank(request.getService())
|
||||
|| !isBlank(request.getSeverity())
|
||||
|| !isBlank(request.getDescription())
|
||||
|| !isBlank(request.getTimeRange());
|
||||
}
|
||||
|
||||
String buildTaskPrompt(AIOpsRequest request) {
|
||||
StringBuilder prompt = new StringBuilder();
|
||||
prompt.append("你是企业级 SRE,接到了自动化告警排查任务。请结合工具调用,执行**规划→执行→再规划**的闭环,并最终按照固定模板输出《告警分析报告》。禁止编造虚假数据,如连续多次查询失败需诚实反馈无法完成的原因。");
|
||||
prompt.append("\n\n本次告警输入:\n");
|
||||
prompt.append(buildQuerySummary(request));
|
||||
if (hasAlertPayload(request)) {
|
||||
prompt.append("\n\nAIOps scope mode: PAYLOAD_TARGETED\n");
|
||||
prompt.append("- The request includes an alert payload. Treat the supplied alert payload as the primary and only main diagnosis target.\n");
|
||||
prompt.append("- The final report must focus on the supplied alert fields such as alertName, service, severity, description, and timeRange.\n");
|
||||
prompt.append("- You may call queryPrometheusAlerts only to verify whether the supplied alert is still active or to identify related risk/context.\n");
|
||||
prompt.append("- If queryPrometheusAlerts returns unrelated active alerts, do not create full root-cause or remediation sections for them.\n");
|
||||
prompt.append("- Mention unrelated active alerts only briefly in a Related Risk section when they help explain the supplied alert.\n");
|
||||
} else {
|
||||
prompt.append("\n\nAIOps scope mode: AUTO_DISCOVERY\n");
|
||||
prompt.append("- The request does not include alert payload fields. First call queryPrometheusAlerts to discover current active/firing alerts.\n");
|
||||
prompt.append("- Prefer P0/P1 alerts or the longest-running firing alerts, then diagnose one or more alerts based on severity and evidence.\n");
|
||||
prompt.append("- Use metrics, logs, and knowledge-base evidence before producing the final alert analysis report.\n");
|
||||
}
|
||||
return prompt.toString();
|
||||
}
|
||||
|
||||
private DiagnosisSession startDiagnosisSession(String sessionId, AIOpsRequest request) {
|
||||
DiagnosisSession session = diagnosisSessionRepository.findBySessionId(sessionId)
|
||||
.orElseGet(() -> DiagnosisSession.builder()
|
||||
.sessionId(sessionId)
|
||||
.agentFlow("AI_OPS")
|
||||
.build());
|
||||
session.setQuery(buildQuerySummary(request));
|
||||
session.setStatus("RUNNING");
|
||||
session.setAgentFlow("AI_OPS");
|
||||
session.setAnswer(null);
|
||||
session.setTotalDurationMs(null);
|
||||
session.setTotalTokenCount(null);
|
||||
session.setStepCount(null);
|
||||
session.setToolCallCount(null);
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Planner Agent
|
||||
*/
|
||||
@@ -205,25 +290,33 @@ public class AiOpsService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 agent_step 汇总指标回填 diagnosis_session */
|
||||
/** 从 agent_step 和 tool_invocation 汇总指标回填 diagnosis_session */
|
||||
private void backfillSessionMetrics(DiagnosisSession session) {
|
||||
try {
|
||||
List<AgentStep> steps = agentStepRepository.findBySessionIdOrderByStepIndex(session.getSessionId());
|
||||
if (steps.isEmpty()) return;
|
||||
|
||||
int totalTokens = 0;
|
||||
int stepCount = 0;
|
||||
int toolCallCount = 0;
|
||||
for (AgentStep s : steps) {
|
||||
stepCount++;
|
||||
if (s.getTokenCount() != null) totalTokens += s.getTokenCount();
|
||||
if (Boolean.TRUE.equals(s.getHasToolCall())) toolCallCount++;
|
||||
}
|
||||
long toolCallCount = toolInvocationRepository.countBySessionId(session.getSessionId());
|
||||
session.setTotalTokenCount(totalTokens);
|
||||
session.setStepCount(stepCount);
|
||||
session.setToolCallCount(toolCallCount);
|
||||
session.setToolCallCount(Math.toIntExact(toolCallCount));
|
||||
} catch (Exception e) {
|
||||
logger.warn("回填会话指标失败: sessionId={}", session.getSessionId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendField(StringBuilder builder, String label, String value) {
|
||||
if (!isBlank(value)) {
|
||||
builder.append("\n- ").append(label).append(": ").append(value.trim());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBlank(String value) {
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user