ARTICLE · 1112494
《先懂控制,再懂AI》第 2 课 为什么液位必须自动,流量却可以手动?

各位工友,周五好,我是小马。
先道一声:国庆节快乐! 今天是假期第二天,别人在朋友圈晒景区、晒高速,咱们晒的是 DCS 画面和巡检记录——假期的厂子一秒不停,总得有人盯屏。 正好,这一课讲的就是一个"没人盯屏会发生什么"的故事——值班间隙读它,再合适不过。
周二第 1 课咱们搞明白了一件事:阀门"自己会动",是因为背后有个回路在替你盯、替你拧。
这一课回答一个更扎心的反差——
这不是习惯问题,不是"老厂就这么弄",是物理。今天用一个储罐,把它讲透。
先讲个真事:操作工去巡检的那一个半小时
一个储罐,正常液位 3.00 米,罐高 5 米,高报警 4.20 米。
进料 100 m³/h,出料也是 100 m³/h——出料是泵打的,不随液位变。
这个罐的液位是手动的。早上 8 点整,操作工把进料阀调好,液位稳稳地停在 3 米。然后他拿了对讲机去现场巡检了。
8 点半,上游装置提量,进料多了 8%,变成 108 m³/h。
操作工 9 点半才回到中控。
你猜这时候液位是多少?

答案:4.5米,离满罐只剩半米
先把结果放这儿,下面第 05 节真跑验证:
液位手动:9 点 11 分(扰动后 71 分钟)就触发了高报警。操作工回来时,液位 4.50 米,离满罐只剩 0.50 米。
液位自动:同样的扰动,液位全程最高 3.16 米,稳稳的。
再看旁边那条流量管线——上游是同一个扰动,工况也是变了 8%:
阀位没动,流量从 100 变成了 108 m³/h
1.5 小时过去,还是 108
工艺允许波动 ±10,没超
同样的扰动、同样没人管一个半小时,一个报了警差点满罐,一个屁事没有。
凭什么?

为什么:一个会“攒”,一个不会
一句话:
流量:阀位定,流量就定。 阀开 50%,上游压力变了 8%,流量就从 100 跳到 108——然后停在那儿。它没有地方"存"东西,它是个"过程量",过去的事不影响现在。
液位:罐是个"存量"。 进来 108、出去 100,每小时多出来的 8 立方米没地方去,全变成了液位。它不会自己消失,只会一直堆。
这就是控制里最重要的一个分类:
自衡对象:被控变量自己变了之后,会反过来"帮自己"找回平衡
无自衡对象:被控变量变了,什么都不会跟着变——偏差一路走到黑
现场判断方法就一句:问它"你自己会往回找吗?"
| 变量 | 它自己会往回找吗 | 类型 | 手动行不行 |
|---|---|---|---|
| 液位 | 无自衡 | 不行 |
⚠ 注意液位那一行有个前提:出料得是泵
泵的排量不看你液位多高,它只管打。如果出料是靠液位自己压出去的(重力阀),液位一高出料也变大,它就有自衡了——那是另一种很有意思的回路,咱们第 7 课专门讲。

更值钱的一句:你调的不是液位,而是“差”
这堂课真正想让你带走的是这句:
液位只是那杆秤。
液位在涨 = 进大于出
液位在掉 = 出大于进
液位稳 = 平衡
你动液位阀,其实是在消那个"差"。 而只要"差"存在,液位就一直在变——所以它不能等,必须时时刻刻有人(或自动)盯着。
流量不用管什么平衡,它只需要跟住设定值。跟住之后,它自己就站住了。
这就是为什么液位回路出事是事故(溢罐、抽空、泵汽蚀、憋压),而流量回路出事多数只是质量偏差——一个碰安全,一个碰指标。

真跑一遍:一张表看懂
同样的扰动(+8%),同样的操作工(第 30 分钟去现场,1.5 小时后回来),四种组合一起跑:
| 回路 | 方式 | 扰动后30分钟 | 扰动后60分钟 | 扰动后90分钟 |
|---|---|---|---|---|
| 手动 | +0.50 m | +1.00 m | +1.50 m | |
| +8.00 m³/h | +8.00 m³/h |
盯着看两张表的差别:
液位手动那行:0.50 → 1.00 → 1.50。每小时正好涨 1 米,一条直线,不停。
流量手动那行:8.00 → 8.00 → 8.00。一步到位,然后就不动了。
这就是"攒"和"不攒"的区别,写在数字里。
再看其余几行:
液位自动:扰动后 71 分钟报警 vs 全程最高 3.16 米——自动把那条直线的斜率干成了 0
流量自动:全程最大偏差 4.7 m³/h,比手动的 8 还大?对——因为流量测量有噪声,自动在"追着噪声跑"。但它在 ±10 以内,而且不会跑飞。
📷 配图:跑完第 07 节的画图代码会自动生成 level_flow_compare.png。上图液位那条红线一路涨过报警线,下图流量那条红线跳上去就平了——两张图放一起,这一课就讲完了。

