系列第十篇:Agent 数据分析可视化与自动化 Workflow 编排
前言
九篇文章下来,我们的 Agent 已经能对话、能协作、能查库、能记忆、能防御。但在企业日常运营中,有两个高频场景始终缺少最后一块拼图:
场景一:领导说"给我出一份本月各楼栋能耗对比图"——Agent 能查出数据,但只能返回文字表格,不能生成图表。
场景二:运维团队说"每天早上 8 点自动巡检所有区域的能耗异常,有问题就发日报"——Agent 能力再强,也只会在用户主动提问时才能工作。
本文将补齐这两块拼图:让 Agent 具备数据分析 + 可视化图表生成能力,以及 让 Agent 脱离用户手动触发,实现定时自动化运行。
一、数据分析与可视化:让 Agent 从"说"到"画"
1.1 为什么需要可视化?
人类大脑对视觉信息的处理效率远高于纯文本。同样一组数据:
文本形式:
A栋: 45,230 kWh
B栋: 67,890 kWh ← 哪个最高?需要逐行比较
C栋: 89,340 kWh
D栋: 32,100 kWh
图表形式:
C栋 ████████████████████ 89,340
B栋 ███████████████ 67,890
A栋 ██████████ 45,230
D栋 ███████ 32,100
在数据分析场景中,Agent 需要能够:
- 分析数据:统计、聚合、对比、趋势识别
- 选择图表类型:根据数据特征自动选择最合适的可视化方式
- 生成图表:输出可嵌入报告或网页的可视化结果
1.2 三种图表生成方案对比
┌────────────────────────────────────────────────────────────────┐
│ Agent 图表生成方案对比 │
│ │
│ 方案 实现方式 优点 缺点 │
│ ────────────────────────────────────────────────────────── │
│ ECharts 前端渲染 交互性强 需要前端页面 │
│ (推荐) 生成 JSON 效果专业 Agent 输出 JSON │
│ 配置 国产开源 │
│ │
│ Matplotlib 后端 Python 独立图片 需要 Python 环境 │
│ /JFreeChart 生成 PNG 可嵌入文档 Java图表库较弱 │
│ │
│ Mermaid Markdown 纯文本 样式有限 │
│ ASCII Art 语法 无需依赖 只适合简单图表 │
│ │
│ 推荐策略: │
│ · Web 应用 → ECharts(生成 JSON 配置,前端渲染) │
│ · 报告导出 → JFreeChart(生成 PNG 嵌入 PDF) │
│ · 纯对话 → ASCII Art / Mermaid(无前端依赖) │
│ │
└────────────────────────────────────────────────────────────────┘
二、数据分析引擎:Agent 的结构化分析能力
2.1 分析类型定义
/**
* 数据分析类型枚举
*
* Agent 需要识别用户想做什么类型的分析,
* 然后选择对应的分析策略和可视化方式。
*/
@Getter
public enum AnalysisType {
COMPARISON(1, "对比分析",
"比较多个对象的指标差异",
ChartType.BAR),
TREND(2, "趋势分析",
"分析指标随时间的变化趋势",
ChartType.LINE),
DISTRIBUTION(3, "分布分析",
"分析数据的分布特征和占比",
ChartType.PIE),
RANKING(4, "排名分析",
"按指标值排序,找出 Top/Bottom N",
ChartType.HORIZONTAL_BAR),
CORRELATION(5, "关联分析",
"分析两个或多个指标之间的相关性",
ChartType.SCATTER),
ANOMALY(6, "异常检测",
"识别数据中的异常点和偏离趋势",
ChartType.LINE_WITH_MARKERS);
private final Integer key;
private final String value;
private final String description;
private final ChartType defaultChartType;
AnalysisType(Integer key, String value,
String description, ChartType defaultChartType) {
this.key = key;
this.value = value;
this.description = description;
this.defaultChartType = defaultChartType;
}
}
/**
* 图表类型枚举
*/
@Getter
public enum ChartType {
BAR(1, "柱状图", "适合分类对比"),
LINE(2, "折线图", "适合时间序列趋势"),
PIE(3, "饼图", "适合占比分析"),
HORIZONTAL_BAR(4, "条形图", "适合排名展示"),
SCATTER(5, "散点图", "适合关联分析"),
LINE_WITH_MARKERS(6, "标注折线图", "适合异常点标记"),
STACKED_BAR(7, "堆叠柱状图", "适合多维度对比"),
HEATMAP(8, "热力图", "适合时段×区域分析");
private final Integer key;
private final String value;
private final String suitableFor;
ChartType(Integer key, String value, String suitableFor) {
this.key = key;
this.value = value;
this.suitableFor = suitableFor;
}
}
2.2 数据分析 Agent
/**
* 数据分析 Agent
*
* 职责:
* 1. 理解用户的分析需求 → 确定分析类型
* 2. 调用 TextToSqlTool 获取数据
* 3. 对数据进行统计分析
* 4. 选择合适的图表类型
* 5. 生成 ECharts 配置 JSON
* 6. 生成分析结论文本
*/
@Service
public class DataAnalysisAgent {
private final ChatLanguageModel analysisModel;
private final TextToSqlTool sqlTool;
/**
* 执行完整的数据分析流程
*
* @param userQuery 用户的分析需求
* @return 分析结果(含图表配置 + 文字结论)
*/
public AnalysisResult analyze(String userQuery) {
// Step 1: 分析意图识别
AnalysisIntent intent = recognizeIntent(userQuery);
log.info("分析意图: type={}, entities={}, timeRange={}",
intent.analysisType(), intent.entities(),
intent.timeRange());
// Step 2: 数据获取
String rawData = sqlTool.queryDatabase(
intent.sqlQueryDescription());
// Step 3: 数据结构化解析
StructuredData data = parseStructuredData(rawData);
// Step 4: 统计分析
StatisticalSummary stats = computeStatistics(
data, intent.analysisType());
// Step 5: 图表配置生成
EChartsConfig chartConfig = generateChartConfig(
intent.analysisType(), intent.chartType(),
data, stats);
// Step 6: 分析结论生成
String conclusion = generateConclusion(
userQuery, intent, data, stats);
return new AnalysisResult(
conclusion, chartConfig, stats, data);
}
/**
* 分析意图识别
*
* 用 LLM 从自然语言中提取:
* - 分析类型(对比/趋势/排名/...)
* - 分析实体(哪些区域/设备/指标)
* - 时间范围
* - 推荐的图表类型
*/
private AnalysisIntent recognizeIntent(String userQuery) {
String prompt = String.format("""
你是一个数据分析意图识别专家。请分析用户的需求,提取以下信息:
## 可选分析类型
1. COMPARISON: 比较多个对象的指标(如"A栋和B栋的能耗对比")
2. TREND: 时间序列趋势(如"最近半年每月用电量变化")
3. DISTRIBUTION: 占比分布(如"各区域能耗占总能耗的比例")
4. RANKING: 排名(如"能耗最高的5个区域")
5. ANOMALY: 异常检测(如"最近有没有异常的能耗飙升")
## 可选图表类型
BAR, LINE, PIE, HORIZONTAL_BAR, STACKED_BAR, HEATMAP,
LINE_WITH_MARKERS
按 JSON 返回:
{
"analysisType": "...",
"chartType": "...",
"entities": ["区域/设备名称"],
"metrics": ["能耗指标"],
"timeRange": "时间范围描述",
"sqlQueryDescription": "转化为数据库查询的自然语言描述"
}
用户需求:%s
""", userQuery);
String result = analysisModel.generate(
UserMessage.from(prompt)).content().text().trim();
return parseIntent(result);
}
}
2.3 ECharts 配置生成
这是核心部分——将分析数据转换为 ECharts 的 JSON 配置。前端拿到这个 JSON 后可以直接渲染出专业的图表。
/**
* ECharts 配置生成器
*
* 设计思路:
* Agent 不生成 HTML/JS 代码,而是生成 ECharts 的 option JSON。
* 前端有一个通用的图表渲染组件,接收 JSON 即可渲染。
*
* 这样设计的好处:
* 1. Agent 的输出是结构化 JSON,不是代码(安全性高)
* 2. 前端统一管控样式和交互(一致性)
* 3. JSON 可以缓存/存储/转发(灵活性)
*/
@Service
public class EChartsConfigGenerator {
/**
* 根据分析类型和数据生成 ECharts 配置
*/
public EChartsConfig generate(AnalysisType analysisType,
ChartType chartType,
StructuredData data,
StatisticalSummary stats) {
return switch (chartType) {
case BAR -> generateBarChart(data, stats);
case LINE -> generateLineChart(data, stats);
case PIE -> generatePieChart(data, stats);
case HORIZONTAL_BAR -> generateHorizontalBar(data, stats);
case STACKED_BAR -> generateStackedBar(data, stats);
case HEATMAP -> generateHeatmap(data, stats);
case LINE_WITH_MARKERS ->
generateLineWithAnomalyMarkers(data, stats);
default -> generateBarChart(data, stats);
};
}
/**
* 柱状图:各楼栋能耗对比
*
* 示例场景:"A/B/C/D栋本月能耗对比"
*/
private EChartsConfig generateBarChart(StructuredData data,
StatisticalSummary stats) {
Map<String, Object> option = new LinkedHashMap<>();
// 标题
option.put("title", Map.of(
"text", stats.title(),
"subtext", stats.subtitle(),
"left", "center"
));
// 提示框
option.put("tooltip", Map.of(
"trigger", "axis",
"axisPointer", Map.of("type", "shadow"),
"formatter", "{b}: {c} kWh"
));
// X轴:分类(楼栋名称)
option.put("xAxis", Map.of(
"type", "category",
"data", data.labels(),
"axisLabel", Map.of(
"rotate", data.labels().size() > 6 ? 30 : 0
)
));
// Y轴:数值
option.put("yAxis", Map.of(
"type", "value",
"name", "能耗 (kWh)",
"axisLabel", Map.of(
"formatter", "{value}"
)
));
// 数据系列
List<Map<String, Object>> series = List.of(
Map.of(
"name", "能耗",
"type", "bar",
"data", data.values(),
"itemStyle", Map.of(
"color", Map.of(
"type", "linear",
"x", 0, "y", 0, "x2", 0, "y2", 1,
"colorStops", List.of(
Map.of("offset", 0,
"color", "#5470C6"),
Map.of("offset", 1,
"color", "#91CC75")
)
)
),
// 标注平均线
"markLine", Map.of(
"data", List.of(
Map.of("type", "average", "name", "平均值")
)
)
)
);
option.put("series", series);
// 图例和网格
option.put("grid", Map.of(
"left", "10%", "right", "10%", "bottom", "15%"));
return new EChartsConfig("bar", option);
}
/**
* 折线图:能耗时间趋势
*
* 示例场景:"A栋最近30天每日用电量趋势"
*/
private EChartsConfig generateLineChart(StructuredData data,
StatisticalSummary stats) {
Map<String, Object> option = new LinkedHashMap<>();
option.put("title", Map.of(
"text", stats.title(),
"subtext", stats.subtitle(),
"left", "center"
));
option.put("tooltip", Map.of(
"trigger", "axis"
));
// X轴:日期
option.put("xAxis", Map.of(
"type", "category",
"data", data.labels(),
"boundaryGap", false
));
// Y轴
option.put("yAxis", Map.of(
"type", "value",
"name", "能耗 (kWh)"
));
// 数据系列(带面积填充)
option.put("series", List.of(
Map.of(
"name", "每日能耗",
"type", "line",
"data", data.values(),
"smooth", true, // 平滑曲线
"areaStyle", Map.of( // 面积填充
"opacity", 0.3
),
// 标注最大/最小值
"markPoint", Map.of(
"data", List.of(
Map.of("type", "max", "name", "最高"),
Map.of("type", "min", "name", "最低")
)
),
// 标注平均线
"markLine", Map.of(
"data", List.of(
Map.of("type", "average", "name", "平均")
)
)
)
));
// 数据缩放(支持拖拽查看局部)
option.put("dataZoom", List.of(
Map.of("type", "inside", "start", 0, "end", 100),
Map.of("type", "slider", "start", 0, "end", 100)
));
return new EChartsConfig("line", option);
}
/**
* 饼图:能耗占比分布
*
* 示例场景:"各区域能耗占总能耗的比例"
*/
private EChartsConfig generatePieChart(StructuredData data,
StatisticalSummary stats) {
// 组装数据项
List<Map<String, Object>> pieData = new ArrayList<>();
for (int i = 0; i < data.labels().size(); i++) {
pieData.add(Map.of(
"name", data.labels().get(i),
"value", data.values().get(i)
));
}
Map<String, Object> option = new LinkedHashMap<>();
option.put("title", Map.of(
"text", stats.title(),
"left", "center"
));
option.put("tooltip", Map.of(
"trigger", "item",
"formatter", "{b}: {c} kWh ({d}%%)"
));
option.put("series", List.of(
Map.of(
"type", "pie",
"radius", List.of("35%%", "65%%"), // 环形饼图
"data", pieData,
"emphasis", Map.of(
"itemStyle", Map.of(
"shadowBlur", 10,
"shadowOffsetX", 0,
"shadowColor", "rgba(0, 0, 0, 0.5)"
)
),
"label", Map.of(
"formatter", "{b}\n{d}%%"
)
)
));
option.put("legend", Map.of(
"orient", "vertical",
"left", "left"
));
return new EChartsConfig("pie", option);
}
/**
* 带异常标注的折线图
*
* 示例场景:"最近30天能耗趋势,标注异常点"
*
* 这是最实用的图表类型之一——不仅展示趋势,
* 还能直观地指出哪些天的数据异常。
*/
private EChartsConfig generateLineWithAnomalyMarkers(
StructuredData data, StatisticalSummary stats) {
// 检测异常点(超出均值 ± 2 标准差)
double mean = stats.mean();
double stdDev = stats.standardDeviation();
double upperBound = mean + 2 * stdDev;
double lowerBound = mean - 2 * stdDev;
List<Map<String, Object>> markPointData = new ArrayList<>();
for (int i = 0; i < data.values().size(); i++) {
double val = ((Number) data.values().get(i)).doubleValue();
if (val > upperBound) {
markPointData.add(Map.of(
"name", "偏高",
"coord", List.of(data.labels().get(i), val),
"value", val,
"itemStyle", Map.of("color", "#EE6666")
));
} else if (val < lowerBound) {
markPointData.add(Map.of(
"name", "偏低",
"coord", List.of(data.labels().get(i), val),
"value", val,
"itemStyle", Map.of("color", "#91CC75")
));
}
}
Map<String, Object> option = new LinkedHashMap<>();
option.put("title", Map.of(
"text", stats.title(),
"subtext", String.format(
"正常范围: %.0f ~ %.0f kWh (均值 ± 2σ)",
lowerBound, upperBound),
"left", "center"
));
option.put("xAxis", Map.of(
"type", "category",
"data", data.labels(),
"boundaryGap", false
));
option.put("yAxis", Map.of(
"type", "value",
"name", "能耗 (kWh)"
));
option.put("series", List.of(
Map.of(
"name", "每日能耗",
"type", "line",
"data", data.values(),
"smooth", true,
// 正常范围标注
"markArea", Map.of(
"silent", true,
"data", List.of(List.of(
Map.of("yAxis", lowerBound,
"itemStyle", Map.of(
"color", "rgba(145,204,117,0.1)")),
Map.of("yAxis", upperBound)
))
),
// 异常点标注
"markPoint", Map.of(
"data", markPointData,
"symbol", "triangle",
"symbolSize", 15
),
// 平均线
"markLine", Map.of(
"data", List.of(
Map.of("type", "average", "name", "平均")
)
)
)
));
return new EChartsConfig("line", option);
}
}
/**
* ECharts 配置封装
*/
public record EChartsConfig(
String chartType, // bar / line / pie / ...
Map<String, Object> option // ECharts option JSON
) {
/**
* 序列化为 JSON 字符串(传输给前端)
*/
public String toJson() {
return JsonUtils.toJson(option);
}
}
2.4 分析结论自动生成
/**
* 分析结论生成器
*
* 将数据统计结果转化为专业的自然语言分析结论。
*
* 关键原则:
* 1. 结论必须有数据支撑(每个结论都引用具体数值)
* 2. 区分"事实"和"推测"(推测要明确标注"可能")
* 3. 给出可操作的建议
*/
@Service
public class ConclusionGenerator {
private final ChatLanguageModel conclusionModel;
public String generate(String userQuery,
AnalysisIntent intent,
StructuredData data,
StatisticalSummary stats) {
String prompt = String.format("""
你是一个专业的能源数据分析师。请根据以下数据和分析结果,
撰写一段专业的分析结论。
## 用户需求
%s
## 分析类型
%s
## 数据统计摘要
- 数据点数: %d
- 均值: %.1f kWh
- 最大值: %.1f kWh (%s)
- 最小值: %.1f kWh (%s)
- 标准差: %.1f
- 变化率: %s
## 原始数据
%s
## 写作要求
1. 结构:核心发现 → 数据支撑 → 原因分析(如有) → 建议
2. 所有数值保留单位 (kWh)
3. 百分比保留一位小数
4. 控制在 200 字以内
5. 区分事实和推测(推测用"可能"标注)
""",
userQuery,
intent.analysisType().getValue(),
stats.dataPointCount(),
stats.mean(),
stats.max(), stats.maxLabel(),
stats.min(), stats.minLabel(),
stats.standardDeviation(),
stats.changeRate(),
data.toBriefString());
return conclusionModel.generate(
UserMessage.from(prompt)).content().text().trim();
}
}
2.5 Agent 工具封装
@Component
public class ChartGenerationTool {
private final DataAnalysisAgent analysisAgent;
@Tool("对用户的数据分析需求生成可视化图表和分析结论。" +
"当用户需要'对比图'、'趋势图'、'占比图'、" +
"'排名图'等可视化分析时使用此工具。" +
"输入分析需求的自然语言描述," +
"返回包含图表配置(JSON)和分析结论的结果。")
public String generateChart(String analysisRequirement) {
AnalysisResult result = analysisAgent.analyze(
analysisRequirement);
return String.format("""
## 分析结论
%s
## 图表配置 (ECharts JSON)
```
json
%s
```
## 统计摘要
- 数据点数: %d
- 均值: %.1f kWh
- 最大: %.1f kWh
- 最小: %.1f kWh
""",
result.conclusion(),
result.chartConfig().toJson(),
result.stats().dataPointCount(),
result.stats().mean(),
result.stats().max(),
result.stats().min());
}
}
2.6 前端图表渲染组件
vue
<!-- Vue3 通用图表组件 -->
<template>
<div ref="chartContainer" class="agent-chart"
:style="{ width: width, height: height }">
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import * as echarts from 'echarts'
const props = defineProps({
/** ECharts option JSON(Agent 输出的配置) */
chartOption: { type: Object, required: true },
width: { type: String, default: '100%' },
height: { type: String, default: '400px' }
})
const chartContainer = ref(null)
let chartInstance = null
onMounted(() => {
chartInstance = echarts.init(chartContainer.value)
renderChart()
})
watch(() => props.chartOption, renderChart, { deep: true })
function renderChart() {
if (!chartInstance || !props.chartOption) return
// 合并默认样式(保证一致性)
const mergedOption = {
...defaultTheme,
...props.chartOption
}
chartInstance.setOption(mergedOption, true)
}
// 默认主题配置(统一所有图表的视觉风格)
const defaultTheme = {
textStyle: { fontFamily: 'PingFang SC, Microsoft YaHei' },
color: ['#5470C6', '#91CC75', '#FAC858', '#EE6666',
'#73C0DE', '#3BA272', '#FC8452', '#9A60B4']
}
</script>
三、自动化 Workflow:让 Agent 脱离手动触发
3.1 为什么需要自动化?
目前的 Agent 都是 “被动响应”——用户不问,Agent 就不动。但企业中有大量的 周期性、规则性 任务:
- 每天早上 8 点:自动生成昨日能耗日报
- 每周一上午 9 点:自动生成上周能耗周报
- 每小时:自动巡检所有区域能耗是否异常
- 每月 1 号:自动生成上月能耗分析报告并发送给管理层
- 触发式:当某区域能耗超过阈值时,自动执行分析流程
这些任务不需要人类每次手动触发——Agent 应该能 自动感知、自动决策、自动执行。
3.2 Workflow 引擎架构
┌──────────────────────────────────────────────────────────────┐
│ Agent Workflow 引擎 │
│ │
│ ┌───────────┐ │
│ │ 触发器 │ │
│ │ Trigger │ │
│ │ │ │
│ │ · Cron │──┐ │
│ │ · 事件 │ │ ┌──────────────┐ ┌──────────────┐ │
│ │ · 阈值 │──┼───→│ Workflow │───→│ Action │ │
│ │ · 手动 │ │ │ Pipeline │ │ Executor │ │
│ └───────────┘ │ │ (编排引擎) │ │ (执行引擎) │ │
│ │ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ │ ▼ ▼ │
│ │ ┌──────────┐ ┌──────────┐ │
│ │ │ Step │ │ 日志 │ │
│ │ │ Manager │ │ 审计 │ │
│ │ └──────────┘ └──────────┘ │
│ │ │
│ ┌──────────────▼────────────────────────────────────────┐ │
│ │ XXL-JOB 调度中心 │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
3.3 Workflow 定义模型
java
/**
* Workflow 定义
*
* 一个 Workflow 由多个 Step 组成,
* 每个 Step 可以是一次 Agent 调用、一次工具调用、
* 或一个条件判断。
*/
@Table(name = "agent_workflow")
public class WorkflowDefinition {
@Id
private String workflowId;
private String name; // "每日能耗巡检"
private String description;
private String cronExpression; // "0 0 8 * * ?"
private boolean enabled;
@Column(columnDefinition = "json")
private List<WorkflowStep> steps;
private String createdBy;
private Instant createdAt;
}
/**
* Workflow 步骤定义
*/
public record WorkflowStep(
int stepIndex,
String stepName,
StepType type,
String config, // JSON 格式的步骤配置
String conditionExpr, // 条件表达式(可选)
String onError // SKIP / ABORT / RETRY
) {
public enum StepType {
AGENT_CHAT, // Agent 对话(分析/生成)
TOOL_CALL, // 工具调用
SQL_QUERY, // 数据库查询
CONDITION, // 条件判断
NOTIFICATION, // 发送通知
CHART_GENERATION, // 生成图表
HUMAN_APPROVAL // 等待人工审批
}
}
3.4 预置 Workflow 模板
java
/**
* Workflow 模板工厂
*
* 提供常见的预置 Workflow 模板,
* 用户可以在模板基础上修改参数。
*/
@Component
public class WorkflowTemplateFactory {
/**
* 模板1: 每日能耗日报
*
* 执行流程:
* 1. 查询昨日各区域能耗数据
* 2. 与前日/上周同期对比
* 3. 识别异常区域
* 4. 生成日报文本 + 图表
* 5. 发送给相关人员
*/
public WorkflowDefinition dailyReportTemplate() {
return WorkflowDefinition.builder()
.workflowId("daily-energy-report")
.name("每日能耗日报")
.description("每天早上8点自动生成昨日能耗日报")
.cronExpression("0 0 8 * * ?")
.enabled(true)
.steps(List.of(
new WorkflowStep(0,
"查询昨日数据",
WorkflowStep.StepType.SQL_QUERY,
"""
{
"query": "查询昨日各区域每日能耗数据",
"timeRange": "yesterday"
}
""",
null, "ABORT"
),
new WorkflowStep(1,
"对比分析",
WorkflowStep.StepType.AGENT_CHAT,
"""
{
"prompt": "分析昨日各区域能耗数据,与前日和上周同期对比,指出异常区域"
}
""",
null, "ABORT"
),
new WorkflowStep(2,
"生成趋势图",
WorkflowStep.StepType.CHART_GENERATION,
"""
{
"prompt": "生成昨日各区域能耗对比柱状图",
"chartType": "BAR"
}
""",
null, "SKIP"
),
new WorkflowStep(3,
"生成日报",
WorkflowStep.StepType.AGENT_CHAT,
"""
{
"prompt": "基于以上分析结果,生成一份简洁的能耗日报,包含:概况、异常、建议",
"format": "markdown"
}
""",
null, "ABORT"
),
new WorkflowStep(4,
"发送日报",
WorkflowStep.StepType.NOTIFICATION,
"""
{
"recipients": ["energy-managers"],
"channel": "email",
"subject": "能耗日报 - ${date}"
}
""",
null, "SKIP"
)
))
.build();
}
/**
* 模板2: 实时异常巡检
*
* 执行流程:
* 1. 查询所有区域的当前能耗
* 2. 与阈值配置对比
* 3. 如果有异常:触发深度分析
* 4. 生成告警并通知
*/
public WorkflowDefinition anomalyPatrolTemplate() {
return WorkflowDefinition.builder()
.workflowId("energy-anomaly-patrol")
.name("能耗异常巡检")
.description("每小时自动巡检所有区域能耗异常")
.cronExpression("0 0 * * * ?")
.enabled(true)
.steps(List.of(
new WorkflowStep(0,
"获取实时数据",
WorkflowStep.StepType.SQL_QUERY,
"""
{
"query": "查询所有区域今日实时能耗和告警阈值配置"
}
""",
null, "ABORT"
),
new WorkflowStep(1,
"阈值判断",
WorkflowStep.StepType.CONDITION,
"""
{
"expression": "any_area_exceeds_threshold",
"description": "是否有任何区域的能耗超过告警阈值"
}
""",
null, "ABORT"
),
new WorkflowStep(2,
"异常深度分析",
WorkflowStep.StepType.AGENT_CHAT,
"""
{
"prompt": "对超出阈值的区域进行深度分析:查近7天趋势、可能的原因、建议措施"
}
""",
"step[1].result == 'true'", // 只在有异常时执行
"SKIP"
),
new WorkflowStep(3,
"发送告警",
WorkflowStep.StepType.NOTIFICATION,
"""
{
"recipients": ["area-managers"],
"channel": "sms",
"level": "WARNING"
}
""",
"step[1].result == 'true'",
"SKIP"
)
))
.build();
}
/**
* 模板3: 月度分析报告
*
* 执行流程:
* 1. 查询上月全量数据
* 2. 多维度统计分析(同比、环比、排名、分布)
* 3. 生成多张图表
* 4. 撰写完整分析报告
* 5. 人工审批
* 6. 发送给管理层
*/
public WorkflowDefinition monthlyReportTemplate() {
return WorkflowDefinition.builder()
.workflowId("monthly-energy-report")
.name("月度能耗分析报告")
.description("每月1号自动生成上月能耗分析报告")
.cronExpression("0 0 9 1 * ?")
.enabled(true)
.steps(List.of(
new WorkflowStep(0, "数据采集",
WorkflowStep.StepType.SQL_QUERY,
"""
{"query": "查询上月所有区域的每日能耗、设备能耗、告警记录"}
""", null, "ABORT"),
new WorkflowStep(1, "同比环比分析",
WorkflowStep.StepType.AGENT_CHAT,
"""
{"prompt": "进行同比(与去年同月)和环比(与上月)分析"}
""", null, "ABORT"),
new WorkflowStep(2, "排名分析",
WorkflowStep.StepType.CHART_GENERATION,
"""
{"prompt": "生成各区域月度能耗排名柱状图", "chartType": "HORIZONTAL_BAR"}
""", null, "SKIP"),
new WorkflowStep(3, "趋势分析",
WorkflowStep.StepType.CHART_GENERATION,
"""
{"prompt": "生成近6个月的月度能耗趋势折线图", "chartType": "LINE"}
""", null, "SKIP"),
new WorkflowStep(4, "占比分析",
WorkflowStep.StepType.CHART_GENERATION,
"""
{"prompt": "生成各区域能耗占比饼图", "chartType": "PIE"}
""", null, "SKIP"),
new WorkflowStep(5, "撰写报告",
WorkflowStep.StepType.AGENT_CHAT,
"""
{
"prompt": "综合以上分析,撰写一份完整的月度能耗分析报告。包含:执行摘要、数据分析、异常事件、优化建议、下月预测",
"format": "markdown"
}
""", null, "ABORT"),
new WorkflowStep(6, "人工审批",
WorkflowStep.StepType.HUMAN_APPROVAL,
"""
{"approvers": ["energy-director"], "timeout": "24h"}
""", null, "ABORT"),
new WorkflowStep(7, "发送报告",
WorkflowStep.StepType.NOTIFICATION,
"""
{
"recipients": ["management-team"],
"channel": "email",
"attachPdf": true
}
""", null, "SKIP")
))
.build();
}
}
3.5 Workflow 执行引擎
java
/**
* Workflow 执行引擎
*
* 核心设计:
* 1. 每个 Step 的执行结果写入上下文(Context)
* 2. 后续 Step 可以引用前序 Step 的结果
* 3. 条件 Step 根据表达式决定是否跳过后续 Step
* 4. 每一步都有日志和审计记录
*/
@Service
public class WorkflowExecutor {
private final AgentService agentService;
private final SqlQueryExecutor sqlExecutor;
private final ChartGenerationTool chartTool;
private final NotificationService notificationService;
private final HumanInTheLoopManager hitlManager;
private final WorkflowLogRepository logRepo;
/**
* 执行完整的 Workflow
*
* @param workflow Workflow 定义
* @param triggerContext 触发上下文(如定时触发的日期信息)
* @return 执行结果
*/
public WorkflowExecutionResult execute(
WorkflowDefinition workflow,
Map<String, Object> triggerContext) {
String executionId = UUID.randomUUID().toString();
log.info("开始执行 Workflow: id={}, name={}, execution={}",
workflow.getWorkflowId(), workflow.getName(), executionId);
// 执行上下文:存储每个 Step 的输出
Map<String, Object> context = new HashMap<>(triggerContext);
List<StepExecutionRecord> records = new ArrayList<>();
for (WorkflowStep step : workflow.getSteps()) {
long stepStart = System.currentTimeMillis();
// 条件检查:是否需要执行此 Step
if (step.conditionExpr() != null
&& !evaluateCondition(step.conditionExpr(),
context)) {
records.add(StepExecutionRecord.skipped(
step.stepIndex(), step.stepName()));
log.debug("Step {} 条件不满足,跳过",
step.stepIndex());
continue;
}
try {
// 执行 Step
Object result = executeStep(step, context);
// 将结果写入上下文
context.put(
"step[" + step.stepIndex() + "].result",
result);
long duration = System.currentTimeMillis() - stepStart;
records.add(StepExecutionRecord.success(
step.stepIndex(), step.stepName(),
result, duration));
log.info("Step {} 完成: {} ({}ms)",
step.stepIndex(), step.stepName(), duration);
} catch (Exception e) {
long duration = System.currentTimeMillis() - stepStart;
records.add(StepExecutionRecord.failed(
step.stepIndex(), step.stepName(),
e.getMessage(), duration));
log.error("Step {} 失败: {}",
step.stepIndex(), e.getMessage());
// 根据错误策略决定
switch (step.onError()) {
case "ABORT" -> {
log.error("Workflow 中止执行");
return WorkflowExecutionResult.failed(
executionId, records,
"Step " + step.stepIndex()
+ " 失败: " + e.getMessage());
}
case "SKIP" -> log.warn("Step {} 失败,继续执行",
step.stepIndex());
case "RETRY" -> {
// 重试一次
try {
Object result = executeStep(step, context);
context.put(
"step[" + step.stepIndex() + "].result",
result);
records.add(StepExecutionRecord.success(
step.stepIndex(), step.stepName(),
result, duration));
} catch (Exception retryEx) {
return WorkflowExecutionResult.failed(
executionId, records,
"Step " + step.stepIndex()
+ " 重试仍失败");
}
}
}
}
}
log.info("Workflow 执行完成: {}", executionId);
return WorkflowExecutionResult.success(executionId, records);
}
/**
* 执行单个 Step(按类型分发)
*/
private Object executeStep(WorkflowStep step,
Map<String, Object> context)
throws Exception {
Map<String, Object> config = JsonUtils.parseMap(
step.config());
return switch (step.type()) {
case AGENT_CHAT -> {
String prompt = resolveVariables(
(String) config.get("prompt"), context);
yield agentService.chat(prompt);
}
case SQL_QUERY -> {
String query = resolveVariables(
(String) config.get("query"), context);
yield sqlExecutor.queryByNaturalLanguage(query);
}
case CHART_GENERATION -> {
String prompt = resolveVariables(
(String) config.get("prompt"), context);
yield chartTool.generateChart(prompt);
}
case NOTIFICATION -> {
String[] recipients = ((List<String>)
config.get("recipients")).toArray(String[]::new);
String lastResult = String.valueOf(
context.get("step[" + (step.stepIndex() - 1)
+ "].result"));
yield notificationService.sendBatch(
recipients, lastResult);
}
case CONDITION -> {
String expr = (String) config.get("expression");
yield evaluateCondition(expr, context)
? "true" : "false";
}
case HUMAN_APPROVAL -> {
// 阻塞等待人工审批
yield hitlManager.requestConfirmation(
"workflow-approval",
"Workflow 需要您的审批",
step.stepName(),
PendingConfirmation.ConfirmationType.APPROVAL,
context,
Duration.parse((String)
config.getOrDefault("timeout", "PT1H"))
);
}
};
}
/**
* 变量替换:将 ${variable} 替换为上下文中的值
*
* 例如: "能耗日报 - ${date}" → "能耗日报 - 2026-08-27"
*/
private String resolveVariables(String template,
Map<String, Object> context) {
String result = template;
for (var entry : context.entrySet()) {
result = result.replace(
"${" + entry.getKey() + "}",
String.valueOf(entry.getValue()));
}
return result;
}
}
3.6 XXL-JOB 集成调度
项目使用 XXL-JOB 作为任务调度中心,我们将 Workflow 引擎接入 XXL-JOB:
java
/**
* XXL-JOB 调度任务入口
*
* 每个预置的 Workflow 对应一个 XXL-JOB Job。
* 通过 XXL-JOB 的管理后台可以:
* - 调整 Cron 表达式
* - 手动触发执行
* - 查看执行日志
* - 暂停/恢复任务
*/
@Component
public class AgentWorkflowJobs {
private final WorkflowExecutor executor;
private final WorkflowDefinitionRepository workflowRepo;
/**
* 每日能耗日报 Job
* XXL-JOB 配置: Cron = "0 0 8 * * ?"
*/
@XxlJob("dailyEnergyReportJob")
public void dailyReportJob() {
WorkflowDefinition workflow = workflowRepo
.findById("daily-energy-report");
Map<String, Object> triggerContext = Map.of(
"date", LocalDate.now().minusDays(1).toString(),
"reportType", "daily",
"triggerTime", Instant.now().toString()
);
WorkflowExecutionResult result = executor.execute(
workflow, triggerContext);
if (result.success()) {
XxlJobHelper.log("日报生成成功");
} else {
XxlJobHelper.log("日报生成失败: " + result.error());
XxlJobHelper.handleFail(result.error());
}
}
/**
* 能耗异常巡检 Job
* XXL-JOB 配置: Cron = "0 0 * * * ?"
*/
@XxlJob("energyAnomalyPatrolJob")
public void anomalyPatrolJob() {
WorkflowDefinition workflow = workflowRepo
.findById("energy-anomaly-patrol");
Map<String, Object> triggerContext = Map.of(
"date", LocalDate.now().toString(),
"triggerTime", Instant.now().toString()
);
WorkflowExecutionResult result = executor.execute(
workflow, triggerContext);
if (!result.success()) {
XxlJobHelper.handleFail(result.error());
}
}
/**
* 月度能耗报告 Job
* XXL-JOB 配置: Cron = "0 0 9 1 * ?"
*/
@XxlJob("monthlyEnergyReportJob")
public void monthlyReportJob() {
WorkflowDefinition workflow = workflowRepo
.findById("monthly-energy-report");
YearMonth lastMonth = YearMonth.now().minusMonths(1);
Map<String, Object> triggerContext = Map.of(
"year", lastMonth.getYear(),
"month", lastMonth.getMonthValue(),
"monthName", lastMonth.toString(),
"reportType", "monthly"
);
WorkflowExecutionResult result = executor.execute(
workflow, triggerContext);
if (!result.success()) {
XxlJobHelper.handleFail(result.error());
}
}
/**
* 通用 Workflow 触发 Job(支持自定义 Workflow)
*
* XXL-JOB 参数: workflowId=xxx
* 通过 Job 参数动态指定要执行的 Workflow
*/
@XxlJob("genericWorkflowJob")
public void genericWorkflowJob() {
String workflowId = XxlJobHelper.getJobParam();
if (workflowId == null || workflowId.isBlank()) {
XxlJobHelper.handleFail("缺少 workflowId 参数");
return;
}
WorkflowDefinition workflow = workflowRepo
.findById(workflowId);
if (workflow == null) {
XxlJobHelper.handleFail(
"Workflow 不存在: " + workflowId);
return;
}
WorkflowExecutionResult result = executor.execute(
workflow, Map.of(
"triggerTime", Instant.now().toString()));
if (!result.success()) {
XxlJobHelper.handleFail(result.error());
}
}
}
3.7 Workflow 执行日志与监控
java
/**
* Workflow 执行日志服务
*
* 记录每次 Workflow 执行的完整过程,
* 用于问题排查和执行审计。
*/
@Service
public class WorkflowLogService {
private final WorkflowLogRepository logRepo;
/**
* 保存执行日志
*/
@Transactional
public void saveExecutionLog(String executionId,
String workflowId,
WorkflowExecutionResult result) {
WorkflowExecutionLog log = new WorkflowExecutionLog();
log.setExecutionId(executionId);
log.setWorkflowId(workflowId);
log.setStatus(result.success() ? "SUCCESS" : "FAILED");
log.setTotalSteps(result.records().size());
log.setSucceededSteps((int) result.records().stream()
.filter(StepExecutionRecord::success).count());
log.setFailedSteps((int) result.records().stream()
.filter(r -> !r.success()).count());
log.setSkippedSteps((int) result.records().stream()
.filter(StepExecutionRecord::skipped).count());
log.setTotalDurationMs(result.records().stream()
.mapToLong(StepExecutionRecord::durationMs).sum());
log.setErrorMessage(result.error());
log.setStepDetails(JsonUtils.toJson(result.records()));
log.setCreatedAt(Instant.now());
logRepo.save(log);
}
/**
* 查询执行历史
*/
public List<WorkflowExecutionLog> getExecutionHistory(
String workflowId, int limit) {
return logRepo.findByWorkflowId(
workflowId, PageRequest.of(0, limit));
}
/**
* Workflow 执行统计(用于监控面板)
*/
public WorkflowStats getStats(String workflowId,
LocalDate since) {
List<WorkflowExecutionLog> logs =
logRepo.findByWorkflowIdSince(workflowId, since);
return new WorkflowStats(
logs.size(),
(int) logs.stream()
.filter(l -> "SUCCESS".equals(l.getStatus()))
.count(),
(int) logs.stream()
.filter(l -> "FAILED".equals(l.getStatus()))
.count(),
logs.stream()
.mapToLong(WorkflowExecutionLog::getTotalDurationMs)
.average().orElse(0)
);
}
}
四、数据分析 + Workflow 的完整实战场景
场景:每月1号自动生成上月能耗分析报告
┌─── Workflow 执行链 ──────────────────────────────────────────────┐
│ │
│ [08:00] 定时触发 │
│ │ │
│ Step 0: SQL查询上月全量数据 │
│ → 获取 30天 × 5个区域 = 150条记录 │
│ │ │
│ Step 1: Agent 同比环比分析 │
│ → "8月总能耗 234,500 kWh,同比增长12.3%%,环比下降5.1%%" │
│ │ │
│ Step 2: 生成排名柱状图 (ECharts JSON) │
│ → { title: "各区域月度能耗排名", xAxis: [...], ... } │
│ │ │
│ Step 3: 生成趋势折线图 │
│ → { title: "近6月能耗趋势", series: [...] } │
│ │ │
│ Step 4: 生成占比饼图 │
│ → { title: "各区域能耗占比", series: [...] } │
│ │ │
│ Step 5: Agent 撰写完整报告 │
│ → Markdown 格式的 2000 字分析报告 │
│ │ │
│ Step 6: [HITL] 等待能源主管审批 │
│ → 推送审批请求 → 主管查看报告 → 点击"批准" │
│ │ │
│ Step 7: 发送报告 │
│ → 邮件发送给管理团队,附带3张图表和报告正文 │
│ │
└──────────────────────────────────────────────────────────────────┘
五、十篇文章完整知识图谱
┌──────────────────────────────────────────────────────────────────────┐
│ Java AI Agent 实战系列 · 完整路线图 │
│ │
│ 第一篇 ── 单体 Agent Tool Calling + RAG + Memory │
│ 第二篇 ── Multi-Agent + MCP LangGraph4j + MCP 工具标准化 │
│ 第三篇 ── 评估 + A2A 四维评估 + LLM-Judge + 跨组织通信 │
│ 第四篇 ── 记忆 + 画像 三层记忆 + 画像自动提取 │
│ 第五篇 ── 可观测性 + 成本 链路追踪 + Token 精算 │
│ 第六篇 ── RAG 优化 混合检索 + 重排序 + 知识同步 │
│ 第七篇 ── 安全攻防 Prompt注入防御 + 权限沙箱 │
│ 第八篇 ── 国产模型 + 推理 四大模型 + Plan-Execute + Reflection │
│ 第九篇 ── Text-to-SQL + HITL 自然语言查库 + 人机协作 │
│ 第十篇 ── 可视化 + Workflow 图表生成 + 自动化编排 (本篇) │
│ │
│ 完整能力图谱: │
│ 能构建 → 能协作 → 能评估 → 能记忆 → 能运维 → 能检索 │
│ → 能防御 → 能落地 → 能查数据 → 能可视化 + 能自动化 │
│ │
└──────────────────────────────────────────────────────────────────────┘
368



被折叠的 条评论
为什么被折叠?



