负荷预测和调度是智慧能源系统的两个核心动作。把这两个动作封装在一个Agent里,本质上是让软件拥有一个完整的感知—推理—决策闭环。感知层面,Agent需要连续读取电表、气象站、生产排班等数据;推理层面,它要把历史负荷时间序列转化为未来数小时或数日的概率分布,并理解预测不确定性;决策层面,它依据概率预测、电价曲线和储能/可调负荷的约束,求解出成本最小或可靠性最高的调度方案。和传统SCADA系统的最大区别在于,Agent不依赖人工设参数,而是自己维护特征工程、模型版本和调度规则。

实现这样一个Agent,比较务实的做法是从微服务或单进程多线程开始:数据接入层写一个定时任务,每15分钟拉取一次原始数据并写入时序数据库;特征服务负责清洗和生成滞后特征、滚动统计特征;预测服务定期训练或加载在线模型;调度服务则根据预测输出和当前状态计算控制指令。这个架构并不需要一开始就上复杂的多智能体框架,先让单Agent闭环跑起来,后续再拆分为多个子Agent。
负荷预测:为什么需要概率输出而不是单点结果
如果你做过负荷预测,一定见过MAPE、RMSE这类单点评估指标。它们在模型离线比较时有用,但到了调度环节,只知道一个“平均预测值”反而会误导决策。假设预测明日中午负荷为500kW,实际可能落在450kW到550kW区间。如果调度只按500kW安排储能充电,一旦实际冲到550kW,就会触发需量电费惩罚。更合理的做法是让模型输出分位数,例如P10、P50、P90。P90预测值意味着实际负荷有90%的概率不超过该值。调度系统可以基于P90来做容量预留,基于P10来决定最低购电基准。
工程上实现概率预测,LightGBM的分位数目标函数非常方便。普通回归优化的是平方损失或绝对损失,而分位数回归优化的是pinball loss。下面这段代码演示了如何构造训练数据,并用三个分位数目标分别训练三个LightGBM模型。代码里对类别特征和缺失值做了简单处理,实际项目中还可以加入温度、湿度、星期、小时、节假日等外部特征。
import pandas as pd
import lightgbm as lgb
from sklearn.model_selection import TimeSeriesSplit
def train_quantile_model(df, quantile):
# df包含特征列:hour, dow, temp, load_lag1, load_lag24 ...
features = [c for c in df.columns if c not in ["load", "ds"]]
X = df[features]
y = df["load"]
params = {
"objective": "quantile",
"alpha": quantile,
"learning_rate": 0.05,
"num_leaves": 31,
"min_data_in_leaf": 20,
"verbosity": -1,
}
model = lgb.train(params, lgb.Dataset(X, y), num_boost_round=300)
return model
# 假设已有按时间排序的DataFrame
models = {}
for q in [0.1, 0.5, 0.9]:
models[q] = train_quantile_model(train_df, q)
# 预测未来24小时
future_X = build_future_features(current_state)
forecast = {q: models[q].predict(future_X) for q in models}
三个模型独立训练,推理时对同一批特征输出三条预测曲线。如果发现P10和P90之间的区间过宽,说明模型对当前场景不确定,调度模块就应当增加保守约束。另外,分位数损失并不要求数据服从正态分布,对尖峰负荷和异常波动有更好的鲁棒性。在线更新时,可以使用滚动窗口重训练,或者每天凌晨重新训练一次,把新一天的数据纳入。
特征工程与异常检测
负荷预测的特征工程关键不在于堆数量,而在于保留时间因果性。比如load_lag1是上一时刻负荷,load_lag24是24小时前,load_rolling_mean_24h是过去24小时均值。生成这些滞后特征时要注意,未来特征不能带未来信息,否则离线评估虚高,上线后完全失效。另外,节假日、极端天气、生产班次切换都会造成负荷突变。可以引入一个轻量级异常检测模块,用孤立森林或移动平均残差来判断当前输入是否偏离正常模式。一旦发现异常,Agent可以切换为保守调度策略,避免把预测误差放大到执行层。
智能调度:从预测到可执行指令
调度层拿到概率负荷预测后,需要求解一个带约束的优化问题。典型场景包括:园区内有光伏、储能、可削减负荷,外部有分时电价和需量电费。目标函数是最小化当日总电费,约束包括储能SOC上下限、充放电功率限制、负荷平衡方程等。这个问题可以写成线性规划,用PuLP或OR-Tools求解。下面是PuLP实现的一个简化版本,预测负荷使用P50值,光伏出力和电价都假设已知。
import pulp
def optimal_dispatch(load_forecast, pv_forecast, price, batt_cap=200, batt_pmax=50):
T = len(load_forecast)
prob = pulp.LpProblem("energy_schedule", pulp.LpMinimize)
grid_in = [pulp.LpVariable(f"grid_in_{t}", 0, 300) for t in range(T)]
grid_out = [pulp.LpVariable(f"grid_out_{t}", 0, 0) for t in range(T)] # 不上网
batt_ch = [pulp.LpVariable(f"batt_ch_{t}", 0, batt_pmax) for t in range(T)]
batt_dis = [pulp.LpVariable(f"batt_dis_{t}", 0, batt_pmax) for t in range(T)]
soc = [pulp.LpVariable(f"soc_{t}", 0.1, 0.9) for t in range(T+1)]
soc[0].setInitialValue(0.5)
soc[0].fixValue()
cost = pulp.lpSum(price[t] * grid_in[t] for t in range(T))
prob += cost
for t in range(T):
prob += grid_in[t] + batt_dis[t] + pv_forecast[t] == load_forecast[t] + batt_ch[t]
prob += soc[t+1] == soc[t] + 0.9 * batt_ch[t] / batt_cap - batt_dis[t] / (0.95 * batt_cap)
prob.solve(pulp.PULP_CBC_CMD(msg=0))
return [pulp.value(grid_in[t]) for t in range(T)], [pulp.value(batt_ch[t]) for t in range(T)], [pulp.value(batt_dis[t]) for t in range(T)]
上面的模型只考虑单日优化,如果要考虑连续滚动,可以把它做成模型预测控制(MPC):每次只执行第一个时刻的调度指令,下一时刻用新数据重新求解。这种滚动优化的方式天然适合Agent框架,因为每次决策都基于最新观测和最新预测,避免了整日计划在早晨预测偏差大时被锁死。实际部署时,储能充放电效率不为1,可以用更精确的线性分段近似或直接上混合整数规划。
工程落地的关键点与Agent能力扩展
智慧能源Agent真正难的地方不在算法本身,而在闭环稳定性。预测模型的偏差会传导到调度模块,如果调度只依赖点预测,偏差大的时候系统会频繁充放电,导致设备损耗和额外成本。解决办法之一是把预测概率区间直接编码进优化约束。例如要求P90负荷减去P10负荷的差额必须被储能和电网备用容量覆盖,或者把预测残差的历史分布加到SOC安全边界上。这样,Agent在不确定性高的时段会自动留出更大裕量。
另一个常被忽视的问题是数据漂移。用户生产行为变化、季节转换、电价政策调整都会让模型逐渐失效。Agent需要有一个在线评估机制,每天对比预测值和实际值的分位数覆盖率。如果P90覆盖率远高于90%,说明模型过于保守;如果低于90%,说明低估了波动。覆盖率漂移可以作为触发模型重训练或特征更新的信号。这个评估不需要复杂的监控系统,一个定时任务加几个数据库查询就能完成。
从Agent能力扩展角度,负荷预测和调度只是第一步。后续可以加入价格预测子Agent、设备故障诊断子Agent、需求响应协商子Agent。这些子Agent之间通过消息队列或事件总线通信,各自拥有独立的模型和策略,由一个协调器统一仲裁。比如当负荷预测Agent发现次日中午可能出现尖峰,调度Agent可以提前向需求响应Agent请求可调负荷资源,协商出一个削减量后再重新优化。这种分层设计既保持了单个Agent的简洁性,又让系统具备了处理复杂场景的能力。对于大多数能源项目,先把单Agent闭环做好,再考虑多Agent协同,是更可靠的路径。