报警喊人,联锁动手:还有第三道防线
第 05 节那个罐,从头到尾其实站在三道防线后面:
| 防线 | 谁在干活 | 动作值(本例) | 动作之后 |
|---|---|---|---|
| 4.20 m | |||
| 自己动手 | 4.40 m | 自动启动备用出料泵 |
第 05 节咱们只跑到了第二道:报警响了,人不在,液位接着涨。
现在把第三道接上——液位一到 4.40,自动启动备用出料泵(出料从 100 提到 160 m³/h),把料抽到 3.60 再停泵。同一个扰动、同一串噪声,两条命运:
| 不投联锁 | 投上联锁 | |
|---|---|---|
| 4.40 m | ||
| 3.75 m,安全 |
看最后一行:联锁在第 84 分钟动手,比操作工回来早了 6 分钟。 就这 6 分钟,罐从"离满罐剩半米"变成了"3.75 米,没事"。
三个要记住的点:
联锁值比报警值高一档。 4.20 喊人、4.40 动手——中间那 0.20 米,就是留给"人"的时间。两个值要是一样,等于没给操作工机会。
联锁不是"调节",是"保命"。 看它的曲线:液位在 3.60~4.40 之间来回跑——因为它只有"开泵 / 停泵"两档,不会像 PID 那样细调。联锁一动作,就说明前两道防线全都失守了。
方向永远朝安全。 液位高高 → 启动备用出料泵(让料变少);液位低低 → 启动备用进料泵(让料变多)。常说的"高报警开出口备泵、低报警开入口备泵",就是这个规矩的两个方向。
⚠ 一条现场铁律
联锁不能随便旁路。旁路要审批、要限时、要挂牌;联锁动作之后,必须先查明原因、确认工艺正常,才能复位——它是保险,不是工具。
严格讲,真正碰安全的那一层联锁(SIS / ESD)是独立于 DCS 的另一套系统:自己的检测元件、自己的执行机构、自己的逻辑,不跟 DCS 共用。这一课先用"高液位启动备用泵"这种软联锁把概念讲明白;SIS 那套、联锁台账、旁路管理,咱们后面专门开课。

