课程首页· 术语表· 第 5 课 / 共 5 课

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 时的代码选型。两条路,各有取舍:

代码选型:冻结版 vs 继任版
用 dreamerv3-torch(方案原选,commit 6ef8646)迁移到 r2dreamer
状态仓库归档、冻结活跃维护、更快
与方案代码骨架一一对应(dreamer.py/models.py/networks.py/envs/)结构不同,方案那段代码需重映射
可复现性冻结 = 任何人都能复现同一结果仍在演进,结果可能漂移
长期维护有 bug 没人修持续更新
适合谁6 个月 PoC / 跟方案走已验证可行性后、要长期演进

我的建议(默认):PoC 阶段仍用 dreamerv3-torch 的 6ef8646。理由是——方案 [§2.1.4] 的整段代码骨架(envs/hvac.pydreamer.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。

参考来源 NM512/dreamerv3-torch(归档状态、继任者提示) · 方案文档 §1.5 / §2.3(推理服务化)/ §4(安全门控) · 调研报告 §3.3.4(商用成熟度) · 仓库归档事实核实于 2026-07