Lesson 05 · 落地
集成进 Pivot:推理服务 / Java 调用 / 2026 版本现实
训好的 Actor 怎么接进 os-server,以及——方案钉的那个代码版本,现在还能用吗。
前四课都在讲"训练"。这一课讲"训完之后":那个学会了控空调的 Actor,怎么变成一个服务、被 Java 后端调、最终下发到冷机。同时,这一课要回答一个你迟早会撞上的问题——方案 §2.1.1 推荐 NM512/dreamerv3-torch commit 6ef8646,但这个仓库在 2026 年 7 月已经归档了。怎么办。
推理时只用到 Actor(训练时那套全用不上)
先记住一个关键事实,它能帮你理解整段推理代码:部署时只需要 Actor,不需要 Critic、不需要想象 rollout、不需要训练世界模型。训练时那套庞大的"做梦"机器,上线后只用其中一小撮——Actor 做一次前向,输出控制动作。所以方案 [§7.2.2] 才敢说"推理用 CPU 即可,<50ms"。
训练时(第 3、4 课): 推理时(上线后): WorldModel + Actor + Critic 只留 Actor(策略网络) + 想象 rollout(数百万次) + 一个轻量 RSSM step(更新隐状态) → 耗时、要 GPU → <50ms,CPU 够
单步推理到底在做什么
方案 [§2.3.3] 的推理服务,核心就一件事:给定楼当前的状态,Actor 输出控制动作。但有一个训练时不存在、推理时必须处理的东西——隐状态:
Actor 有"记忆",必须一步步喂
第 2 课讲 RSSM 是"循环"模型(Recurrent)——它对当前状态的理解,依赖之前的历史。这就像看一部剧:你不能只看第 20 分钟那一帧就懂剧情,你得带着前 19 分钟的记忆看。
所以推理时,Actor 不能只看"当前传感器读数",它还需要一个叫隐状态(hidden state)的东西——它是对这栋楼"近期历史"的压缩。每来一次新读数,就用它更新隐状态,再基于更新后的隐状态出动作。
这就是方案 [§2.3.3] 推理代码里这一行的含义(简化自方案骨架):
# 每栋楼维护一个独立的 RSSM 隐状态(它的"记忆") _agent_state = {} # {building_code: 隐状态} async def predict(req): obs = {'vector': 当前传感器状态} state = _agent_state.get(req.building_code) # 取出这栋楼的记忆 # Actor 前向:用 (新观测, 旧隐状态) → 更新隐状态 + 输出动作 with torch.no_grad(): policy_output, state = _model(obs, reset=False, state=state, training=False) _agent_state[req.building_code] = state # 存回这栋楼的新记忆 return policy_output['action'] # 控制动作 → 安全门控 → MQTT
为什么"每栋楼一个隐状态"
这是方案推理设计里最容易写错的一点。隐状态不能在楼与楼之间串——A 栋楼的热历史和 B 栋楼毫无关系。如果共用一个隐状态,等于让 A 的"记忆"污染 B 的决策。
| 场景 | 对隐状态做什么 |
|---|---|
| 正常推理 | 读出该楼的旧隐状态 → 更新 → 存回 |
| 回退到 PID(安全门控触发,方案 [§4.3]) | 清掉该楼隐状态(/v1/reset/{building_code}) |
| 从 PID 恢复 MBRL | 隐状态从零重新累积(需几个周期才"热"起来) |
方案里那个 /v1/reset/{building_code} 端点,就是干"清隐状态"这件事——回退 PID 时必须调它,否则 MBRL 恢复后会带着过期记忆乱来。
Java 后端怎么调
方案 [§1.5] 的关键决策:推理服务是独立 Python 进程,不嵌进 Java 的 os-server。os-server 里的 HVAC MBRL 智能体通过 HTTP 调它,拿到动作后走标准的 Action Gateway → MQTT 下发(和现有智能体共用一套审计、风险分级)。方案 [§2.3.3] 的 Java 侧就是一次普通 HTTP 调用:
public MbrlPrediction predict(String buildingCode, List<Double> stateVector) { return restClient.post() .uri("/v1/predict") .body(Map.of("building_code", buildingCode, "state_vector", stateVector)) .retrieve() .body(MbrlPrediction.class); // 动作向量 → 安全门控 → MQTT }
为什么用 FastAPI 而不是 Triton/ONNX?方案 [§2.3.1] 给了详尽对比,一句话结论:DreamerV3 的 RSSM 有循环结构,ONNX 导出不可行;Triton 对我们这种低频单模型场景是过度设计。一个 FastAPI + PyTorch 容器,50ms 延迟,HVAC 5-15min 一次控制,绰绰有余。
必须面对的现实:方案钉的代码版本已归档
这直接影响你启动 PoC 时的代码选型。两条路,各有取舍:
| 用 dreamerv3-torch(方案原选,commit 6ef8646) | 迁移到 r2dreamer | |
|---|---|---|
| 状态 | 仓库归档、冻结 | 活跃维护、更快 |
| 与方案代码骨架 | 一一对应(dreamer.py/models.py/networks.py/envs/) | 结构不同,方案那段代码需重映射 |
| 可复现性 | 冻结 = 任何人都能复现同一结果 | 仍在演进,结果可能漂移 |
| 长期维护 | 有 bug 没人修 | 持续更新 |
| 适合谁 | 6 个月 PoC / 跟方案走 | 已验证可行性后、要长期演进 |
我的建议(默认):PoC 阶段仍用 dreamerv3-torch 的 6ef8646。理由是——方案 [§2.1.4] 的整段代码骨架(envs/hvac.py、dreamer.py、超参表)都是围绕它写的,且冻结意味着团队任何人都能复现同一结果,这对 PoC 的可信度比对"最新"更重要。仓库归档≠不能用,它只是不再更新。等 PoC 验证可行、要长期演进时,再评估迁到 r2dreamer——那是一个独立的技术决策,不在 6 个月 PoC 范围内。
推理时只用 Actor(训练那套做梦机器用不上),所以 CPU <50ms 够用。关键是每栋楼维护一个独立隐状态(Actor 的"记忆",不能跨楼串),回退 PID 时要清掉它。os-server 通过 HTTP 调 FastAPI 推理服务、拿动作后走安全门控 + Action Gateway 下发。现实是方案钉的 dreamerv3-torch 已归档——PoC 建议仍用冻结版(与方案骨架对应、可复现),长期演进再迁 r2dreamer。
推理服务为什么要"每栋楼维护一个独立的隐状态",而不是全局共用一个?
学完了,下一步是动手。建议顺序:① 先按姊妹课 teaching-energyplus 把 Sinergym+EnergyPlus 跑通一个能 reset/step 的环境;② 再按本课第 4 课加 envs/hvac.py 接进 DreamerV3;③ 训出 checkpoint 后按本课接推理服务。任何一步卡住都来问我——尤其是"仓库归档了某依赖装不上"这类版本问题,我帮你查历史 issue。