想动手的往这儿看
不想敲代码的工友,看到这儿结论已经完整了,可以直接跳到第 08 节。
新建文件 level_vs_flow.py:
import numpy as npDT = 1.0T_END = 9000 # 2.5 小时T_DIST = 1800 # 30 分钟:上游工况变化T_GONE = 1800 # 操作工去现场T_BACK = 7200 # 1.5 小时后回中控DIST = 0.08 # 上游扰动 +8%CHECK = [T_DIST + 1800, T_DIST + 3600, T_DIST + 5400] # 扰动后 30/60/90 分钟V0 = 50.0# ================= 液位对象(积分 / 无自衡)=================A_TANK = 8.0 # 罐截面积 m²L_SP = 3.00 # 设定液位 mL_HI = 4.20 # 高报警 mTANK_H = 5.00 # 罐高(满罐)mKV_L = 2.0 # 进料阀增益:1% 阀位 = 2 m³/hdef run_level(kind, Kp=12.0, Ti=900.0, seed=42):"""kind: 'auto' / 'manual' -> (时间序列液位, 阀位)"""rng = np.random.default_rng(seed)n = int(T_END / DT)L = np.zeros(n)V = np.zeros(n)L[0] = L_SPV[0] = V0integ = 0.0Lmf = L_SPfor k in range(1, n):t = k * DTdist = DIST if t >= T_DIST else 0.0# --- 测量(液位计噪声很小)---Lmf += (L[k - 1] + rng.normal(0, 0.01) - Lmf) * DT / 5.0e = L_SP - Lmf # 液位低了 e>0 → 开大进料阀# --- 控制方式 ---if kind == 'auto':integ += e * DTV[k] = np.clip(V0 + Kp * e + Kp / Ti * integ, 0, 100)else:# 操作工:在线时每 120 秒看一次,一次最多动 1%V[k] = V[k - 1]online = (t < T_GONE) or (t >= T_BACK)if online and k % 120 == 1 and abs(e) > 0.05:V[k] = np.clip(V[k - 1] + np.clip(0.8 * e, -1.0, 1.0), 0, 100)# --- 对象:进料(泵出料,不随液位变) → 液位积分 ---F_in = (100.0 + KV_L * (V[k] - V0)) * (1.0 + dist) / 3600.0 # m³/sF_out = 100.0 / 3600.0L[k] = L[k - 1] + (F_in - F_out) / A_TANK * DTreturn L, V# ================= 流量对象(静态 / 自衡)=================F_SP = 100.0 # 设定流量 m³/hF_TOL = 10.0 # 工艺允许波动 ±10 m³/hKV_F = 2.0 # 阀位 → 流量增益(同 2 m³/h per %)TAU_F = 3.0 # 流量对象时间常数(秒级,很快)def run_flow(kind, Kp=0.4, Ti=60.0, seed=42):rng = np.random.default_rng(seed)n = int(T_END / DT)F = np.zeros(n)V = np.zeros(n)F[0] = F_SPV[0] = V0integ = 0.0Fmf = F_SPfor k in range(1, n):t = k * DTdist = DIST if t >= T_DIST else 0.0Fmf += (F[k - 1] + rng.normal(0, 1.0) - Fmf) * DT / 2.0e = F_SP - Fmf # 流量低了 e>0 → 开大阀if kind == 'auto':integ += e * DTV[k] = np.clip(V0 + Kp * e + Kp / Ti * integ, 0, 100)else:# 操作工把阀固定在一个开度就走了(流量不需要人盯)V[k] = V[k - 1]# --- 对象:阀位定,流量就定(一阶,很快到位)---F[k] = F[k - 1] + (KV_F * V[k] * (1.0 + dist) - F[k - 1]) / TAU_F * DTreturn F, V# ================= 汇总输出 =================print(f'{"回路":<6}{"方式":<6}{"30分钟":>16}{"60分钟":>16}{"90分钟":>16}{"单位":>8}')rows = []for name, runner, unit in [('液位', run_level, 'm'),('流量', run_flow, 'm³/h'),]:base = L_SP if name == '液位' else F_SPfor kind, cn in [('auto', '自动'), ('manual', '手动')]:y, _ = runner(kind)devs = [y[t] - base for t in CHECK]rows.append((name, cn, devs, unit, y))print(f'{name:<6}{cn:<6}' + ''.join(f'{d:>+16.2f}' for d in devs) + f'{unit:>8}')print()Lm, _ = run_level('manual')La, _ = run_level('auto')over = np.where(Lm > L_HI)[0]print(f'液位手动:扰动后 {int((over[0] - T_DIST) / 60)} 分钟触发高报警({L_HI} m),'f'到操作工回来时液位 {Lm[T_BACK]:.2f} m(离满罐只剩 {5.0 - Lm[T_BACK]:.2f} m)')print(f'液位自动:全程最高 {La[T_DIST:].max():.2f} m,最低 {La[T_DIST:].min():.2f} m')Fm, _ = run_flow('manual')Fa, _ = run_flow('auto')print(f'流量手动:扰动后稳定在 {Fm[T_DIST:].mean():.1f} m³/h(偏离设定 {Fm[T_DIST:].mean() - F_SP:+.1f}),'f'最大 {Fm[T_DIST:].max():.1f},全程未超工艺允许 ±{F_TOL:.0f}')print(f'流量自动:全程最大偏差 {np.abs(Fa[T_DIST:] - F_SP).max():.1f} m³/h')
跑完,你的输出和我一个字不差:
回路 方式 30分钟 60分钟 90分钟 单位液位 自动 +0.14 -0.01 -0.03 m液位 手动 +0.50 +1.00 +1.50 m流量 自动 +0.06 -0.14 +0.01 m³/h流量 手动 +8.00 +8.00 +8.00 m³/h液位手动:扰动后 71 分钟触发高报警(4.2 m),到操作工回来时液位 4.50 m(离满罐只剩 0.50 m)液位自动:全程最高 3.16 m,最低 2.97 m流量手动:扰动后稳定在 108.0 m³/h(偏离设定 +8.0),最大 108.0,全程未超工艺允许 ±10流量自动:全程最大偏差 4.7 m³/h
再把第 06 节说的第三道防线(联锁)接上,跑一次对比:
# ============ 再加一道联锁:液位高高 → 自动启动备用出料泵 ============L_HH = 4.40 # 高高联锁值:比高报警 4.20 再高一档(先喊人,后动手)L_RST = 3.60 # 备用泵停泵值:留 0.8 m 回差,防止泵在临界点反复启停B_PUMP = 60.0 # 备用出料泵容量 m³/h(启动后总出料 100 → 160)def run_level_interlock(interlock=True, seed=42):"""和第 05 节同模型、同噪声,只是多了一个联锁开关。"""rng = np.random.default_rng(seed)n = int(T_END / DT)L = np.zeros(n)V = np.zeros(n)L[0] = L_SPV[0] = V0integ = 0.0Lmf = L_SPpump = Falsestarts = []stops = []for k in range(1, n):t = k * DTdist = DIST if t >= T_DIST else 0.0Lmf += (L[k - 1] + rng.normal(0, 0.01) - Lmf) * DT / 5.0e = L_SP - LmfV[k] = V[k - 1]online = (t < T_GONE) or (t >= T_BACK)if online and k % 120 == 1 and abs(e) > 0.05:V[k] = np.clip(V[k - 1] + np.clip(0.8 * e, -1.0, 1.0), 0, 100)# ---- 第三道防线:联锁(报警喊人,联锁动手)----if interlock and (not pump) and L[k - 1] >= L_HH:pump = Truestarts.append(t)elif pump and L[k - 1] <= L_RST:pump = Falsestops.append(t)F_in = (100.0 + KV_L * (V[k] - V0)) * (1.0 + dist) / 3600.0F_out = (100.0 + (B_PUMP if pump else 0.0)) / 3600.0L[k] = L[k - 1] + (F_in - F_out) / A_TANK * DTreturn L, V, starts, stopsfor name, il in [('不投联锁', False), ('投上联锁', True)]:L, V, starts, stops = run_level_interlock(il)print(f'{name}:全程最高 {L.max():.2f} m,操作工回来时 {L[7200]:.2f} m,'f'150 分钟时 {L[-1]:.2f} m')if starts:print(f' 联锁第 {starts[0] / 60:.0f} 分钟动作(扰动后第 {(starts[0] - T_DIST) / 60:.0f} 分钟):'f'液位到 {L_HH} m,启动备用出料泵')print(f' 第 {stops[0] / 60:.1f} 分钟抽回 {L_RST} m 停泵:泵只跑了 'f'{(stops[0] - starts[0]) / 60:.1f} 分钟')
输出:
不投联锁:全程最高 4.55 m,操作工回来时 4.50 m,150 分钟时 3.93 m投上联锁:全程最高 4.40 m,操作工回来时 3.75 m,150 分钟时 3.51 m联锁第 114 分钟动作(扰动后第 84 分钟):液位到 4.4 m,启动备用出料泵第 121.4 分钟抽回 3.6 m 停泵:泵只跑了 7.4 分钟
想出图的话,接着跑这段:
import matplotlib.pyplot as pltplt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei'] # 中文字体plt.rcParams['axes.unicode_minus'] = False # 负号正常显示La, _ = run_level('auto')Lm, _ = run_level('manual')Fa, _ = run_flow('auto')Fm, _ = run_flow('manual')t = np.arange(len(La)) * DTfig, axes = plt.subplots(2, 1, figsize=(11, 8), sharex=True)ax = axes[0]ax.axvspan(T_GONE, T_BACK, color='#f0f0f0', zorder=0, label='操作工不在(现场巡检)')ax.plot(t, La, color='#2f6fb3', lw=1.6, label='液位 · 自动')ax.plot(t, Lm, color='#c0392b', lw=1.6, label='液位 · 手动')ax.axhline(L_SP, color='#444', ls=':', lw=1)ax.axhline(L_HI, color='#e08b1f', ls='--', lw=1.2, label=f'高报警 {L_HI} m')ax.axhline(TANK_H, color='#a32d2d', ls='--', lw=1.2, label=f'满罐 {TANK_H} m')ax.axvline(T_DIST, color='#888', ls=':', lw=1)ax.set_ylabel('储罐液位 (m)')ax.set_ylim(2.8, 5.3)ax.legend(loc='upper left', fontsize=9, ncol=2)ax.grid(True, alpha=0.3)ax = axes[1]ax.axvspan(T_GONE, T_BACK, color='#f0f0f0', zorder=0, label='操作工不在(现场巡检)')ax.plot(t, Fa, color='#2f6fb3', lw=1.6, label='流量 · 自动')ax.plot(t, Fm, color='#c0392b', lw=1.6, label='流量 · 手动')ax.axhline(F_SP, color='#444', ls=':', lw=1)ax.axhspan(F_SP - F_TOL, F_SP + F_TOL, color='#eaf5ef', zorder=0, label='工艺允许 ±10')ax.axvline(T_DIST, color='#888', ls=':', lw=1)ax.set_ylabel('管道流量 (m³/h)')ax.set_xlabel('时间 (秒)')ax.set_ylim(85, 115)ax.legend(loc='upper left', fontsize=9, ncol=2)ax.grid(True, alpha=0.3)plt.tight_layout()plt.savefig('level_flow_compare.png', dpi=130)plt.show()

