第一幕 把非确定性圈养起来
第一幕 把非确定性圈养起来
先划边界,再谈信任
一个让大模型控制真实设备的系统,第一件事不是信任模型,是把模型圈起来。
圈起来的意思是,把确定性边界画清楚:哪些东西钉死,哪些东西流动。钉死的越多,模型能砸的场子越小。这条线画完之后,模型输出成了系统里唯一的非确定性来源,被三重闸门兜着:schema 校验、CallerPolicy 双墙、审计。
三个钉死
设备端钉死。固件是 no_std Rust 写的确定状态机:设备会什么动作、参数长什么样,编译期就写死,不随运行变化。
提示词写死。给模型的系统提示是固定文本;设备信息以工具描述和查询结果的形式出现,不拼装进提示词。
工具集钉死。模型能调的工具是固定六个:
| 工具 | 作用 |
|---|---|
iot::query | 读传感器与执行器状态 |
iot::control | 控制执行器 |
iot::list_devices | 列出设备 |
iot::set_alias | 设置别名 |
iot::set_note | 设置描述 |
iot::set_location | 设置位置 |
流动的只有数据:设备能力、状态、遥测。
设备能力 = 编译期契约
关键是设备能力怎么进系统。有个方案当场被毙了:主机侧写设备配置文件。写死的配置文件等于双真相源,设备变了主机不知道。替代方案是让设备自描述——上电时经 iot/{id}/info 上浮自己的能力:
{
"device_id": "env_sensor",
"name": "环境传感器",
"sensors": [
{ "metric": "temp", "unit": "celsius" },
{ "metric": "humidity", "unit": "percent", "range": [0, 100] }
],
"actuators": [
{ "id": "bed_fan", "name": "夹式风扇", "type": "enum",
"values": ["off", "low", "mid", "high"], "risk": "low", "action": "set_level" }
]
}注意"送达路径不是生成路径":这份自描述不是设备临时生成的,是固件编译期写死的契约,info 只是把它送过来。动作集合、参数名、类型、范围,全是编译期常量。模型只能按契约构造 iot::control 的参数。
Agent 侧只缓存校验过的能力镜像。镜像与设备实际描述对不上时,以设备侧最新描述同步,但同步要过能力白名单加审计。就算设备被攻陷,也只能改自己设备的参数形状,引不进白名单外的动作类型。设备是能力的唯一权威源,Agent 不发明能力。
校验层:ioctl 的复仇
模型说要调 iot::control,参数不会直接飞向设备。链路是:LLM 构造参数 → 契约校验 → MQTT 下发 → 固件钳位 → ack 回执。
// cmd
{ "id": "c-123", "cmd": "set_level", "params": { "value": "high" } }
// ack(必带执行后全量状态)
{ "id": "c-123", "ok": true, "error": null, "state": { "bed_fan": "high" } }这条路径可以对着 ioctl 看:
| ioctl | 本项目 |
|---|---|
| request + arg | cmd 载荷 {cmd, params} |
| 驱动侧参数校验 | iot 契约校验 |
| 返回值 / errno | ack {ok, error, state} |
| read / write(数据流) | telemetry |
ioctl 被骂了几十年:参数检查混乱、类型不安全。这里参数形状钉死在固件契约上,校验层按契约拒绝,固件执行前再钳位一次,非法参数直接回 ack 错误,连执行都到不了。内存安全归 Rust,语义合法性归契约校验,两层各管各的。
用户数据:唯一的例外
能力全在设备侧,只有一个例外:位置。设备知道自己会什么、叫什么,但它不知道自己被放在了哪。所以 alias(别名)、notes(描述)、location(位置)三字段是用户覆盖层:非空覆盖写、空串删除;聊天里让模型改,或者设备管理页自己改,双通道写同一份数据。
覆盖层是注入面,所以有约束:去控制字符、长度上限。模型说出来的别名,也得先过一遍。
收个尾
这条边界被一个外部实验验证过:HomeBench 评测要接入 42 台设备,翻译器生成 42 份设备定义,mock 容器经 MQTT info 上浮,后端零代码改动。从宿舍到任意家庭,能力契约一行没改。
还有个小修复值得记:iot::query 原本要求 device_id,模型不知道有哪些设备时就开始臆造 id。改成无参时先返回设备清单,模型拿真实 id 再逐台查。真实模型实测,零臆造。背后还是同一条原则:把非确定性圈进一个它没法作恶的形状里。
