
从 “被动救火” 到 “主动预判”:用 NeuralProphet 搭建运维数据 AI 预测体系
作者 | 崔皓
审校 | 重楼
开篇
对多数运维工程师而言,日常工作更像一场 “被动的消防演练”—— 紧盯着监控面板上跳动的 CPU 使用率、内存占用、磁盘容量与网络流量,等数值触达告警阈值时,再匆忙排查问题、扩容资源、处理故障。这种 “盯数据 - 等告警 - 忙救火” 的模式,看似能保障系统稳定,却藏着难以规避的痛点:当业务高峰期突然来临,CPU 使用率骤升导致服务卡顿;当磁盘空间在深夜悄然耗尽,核心业务中断才触发告警;当网络流量突发峰值冲垮带宽,用户投诉已堆积成山……
我们总在事后补救,却很少能提前回答:“1 小时后 CPU 会不会过载?”“3 天后磁盘空间是否够用?”“下周这个时段网络流量会不会突破阈值?” 并非运维工程师不愿主动预判,而是传统运维中,既缺乏能捕捉系统指标时间规律的工具,也没有低成本落地的预测方法 —— 直到 NeuralProphet 的出现,让 “提前预测运维数据” 从复杂的 AI 课题,变成了像搭乐高积木一样可落地的实践。
想象一下用乐高积木搭建一个模型:每一块积木都有特定的形状和功能,有的负责搭建底座(对应数据的基础趋势),有的负责拼接循环结构(对应指标的周期性波动),有的负责填补细节(对应突发异常的修正),将这些积木按逻辑组合,就能从零散部件变成完整的、可复用的模型。NeuralProphet 处理运维时间序列数据(如 CPU 使用率、内存波动)的方式,与此几乎完全一致。
它本质上是 Facebook 经典预测模型 Prophet 的 “升级版”——Prophet 曾因将复杂时间序列拆解为 “趋势、周期、节假日” 等可解释模块而风靡运维圈,但在面对运维数据的 “短期高频波动”(如每 10 分钟一次的 CPU 骤升)时,常因缺乏 “局部上下文” 建模能力导致预测偏差;同时,其基于 Stan 的后端架构,也让普通运维工程师难以根据实际场景调整参数。
而 NeuralProphet 的诞生,正是为了解决这些痛点。它完整保留了 Prophet “模块化拆解数据” 的核心优势 —— 比如将 CPU 使用率数据拆成 “长期增长趋势(业务扩容导致的使用率缓步上升)”“日周期波动(早 9 晚 6 的办公高峰)”“周周期波动(工作日与周末的负载差异)” 等独立模块,让非 AI 背景的运维人员也能看懂预测逻辑;同时,它通过引入 “自回归组件”(能捕捉近 1 小时内的短期波动)和 “PyTorch 后端”(支持灵活调参与轻量化部署),完美弥补了 Prophet 的短板,既能精准预测下 10 分钟的 CPU 峰值,也能适配从边缘服务器到云集群的不同运维场景。
上面说了这么多,其实就是一句话:NeuralProphet 非常好用,我们可以用NeuralProphet 模型来预测系统指标(CPU、内存、磁盘、网络)。
那么如何用 NeuralProphet 这个模型呢, 我的思路也比较简单粗暴, 如下图所示。
为了大家理解方便,我们举个简单的例子,我们使用“ 8 月份的数据”(CPU 等)来预测“下一个小时的数据”,然后把“8 月份的数据”+“下一个小时的数据”预测“再下一个小时的数据”。依次类推,有点俄罗斯套娃的感觉,实际情况也是如此。CPU 的使用率数据会在系统中不断产生,有了历史数据可以帮助我们预测下个时间段(一小时)的数据,同时下个时间段的数据也会成为历史数据,为后面数据的预测发光发热。
好了,有了目标接下来就好办了,整个实战案例的思路如下图所示。
首先,生成历史数据,也就是真实的系统指标数据,模拟真实数据分为 CPU、内存、磁盘、网络,每 10 分钟生成一条数据。
接着,利用已经生成的历史数据训练模型,模型就用NeuralProphet,生成的模型保存备用。
然后,利用训练好的模型预测下一个小时的系统指标数据,例如:现在时间是 9 月 1 日的 00:00:00, 我们要预测 从 00:00:00 到 01:00:00 的 CPU 使用率,由于预测数据也是 10 分钟一条,所以会生成 6 条 CPU 使用率的数据。
最终的效果如下,蓝色的线条为实际数据,橙色线条为预测值,红色的文字为误差比,也就是实际值与预测值之间存在的差距。
NeuralProphet 的核心理念
好了,说明了目的(系统数据预测)以及方法(利用 NeuralProphet 预测)之后,需要介绍主角NeuralProphet。
NeuralProphet 的核心理念非常直观:最终的预测值为“独立组件”预测值的总和。简单来说,多个“独立组件”等同于从多维度思考。把不同维度预测数据的模块得到的结果加起来就是全面的预测。你可以想象有一个复杂问题,让多个不同的专家一起思考提出方案,然后整合他们的方案得到最终方案。
但是这些预测模型或者说思考方案总得通过一个公式表示一下, 要不我们也不好描述,于是就有了下面的公式。:
ŷt = T(t) + S(t) + E(t) + F(t) + A(t) + L(t)
看到公式这么复杂,我是有点懵的,用一个列表表示,通过“说人话”对其进行解释。
组件符号 | 简单描述 |
T(t) | 趋势 (Trend):数据随时间变化的长期基础走向(例如,增长或下降)。 |
S(t) | 季节性 (Seasonality):在固定周期内重复出现的模式(例如,每周、每年的周期)。 |
E(t) | 事件 (Events):特殊日期(如节假日)对数据产生的短期影响。 |
F(t) | 未来回归量 (Future Regressors):其未来值已知的外部变量(例如,已计划的营销活动)。 |
A(t) | 自回归 (Auto-Regression):近期历史观测值对未来值的直接影响。 |
L(t) | 滞后回归量 (Lagged Regressors):其未来值未知的外部变量(例如,昨天的天气)。 |
下面再花一点点篇幅对各个组件进行介绍,特别是自回归。如果对这部分不感兴趣或者已经有所了解的同学,可以自行跳到实战环节,从“安装依赖”开始看。
趋势 (T(t)):整体走向
趋势组件捕捉的是时间序列总体、长期的发展方向。为了使趋势线能够适应现实世界中的变化,NeuralProphet 引入了变化点 (changepoints) 的概念。假设你在开车,正在直线行驶,路上的一个转弯,你的方向(或速度)就要根据这个转弯发生变化,这个转弯就是变化点 (changepoints) 。
NeuralProphet 将趋势建模为一个“分段线性”序列。数据变化的趋势基本就是一条直线,直线可以在变化点 (changepoints)改变方向。让模型识别在数据中反复出现的变化模式,从而知道在变化点转弯--改变方向。
线性
分段线性
季节性 (S(t)):周期性节律
季节性是指在固定时期内发生的可预测、重复的模式,重点是可预测和重复。比如:劳动节、儿童节、春节人们会更多出游购物,零售店的销售额通常在周末达到高峰。冰淇淋的销量在夏季会显著增加,等等。
NeuralProphet 利用傅里叶项(本质上是正弦和余弦函数的组合)使模型能够同时捕捉多种季节性,例如,一个模型可以同时识别出数据中的每日、每周和年度模式。
此外,季节性还具备如下特性:
• 加法性 (Additive) 季节性:一家冰淇淋店每到夏天,销量总是在平日基础上固定增加50份,这个增量不受公司规模变化的影响。
• 乘法性 (Multiplicative) 季节性:一家电商公司每逢节假日的销售额,总是能达到当年平均水平的两倍,因此公司规模越大,节假日带来的销量增长就越多。
自回归 (A(t)):近期历史的影响
自回归 (Auto-Regression, AR) 在短期预测方面表现突出。在介绍自回归之前先说说什么是回归,回归是通过外部变量(如促销活动)与目标指标(CPU 使用率)的关系,量化外部因素对目标的影响,比如 “电商平台的促销能让服务器的 CPU 负载提升 15%”;而自回归是回归的特殊形式,不依赖外部变量,仅通过目标指标自身的历史数据(如过去 1 小时的 CPU 使用率)预测未来值,核心是捕捉 “历史惯性”—— 比如 “上 10 分钟 CPU 使用率超 80%,下 10 分钟大概率维持高负载”,也就是 “同一变量过去影响未来” 。
所以,自回归的核心思想:“最近发生的事情是对接下来会发生的事情产生影响。” 模型会回顾过去特定数量的数据点,这些数据点被称为“滞后项 (lags)”。例如,如果我们使用 5 个滞后项,模型就会查看最近的 5 个观测值来帮助预测下一个值。
好了到这里,可能有人感觉趋势和自回归有点像,都是通过历史预测未来,所以我们停下来,给他们做一个小区分。趋势是运维指标长期的宏观走向(比如业务增长导致 CPU 使用率每月稳步上升 2%),自回归则是指标短期的实时动态(比如上 10 分钟 CPU 突升 15%,下 10 分钟大概率维持高负载)。
NeuralProphet 的 AR 模块基于一个名为 AR-Net 的架构,它可以配置为两种模式,兼具简单性和强大的功能:
• 线性 AR:这是一种简单直接的方法。它假设每个过去的值都对预测有一个直接的、加权的线性影响。这种方式非常容易解释,你可以清楚地看到每个滞后项对预测的贡献大小。
• 深度 AR:这是一种更高级的方法,它使用一个小型神经网络(即 AR-Net)来发现过去值与未来预测之间复杂的、非线性的关系。这通常可以提高预测的准确性,但其内部工作机制不如线性 AR 那样易于直接解读。
回归量与事件:外部影响因素
回归量是帮助预测目标的外部变量,可以理解为“外援”。例如:“要预测冰淇淋销量(目标),了解每日温度(“外援”=回归量)会非常有帮助。”
NeuralProphet 可以处理多种类型的外部影响因素,下表对它们进行了比较:
类型 | 关键特征 | 简单示例 |
滞后回归量 | 历史值已知,未来值未知。功能上与自回归模块相同,但使用外部变量作为输入。 | 利用昨天的天气数据来预测今天的能源消耗。 |
未来回归量 | 历史和未来值都已知。 | 在销售预测中包含已提前计划好的市场营销活动的日期。 |
事件与节假日 | 特殊的一次性或重复性日期。它们被建模为二元变量,并且可以自动包含特定国家的节假日。 | 对每年“黑色星期五”购物节期间出现的销售高峰进行建模。 |
上面已经了解了所有的构建模块,NeuralProphet模型就是将每个独立组件生成的预测值(趋势 + 季节性 + 自回归 + 回归量等)全部加在一起,得到最终的综合预测值,可以理解为“汇总专家意见”。这种方法的最大好处是可解释性 (explainability)。由于每个组件可以独立建模,用户可以单独绘制每个组件的图表,从而确切地了解是哪个因素在影响最终预测结果。
安装依赖
好了理论的部分已经讲完,如果没有听懂也不要紧,直接实操帮助理解,如果还是没有听懂,那就回到开头再看一遍。接下来,我们需要安装必要的环境,先确认已安装conda,conda 是老演员了,安装的链接放到这里,https://anaconda.org/anaconda/conda。
为了本次实践创建虚拟环境,如下:
conda create -n forecast pythnotallow=3.12
创建完毕之后,启用虚拟环境,如下:
conda activate forecast
然后再虚拟环境中,安装相关的依赖包,如下:
# 基于 PyTorch 的时间序列预测工具,用来训练模型并进行预测
pip install neuralprophet
# 用于处理Excel的库,演示用数据保存在Excel
pip install openpyxl
# 用于构建网页应用界面
pip install streamlit
生成“历史数据”
安装完了环境,我们开始造一些数据,方便后面测试,在造数据之前先回顾一下我们要做的事情,如下图所示。
一般而言,我们在预测未来的系统指标的时候,需要利用历史数据,未来演示的需要我们用代码生成所谓的“历史数据”。在真实的场景中,大家可以按照我后面说的数据格式,将系统指标(CPU、内存、磁盘、网络)的信息填入到 XSL 中,为后面训练预测模型做好准备。在forecast目录中新建python脚本。
cd /forecast
# 直接编辑,在保存时会生成文件
vim history_data.py
import numpy as np
import pandas as pd
# ============================
# 参数配置
# ============================
start_time = pd.Timestamp("2025-08-01 00:00:00")
end_time = pd.Timestamp("2025-08-31 23:59:59")
rng = pd.date_range(start=start_time, end=end_time, freq="10min")
periods = len(rng)
np.random.seed(42) # 保证可复现
# ============================
# 辅助函数:日内波动模式
# ============================
def daily_pattern(minutes, amplitude=1.0, phase_shift=0):
radians = (minutes / (24 * 60)) * 2 * np.pi
return 0.5 + 0.5 * np.sin(radians - 0.3 + phase_shift)
minutes_from_start = (np.arange(periods) * 10) % (24 * 60)
weekday = rng.weekday # 0=周一
hours = rng.hour
# ============================
# CPU
# ============================
cpu_base = 10 + 5 * (np.random.rand(periods) - 0.5)
cpu_daily = 25 * daily_pattern(minutes_from_start)
cpu = cpu_base + cpu_daily * np.where(weekday < 5, 1.0, 0.7)
spike_mask = (np.random.rand(periods) < 0.005) # 偶发高峰
cpu[spike_mask] += np.random.uniform(20, 60, size=spike_mask.sum())
# ============================
# 内存
# ============================
mem_base = 50 + 10 * np.sin((np.arange(periods) / periods) * 2 * np.pi)
mem_noise = np.random.normal(0, 2.0, size=periods)
mem_weekday_effect = np.where(weekday < 5, 1.02, 0.99)
memory_used = mem_base * mem_weekday_effect + mem_noise
mem_spike_mask = (np.random.rand(periods) < 0.002)
memory_used[mem_spike_mask] += np.random.uniform(5, 20, size=mem_spike_mask.sum())
memory_used = np.clip(memory_used, 10, 99.5)
# ============================
# 磁盘使用率
# ============================
disk_start, disk_end = 60.0, 62.0
disk_trend = np.linspace(disk_start, disk_end, periods)
disk_noise = np.random.normal(0, 0.05, size=periods)
disk_used = np.clip(disk_trend + disk_noise, 20, 99.9)
# ============================
# 网络流量
# ============================
net_in_base = 5 * daily_pattern(minutes_from_start) + 1.0
net_out_base = 3 * daily_pattern(minutes_from_start) + 0.5
lunch_mask = ((hours >= 11) & (hours <= 13))
net_in = net_in_base * (1.2 * np.where(weekday < 5, 1, 0.9))
net_out = net_out_base * (1.15 * np.where(weekday < 5, 1, 0.9))
net_in[lunch_mask] *= 1.3
net_out[lunch_mask] *= 1.2
burst_mask = (np.random.rand(periods) < 0.003)
net_in[burst_mask] += np.random.uniform(20, 200, size=burst_mask.sum())
net_out[burst_mask] += np.random.uniform(5, 80, size=burst_mask.sum())
# ============================
# 组合 DataFrame
# ============================
df_aug = pd.DataFrame({
"ds": rng,
"cpu": np.round(np.clip(cpu, 0, 100), 2),
"memory": np.round(memory_used, 2),
"disk": np.round(disk_used, 2),
"net_in": np.round(net_in, 3),
"net_out": np.round(net_out, 3)
})
# ============================
# 每小时统计(平均、最大、最小)
# ============================
df_hourly = df_aug.set_index("ds").resample("1h").agg(
{
"cpu": ["mean", "max", "min"],
"memory": ["mean", "max", "min"],
"disk": ["mean", "max", "min"],
"net_in": ["mean", "max", "min"],
"net_out": ["mean", "max", "min"],
}
)
df_hourly.columns = ["_".join(col).strip() for col in df_hourly.columns.values]
df_hourly.reset_index(inplace=True)
# ============================
# 为每个指标创建单独的sheet(ds:日期,y:值)
# ============================
# CPU sheet
df_cpu = pd.DataFrame({
"ds": rng,
"y": cpu
})
# Memory sheet
df_memory = pd.DataFrame({
"ds": rng,
"y": memory_used
})
# Disk sheet
df_disk = pd.DataFrame({
"ds": rng,
"y": disk_used
})
# Network In sheet
df_net_in = pd.DataFrame({
"ds": rng,
"y": net_in
})
# Network Out sheet
df_net_out = pd.DataFrame({
"ds": rng,
"y": net_out
})
# ============================
# 导出到 Excel
# ============================
output_file = "server_metrics_2025_08.xlsx"
with pd.ExcelWriter(output_file, engine="openpyxl") as writer:
df_aug.to_excel(writer, index=False, sheet_name="10min_data")
df_hourly.to_excel(writer, index=False, sheet_name="hourly_stats")
# 添加各个指标的单独sheet
df_cpu.to_excel(writer, index=False, sheet_name="cpu")
df_memory.to_excel(writer, index=False, sheet_name="memory")
df_disk.to_excel(writer, index=False, sheet_name="disk")
df_net_in.to_excel(writer, index=False, sheet_name="net_in")
df_net_out.to_excel(writer, index=False, sheet_name="net_out")
print(f"✅ 数据已生成并导出到 {output_file}")
代码详细内容如下:
- 参数配置:设定 2025 年 8 月 1 日至 31 日的时间范围,按 10 分钟间隔生成时间序列,固定随机种子确保数据可复现。
- 函数定义:编写日内波动模式函数,通过三角函数计算,模拟指标一天内随分钟变化的周期性波动规律。
- 时间计算:算出每个时间点对应的起始分钟数、星期几和小时数,为后续指标模拟提供时间维度数据。
- CPU 生成:先确定 CPU 基础值范围,叠加日内波动,按工作日和周末调整幅度,再用随机掩码添加偶发高峰,限制值在 0-100%。
- 内存生成:以正弦曲线为内存基础趋势,叠加随机噪声,结合工作日差异调整,加偶发峰值后限制在 10%-99.5%。
- 磁盘生成:设定磁盘使用率从 60.0 到 62.0 的线性增长趋势,叠加小噪声,将值限制在 20%-99.9%。
- 网络生成:为网络流入、流出设定日内波动基础值,按工作日和午餐时段调整,用随机掩码加突发流量,确保值非负。
- 组合数据:将所有指标数据与时间序列组合,生成含完整指标的 DataFrame,保留指定小数位数。
- 小时统计:对原始 10 分钟数据按小时重采样,计算各指标每小时的均值、最大值、最小值,整理成统计 DataFrame。
- 单表创建:为 CPU、内存等 5 个指标各建单独 DataFrame,仅含 “时间(ds)” 和 “指标值(y)” 两列。
- 导出 Excel:用 ExcelWriter 将所有数据导出到 “server_metrics_2025_08.xlsx”,包含多类数据表。
- 完成提示:打印数据已生成并导出到指定文件的提示信息。
执行如下命令,命令完成之后在python脚本所在目录生成历史数据的excel文件。
python history_data.py
如下图所示,会在 history_data.py 相同的目录下生成“server_metrics_2025_08.xlsx”的文件。
如下图所示,在生成的excel中的 “ds”会显示采集时间,我们按照 10 分钟一次采集数据,同时在“y”列显示的是 CPU 的使用率。
在上图的下部的“sheet”中,列出了不同维度的数据,按从左到右的顺序分别是:
- 10min_data:包含所有指标的数据
- hourly_stats:所有指标按小时统计数据
- cpu:cpu使用率
- memory:内存使用率
- disk:磁盘使用率
- net_in:网络入流量
- net_out:网络出流量
训练模型
我们通过代码的方式生成了系统指标的历史数据,接着会根据这些数据训练模型。
在forecast目录中新建python脚本,如下:
cd /forecast
# 直接编辑,在保存时会生成文件
vim train_model.py
下面代码用来训练历史数据,这里只选择 cpu 进行训练, 其他的 sheet 中存放了内存、磁盘和网络信息,在代码中已经 remark 上了,有兴趣的同学可以自行训练。
from neuralprophet import NeuralProphet
import pandas as pd
import warnings
from neuralprophet import save
# 忽略警告
warnings.filterwarnings('ignore')
# 1. 读取Excel数据
excel_path = 'server_metrics_2025_08.xlsx'
sheet_name = 'cpu'
# sheet_name = 'memory'
# sheet_name = 'disk'
# sheet_name = 'net_in'
# sheet_name = 'net_out'
df = pd.read_excel(excel_path, sheet_name=sheet_name)
# 只保留 ds 和 y 列
df = df[['ds', 'y']]
# 2. 初始化并训练模型
m = NeuralProphet(
changepoints_range=0.8, # 只在前80%历史数据中寻找变点,避免对最新数据过拟合
trend_reg=1, # 适度正则化,防止趋势过度拟合
seasonality_reg=0.5, # 适度正则化,防止季节性过拟合
n_lags=144, # n_lags越大,可用训练样本越少
ar_reg=0.7, # 轻微正则化自回归系数,防止过拟合
n_forecasts=6, # 预测的步数,10分数间隔的数据生成6条
collect_metrics={ "MAE": "MeanAbsoluteError", "MAPE": "MeanAbsolutePercentageError" },
)
# 3. 分割训练集和验证集
df_train, df_val = m.split_df(df, valid_p=0.2)
# 4. 训练模型
m.fit(df_train, validation_df=df_val, freq="10min")
# 5. 保存模型到本地目录
save(m, sheet_name + ".np")
这里稍微对代码部分做解释;
- 导入工具:引入 NeuralProphet 模型、pandas 数据处理库、警告忽略工具及模型保存函数。
- 忽略警告:关闭程序运行中的警告提示,使输出更简洁。
- 读取数据:从 “server_metrics_2025_08.xlsx”Excel 文件中读取指定工作表(默认 cpu,可切换为 memory、disk 等)的数据。
- 数据筛选:仅保留数据中的 “ds”(时间)和 “y”(指标值)两列,符合 NeuralProphet 模型要求的输入格式。
- 模型初始化:创建 NeuralProphet 模型实例,配置核心参数 —— 在前 80% 数据中寻找趋势变点,对趋势和季节性特征进行适度正则化,设置 144 个历史滞后项捕捉自回归效应,轻微正则化自回归系数,指定预测 6 个时间步(10 分钟间隔),并收集 MAE 和 MAPE 评估指标。
- 数据分割:将数据集按 8:2 比例拆分为训练集(df_train)和验证集(df_val),用于模型训练和性能验证。
- 模型训练:使用训练集训练模型,同时用验证集评估效果,指定数据时间间隔为 10 分钟。
- 保存模型:将训练好的模型以 “指标名称.np”(如 cpu.np)的格式保存到本地,便于后续加载使用。
预测系统指标
上面的代码会生成一个 np 为后缀的文件,.np 文件是一个模型检查点文件,它是一个包含了模型完整状态的快照,其核心内容是模型在训练后学到的所有参数(权重),以及恢复训练所必需的模型配置和优化器状态。前面通过命令pip install neuralprophet 安装neuralprophet 的基础模型,此时只需要通过历史数据训练成.np 的模型检查点文件,再将这个文件与之前安装的neuralprophet 基础模型进行合并,就是完整的模型了,再用这个完整的模型预测外来数据。只不过,开发者看到的是.np 的文件,而不用关心模型是如何合并以及预测的,这一系列操作都是neuralprophet 框架帮我完成了。接下来,我们来生成一个 UI 界面,对外来数据进行预测,并且展示对应内容。
在forecast目录中新建python 代码,代码中的 UI 界面 streamlit 生成。
cd /forecast
# 直接编辑,在保存时会生成文件
vim app.py
由于篇幅关系,我们只展示部分核心代码,如下:
import streamlit as st
import pandas as pd
import plotly.graph_objects as go
from neuralprophet import load
import warnings
import numpy as np
from datetime import datetime, timedelta
import sys
def generate_forecast(df, sheet_name):
"""
生成预测的独立函数
按步数遍历,步数1取yhat1,步数2取yhat2...
所有预测值都放到yhat1列中
"""
try:
# 在预测函数中仅保留'ds'和'y'列用于模型训练
model_df = df[['ds', 'y']].copy()
# 加载预训练模型
model_path = f"{sheet_name}.np"
model = load(model_path)
# 生成预测
future_df = model.make_future_dataframe(model_df, n_historic_predictions=False)
forecast_result = model.predict(future_df, decompose=False)
print(forecast_result)
# 获取最后一个数据点
last_data_point = model_df['ds'].max()
# 处理预测结果
forecast_long = []
last_actual_idx = forecast_result['y'].last_valid_index() if 'y' in forecast_result.columns else -1
start_idx = last_actual_idx + 1 if last_actual_idx + 1 < len(forecast_result) else 0
# 生成预测步骤,按步数取对应yhat,统一放到yhat1列
for step in range(1, FORECAST_STEPS + 1):
yhat_col = f'yhat{step}' # 步数1取yhat1,步数2取yhat2...
current_idx = start_idx + (step - 1)
if current_idx >= len(forecast_result):
current_idx = len(forecast_result) - 1
st.warning(f"预测数据不足,使用最后一条数据预测第{step}步")
if current_idx < len(forecast_result):
date = forecast_result.loc[current_idx, 'ds']
if pd.notna(date) and yhat_col in forecast_result.columns and pd.notna(forecast_result.loc[current_idx, yhat_col]):
forecast_time = last_data_point + step * FORCE_TIME_INTERVAL
# 所有预测值都放到yhat1列
forecast_entry = {
'ds': forecast_time,
'yhat1': forecast_result.loc[current_idx, yhat_col],
'step': step
}
forecast_long.append(forecast_entry)
else:
st.warning(f"第{step}步预测值缺失,使用历史平均值")
forecast_time = last_data_point + step * FORCE_TIME_INTERVAL
avg_value = model_df['y'].mean()
forecast_entry = {
'ds': forecast_time,
'yhat1': avg_value,
'step': step
}
forecast_long.append(forecast_entry)
else:
forecast_time = last_data_point + step * FORCE_TIME_INTERVAL
avg_value = model_df['y'].mean()
forecast_entry = {
'ds': forecast_time,
'yhat1': avg_value,
'step': step
}
forecast_long.append(forecast_entry)
# 转换为DataFrame
forecast_long = pd.DataFrame(forecast_long)
forecast_merged = forecast_long.merge(df[['ds', 'y']], on='ds', how='left')
return forecast_merged
except Exception as e:
st.error(f"预测出错: {str(e)}")
st.info("可能的原因:模型文件不存在、数据格式不正确或模型训练不完整")
return None
代码的主要内容如下:
- 数据筛选:从输入的历史数据中,仅保留 “ds(时间列)” 和 “y(指标值列)” 并复制,形成模型可识别的输入数据(model_df)。
- 加载模型:cpu.np,通过 NeuralProphet 的 load 函数加载本地预训练好的模型。
- 生成预测数据:调用模型的 make_future_dataframe 方法,基于历史数据创建未来预测所需的时间框架(关闭 “包含历史预测” 功能,仅生成未来数据);再用 predict 方法生成预测结果,关闭 “成分分解” 功能(不拆分趋势、季节性等组件),并打印预测结果供调试查看。
- 确定时间基准:提取历史数据中最新的时间点(last_data_point),作为后续计算 “未来预测时间” 的基准。
- 初始化存储列表:创建空列表 forecast_long,用于暂存每一步的预测结果;同时找到历史数据在预测结果中的最后有效索引(last_actual_idx),确定未来预测的起始位置(start_idx)。
- 循环生成预测:按设定的预测步数(FORECAST_STEPS)循环,每一步对应一个预测列(如第 1 步取 yhat1、第 2 步取 yhat2):
a. 计算当前步在预测结果中的索引(current_idx),若索引超出预测结果范围,自动调整为最后一条数据的索引,并通过 Streamlit 给出 “预测数据不足” 的警告;
b. 若索引有效,先获取对应时间,若 “时间非空、预测列存在、预测值非空”,则以 “历史最新时间 + 当前步数 × 时间间隔(FORCE_TIME_INTERVAL)” 为预测时间,将 “时间、该步预测值(存入 yhat1)、步数” 组成字典存入列表;
c. 若预测值缺失或时间无效,通过 Streamlit 给出 “预测值缺失” 的警告,用历史指标的平均值作为预测值,同样按上述格式存入列表;
d. 若索引无效(极端情况),直接用历史平均值作为预测值,生成对应时间和数据存入列表。
然后,通过如下命令执行应用查看结果。
streamlit run app.py
如下图所示,在弹出的页面中选择“cpu”,以及展示的开始和结束时间,然后点击“生成预测”。在右边的界面中会展示预测的曲线(橙色),这就是我们利用 8 月份 CPU 使用率的模型预测出来,9 月 1 日 00:00 到 02:50 的数据,每 10 分钟产生一个数据点。
如果把鼠标放到任何一个数据点上,可以看到如下图所示的红色字体“误差比”,描述了预测值与实际值之间的误差情况。
接入LLM进行对话
好,到这里,数据预测的功能完成了,接着来到最后一步,我们可以与历史数据进行“对话”,让大模型基于数据给出运维存在的风险,并提出建议。
接下来,还是开始实战操作,先安装一些依赖。
# python-dotenv:可以使用load_dotenv从同目录的.env文件中读取配置
pip install python-dotenv openai
由于要使用 DeepSeek 模型,所以要创建.env文件,用来保存 DeepSeek API Key。
DEEPSEEK_API_KEY=sk-*********
新建config.py文件,用于管理配置参数,包括:项目根目录(使用 Path 获取当前文件所在目录)、Excel数据文件的路径(指向服务器指标数据文件)、需要监控的服务器资源类型列表(CPU、内存、磁盘、网络入流量和出流量),以及这些资源类型的中文显示映射(便于用户界面展示),同时还定义了日期时间的标准格式化模式。
import os
from pathlib import Path
# 项目根目录
ROOT_DIR = Path(__file__).parent
# Excel文件路径
EXCEL_PATH = os.path.join(ROOT_DIR, 'server_metrics_2025_08.xlsx')
# 资源类型列表
RESOURCE_TYPES = ['cpu', 'memory', 'disk', 'net_in', 'net_out']
# 资源类型中文映射
RESOURCE_TYPE_MAP = {
'cpu': 'CPU使用率',
'memory': '内存使用率',
'disk': '磁盘使用率',
'net_in': '网络入流量',
'net_out': '网络出流量'
}
# 时间格式
DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_FORMAT = '%Y-%m-%d'
新建logger.py文件,这是一个日志配置模块,它使用 Python 的 logging 库来设置应用程序的日志记录功能。该模块首先在项目目录下创建 logs 文件夹,然后配置日志记录的格式(包含时间戳、日志级别和消息内容)和日期格式。它会根据当前日期创建日志文件(格式如 chat_app_20250919.log),并设置日志记录同时输出到文件和控制台。日志级别被设置为 INFO,这意味着它会记录信息性消息、警告和错误。最后创建了一个名为 logger 的日志记录器实例,供其他模块使用来记录应用程序的运行状态和调试信息。
import logging
import os
from datetime import datetime
from pathlib import Path
# 创建logs目录
log_dir = Path(__file__).parent / 'logs'
log_dir.mkdir(exist_ok=True)
# 配置日志格式
log_format = '%(asctime)s - %(levelname)s - %(message)s'
date_format = '%Y-%m-%d %H:%M:%S'
# 创建日志文件名(包含日期)
log_file = log_dir / f'chat_app_{datetime.now().strftime("%Y%m%d")}.log'
# 配置日志
logging.basicConfig(
level=logging.INFO,
format=log_format,
datefmt=date_format,
handlers=[
logging.FileHandler(log_file, encoding='utf-8'),
logging.StreamHandler()
]
)
# 创建logger实例
logger = logging.getLogger(__name__)
新建chat_app.py文件如下,代码中会引用config.py和logger.py。 由于篇幅原因我们展示核心函数如下:
def initialize_openai_client():
"""初始化OpenAI客户端
功能:
- 从环境变量获取API密钥
- 初始化DeepSeek API客户端
- 配置API基础URL
返回:
- OpenAI客户端实例 或 None(如果初始化失败)
"""
# 从环境变量获取API密钥
api_key = os.getenv('DEEPSEEK_API_KEY')
if not api_key:
st.error("未找到DEEPSEEK_API_KEY环境变量")
return None
# 创建并返回配置好的客户端实例
return OpenAI(
api_key=api_key,
base_url="https://api.deepseek.com/v1" # DeepSeek API的基础URL
)
def generate_prompt(start_dt, end_dt, resource_type=None):
"""生成提示词:支持时间点或时间范围。
- 当 start_dt == end_dt 时,生成“在 X 时 ...”的时间点提示
- 当不相等时,生成“在 X 至 Y 这段时间 ...”的时间范围提示
"""
if not start_dt or not end_dt:
return ""
start_str = start_dt.strftime("%Y-%m-%d %H:%M:%S")
end_str = end_dt.strftime("%Y-%m-%d %H:%M:%S")
resource_name = RESOURCE_TYPE_MAP.get(resource_type, resource_type) if resource_type else None
if start_dt == end_dt:
if resource_name:
return f"在{start_str} 时服务器的{resource_name}情况如何?请进行分析,指出异常与风险。"
return f"在{start_str} 时服务器的整体运行情况如何?请进行分析,指出异常与风险。"
else:
if resource_name:
return f"在{start_str} 至 {end_str} 这段时间内,服务器的{resource_name}情况如何?请进行分析,指出异常与风险。"
return f"在{start_str} 至 {end_str} 这段时间内,服务器的整体运行情况如何?请进行分析,指出异常与风险。"
def filter_data(df, date, time=None, resource_type=None, end_date=None, end_time=None):
"""根据选择的日期时间范围和资源类型筛选数据"""
if df is None:
logger.warning("输入的DataFrame为空")
return None
logger.info(f"开始筛选数据,起始日期:{date}, 起始时间:{time or '00:00:00'}")
logger.info(f"结束日期:{end_date or date}, 结束时间:{end_time or '23:59:59'}")
logger.info(f"资源类型:{resource_type or '所有资源'}")
# 构建开始日期时间
start_datetime = pd.Timestamp.combine(date, time) if time else pd.Timestamp.combine(date, pd.Timestamp.min.time())
# 构建结束日期时间
if end_date and end_time:
end_datetime = pd.Timestamp.combine(end_date, end_time)
elif end_date:
end_datetime = pd.Timestamp.combine(end_date, pd.Timestamp.max.time())
else:
end_datetime = pd.Timestamp.combine(date, pd.Timestamp.max.time())
logger.info(f"筛选时间范围:{start_datetime} 至 {end_datetime}")
# 筛选时间范围内的数据
filtered_df = df[(df['ds'] >= start_datetime) & (df['ds'] <= end_datetime)]
# 记录筛选结果
logger.info(f"时间范围内的数据条数:{len(filtered_df)}")
# 如果选择了资源类型,只返回相关列
if resource_type and resource_type in df.columns:
filtered_df = filtered_df[['ds', resource_type]]
logger.info(f"已筛选资源类型:{resource_type}")
# 记录最终结果
logger.info(f"最终筛选结果数据条数:{len(filtered_df)}")
if not filtered_df.empty:
logger.info(f"数据时间范围:{filtered_df['ds'].min()} 至 {filtered_df['ds'].max()}")
if resource_type:
stats = filtered_df[resource_type].describe()
logger.info(f"资源统计信息:\n{stats}")
else:
logger.warning("筛选结果为空")
return filtered_df
其中initialize_openai_client函数连接系统与 DeepSeek 大模型的 “桥梁”,创建大模型调用客户端。从环境变量中读取DEEPSEEK_API_KEY,配置 DeepSeek API 的 URL(https://api.deepseek.com/v1),生成并返回可直接用于调用的客户端实例。
执行如下命令,运行对话应用。
streamlit run chat_app.py
如下图所示,可以选择一段时间以及对应的系统指标(CPU 使用率),此时会自动填写提示词,让 DeepSeek 协助分析异常和风险,然后点击“开始分析”按钮。在右侧的对话框中就可以看到“CPU 使用率分析报告”的详细信息了。
作者介绍
崔皓,51CTO社区编辑,资深架构师,拥有18年的软件开发和架构经验,10年分布式架构经验。
参考论文