【🤖AI视角】液位的趋势里,藏着比液位更之前的东西
🤖 AI 视角
第 1 课说过一句话:被闭环控制的变量,拿去做纯趋势外推必失效——因为回路会把它拉回设定值,历史趋势里没有可外推的规律。
这一课再补一刀,而且这一刀更狠:
液位这种积分对象,连"均值回归"都没有。
这意味着什么?
对流量、温度这种自衡变量,历史数据是"围绕均值波动"的——平均值有意义,它就是正常工况。所以拿它们做异常检测、做预测,模型有得学。
对液位,历史数据是"一路漂移"的——它没有"正常水平"可回归,平均值毫无意义。你直接拿液位历史数据去训练预测模型,基本是白干。
那 AI 在液位上还有用吗?有,而且更有用——但要看对东西。
看液位曲线,AI 能看出的"在涨",人也能看出,没有超额价值
真正值钱的是进料流量 − 出料流量的累积差。这个东西,AI 可以 7×24 小时、对几十个罐同时算,人做不到
这就是"机理 + 数据"结合的最小案例:给模型一个"积分"的机理,它只要盯两条流量曲线,就能提前半小时告诉你哪个罐要报警。
一句话总结:
这也是为什么厂里做 AI 预警,液位、压力这类"存量"变量的提前量,普遍比流量大得多——因为它们背后有一个可以算的平衡。
而不懂这个平衡的人做 AI,只会得出"液位预测不准,AI 没用"的结论。 和第 1 课那个趋势外推的坑,是同一个坑,换了个姿势再踩一次。

留个作业
📝 作业 A(基础):把 DIST 从 0.08 改成 0.02,重跑。对比表里三列数字——全变成了原来的 1/4,但方向没变,还在涨。 扰动小了,"攒"这件事没有停。
📝 作业 B(进阶):把 A_TANK 从 8.0 改成 2.0(小罐),重跑。同样的 8% 扰动,小罐 17 分钟就报警、29 分钟就满罐。 你就明白为什么现场喜欢用大罐"扛"扰动——罐就是缓冲,容量就是时间。
📝 作业 C(挑战):把出料从"泵"改成"靠液位自己压出去"——把 run_level 里这一行 F_out = 100.0 / 3600.0 改成 F_out = 57.74 * np.sqrt(max(L[k - 1], 0.0)) / 3600.0,重跑。你会看到液位偏差 +0.31 → +0.43 → +0.47,越涨越慢,最后自己稳住了。 这就是"自衡"——第 7 课的主题,咱们到时候细说。
📝 选做 · 联锁:把联锁回差改小——L_RST 从 3.60 改成 4.35(回差只剩 0.05 m),重跑。备用泵会在 4.35 和 4.40 之间来回启停 3 次,总共只跑了 1.4 分钟,液位根本没压下去(操作工回来时还是 4.38 m)。 现场为什么怕"泵频繁启停"?回差到底是干什么的?跑一遍就懂了。
这几个作业,不用敲代码也能做。 打开「仿真实验室」(文末阅读原文直达,打开就是本课模块),右侧有现成按钮——「作业 A:扰动小一点」「作业 B:换成小罐」「作业 C:重力出料」,点一下、按开始,现象直接画出来。液位和流量两个回路的手/自动是分开切的,四种组合随便试——表里那四行文章数字,你跑出来会一位不差。联锁那道也进实验室了:右侧「第三道防线」开关一开(或直接点「作业 D:加一道联锁」),液位一到 4.40 m,备用出料泵就自己动手——你亲眼看看它比操作工早了几分钟。
做完把结果发评论区,下周挑典型的讲。
老规矩:跑代码遇到任何报错,把完整报错信息贴评论区,我一个个回。你的问题,就是下一篇选题。
下期预告
下周二 10 月 6 日,《先懂控制,再懂AI》第 3 课:《同一台泵的两个回路,为啥一个好调一个调不住?》——同一个泵,两条管线,一个阀好调得像玩具,一个怎么调都振。答案在"对象特性"里。

有疑问或岗位困惑也欢迎评论区留言,你的问题
就是下一篇选题。


点击"阅读原文" 进入《仿真实验室》